FontForge script debug

Posted in :

我的字型,在使用 fontforge 在做 SelectAll() + ExpandStroke() 會掛掉,要怎麼知道是掛在那一個 glyph?

首先,我們要在 script 裡寫入資料到外部檔案,需要這2個 function:

WriteStringToFile("string","Filename"[,append])
Creates the file named "Filename" and writes the string to it. If the append flag is present and non-zero, then the string will be appended to the file. This deals with null-terminated strings, not with byte arrays. Returns -1 on failure otherwise the number of bytes written. It can execute with no current font.
LoadStringFromFile("filename") Reads the entire file into a string. Returns a null string for a non-existant or empty file. The returned string will have enough bytes allocated to hold the entire file plus one trailing NUL byte. If the file contains a NUL itself, fontforge will think the string ends there. It can execute with no current font.

測試 FontForge Array 的功能:

Array(size)Allocates an array of the indicated size.

a = Array(10)
i = 0;
while ( i<10 )
   a[i] = i++
endloop
a[3] = "string"
a[4] = Array(10)
a[4][0] = "Nested array";

上面是標準的用法,也可以使用 StrSplit(str,” “) 動態產生 string array.

也可以這樣使用:

a=[1,2,3,4,5]
Print("a2:",a[2])

這個範例,可以取得輸出值:3


下面的範例,會依序把字串中的字,一個一個寫入並覆蓋到 debug_fontforge.txt 裡:

str="abcdefg"
str_length = Strlen(str)
i=0;
while ( i<str_length )
	current_char = Strsub(str,i,i+1)
	Print( "get char:", current_char)
	WriteStringToFile(current_char,"debug_fontforge.txt")
   	++i
endloop

Strsub 是滿方便的,可是無法處理 multi-byte 的中文字!改用下面的StrSplit就可以解決中文字問題。

StrSplit(str,delimiter[,max-cnt])
Splits the string at every occurance of the delimiter and produces an array of sub-strings.
StrSplit("The quick brown box"," ") yields
["The", "quick","brown","box"]
If max-cnt is specified then it will limit the number of sub-strings in the array
StrSplit("The quick brown box"," ",2) yields
["The", "quick brown box"]

把字串換成 unicode 數字:

Ucs4(str)
Takes a string in Utf8 encoding and returns an array of integers, one for each unicode character in the string.

程式會掛掉的fontforge script

 SelectAll()
 ExpandStroke(18,45,18,18,0,1)

解法:

txt_filename = "ttf_chars.txt"
txt_exist = FileAccess(txt_filename)
if(txt_exist == 0)
 str = LoadStringFromFile(txt_filename)
 str_array = StrSplit(str," ") 
 str_length = 15144
 Print( "Strlen:", str_length)

 i=0;
 while ( i<str_length )
 current_char = str_array[i]
 #Print( "process glyph:", current_char, Ucs4(current_char))
 WriteStringToFile(current_char,"debug_fontforge.txt")

 Select(Ucs4(current_char)[0])

 ExpandStroke(18,45,18,18,0,1)
     ++i
 endloop
else
 Print( "file not exist:", txt_filename)
endif

程式說明:

使用 SelectAll() 無法得知是那一個字讓程式中斷掉,所以要改用Select() 來指定特定的字,來進行ExpandStroke.

遇到會讓程式掛掉的字,就到 ttf_chars.txt 把有問題的字先移掉,最後再針對會掛掉的字的清單做處理。由於已經確定有問題的字以上,是沒問題的,所以需要再執行一次沒問題的字。執行完後,再把有問題的字以下拿出來再跑一次,最後就可以成功地拿到都ExpandStroke後的字型檔。

由於是使用空格當作分隔字元,所以 ttf_chars.txt 裡不用去包括空格.


上面的程式有一個問題,就是遇到處理超過 10,000 筆的資料時,最後使用的 Save 會成功,但實際卻沒有存進去。解法,改用下面的 script:

fontname = "Bakudai"
project_weight = "Bold"
project_weight_attrib = "Bold"
project_is_bold = 1
expandstroke_width = 18

# 1: Bold
# 2: Thin
expandstroke_flag = 1

fontname_with_weight = fontname + "-" + project_weight
project_folder = fontname_with_weight + ".sfdir"
fontname_ttf_filename = fontname_with_weight + ".ttf"

