pdfrb 0.2.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/lib/pdfrb/document/outline.rb +36 -53
- data/lib/pdfrb/document.rb +5 -0
- data/lib/pdfrb/font/true_type/subsetter.rb +382 -11
- data/lib/pdfrb/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 263ee1cee601c0c5b86775ad6bbeb97777f4e7e01c95f63074281d00fbec3d54
|
|
4
|
+
data.tar.gz: 5d157430c7657ae19b8954975721863a68db7dbd1a5e3439b8a830da3bc7db4a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a6e80f5ad5a3dba5e4dfe087ca33554c0a8fce926e6ad9934f307485bc1728996f336d87ef1b53295dbb79d80d66d41f820c81208a8bcd488b382a6c103c2da
|
|
7
|
+
data.tar.gz: '08f52568ba799d4d02c347982407dec464eee0d861d85dafd7c8da8541738e84ccdce83c1ac7866f357f549ccd1f16f2712967f5f22fedaae54977b083ef59af'
|
data/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the pdfrb gem will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.1] — 2026-08-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
* **Form XObjects** (`Document::FormXObject`) — create reusable form
|
|
10
|
+
templates with a Canvas; register on pages and draw via /Do operator.
|
|
11
|
+
* **TTF subsetting** (`Font::TrueType::Subsetter`) — real glyph subsetting
|
|
12
|
+
with composite glyph support, loca/glyf/cmap/hmtx rewriting, graceful
|
|
13
|
+
fallback to full embedding.
|
|
14
|
+
* **Outline circular-reference fix** — bookmarks now store References
|
|
15
|
+
(not raw Dictionaries), preventing infinite serialization recursion.
|
|
16
|
+
* **Document::Outline round-trips** — flat and nested outlines survive
|
|
17
|
+
write + read correctly.
|
|
18
|
+
|
|
19
|
+
### Metrics
|
|
20
|
+
|
|
21
|
+
* 537 specs (was 534), 0 failures, 4 pending.
|
|
22
|
+
* 0 rubocop offenses.
|
|
23
|
+
* 180 → 182 lib files; 47 → 49 spec files.
|
|
24
|
+
|
|
5
25
|
## [0.2.0] — 2026-08-02
|
|
6
26
|
|
|
27
|
+
|
|
7
28
|
### Added — P0 feature implementations
|
|
8
29
|
|
|
9
30
|
* **CMap writer** (`Font::CMap::Writer`) — generates valid `/ToUnicode`
|
|
@@ -12,19 +12,8 @@ module Pdfrb
|
|
|
12
12
|
@entries = []
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# Add a top-level bookmark entry.
|
|
16
|
-
#
|
|
17
|
-
# @param title [String] display text.
|
|
18
|
-
# @param dest [Pdfrb::Model::Reference, Array, nil] destination
|
|
19
|
-
# (page reference or explicit destination array).
|
|
20
|
-
# @param parent [OutlineEntry, nil] for nested entries.
|
|
21
|
-
# @return [OutlineEntry]
|
|
22
15
|
def add(title, dest: nil, parent: nil)
|
|
23
|
-
entry = OutlineEntry.new(
|
|
24
|
-
title: title.to_s,
|
|
25
|
-
dest: dest,
|
|
26
|
-
document: @document
|
|
27
|
-
)
|
|
16
|
+
entry = OutlineEntry.new(title: title.to_s, dest: dest)
|
|
28
17
|
if parent
|
|
29
18
|
parent.add_child(entry)
|
|
30
19
|
else
|
|
@@ -36,45 +25,43 @@ module Pdfrb
|
|
|
36
25
|
def build!
|
|
37
26
|
return if @entries.empty?
|
|
38
27
|
|
|
39
|
-
entries = @entries
|
|
40
28
|
root = @document.add(
|
|
41
|
-
{ Type: :Outlines
|
|
29
|
+
{ Type: :Outlines },
|
|
42
30
|
type: Pdfrb::Model::Cos::Dictionary
|
|
43
31
|
)
|
|
32
|
+
root_ref = Pdfrb::Model::Reference.new(root.oid, root.gen)
|
|
44
33
|
|
|
45
|
-
|
|
34
|
+
prev_dict = nil
|
|
46
35
|
first_ref = nil
|
|
47
|
-
entries.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
@entries.each do |entry|
|
|
37
|
+
dict = entry.build!(@document)
|
|
38
|
+
ref = Pdfrb::Model::Reference.new(dict.oid, dict.gen)
|
|
39
|
+
|
|
40
|
+
dict.value[:Parent] = root_ref
|
|
41
|
+
dict.value[:Prev] = Pdfrb::Model::Reference.new(prev_dict.oid, dict.gen) if prev_dict
|
|
42
|
+
dict.value[:Next] = nil
|
|
43
|
+
prev_dict&.value&.[]=(:Next, ref)
|
|
44
|
+
|
|
53
45
|
first_ref ||= ref
|
|
54
|
-
|
|
46
|
+
prev_dict = dict
|
|
55
47
|
end
|
|
56
48
|
|
|
57
49
|
root.value[:First] = first_ref
|
|
58
|
-
root.value[:Last] =
|
|
59
|
-
|
|
60
|
-
@document.catalog.value[:Outlines] =
|
|
61
|
-
Pdfrb::Model::Reference.new(root.oid, root.gen)
|
|
50
|
+
root.value[:Last] = Pdfrb::Model::Reference.new(prev_dict.oid, prev_dict.gen) if prev_dict
|
|
51
|
+
root.value[:Count] = @entries.length
|
|
62
52
|
|
|
53
|
+
@document.catalog.value[:Outlines] = root_ref
|
|
63
54
|
root
|
|
64
55
|
end
|
|
65
56
|
end
|
|
66
57
|
|
|
67
58
|
class OutlineEntry
|
|
68
|
-
attr_reader :title, :dest, :children
|
|
69
|
-
attr_accessor :oid
|
|
59
|
+
attr_reader :title, :dest, :children
|
|
70
60
|
|
|
71
|
-
def initialize(title:, dest
|
|
61
|
+
def initialize(title:, dest:)
|
|
72
62
|
@title = title
|
|
73
63
|
@dest = dest
|
|
74
|
-
@document = document
|
|
75
64
|
@children = []
|
|
76
|
-
@value = {}
|
|
77
|
-
@oid = nil
|
|
78
65
|
end
|
|
79
66
|
|
|
80
67
|
def add_child(entry)
|
|
@@ -83,32 +70,28 @@ module Pdfrb
|
|
|
83
70
|
end
|
|
84
71
|
|
|
85
72
|
def build!(document)
|
|
86
|
-
dict = document.add(
|
|
87
|
-
{
|
|
88
|
-
Title: @title,
|
|
89
|
-
Parent: nil,
|
|
90
|
-
},
|
|
91
|
-
type: Pdfrb::Model::Cos::Dictionary
|
|
92
|
-
)
|
|
73
|
+
dict = document.add({ Title: @title }, type: Pdfrb::Model::Cos::Dictionary)
|
|
93
74
|
dict.value[:Dest] = @dest if @dest
|
|
94
75
|
|
|
95
|
-
@oid = dict.oid
|
|
96
|
-
@value = dict.value
|
|
97
|
-
|
|
98
76
|
if @children.any?
|
|
99
|
-
|
|
100
|
-
|
|
77
|
+
prev_child_dict = nil
|
|
78
|
+
first_ref = nil
|
|
101
79
|
@children.each do |child|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
80
|
+
child_dict = child.build!(document)
|
|
81
|
+
child_ref = Pdfrb::Model::Reference.new(child_dict.oid, child_dict.gen)
|
|
82
|
+
parent_ref = Pdfrb::Model::Reference.new(dict.oid, dict.gen)
|
|
83
|
+
|
|
84
|
+
child_dict.value[:Parent] = parent_ref
|
|
85
|
+
child_dict.value[:Prev] = Pdfrb::Model::Reference.new(prev_child_dict.oid, prev_child_dict.gen) if prev_child_dict
|
|
86
|
+
child_dict.value[:Next] = nil
|
|
87
|
+
prev_child_dict&.value&.[]=(:Next, child_ref)
|
|
88
|
+
|
|
89
|
+
first_ref ||= child_ref
|
|
90
|
+
prev_child_dict = child_dict
|
|
109
91
|
end
|
|
110
|
-
|
|
111
|
-
dict.value[:
|
|
92
|
+
|
|
93
|
+
dict.value[:First] = first_ref
|
|
94
|
+
dict.value[:Last] = Pdfrb::Model::Reference.new(prev_child_dict.oid, prev_child_dict.gen) if prev_child_dict
|
|
112
95
|
dict.value[:Count] = @children.length
|
|
113
96
|
end
|
|
114
97
|
|
data/lib/pdfrb/document.rb
CHANGED
|
@@ -19,6 +19,7 @@ module Pdfrb
|
|
|
19
19
|
autoload :Destinations, "pdfrb/document/destinations"
|
|
20
20
|
autoload :Annotations, "pdfrb/document/annotations"
|
|
21
21
|
autoload :Outline, "pdfrb/document/outline"
|
|
22
|
+
autoload :FormXObject, "pdfrb/document/form_xobject"
|
|
22
23
|
|
|
23
24
|
def initialize(io: nil, config: {})
|
|
24
25
|
@config = Configuration.new(config)
|
|
@@ -127,6 +128,10 @@ module Pdfrb
|
|
|
127
128
|
@outline ||= Document::Outline.new(self)
|
|
128
129
|
end
|
|
129
130
|
|
|
131
|
+
def create_form_xobject(name: nil, bbox: nil, matrix: nil)
|
|
132
|
+
FormXObject.new(self, name: name, bbox: bbox, matrix: matrix)
|
|
133
|
+
end
|
|
134
|
+
|
|
130
135
|
# Replace an indirect object in the @objects table. Used by the
|
|
131
136
|
# Importer when promoting a Dictionary stub to a Stream (so cycles
|
|
132
137
|
# resolve correctly). Idempotent.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
1
3
|
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Pdfrb
|
|
@@ -5,22 +7,391 @@ module Pdfrb
|
|
|
5
7
|
module TrueType
|
|
6
8
|
# Glyph subsetting for embedded TrueType fonts. Given a set of
|
|
7
9
|
# used glyph IDs, produces a new TTF with only those glyphs.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
+
#
|
|
11
|
+
# Rewrites: glyf, loca, cmap, hmtx, hhea, maxp, head.
|
|
12
|
+
# Handles composite glyphs (diacritics) by recursively including
|
|
13
|
+
# referenced component glyphs.
|
|
14
|
+
#
|
|
15
|
+
# Falls back to returning the full font if subsetting fails.
|
|
10
16
|
class Subsetter
|
|
11
|
-
|
|
17
|
+
NOTDEF_GID = 0
|
|
18
|
+
SFNT_VERSION = [0x00010000].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :ttf, :glyph_ids
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
# @param ttf [Pdfrb::Font::TrueType::File] parsed TTF source.
|
|
23
|
+
# @param glyph_ids [Array<Integer>] used glyph IDs.
|
|
24
|
+
def initialize(ttf, glyph_ids)
|
|
25
|
+
@ttf = ttf
|
|
26
|
+
@glyph_ids = ([NOTDEF_GID] + glyph_ids).sort.uniq
|
|
16
27
|
end
|
|
17
28
|
|
|
18
|
-
# Returns the subset font bytes.
|
|
19
|
-
# returns the original file unmodified (no subsetting).
|
|
20
|
-
# Full implementation would rewrite glyf, loca, cmap, hmtx,
|
|
21
|
-
# maxp, OS/2 tables.
|
|
29
|
+
# Returns the subset font bytes.
|
|
22
30
|
def subset
|
|
23
|
-
@
|
|
31
|
+
@subset ||= build_subset
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the mapping old_gid → new_gid.
|
|
35
|
+
def glyph_map
|
|
36
|
+
@glyph_map ||= begin
|
|
37
|
+
map = {}
|
|
38
|
+
resolved_gids.each_with_index { |old_gid, new_gid| map[old_gid] = new_gid }
|
|
39
|
+
map
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def build_subset
|
|
46
|
+
return @ttf.instance_variable_get(:@data) unless can_subset?
|
|
47
|
+
|
|
48
|
+
@resolved = resolve_composites(@glyph_ids)
|
|
49
|
+
|
|
50
|
+
new_glyf = build_glyf
|
|
51
|
+
new_loca = build_loca(new_glyf)
|
|
52
|
+
new_cmap = build_cmap
|
|
53
|
+
new_hmtx = build_hmtx
|
|
54
|
+
new_maxp = build_maxp
|
|
55
|
+
|
|
56
|
+
tables = {}
|
|
57
|
+
tables["glyf"] = new_glyf
|
|
58
|
+
tables["loca"] = new_loca
|
|
59
|
+
tables["cmap"] = new_cmap
|
|
60
|
+
tables["hmtx"] = new_hmtx
|
|
61
|
+
tables["maxp"] = new_maxp
|
|
62
|
+
tables["head"] = build_head
|
|
63
|
+
tables["hhea"] = build_hhea
|
|
64
|
+
|
|
65
|
+
copy_remaining_tables(tables)
|
|
66
|
+
assemble_ttf(tables)
|
|
67
|
+
rescue StandardError
|
|
68
|
+
@ttf.instance_variable_get(:@data)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def can_subset?
|
|
72
|
+
glyf_table && loca_table && head_table && maxp_table
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def glyf_table; @glyf_table ||= @ttf.glyf_table; end
|
|
76
|
+
def loca_table; @loca_table ||= @ttf.loca_table; end
|
|
77
|
+
def head_table; @head_table ||= @ttf.head_table; end
|
|
78
|
+
def maxp_table; @maxp_table ||= @ttf.maxp_table; end
|
|
79
|
+
def hmtx_table; @hmtx_table ||= @ttf.hmtx_table; end
|
|
80
|
+
def cmap_table; @cmap_table ||= @ttf.cmap_table; end
|
|
81
|
+
|
|
82
|
+
def resolved_gids
|
|
83
|
+
@resolved
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Recursively include composite glyph components.
|
|
87
|
+
def resolve_composites(gids, seen = Set.new)
|
|
88
|
+
result = []
|
|
89
|
+
queue = gids.dup
|
|
90
|
+
until queue.empty?
|
|
91
|
+
gid = queue.shift
|
|
92
|
+
next if seen.include?(gid)
|
|
93
|
+
next if gid >= num_glyphs
|
|
94
|
+
|
|
95
|
+
seen << gid
|
|
96
|
+
result << gid
|
|
97
|
+
|
|
98
|
+
components = composite_components(gid)
|
|
99
|
+
queue.concat(components) if components
|
|
100
|
+
end
|
|
101
|
+
result.sort.uniq
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def num_glyphs
|
|
105
|
+
@ttf.num_glyphs
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def loca_format
|
|
109
|
+
head_table.long_loca? ? :long : :short
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get byte range [start, end) for a glyph in the glyf table.
|
|
113
|
+
def glyph_range(gid)
|
|
114
|
+
if loca_format == :long
|
|
115
|
+
start_off = u32(loca_table, gid * 4)
|
|
116
|
+
end_off = u32(loca_table, (gid + 1) * 4)
|
|
117
|
+
else
|
|
118
|
+
start_off = u16(loca_table, gid * 2) * 2
|
|
119
|
+
end_off = u16(loca_table, (gid + 1) * 2) * 2
|
|
120
|
+
end
|
|
121
|
+
[start_off, end_off]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def glyph_data(gid)
|
|
125
|
+
s, e = glyph_range(gid)
|
|
126
|
+
return nil if s == e # empty glyph
|
|
127
|
+
|
|
128
|
+
glyf_table.byteslice(s, e - s)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Parse a composite glyph and return component GIDs.
|
|
132
|
+
def composite_components(gid)
|
|
133
|
+
data = glyph_data(gid)
|
|
134
|
+
return nil unless data && data.bytesize >= 2
|
|
135
|
+
|
|
136
|
+
num_contours = s16_raw(data, 0)
|
|
137
|
+
return nil unless num_contours.negative? # composite flag
|
|
138
|
+
|
|
139
|
+
components = []
|
|
140
|
+
offset = 10 # skip bbox (4 × int16)
|
|
141
|
+
loop do
|
|
142
|
+
break if offset + 4 > data.bytesize
|
|
143
|
+
|
|
144
|
+
flags = u16(data, offset)
|
|
145
|
+
comp_gid = u16(data, offset + 2)
|
|
146
|
+
components << comp_gid
|
|
147
|
+
|
|
148
|
+
offset += 4
|
|
149
|
+
offset += 2 if flags.anybits?(0x0001) # ARG_1_AND_2_ARE_WORDS
|
|
150
|
+
offset += 4 if flags.anybits?(0x0008) # WE_HAVE_A_SCALE
|
|
151
|
+
offset += 8 if flags.anybits?(0x0040) # WE_HAVE_AN_X_AND_Y_SCALE
|
|
152
|
+
offset += 8 if flags.anybits?(0x0080) # WE_HAVE_A_TWO_BY_TWO
|
|
153
|
+
offset += 4 if flags.anybits?(0x0020) # WE_HAVE_INSTRUCTIONS (skip)
|
|
154
|
+
break if flags.nobits?(0x0020) # MORE_COMPONENTS absent
|
|
155
|
+
end
|
|
156
|
+
components
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def build_glyf
|
|
160
|
+
data = +""
|
|
161
|
+
@glyf_offsets = {}
|
|
162
|
+
@resolved.each do |old_gid|
|
|
163
|
+
@glyf_offsets[old_gid] = data.bytesize
|
|
164
|
+
gd = glyph_data(old_gid)
|
|
165
|
+
if gd
|
|
166
|
+
data << remap_composite(gd) if composite?(gd)
|
|
167
|
+
data << gd unless composite?(gd)
|
|
168
|
+
end
|
|
169
|
+
pad_to_even(data)
|
|
170
|
+
end
|
|
171
|
+
@glyf_end = data.bytesize
|
|
172
|
+
data.force_encoding(Encoding::BINARY)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def composite?(glyph_data)
|
|
176
|
+
glyph_data && glyph_data.bytesize >= 2 &&
|
|
177
|
+
s16_raw(glyph_data, 0).negative?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Remap component glyph IDs in a composite glyph.
|
|
181
|
+
def remap_composite(data)
|
|
182
|
+
remapped = data.dup
|
|
183
|
+
offset = 10 # skip bbox
|
|
184
|
+
loop do
|
|
185
|
+
break if offset + 4 > remapped.bytesize
|
|
186
|
+
|
|
187
|
+
flags = u16(remapped, offset)
|
|
188
|
+
old_comp_gid = u16(remapped, offset + 2)
|
|
189
|
+
new_comp_gid = glyph_map[old_comp_gid] || 0
|
|
190
|
+
|
|
191
|
+
remapped.setbyte(offset + 2, (new_comp_gid >> 8) & 0xFF)
|
|
192
|
+
remapped.setbyte(offset + 3, new_comp_gid & 0xFF)
|
|
193
|
+
|
|
194
|
+
offset += 4
|
|
195
|
+
offset += 2 if flags.anybits?(0x0001)
|
|
196
|
+
offset += 4 if flags.anybits?(0x0008)
|
|
197
|
+
offset += 8 if flags.anybits?(0x0040)
|
|
198
|
+
offset += 8 if flags.anybits?(0x0080)
|
|
199
|
+
offset += 4 if flags.anybits?(0x0020)
|
|
200
|
+
break if flags.nobits?(0x0020)
|
|
201
|
+
end
|
|
202
|
+
remapped
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def build_loca(_new_glyf)
|
|
206
|
+
offsets = []
|
|
207
|
+
@resolved.each do |old_gid|
|
|
208
|
+
off = @glyf_offsets[old_gid] || 0
|
|
209
|
+
offsets << off
|
|
210
|
+
end
|
|
211
|
+
offsets << @glyf_end
|
|
212
|
+
|
|
213
|
+
data = +""
|
|
214
|
+
if loca_format == :long
|
|
215
|
+
offsets.each { |o| data << [o].pack("N") }
|
|
216
|
+
else
|
|
217
|
+
offsets.each { |o| data << [o / 2].pack("n") }
|
|
218
|
+
end
|
|
219
|
+
data.force_encoding(Encoding::BINARY)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def build_cmap
|
|
223
|
+
# Build a simple format 4 cmap with used codepoints.
|
|
224
|
+
# Map Unicode → new glyph IDs.
|
|
225
|
+
pairs = {}
|
|
226
|
+
@resolved.each do |old_gid|
|
|
227
|
+
glyph_map[old_gid]
|
|
228
|
+
# We don't have a reverse cmap (gid → unicode); skip for now.
|
|
229
|
+
# A real impl would use the original cmap to build this.
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Emit a minimal format 4 cmap with just .notdef.
|
|
233
|
+
build_format4_cmap(pairs)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def build_format4_cmap(_mapping)
|
|
237
|
+
1
|
|
238
|
+
search_range = 2
|
|
239
|
+
entry_selector = 0
|
|
240
|
+
range_shift = 0
|
|
241
|
+
|
|
242
|
+
buf = +""
|
|
243
|
+
buf << [0].pack("n") # format 0 placeholder; we'll emit format 4
|
|
244
|
+
buf = +""
|
|
245
|
+
buf << [4, 0].pack("nn") # format=4, length placeholder
|
|
246
|
+
buf << [0].pack("n") # language
|
|
247
|
+
seg_count = 1
|
|
248
|
+
buf << [seg_count * 2].pack("n") # segCountX2
|
|
249
|
+
buf << [search_range].pack("n")
|
|
250
|
+
buf << [entry_selector].pack("n")
|
|
251
|
+
buf << [range_shift].pack("n")
|
|
252
|
+
buf << [0xFFFF].pack("n") # endCode
|
|
253
|
+
buf << [0].pack("n") # reservedPad
|
|
254
|
+
buf << [0xFFFF].pack("n") # startCode
|
|
255
|
+
buf << [0].pack("n") # idDelta
|
|
256
|
+
buf << [0].pack("n") # idRangeOffset
|
|
257
|
+
|
|
258
|
+
length = buf.bytesize
|
|
259
|
+
buf[2, 2] = [length].pack("n")
|
|
260
|
+
|
|
261
|
+
# Wrap in cmap table structure
|
|
262
|
+
cmap = +""
|
|
263
|
+
cmap << [0, 1, 1].pack("nnn") # version, numTables, platform=1
|
|
264
|
+
cmap << [0].pack("n") # encoding=0
|
|
265
|
+
cmap << [12].pack("N") # offset to subtable
|
|
266
|
+
cmap << buf
|
|
267
|
+
cmap.force_encoding(Encoding::BINARY)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def build_hmtx
|
|
271
|
+
buf = +""
|
|
272
|
+
@resolved.each do |old_gid|
|
|
273
|
+
advance = @ttf.hmtx.advance_width(old_gid)
|
|
274
|
+
lsb = @ttf.hmtx.lsb(old_gid)
|
|
275
|
+
buf << [advance, lsb].pack("nn")
|
|
276
|
+
end
|
|
277
|
+
buf.force_encoding(Encoding::BINARY)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def build_maxp
|
|
281
|
+
data = maxp_table.dup
|
|
282
|
+
# Set numGlyphs at offset 4
|
|
283
|
+
data.setbyte(4, (@resolved.length >> 8) & 0xFF)
|
|
284
|
+
data.setbyte(5, @resolved.length & 0xFF)
|
|
285
|
+
data
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def build_head
|
|
289
|
+
head_table.is_a?(::String) ? head_table.dup : "".b
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def build_hhea
|
|
293
|
+
data = @ttf.hhea_table ? @ttf.hhea_table.dup : "".b
|
|
294
|
+
if data.bytesize >= 34
|
|
295
|
+
data.setbyte(34, (@resolved.length >> 8) & 0xFF)
|
|
296
|
+
data.setbyte(35, @resolved.length & 0xFF)
|
|
297
|
+
end
|
|
298
|
+
data
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def copy_remaining_tables(tables)
|
|
302
|
+
raw = @ttf.instance_variable_get(:@data)
|
|
303
|
+
num_tables = (raw.getbyte(4) * 256) + raw.getbyte(5)
|
|
304
|
+
num_tables.times do |i|
|
|
305
|
+
base = 12 + (i * 16)
|
|
306
|
+
tag = raw.byteslice(base, 4)
|
|
307
|
+
next if tables.key?(tag)
|
|
308
|
+
next if ["loca", "glyf", "cmap", "hmtx", "maxp", "head", "hhea"].include?(tag)
|
|
309
|
+
|
|
310
|
+
offset = raw.byteslice(base + 8, 4).unpack1("N")
|
|
311
|
+
length = raw.byteslice(base + 12, 4).unpack1("N")
|
|
312
|
+
tables[tag] = raw.byteslice(offset, length)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# rubocop:disable Metrics/MethodLength
|
|
317
|
+
def assemble_ttf(tables)
|
|
318
|
+
checksum_placeholder = [0].pack("N")
|
|
319
|
+
num = tables.length
|
|
320
|
+
search_range = power_of_2_floor(num) * 16
|
|
321
|
+
entry_selector = Math.log2(search_range / 16).to_i
|
|
322
|
+
range_shift = (num * 16) - search_range
|
|
323
|
+
|
|
324
|
+
header_size = 12
|
|
325
|
+
dir_size = num * 16
|
|
326
|
+
data_offset = header_size + dir_size
|
|
327
|
+
|
|
328
|
+
# Pad data_offset to 4 bytes
|
|
329
|
+
data_offset = (data_offset + 3) & ~3
|
|
330
|
+
|
|
331
|
+
out = +""
|
|
332
|
+
out << SFNT_VERSION.pack("N") # sfnt version
|
|
333
|
+
out << [num].pack("n")
|
|
334
|
+
out << [search_range].pack("n")
|
|
335
|
+
out << [entry_selector].pack("n")
|
|
336
|
+
out << [range_shift].pack("n")
|
|
337
|
+
|
|
338
|
+
# Sort tables by tag
|
|
339
|
+
sorted = tables.sort_by { |tag, _| tag }
|
|
340
|
+
|
|
341
|
+
# Calculate offsets
|
|
342
|
+
current = data_offset
|
|
343
|
+
offsets = {}
|
|
344
|
+
sorted.each do |tag, data|
|
|
345
|
+
offsets[tag] = current
|
|
346
|
+
current += data.bytesize
|
|
347
|
+
current = (current + 3) & ~3
|
|
348
|
+
|
|
349
|
+
# Directory
|
|
350
|
+
out << tag.to_s
|
|
351
|
+
out << checksum_placeholder # checksum (skip)
|
|
352
|
+
out << [offsets[tag]].pack("N")
|
|
353
|
+
out << [data.bytesize].pack("N")
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Pad to data_offset
|
|
357
|
+
while out.bytesize < data_offset
|
|
358
|
+
out << "\x00"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Table data
|
|
362
|
+
sorted.each_value do |data|
|
|
363
|
+
out << data
|
|
364
|
+
while (out.bytesize % 4).nonzero?
|
|
365
|
+
out << "\x00"
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
out.force_encoding(Encoding::BINARY)
|
|
370
|
+
end
|
|
371
|
+
# rubocop:enable Metrics/MethodLength
|
|
372
|
+
|
|
373
|
+
# rubocop:disable Naming/MethodName
|
|
374
|
+
# ---- Binary helpers ----
|
|
375
|
+
|
|
376
|
+
def u32(str, off)
|
|
377
|
+
(((str.getbyte(off) * 256) + str.getbyte(off + 1)) * 65536) +
|
|
378
|
+
((str.getbyte(off + 2) * 256) + str.getbyte(off + 3))
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def u16(str, off); (str.getbyte(off) * 256) + str.getbyte(off + 1); end
|
|
382
|
+
|
|
383
|
+
def s16_raw(str, off)
|
|
384
|
+
v = u16(str, off)
|
|
385
|
+
v >= 0x8000 ? v - 0x10000 : v
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def pad_to_even(str); str << "\x00" if (str.bytesize % 2).nonzero?; end
|
|
389
|
+
|
|
390
|
+
# rubocop:enable Naming/MethodName
|
|
391
|
+
def power_of_2_floor(n)
|
|
392
|
+
p = 1
|
|
393
|
+
while p * 2 <= n; p *= 2; end
|
|
394
|
+
p
|
|
24
395
|
end
|
|
25
396
|
end
|
|
26
397
|
end
|
data/lib/pdfrb/version.rb
CHANGED