ttfunk 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +2 -0
- data/README.rdoc +39 -0
- data/data/fonts/DejaVuSans.ttf +0 -0
- data/data/fonts/comicsans.ttf +0 -0
- data/examples/metrics.rb +46 -0
- data/lib/ttfunk/directory.rb +17 -0
- data/lib/ttfunk/encoding/mac_roman.rb +88 -0
- data/lib/ttfunk/encoding/windows_1252.rb +69 -0
- data/lib/ttfunk/reader.rb +44 -0
- data/lib/ttfunk/resource_file.rb +78 -0
- data/lib/ttfunk/subset/base.rb +141 -0
- data/lib/ttfunk/subset/mac_roman.rb +50 -0
- data/lib/ttfunk/subset/unicode.rb +48 -0
- data/lib/ttfunk/subset/unicode_8bit.rb +63 -0
- data/lib/ttfunk/subset/windows_1252.rb +55 -0
- data/lib/ttfunk/subset.rb +18 -0
- data/lib/ttfunk/subset_collection.rb +72 -0
- data/lib/ttfunk/table/cmap/format00.rb +54 -0
- data/lib/ttfunk/table/cmap/format04.rb +126 -0
- data/lib/ttfunk/table/cmap/subtable.rb +79 -0
- data/lib/ttfunk/table/cmap.rb +34 -0
- data/lib/ttfunk/table/glyf/compound.rb +81 -0
- data/lib/ttfunk/table/glyf/simple.rb +37 -0
- data/lib/ttfunk/table/glyf.rb +64 -0
- data/lib/ttfunk/table/head.rb +44 -0
- data/lib/ttfunk/table/hhea.rb +41 -0
- data/lib/ttfunk/table/hmtx.rb +47 -0
- data/lib/ttfunk/table/kern/format0.rb +62 -0
- data/lib/ttfunk/table/kern.rb +79 -0
- data/lib/ttfunk/table/loca.rb +43 -0
- data/lib/ttfunk/table/maxp.rb +40 -0
- data/lib/ttfunk/table/name.rb +125 -0
- data/lib/ttfunk/table/os2.rb +78 -0
- data/lib/ttfunk/table/post/format10.rb +43 -0
- data/lib/ttfunk/table/post/format20.rb +35 -0
- data/lib/ttfunk/table/post/format25.rb +23 -0
- data/lib/ttfunk/table/post/format30.rb +17 -0
- data/lib/ttfunk/table/post/format40.rb +17 -0
- data/lib/ttfunk/table/post.rb +91 -0
- data/lib/ttfunk/table/simple.rb +14 -0
- data/lib/ttfunk/table.rb +46 -0
- data/lib/ttfunk.rb +102 -0
- metadata +121 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'ttfunk/subset/base'
|
3
|
+
|
4
|
+
module TTFunk
|
5
|
+
module Subset
|
6
|
+
class Unicode8Bit < Base
|
7
|
+
def initialize(original)
|
8
|
+
super
|
9
|
+
@subset = { 0x20 => 0x20 }
|
10
|
+
@unicodes = { 0x20 => 0x20 }
|
11
|
+
@next = 0x21 # apparently, PDF's don't like to use chars between 0-31
|
12
|
+
end
|
13
|
+
|
14
|
+
def unicode?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_unicode_map
|
19
|
+
@subset.dup
|
20
|
+
end
|
21
|
+
|
22
|
+
def use(character)
|
23
|
+
if !@unicodes.key?(character)
|
24
|
+
@subset[@next] = character
|
25
|
+
@unicodes[character] = @next
|
26
|
+
@next += 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def covers?(character)
|
31
|
+
@unicodes.key?(character) || @next < 256
|
32
|
+
end
|
33
|
+
|
34
|
+
def includes?(character)
|
35
|
+
@unicodes.key?(character)
|
36
|
+
end
|
37
|
+
|
38
|
+
def from_unicode(character)
|
39
|
+
@unicodes[character]
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def new_cmap_table(options)
|
45
|
+
mapping = @subset.inject({}) do |map, (code,unicode)|
|
46
|
+
map[code] = unicode_cmap[unicode]
|
47
|
+
map
|
48
|
+
end
|
49
|
+
|
50
|
+
# since we're mapping a subset of the unicode glyphs into an
|
51
|
+
# arbitrary 256-character space, the actual encoding we're
|
52
|
+
# using is irrelevant. We choose MacRoman because it's a 256-character
|
53
|
+
# encoding that happens to be well-supported in both TTF and
|
54
|
+
# PDF formats.
|
55
|
+
TTFunk::Table::Cmap.encode(mapping, :mac_roman)
|
56
|
+
end
|
57
|
+
|
58
|
+
def original_glyph_ids
|
59
|
+
([0] + @unicodes.keys.map { |unicode| unicode_cmap[unicode] }).uniq.sort
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'ttfunk/subset/base'
|
3
|
+
require 'ttfunk/encoding/windows_1252'
|
4
|
+
|
5
|
+
module TTFunk
|
6
|
+
module Subset
|
7
|
+
class Windows1252 < Base
|
8
|
+
def initialize(original)
|
9
|
+
super
|
10
|
+
@subset = Array.new(256)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_unicode_map
|
14
|
+
Encoding::Windows1252::TO_UNICODE
|
15
|
+
end
|
16
|
+
|
17
|
+
def use(character)
|
18
|
+
@subset[Encoding::Windows1252::FROM_UNICODE[character]] = character
|
19
|
+
end
|
20
|
+
|
21
|
+
def covers?(character)
|
22
|
+
Encoding::Windows1252.covers?(character)
|
23
|
+
end
|
24
|
+
|
25
|
+
def includes?(character)
|
26
|
+
code = Encoding::Windows1252::FROM_UNICODE[character]
|
27
|
+
code && @subset[code]
|
28
|
+
end
|
29
|
+
|
30
|
+
def from_unicode(character)
|
31
|
+
Encoding::Windows1252::FROM_UNICODE[character]
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def new_cmap_table(options)
|
37
|
+
mapping = {}
|
38
|
+
@subset.each_with_index do |unicode, cp1252|
|
39
|
+
mapping[cp1252] = unicode_cmap[unicode] if cp1252
|
40
|
+
end
|
41
|
+
|
42
|
+
# yes, I really mean "mac roman". TTF has no cp1252 encoding, and the
|
43
|
+
# alternative would be to encode it using a format 4 unicode table, which
|
44
|
+
# is overkill. for our purposes, mac-roman suffices. (If we were building
|
45
|
+
# a _real_ font, instead of a PDF-embeddable subset, things would probably
|
46
|
+
# be different.)
|
47
|
+
TTFunk::Table::Cmap.encode(mapping, :mac_roman)
|
48
|
+
end
|
49
|
+
|
50
|
+
def original_glyph_ids
|
51
|
+
([0] + @subset.map { |unicode| unicode && unicode_cmap[unicode] }).compact.uniq.sort
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'ttfunk/subset/unicode'
|
2
|
+
require 'ttfunk/subset/unicode_8bit'
|
3
|
+
require 'ttfunk/subset/mac_roman'
|
4
|
+
require 'ttfunk/subset/windows_1252'
|
5
|
+
|
6
|
+
module TTFunk
|
7
|
+
module Subset
|
8
|
+
def self.for(original, encoding)
|
9
|
+
case encoding.to_sym
|
10
|
+
when :unicode then Unicode.new(original)
|
11
|
+
when :unicode_8bit then Unicode8Bit.new(original)
|
12
|
+
when :mac_roman then MacRoman.new(original)
|
13
|
+
when :windows_1252 then Windows1252.new(original)
|
14
|
+
else raise NotImplementedError, "encoding #{encoding} is not supported"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'ttfunk/subset'
|
2
|
+
|
3
|
+
module TTFunk
|
4
|
+
class SubsetCollection
|
5
|
+
def initialize(original)
|
6
|
+
@original = original
|
7
|
+
@subsets = [Subset.for(@original, :mac_roman)]
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](subset)
|
11
|
+
@subsets[subset]
|
12
|
+
end
|
13
|
+
|
14
|
+
# +characters+ should be an array of UTF-16 characters
|
15
|
+
def use(characters)
|
16
|
+
characters.each do |char|
|
17
|
+
covered = false
|
18
|
+
@subsets.each_with_index do |subset, i|
|
19
|
+
if subset.covers?(char)
|
20
|
+
subset.use(char)
|
21
|
+
covered = true
|
22
|
+
break
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if !covered
|
27
|
+
@subsets << Subset.for(@original, :unicode_8bit)
|
28
|
+
@subsets.last.use(char)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# +characters+ should be an array of UTF-16 characters. Returns
|
34
|
+
# an array of subset chunks, where each chunk is another array of
|
35
|
+
# two elements. The first element is the subset number, and the
|
36
|
+
# second element is the string of characters to render with that
|
37
|
+
# font subset. The strings will be encoded for their subset font,
|
38
|
+
# and so may not look (in the raw) like what was passed in, but
|
39
|
+
# they will render correctly with the indicated subset font.
|
40
|
+
def encode(characters)
|
41
|
+
return [] if characters.empty?
|
42
|
+
|
43
|
+
# TODO: probably would be more optimal to nix the #use method,
|
44
|
+
# and merge it into this one, so it can be done in a single
|
45
|
+
# pass instead of two passes.
|
46
|
+
use(characters)
|
47
|
+
|
48
|
+
parts = []
|
49
|
+
current_subset = 0
|
50
|
+
current_char = 0
|
51
|
+
char = characters[current_char]
|
52
|
+
|
53
|
+
loop do
|
54
|
+
while @subsets[current_subset].includes?(char)
|
55
|
+
char = @subsets[current_subset].from_unicode(char)
|
56
|
+
|
57
|
+
if parts.empty? || parts.last[0] != current_subset
|
58
|
+
parts << [current_subset, char.chr]
|
59
|
+
else
|
60
|
+
parts.last[1] << char
|
61
|
+
end
|
62
|
+
|
63
|
+
current_char += 1
|
64
|
+
return parts if current_char >= characters.length
|
65
|
+
char = characters[current_char]
|
66
|
+
end
|
67
|
+
|
68
|
+
current_subset = (current_subset + 1) % @subsets.length
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'ttfunk/encoding/mac_roman'
|
2
|
+
require 'ttfunk/encoding/windows_1252'
|
3
|
+
|
4
|
+
module TTFunk
|
5
|
+
class Table
|
6
|
+
class Cmap
|
7
|
+
|
8
|
+
module Format00
|
9
|
+
attr_reader :language
|
10
|
+
attr_reader :code_map
|
11
|
+
|
12
|
+
# Expects a hash mapping character codes to glyph ids (where the
|
13
|
+
# glyph ids are from the original font). Returns a hash including
|
14
|
+
# a new map (:charmap) that maps the characters in charmap to a
|
15
|
+
# another hash containing both the old (:old) and new (:new) glyph
|
16
|
+
# ids. The returned hash also includes a :subtable key, which contains
|
17
|
+
# the encoded subtable for the given charmap.
|
18
|
+
def self.encode(charmap)
|
19
|
+
next_id = 0
|
20
|
+
glyph_indexes = Array.new(256, 0)
|
21
|
+
glyph_map = { 0 => 0 }
|
22
|
+
|
23
|
+
new_map = charmap.keys.sort.inject({}) do |map, code|
|
24
|
+
glyph_map[charmap[code]] ||= next_id += 1
|
25
|
+
map[code] = { :old => charmap[code], :new => glyph_map[charmap[code]] }
|
26
|
+
glyph_indexes[code] = glyph_map[charmap[code]]
|
27
|
+
map
|
28
|
+
end
|
29
|
+
|
30
|
+
# format, length, language, indices
|
31
|
+
subtable = [0, 262, 0, *glyph_indexes].pack("nnnC*")
|
32
|
+
|
33
|
+
{ :charmap => new_map, :subtable => subtable, :max_glyph_id => next_id+1 }
|
34
|
+
end
|
35
|
+
|
36
|
+
def [](code)
|
37
|
+
@code_map[code] || 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def supported?
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def parse_cmap!
|
47
|
+
length, @language = read(4, "nn")
|
48
|
+
@code_map = read(256, "C*")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module TTFunk
|
2
|
+
class Table
|
3
|
+
class Cmap
|
4
|
+
|
5
|
+
module Format04
|
6
|
+
attr_reader :language
|
7
|
+
attr_reader :code_map
|
8
|
+
|
9
|
+
# Expects a hash mapping character codes to glyph ids (where the
|
10
|
+
# glyph ids are from the original font). Returns a hash including
|
11
|
+
# a new map (:charmap) that maps the characters in charmap to a
|
12
|
+
# another hash containing both the old (:old) and new (:new) glyph
|
13
|
+
# ids. The returned hash also includes a :subtable key, which contains
|
14
|
+
# the encoded subtable for the given charmap.
|
15
|
+
def self.encode(charmap)
|
16
|
+
end_codes = []
|
17
|
+
start_codes = []
|
18
|
+
next_id = 0
|
19
|
+
last = difference = nil
|
20
|
+
|
21
|
+
glyph_map = { 0 => 0 }
|
22
|
+
new_map = charmap.keys.sort.inject({}) do |map, code|
|
23
|
+
old = charmap[code]
|
24
|
+
glyph_map[old] ||= next_id += 1
|
25
|
+
map[code] = { :old => old, :new => glyph_map[old] }
|
26
|
+
|
27
|
+
delta = glyph_map[old] - code
|
28
|
+
if last.nil? || delta != difference
|
29
|
+
end_codes << last if last
|
30
|
+
start_codes << code
|
31
|
+
difference = delta
|
32
|
+
end
|
33
|
+
last = code
|
34
|
+
|
35
|
+
map
|
36
|
+
end
|
37
|
+
|
38
|
+
end_codes << last if last
|
39
|
+
end_codes << 0xFFFF
|
40
|
+
start_codes << 0xFFFF
|
41
|
+
segcount = start_codes.length
|
42
|
+
|
43
|
+
# build the conversion tables
|
44
|
+
deltas = []
|
45
|
+
range_offsets = []
|
46
|
+
glyph_indices = []
|
47
|
+
|
48
|
+
offset = 0
|
49
|
+
start_codes.zip(end_codes).each_with_index do |(a, b), segment|
|
50
|
+
if a == 0xFFFF
|
51
|
+
deltas << 0
|
52
|
+
range_offsets << 0
|
53
|
+
break
|
54
|
+
end
|
55
|
+
|
56
|
+
start_glyph_id = new_map[a][:new]
|
57
|
+
if a - start_glyph_id >= 0x8000
|
58
|
+
deltas << 0
|
59
|
+
range_offsets << 2 * (glyph_indices.length + segcount - segment)
|
60
|
+
a.upto(b) { |code| glyph_indices << new_map[code][:new] }
|
61
|
+
else
|
62
|
+
deltas << -a + start_glyph_id
|
63
|
+
range_offsets << 0
|
64
|
+
end
|
65
|
+
offset += 2
|
66
|
+
end
|
67
|
+
|
68
|
+
# format, length, language
|
69
|
+
subtable = [4, 16 + 8 * segcount + 2 * glyph_indices.length, 0].pack("nnn")
|
70
|
+
|
71
|
+
search_range = 2 * 2 ** (Math.log(segcount) / Math.log(2)).to_i
|
72
|
+
entry_selector = (Math.log(search_range / 2) / Math.log(2)).to_i
|
73
|
+
range_shift = (2 * segcount) - search_range
|
74
|
+
subtable << [segcount * 2, search_range, entry_selector, range_shift].pack("nnnn")
|
75
|
+
|
76
|
+
subtable << end_codes.pack("n*") << "\0\0" << start_codes.pack("n*")
|
77
|
+
subtable << deltas.pack("n*") << range_offsets.pack("n*") << glyph_indices.pack("n*")
|
78
|
+
|
79
|
+
{ :charmap => new_map, :subtable => subtable, :max_glyph_id => next_id+1 }
|
80
|
+
end
|
81
|
+
|
82
|
+
def [](code)
|
83
|
+
@code_map[code] || 0
|
84
|
+
end
|
85
|
+
|
86
|
+
def supported?
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def parse_cmap!
|
93
|
+
length, @language, segcount_x2 = read(6, "nnn")
|
94
|
+
segcount = segcount_x2 / 2
|
95
|
+
|
96
|
+
io.read(6) # skip searching hints
|
97
|
+
|
98
|
+
end_code = read(segcount_x2, "n*")
|
99
|
+
io.read(2) # skip reserved value
|
100
|
+
start_code = read(segcount_x2, "n*")
|
101
|
+
id_delta = read_signed(segcount)
|
102
|
+
id_range_offset = read(segcount_x2, "n*")
|
103
|
+
|
104
|
+
glyph_ids = read(length - io.pos + @offset, "n*")
|
105
|
+
|
106
|
+
@code_map = {}
|
107
|
+
|
108
|
+
end_code.each_with_index do |tail, i|
|
109
|
+
start_code[i].upto(tail) do |code|
|
110
|
+
if id_range_offset[i].zero?
|
111
|
+
glyph_id = code + id_delta[i]
|
112
|
+
else
|
113
|
+
index = id_range_offset[i] / 2 + (code - start_code[i]) - (segcount - i)
|
114
|
+
glyph_id = glyph_ids[index] || 0 # because some TTF fonts are broken
|
115
|
+
glyph_id += id_delta[i] if glyph_id != 0
|
116
|
+
end
|
117
|
+
|
118
|
+
@code_map[code] = glyph_id & 0xFFFF
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'ttfunk/reader'
|
2
|
+
|
3
|
+
module TTFunk
|
4
|
+
class Table
|
5
|
+
class Cmap
|
6
|
+
class Subtable
|
7
|
+
include Reader
|
8
|
+
|
9
|
+
attr_reader :platform_id
|
10
|
+
attr_reader :encoding_id
|
11
|
+
attr_reader :format
|
12
|
+
|
13
|
+
ENCODING_MAPPINGS = {
|
14
|
+
:mac_roman => { :platform_id => 1, :encoding_id => 0 },
|
15
|
+
# use microsoft unicode, instead of generic unicode, for optimal windows support
|
16
|
+
:unicode => { :platform_id => 3, :encoding_id => 1 }
|
17
|
+
}
|
18
|
+
|
19
|
+
def self.encode(charmap, encoding)
|
20
|
+
case encoding
|
21
|
+
when :mac_roman
|
22
|
+
result = Format00.encode(charmap)
|
23
|
+
when :unicode
|
24
|
+
result = Format04.encode(charmap)
|
25
|
+
else
|
26
|
+
raise NotImplementedError, "encoding #{encoding.inspect} is not supported"
|
27
|
+
end
|
28
|
+
|
29
|
+
mapping = ENCODING_MAPPINGS[encoding]
|
30
|
+
|
31
|
+
# platform-id, encoding-id, offset
|
32
|
+
result[:subtable] = [mapping[:platform_id], mapping[:encoding_id],
|
33
|
+
12, result[:subtable]].pack("nnNA*")
|
34
|
+
|
35
|
+
return result
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(file, table_start)
|
39
|
+
@file = file
|
40
|
+
@platform_id, @encoding_id, @offset = read(8, "nnN")
|
41
|
+
@offset += table_start
|
42
|
+
|
43
|
+
parse_from(@offset) do
|
44
|
+
@format = read(2, "n").first
|
45
|
+
|
46
|
+
case @format
|
47
|
+
when 0 then extend(TTFunk::Table::Cmap::Format00)
|
48
|
+
when 4 then extend(TTFunk::Table::Cmap::Format04)
|
49
|
+
end
|
50
|
+
|
51
|
+
parse_cmap!
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def unicode?
|
56
|
+
platform_id == 3 && encoding_id == 1 && format == 4 ||
|
57
|
+
platform_id == 0 && format == 4
|
58
|
+
end
|
59
|
+
|
60
|
+
def supported?
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
def [](code)
|
65
|
+
raise NotImplementedError, "cmap format #{@format} is not supported"
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def parse_cmap!
|
71
|
+
# do nothing...
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
require 'ttfunk/table/cmap/format00'
|
79
|
+
require 'ttfunk/table/cmap/format04'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module TTFunk
|
2
|
+
class Table
|
3
|
+
class Cmap < Table
|
4
|
+
attr_reader :version
|
5
|
+
attr_reader :tables
|
6
|
+
|
7
|
+
def self.encode(charmap, encoding)
|
8
|
+
result = Cmap::Subtable.encode(charmap, encoding)
|
9
|
+
|
10
|
+
# pack 'version' and 'table-count'
|
11
|
+
result[:table] = [0, 1, result.delete(:subtable)].pack("nnA*")
|
12
|
+
return result
|
13
|
+
end
|
14
|
+
|
15
|
+
def unicode
|
16
|
+
@unicode ||= @tables.select { |table| table.unicode? }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def parse!
|
22
|
+
@version, table_count = read(4, "nn")
|
23
|
+
@tables = []
|
24
|
+
|
25
|
+
table_count.times do
|
26
|
+
@tables << Cmap::Subtable.new(file, offset)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'ttfunk/table/cmap/subtable'
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'ttfunk/reader'
|
2
|
+
|
3
|
+
module TTFunk
|
4
|
+
class Table
|
5
|
+
class Glyf
|
6
|
+
class Compound
|
7
|
+
include Reader
|
8
|
+
|
9
|
+
ARG_1_AND_2_ARE_WORDS = 0x0001
|
10
|
+
WE_HAVE_A_SCALE = 0x0008
|
11
|
+
MORE_COMPONENTS = 0x0020
|
12
|
+
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
|
13
|
+
WE_HAVE_A_TWO_BY_TWO = 0x0080
|
14
|
+
WE_HAVE_INSTRUCTIONS = 0x0100
|
15
|
+
|
16
|
+
attr_reader :raw
|
17
|
+
attr_reader :x_min, :y_min, :x_max, :y_max
|
18
|
+
attr_reader :glyph_ids
|
19
|
+
|
20
|
+
Component = Struct.new(:flags, :glyph_index, :arg1, :arg2, :transform)
|
21
|
+
|
22
|
+
def initialize(raw, x_min, y_min, x_max, y_max)
|
23
|
+
@raw = raw
|
24
|
+
@x_min, @y_min, @x_max, @y_max = x_min, y_min, x_max, y_max
|
25
|
+
|
26
|
+
# Because TTFunk only cares about glyphs insofar as they (1) provide
|
27
|
+
# a bounding box for each glyph, and (2) can be rewritten into a
|
28
|
+
# font subset, we don't really care about the rest of the glyph data
|
29
|
+
# except as a whole. Thus, we don't actually decompose the glyph
|
30
|
+
# into it's parts--all we really care about are the locations within
|
31
|
+
# the raw string where the component glyph ids are stored, so that
|
32
|
+
# when we rewrite this glyph into a subset we can rewrite the
|
33
|
+
# component glyph-ids so they are correct for the subset.
|
34
|
+
|
35
|
+
@glyph_ids = []
|
36
|
+
@glyph_id_offsets = []
|
37
|
+
offset = 10 # 2 bytes for each of num-contours, min x/y, max x/y
|
38
|
+
|
39
|
+
loop do
|
40
|
+
flags, glyph_id = @raw[offset, 4].unpack("n*")
|
41
|
+
@glyph_ids << glyph_id
|
42
|
+
@glyph_id_offsets << offset + 2
|
43
|
+
|
44
|
+
break unless flags & MORE_COMPONENTS != 0
|
45
|
+
offset += 4
|
46
|
+
|
47
|
+
if flags & ARG_1_AND_2_ARE_WORDS != 0
|
48
|
+
offset += 4
|
49
|
+
else
|
50
|
+
offset += 2
|
51
|
+
end
|
52
|
+
|
53
|
+
if flags & WE_HAVE_A_TWO_BY_TWO != 0
|
54
|
+
offset += 8
|
55
|
+
elsif flags & WE_HAVE_AN_X_AND_Y_SCALE != 0
|
56
|
+
offset += 4
|
57
|
+
elsif flags & WE_HAVE_A_SCALE != 0
|
58
|
+
offset += 2
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def compound?
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
def recode(mapping)
|
68
|
+
result = @raw.dup
|
69
|
+
new_ids = glyph_ids.map { |id| mapping[id] }
|
70
|
+
|
71
|
+
new_ids.zip(@glyph_id_offsets).each do |new_id, offset|
|
72
|
+
result[offset, 2] = [new_id].pack("n")
|
73
|
+
end
|
74
|
+
|
75
|
+
return result
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'ttfunk/reader'
|
2
|
+
|
3
|
+
module TTFunk
|
4
|
+
class Table
|
5
|
+
class Glyf
|
6
|
+
class Simple
|
7
|
+
attr_reader :raw
|
8
|
+
attr_reader :number_of_contours
|
9
|
+
attr_reader :x_min, :y_min, :x_max, :y_max
|
10
|
+
|
11
|
+
def initialize(raw, number_of_contours, x_min, y_min, x_max, y_max)
|
12
|
+
@raw = raw
|
13
|
+
@number_of_contours = number_of_contours
|
14
|
+
@x_min, @y_min = x_min, y_min
|
15
|
+
@x_max, @y_max = x_max, y_max
|
16
|
+
|
17
|
+
# Because TTFunk is, at this time, a library for simply pulling
|
18
|
+
# metrics out of font files, or for writing font subsets, we don't
|
19
|
+
# really care what the contours are for simple glyphs. We just
|
20
|
+
# care that we've got an entire glyph's definition. Also, a
|
21
|
+
# bounding box could be nice to know. Since we've got all that
|
22
|
+
# at this point, we don't need to worry about parsing the full
|
23
|
+
# contents of the glyph.
|
24
|
+
end
|
25
|
+
|
26
|
+
def compound?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def recode(mapping)
|
31
|
+
raw
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'ttfunk/table'
|
2
|
+
|
3
|
+
module TTFunk
|
4
|
+
class Table
|
5
|
+
class Glyf < Table
|
6
|
+
# Accepts a hash mapping (old) glyph-ids to glyph objects, and a hash
|
7
|
+
# mapping old glyph-ids to new glyph-ids.
|
8
|
+
#
|
9
|
+
# Returns a hash containing:
|
10
|
+
#
|
11
|
+
# * :table - a string representing the encoded 'glyf' table containing
|
12
|
+
# the given glyphs.
|
13
|
+
# * :offsets - an array of offsets for each glyph
|
14
|
+
def self.encode(glyphs, new2old, old2new)
|
15
|
+
result = { :table => "", :offsets => [] }
|
16
|
+
|
17
|
+
new2old.keys.sort.each do |new_id|
|
18
|
+
glyph = glyphs[new2old[new_id]]
|
19
|
+
result[:offsets] << result[:table].length
|
20
|
+
result[:table] << glyph.recode(old2new) if glyph
|
21
|
+
end
|
22
|
+
|
23
|
+
# include an offset at the end of the table, for use in computing the
|
24
|
+
# size of the last glyph
|
25
|
+
result[:offsets] << result[:table].length
|
26
|
+
return result
|
27
|
+
end
|
28
|
+
|
29
|
+
def for(glyph_id)
|
30
|
+
return @cache[glyph_id] if @cache.key?(glyph_id)
|
31
|
+
|
32
|
+
index = file.glyph_locations.index_of(glyph_id)
|
33
|
+
size = file.glyph_locations.size_of(glyph_id)
|
34
|
+
|
35
|
+
if size.zero? # blank glyph, e.g. space character
|
36
|
+
@cache[glyph_id] = nil
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
|
40
|
+
parse_from(offset + index) do
|
41
|
+
raw = io.read(size)
|
42
|
+
number_of_contours, x_min, y_min, x_max, y_max = raw.unpack("n5").map { |i| to_signed(i) }
|
43
|
+
|
44
|
+
@cache[glyph_id] = if number_of_contours == -1
|
45
|
+
Compound.new(raw, x_min, y_min, x_max, y_max)
|
46
|
+
else
|
47
|
+
Simple.new(raw, number_of_contours, x_min, y_min, x_max, y_max)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def parse!
|
55
|
+
# because the glyf table is rather complex to parse, we defer
|
56
|
+
# the parse until we need a specific glyf, and then cache it.
|
57
|
+
@cache = {}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
require 'ttfunk/table/glyf/compound'
|
64
|
+
require 'ttfunk/table/glyf/simple'
|