Print("Open " + project_folder)
Open(project_folder)
SetFontNames(fontname_with_weight,fontname,fontname_with_weight,project_weight_attrib)
if (project_is_bold==1)
	#SetMacStyle(-1)
	SetMacStyle(0x01)
endif
if (is_expand_stroke==1)
	Print("ExpandStroke...")

	txt_filename = "ttf_chars.txt"
	txt_exist = FileAccess(txt_filename)
	if(txt_exist == 0)
		str = LoadStringFromFile(txt_filename)
		str_array = StrSplit(str," ") 
		str_length = 15142
		Print( "Strlen:", str_length)
		i=0;
		while ( i<str_length )
			current_char = str_array[i]
			Print( "process glyph:", current_char, Ucs4(current_char))
			WriteStringToFile(current_char,"debug_fontforge.txt")

			Select(Ucs4(current_char)[0])
			#RemoveOverlap()

			ExpandStroke(expandstroke_width,45,1,1,0,expandstroke_flag)
		   	++i

		   	if(i % 1000 == 0)
				Print("Save project...")
				Save(project_folder)

				WriteStringToFile(current_char,"debug_fontforge_auto_save.txt")
		   	endif
		endloop
	else
		Print( "file not exist:", txt_filename)
	endif

endif

Print("Save project...")
Save(project_folder)

if (is_generate_font==1)
	Print("Generate ttf...")
	Generate(fontname_ttf_filename)
endif
Close()
Print("Close " + project_folder)

除了透過 FontForge script 來改字重,也可以透過 python script , 可以操控的變數更多,功能也更多。

Python Scripting
https://fontforge.org/docs/scripting/python.html

上面做法效果還是不太好,目前的作法是:

  1. 在python script 裡設定 原版regular 的 path,
  2. 建立一個煮菜用的 tmp folder 來做 stroke(),
  3. 每次從原版Regular裡複製 2000個 glyph出來做 stroke(),
  4. 完全都成功的話,再把 2000個glyph搬出來到 成品用的 target folder裡。
  5. 遇到被中斷時,就先直接在 python script 裡把該字加到排除清單(exclusion list) 裡,再直接執行 python script 執行上面的 Step 3.
  6. 最後全部都執行完成,再來手動處理有異常的清單。

實際遇到掛掉的情況:

說明:把中斷時畫面上的 base main 欄位,填入 python script 的 source code 裡,再把掛掉的 unicode 放到 skip list 的陣列,這時候重跑程式就會再接續沒有做完的部份,並避開有問題的 unicode glyph.

所有的字都 stroke() 過後,再把 is_process_interrupe_list 設成 True 就可以把有問題的字,使用其他的 stroke 參數重做一次,就會通過。

附上完整 python script:

#!/usr/bin/env python3
#encoding=utf-8

#execute command:
# /Applications/FontForge.app/Contents/Resources/opt/local/bin/fontforge expand_stroke.py
import sys, fontforge, math
from os import listdir, remove, mkdir
from os.path import join, exists, getsize, basename
import glob

# to copy file.
import shutil

def load_unicode_from_file(filename_input):
    mycode = 0
    myfile = open(filename_input, 'r')
    left_part = 'Encoding: '
    left_part_length = len(left_part)
    for x_line in myfile:
        #print(x_line)
        if left_part == x_line[:left_part_length]:
            right_part = x_line[left_part_length:]
            if ' ' in right_part:
                mychar_array = right_part.split(' ')
                if len(mychar_array) > 0:
                    mycode = int(mychar_array[0])
                    #print("bingo")
                    break
        
    myfile.close()
    return mycode

def load_files_to_set_dict(ff_folder):
    my_set = set()
    my_dict = {}

    size_for_not_empty = 100

    # 取得所有檔案與子目錄名稱
    files = listdir(ff_folder)

    # 以迴圈處理
    for f in files:
        if '.glyph' in f:
            #print('filename:', f)

            # skip hidden files.
            if f[:1] == ".":
                continue

            target_path = join(ff_folder,f)
            filesize = getsize(target_path)

            if filesize <= size_for_not_empty:
                continue

            unicode_info = load_unicode_from_file(target_path)
            #if unicode_info > 0 and unicode_info < 0x110000:
            if unicode_info > 0:
                #print('code:', unicode_info)
                my_set.add(unicode_info)
                my_dict[unicode_info] = f
                #break
    return my_set, my_dict

