|
| 1 | +# Following work by @jiahao, we compute character widths using a combination of |
| 2 | +# * advance widths from GNU Unifont (advance width 512 = 1 en) |
| 3 | +# * UAX 11: East Asian Width |
| 4 | +# * a few exceptions as needed |
| 5 | +# Adapted from http://nbviewer.ipython.org/gist/jiahao/07e8b08bf6d8671e9734 |
| 6 | +# |
| 7 | +# Requires Julia (obviously) and FontForge. |
| 8 | + |
| 9 | +############################################################################# |
| 10 | +# Julia 0.3/0.4 compatibility (taken from Compat package) |
| 11 | +if VERSION < v"0.4.0-dev+1419" |
| 12 | + const UInt16 = Uint16 |
| 13 | +end |
| 14 | + |
| 15 | +############################################################################# |
| 16 | +# Widths from GNU Unifont |
| 17 | + |
| 18 | +universion="7.0.06" |
| 19 | +for fontfile in ["unifont-$universion", "unifont_upper-$universion"] |
| 20 | + isfile("$fontfile.ttf") || download("http://unifoundry.com/pub/unifont-$universion/font-builds/$fontfile.ttf", "$fontfile.ttf") |
| 21 | + isfile("$fontfile.sfd") || run(`fontforge -lang=ff -c "Open(\"$fontfile.ttf\");Save(\"$fontfile.sfd\");Quit(0);"`) |
| 22 | +end |
| 23 | + |
| 24 | +#Read sfdfile for character widths |
| 25 | +function parsesfd(filename::String, CharWidths::Dict{Int,Int}=Dict{Int,Int}()) |
| 26 | + state=:seekchar |
| 27 | + lineno = 0 |
| 28 | + for line in readlines(open(filename)) |
| 29 | + lineno += 1 |
| 30 | + if state==:seekchar #StartChar: nonmarkingreturn |
| 31 | + if contains(line, "StartChar: ") |
| 32 | + codepoint = nothing |
| 33 | + width = nothing |
| 34 | + state = :readdata |
| 35 | + end |
| 36 | + elseif state==:readdata #Encoding: 65538 -1 2, Width: 1024 |
| 37 | + contains(line, "Encoding:") && (codepoint = int(split(line)[3])) |
| 38 | + contains(line, "Width:") && (width = int(split(line)[2])) |
| 39 | + if codepoint!=nothing && width!=nothing && codepoint >= 0 |
| 40 | + CharWidths[codepoint]=width |
| 41 | + state = :seekchar |
| 42 | + end |
| 43 | + end |
| 44 | + end |
| 45 | + CharWidths |
| 46 | +end |
| 47 | +CharWidths=parsesfd("unifont-$universion.sfd") |
| 48 | +CharWidths=parsesfd("unifont_upper-$universion.sfd", CharWidths) |
| 49 | + |
| 50 | +# convert from advance width (512 units to the en) to character width |
| 51 | +for (c,v) in CharWidths |
| 52 | + CharWidths[c] = div(v, 512) |
| 53 | +end |
| 54 | + |
| 55 | +############################################################################# |
| 56 | +# Widths from UAX #11: East Asian Width |
| 57 | + |
| 58 | +isfile("EastAsianWidth.txt") || download("http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt", "EastAsianWidth.txt") |
| 59 | +for line in readlines(open("EastAsianWidth.txt")) |
| 60 | + #Strip comments |
| 61 | + line[1] == '#' && continue |
| 62 | + precomment = split(line, '#')[1] |
| 63 | + #Parse code point range and width code |
| 64 | + tokens = split(precomment, ';') |
| 65 | + length(tokens) >= 2 || continue |
| 66 | + charrange = tokens[1] |
| 67 | + width = strip(tokens[2]) |
| 68 | + #Parse code point range into Julia UnitRange |
| 69 | + rangetokens = split(charrange, "..") |
| 70 | + charstart = uint32("0x"*rangetokens[1]) |
| 71 | + charend = uint32("0x"*rangetokens[length(rangetokens)>1 ? 2 : 1]) |
| 72 | + |
| 73 | + #Assign widths |
| 74 | + for c in charstart:charend |
| 75 | + width=="N" && continue #Ignore neutral characters |
| 76 | + CharWidths[c]=(width=="W" || width=="F") ? 2 : #Wide or full |
| 77 | + (width=="Na"|| width=="H" || width=="A") ? 1 : #Narrow or half or ambiguous (default to narrow in non-East-Asian contexts, which we can assume to be the default) |
| 78 | + error("Unknown East Asian width code: $width for code point: $c") |
| 79 | + end |
| 80 | +end |
| 81 | + |
| 82 | +############################################################################# |
| 83 | +# A few exceptions to the above cases, found by manual comparison |
| 84 | +# to other wcwidth functions. |
| 85 | + |
| 86 | +# Use ../libutf8proc for category codes, rather than the one in Julia, |
| 87 | +# to minimize bootstrapping complexity when a new version of Unicode comes out. |
| 88 | +function catcode(c) |
| 89 | + uint(c) > 0x10FFFF && return 0x0000 # see utf8proc_get_property docs |
| 90 | + return unsafe_load(ccall((:utf8proc_get_property,"../libutf8proc"), Ptr{UInt16}, (Int32,), c)) |
| 91 | +end |
| 92 | + |
| 93 | + |
| 94 | +# use Base.UTF8proc module to get category codes constants, since |
| 95 | +# we aren't goint to change these in utf8proc. |
| 96 | +import Base.UTF8proc |
| 97 | + |
| 98 | +# make sure format control character (category Cf) have width 0, |
| 99 | +# except for the Arabic characters 0x06xx (see unicode std 6.2, sec. 8.2) |
| 100 | +for c in keys(CharWidths) |
| 101 | + if catcode(c)==UTF8proc.UTF8PROC_CATEGORY_CF && |
| 102 | + c ∉ [0x0601,0x0602,0x0603,0x06dd] |
| 103 | + CharWidths[c]=0 |
| 104 | + end |
| 105 | +end |
| 106 | + |
| 107 | +#By definition, should have zero width (on the same line) |
| 108 | +#0x002028 '
' category: Zl name: LINE SEPARATOR/ |
| 109 | +#0x002029 '
' category: Zp name: PARAGRAPH SEPARATOR/ |
| 110 | +CharWidths[0x2028]=0 |
| 111 | +CharWidths[0x2029]=0 |
| 112 | + |
| 113 | +#By definition, should be narrow = width of 1 en space |
| 114 | +#0x00202f ' ' category: Zs name: NARROW NO-BREAK SPACE/ |
| 115 | +CharWidths[0x202f]=1 |
| 116 | + |
| 117 | +#By definition, should be wide = width of 1 em space |
| 118 | +#0x002001 ' ' category: Zs name: EM QUAD/ |
| 119 | +#0x002003 ' ' category: Zs name: EM SPACE/ |
| 120 | +CharWidths[0x2001]=2 |
| 121 | +CharWidths[0x2003]=2 |
| 122 | + |
| 123 | +############################################################################# |
| 124 | +# Non-printable control characters will be assigned a width of zero |
| 125 | +# (wcwidth returns -1 for such characters) |
| 126 | + |
| 127 | +isprintable(c::Union(Char,Integer)) = c <= 0x10ffff && is_valid_char(c) && isprintable_category(catcode(c)) |
| 128 | +isprintable_category(category) = |
| 129 | + !( category==UTF8proc.UTF8PROC_CATEGORY_CN # Unassigned |
| 130 | + || category==UTF8proc.UTF8PROC_CATEGORY_CS # Surrogate |
| 131 | + || category==UTF8proc.UTF8PROC_CATEGORY_CC # Control |
| 132 | + || category==0 # Invalid |
| 133 | + ) |
| 134 | + |
| 135 | +# Question: should we just use Julia's isprint algorithm here? It is different, |
| 136 | +# though it is also based on the character category. |
| 137 | + |
| 138 | +############################################################################# |
| 139 | +# Output (to a file or pipe) for processing by data_generator.rb |
| 140 | +# ... don't bother to output zero widths since that will be the default. |
| 141 | + |
| 142 | +firstc = 0x000000 |
| 143 | +lastv = 0 |
| 144 | +uhex(c) = uppercase(hex(c,4)) |
| 145 | +for c in 0x0000:0x110000 |
| 146 | + v = isprintable(c) ? get(CharWidths, c, 0) : 0 |
| 147 | + if v != lastv || c == 0x110000 |
| 148 | + v < 4 || error("invalid charwidth $v for $c") |
| 149 | + if firstc+1 < c |
| 150 | + println(uhex(firstc), "..", uhex(c-1), "; ", lastv) |
| 151 | + else |
| 152 | + println(uhex(firstc), "; ", lastv) |
| 153 | + end |
| 154 | + firstc = c |
| 155 | + lastv = v |
| 156 | + end |
| 157 | +end |
0 commit comments