def clean_tmp_glyph(ff_tmp):
    for name in glob.glob(ff_tmp + '/*.glyph'):
        remove(name)

def prepare_environment(ff_source, ff_target, ff_tmp, ff_lost):
    if not exists(ff_tmp):
        mkdir(ff_tmp)
    else:
        pass

    if not exists(ff_target):
        mkdir(ff_target)
    else:
        pass

    if not exists(ff_lost):
        mkdir(ff_lost)
    else:
        pass

    # prepare font.props
    ff_source_font_props = join(join("..",ff_target),"font.props")
    ff_target_font_props = join(ff_target,"font.props")
    ff_tmp_font_props = join(ff_tmp,"font.props")

    if not exists(ff_target_font_props):
        shutil.copy(ff_source_font_props, ff_target_font_props)

    if not exists(ff_tmp_font_props):
        shutil.copy(ff_source_font_props, ff_tmp_font_props)

    # clean preview interrupe temp data.
    clean_tmp_glyph(ff_tmp)

def move_tmp_glyph_to_target(ff_tmp,ff_target):
    for name in glob.glob(ff_tmp + '/*.glyph'):
        filename = basename(name)
        target_path = join(ff_target,filename)
        #print(name,filename)
        shutil.move(name,target_path)

def max_expand_stroke(ff_tmp, travel_begin_index, skip_list, stroke_width, stroke_cap, stroke_join,stroke_join_limit,stroke_weight):
    print("Open font... ")
    myfont=fontforge.open(ff_tmp)
    myfont.selection.all()

    idx = 0

    # read chars list from text.
    #for char in mychars:
        #myfont.selection.select(ord(char))

    for glyph in myfont.selection.byGlyphs:
        idx +=1
        #unicode_string = str(hex(glyph.unicode))[2:]
        unicode_int = glyph.unicode
        if unicode_int in skip_list:
            continue

        if unicode_int <= 0:
            continue
        
        #print("Process (%d): %s - %s" % (idx, char, unicode_string))
        print("Begin Main:%d, Begin detail:%d, unicode:%d" % (travel_begin_index,idx,unicode_int))
        
        #default for thin expand stroke
        if stroke_weight == "bold":
            # better version
            glyph.stroke("circular",stroke_width,cap=stroke_cap,join=stroke_join,angle=math.radians(45),removeinternal=True,simplify=True)
            # stable version
            #glyph.stroke("circular",stroke_width,cap=stroke_cap,join=stroke_join,removeinternal=True,simplify=True)
        else:
            # "light"
            # better version
            glyph.stroke("circular",stroke_width,cap=stroke_cap,join=stroke_join,angle=math.radians(45),removeexternal=True,simplify=True,joinlimit=stroke_join_limit)
            # stable version, for interrupe glyphs.
            #glyph.stroke("circular",stroke_width,cap=stroke_cap,join=stroke_join,removeexternal=True,simplify=True)
        #glyph.export("U%s.svg" % (unicode_string))

    print("Done, total:%d" % (idx))
    myfont.save(ff_tmp)


def scan_lost_files_from_folder(ff_source, ff_lost):
    size_for_not_empty = 100

    # 取得所有檔案與子目錄名稱
    files = listdir(ff_source)

    # 以迴圈處理
    for f in files:
        if '.glyph' in f:
            # skip hidden files.
            if f[:1] == ".":
                continue

            source_path = join(ff_source,f)
            filesize = getsize(source_path)

            if filesize <= size_for_not_empty:
                target_path = join(ff_lost,f)
                shutil.copy(source_path, target_path)

ff_source="../your_project_regular.sfdir"
ff_target="your_project_new_weight.sfdir"
ff_tmp="tmp.sfdir"
ff_lost="lost"


prepare_environment(ff_source, ff_target, ff_tmp, ff_lost)

# backup control glyphs
scan_lost_files_from_folder(ff_source, ff_lost)

source_unicode_set = set()
source_unicode_set, source_dict = load_files_to_set_dict(ff_source)
sorted_set=sorted(source_unicode_set)

travel_index = 1
# please modify this value, after interrupt
travel_begin_main = 0
travel_begin_main = 28000

travel_from_index = travel_begin_main + 1

travel_each_max_count = 2000

# empty for new font.
skip_list = []

# add interrupe unicode or debug list here.
skip_list = [20732,49629]

is_process_interrupe_list = False
# if interrupe occur.
is_process_interrupe_list = True

if is_process_interrupe_list:
    travel_begin_main = 0
    sorted_set= skip_list

travel_index = 0
travel_begin_index = 1

#stroke_width = 52
stroke_width = 34
stroke_cap = "round"
stroke_join = "miter"
# weight is ["light" | "bold"]
stroke_weight = "light"
#stroke_weight = "bold"
stroke_join_limit=6

is_glyph_unstroke = False
for item in sorted_set:
    travel_index += 1
    if travel_index >= travel_from_index:
        # for debug.
        #print("debug char:%s (%d) as index:%d" %(chr(item),item,travel_index))
        #break

        # copy files to
        source_path = join(ff_source,source_dict[item])
        target_path = join(ff_tmp,source_dict[item])

        if item >= 0x110000:
            print("item is not in range:", item , ", at file:", source_path)
            continue

        # force to overwrite
        shutil.copy(source_path, target_path)
        is_glyph_unstroke = True

        if travel_index % travel_each_max_count == 0:
            print("expand stroke...")
            max_expand_stroke(ff_tmp, travel_begin_index,skip_list,stroke_width,stroke_cap,stroke_join,stroke_join_limit,stroke_weight)
            move_tmp_glyph_to_target(ff_tmp,ff_target)
            
            # update for next loop begin.
            travel_begin_index = travel_index
            is_glyph_unstroke = False
            #break

if is_glyph_unstroke:
    print("expand stroke(final)...")
    max_expand_stroke(ff_tmp, travel_begin_index,skip_list,stroke_width,stroke_cap,stroke_join,stroke_join_limit,stroke_weight)
    move_tmp_glyph_to_target(ff_tmp,ff_target)

# final
move_tmp_glyph_to_target(ff_lost, ff_target)
print("Finish!")

如果是 font forge script 的說明:

Export(format[, bitmap-size])

For each selected glyph in the current font, this command will export that glyph into a file in the current directory. Format must be a string and must end with one of

  • eps – the selected glyphs will have their splines output into eps files.
  • pdf – the selected glyphs will have their splines output into pdf files.
  • svg – the selected glyphs will have their splines output into svg files.
  • fig – the selected glyphs will have their splines converted (badly) into xfig files.
  • xbm – The second argument specifies a bitmap font size, the selected glyphs in that bitmap font will be output as xbm files.
  • bmp – The second argument specifies a bitmap font size, the selected glyphs in that bitmap font will be output as bmp files.
  • png – The second argument specifies a bitmap font size, the selected glyphs in that bitmap font will be output as png files.

The format may consist entirely of the filetype (extension, see above), or it may include a full filename (with some format control within it) which has the file type as an extension:

"Glyph %n from font %f.svg"
"U+%U.bmp"

If the format constists entirely of a filetype then FontForge will use a format of "%n_%f.<filetype>"

All characters in the format string except for % are copied verbatim. If there is a % then the following character controls behavior:

  • %n – inserts the glyph name (or the first 40 characters of it for long names)
  • %f – inserts the font name (or the first 40 characters)
  • %e – inserts the glyph’s encoding as a decimal integer
  • %u – inserts the glyph’s unicode code point in lower case hex
  • %U – inserts the glyph’s unicode code point in upper case hex
  • %% – inserts a single ‘%’

If you select one of the types that generate an image (xbm, bmp, png) then you must specify a second argument contain a bitmap-size. FontForge will search the list of bitmap strikes in the font data base and use the images in the matching strike. It will NOT rasterize an image for you here. For bitmap strikes just use the pixel size, for grey scale strikes use the (depth of the grey scale<<16) + pixel_size, in most cases the depth will be 8.

If there is a third argument it must also be an integer and provides a set of flags:

  • 1 => Flip the y-axis of exported SVGs with a transform element (instead of rewriting values)
  • 256 => Use current Export dialog settings (other flags are ignored)
  • 512 => Present Export options dialog (if UI is enabled)

相關文章:

莫大毛筆字體的調整 ver 1.30
https://max-everyday.com/2020/03/bakudaifont-ver-1-30/

FontForeg正式版和開發中的版本下載:
https://fontforge.org/en-US/downloads/

Scripting FontForge
https://fontforge.org/docs/scripting/scripting-alpha.html

幫字型加字重
https://max-everyday.com/2020/02/change-weight-for-font/

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *