pdfrb 0.7.45 → 0.7.46
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/lib/pdfrb/content/canvas.rb +1 -0
- data/lib/pdfrb/document/fonts.rb +53 -19
- data/lib/pdfrb/document/images.rb +5 -4
- data/lib/pdfrb/document/pages.rb +8 -3
- data/lib/pdfrb/document/shadings.rb +7 -4
- data/lib/pdfrb/document.rb +1 -0
- data/lib/pdfrb/font/cff/dict.rb +118 -0
- data/lib/pdfrb/font/cff/file.rb +126 -0
- data/lib/pdfrb/font/cff/index.rb +83 -0
- data/lib/pdfrb/font/cff/subsetter.rb +0 -0
- data/lib/pdfrb/font/cff.rb +19 -0
- data/lib/pdfrb/font.rb +1 -0
- data/lib/pdfrb/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28f2fc6a03f24e2ad53f50a61e50221057c15d88d3db91a3c0d8487fa1e47186
|
|
4
|
+
data.tar.gz: 50fa314624a65f648a4e190f6464e335bdfa14f06301d4a280bbaf94c45a01f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f00c0cc7abf3e0caebf3c47095e6bedc8c8c1c884565a14243df2a59fbec04f1731b478253471045ff872aa83024949ff17d4eea13d08b8118d71288f9a6c268
|
|
7
|
+
data.tar.gz: d68c928edc696a330b782321c104a99d876510bb2b1723a30b4f42cc9956f5ac82ca3e5a9d02aff9ac29c4501501088a8c242b10a5e1e9dadff9b843bd85ca98
|
data/lib/pdfrb/content/canvas.rb
CHANGED
|
@@ -363,6 +363,7 @@ module Pdfrb
|
|
|
363
363
|
|
|
364
364
|
def text(str, at:, font:, size:, char_spacing: nil, word_spacing: nil)
|
|
365
365
|
@used_fonts[font] = size
|
|
366
|
+
@document&.fonts&.register_usage(font, str)
|
|
366
367
|
encoded = encode_for_font(str.to_s, font)
|
|
367
368
|
emit_op(Pdfrb::Content::Operator::BeginText)
|
|
368
369
|
emit_op(Pdfrb::Content::Operator::SetTextMatrix, 1, 0, 0, 1, at[0], at[1])
|
data/lib/pdfrb/document/fonts.rb
CHANGED
|
@@ -61,6 +61,15 @@ module Pdfrb
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def used_codepoints(resource); @used_codepoints[resource]; end
|
|
64
|
+
|
|
65
|
+
# Record that +text+ was drawn with +font_dict+ so the
|
|
66
|
+
# write-time subsetter knows which codepoints each embedded
|
|
67
|
+
# font must retain.
|
|
68
|
+
def register_usage(font_dict, text)
|
|
69
|
+
resource = @font_dicts.key(font_dict)
|
|
70
|
+
track(resource, text) if resource
|
|
71
|
+
end
|
|
72
|
+
|
|
64
73
|
def encoding_for(resource); @encodings[resource]; end
|
|
65
74
|
|
|
66
75
|
def encode_text(text, resource)
|
|
@@ -144,21 +153,7 @@ module Pdfrb
|
|
|
144
153
|
next if codepoints.empty?
|
|
145
154
|
|
|
146
155
|
begin
|
|
147
|
-
|
|
148
|
-
subsetter = Pdfrb::Font::TrueType::Subsetter.new(ttf)
|
|
149
|
-
subset = subsetter.subset(codepoints.to_a)
|
|
150
|
-
dict = @font_dicts[resource]
|
|
151
|
-
next unless dict
|
|
152
|
-
|
|
153
|
-
desc_ref = dict.value[:FontDescriptor]
|
|
154
|
-
next unless desc_ref
|
|
155
|
-
|
|
156
|
-
desc = desc_ref.is_a?(Pdfrb::Model::Reference) ? document.object(desc_ref) : desc_ref
|
|
157
|
-
next unless desc
|
|
158
|
-
|
|
159
|
-
fd_stream = document.add({ Length: subset.bytesize }, type: Pdfrb::Model::Cos::Stream)
|
|
160
|
-
fd_stream.stream = subset
|
|
161
|
-
desc.value[:FontFile2] = Pdfrb::Model::Reference.new(fd_stream.oid, fd_stream.gen)
|
|
156
|
+
subset_font(resource, data, codepoints)
|
|
162
157
|
rescue StandardError
|
|
163
158
|
next
|
|
164
159
|
end
|
|
@@ -481,12 +476,51 @@ module Pdfrb
|
|
|
481
476
|
}, type: subtype == :TrueType ? Pdfrb::Model::Type::FontTrueType : Pdfrb::Model::Type::FontType1)
|
|
482
477
|
end
|
|
483
478
|
|
|
479
|
+
# Build the subsetted font bytes for +resource+ and replace the
|
|
480
|
+
# descriptor's font-file stream (FontFile2 for TrueType
|
|
481
|
+
# outlines, FontFile3/OpenType for CFF).
|
|
482
|
+
def subset_font(resource, data, codepoints)
|
|
483
|
+
subset, font_file_key =
|
|
484
|
+
if data.byteslice(0, 4) == "OTTO".b
|
|
485
|
+
# CFF outlines: map codepoints to glyph IDs via the OTF
|
|
486
|
+
# cmap (it addresses CFF glyphs too), subset the 'CFF '
|
|
487
|
+
# table, and rebuild the OTF container.
|
|
488
|
+
cmap = Pdfrb::Font::TrueType::File.new(data).cmap
|
|
489
|
+
gids = codepoints.filter_map { |cp| cmap.glyph_id_for(cp) }.uniq
|
|
490
|
+
[Pdfrb::Font::CFF::Subsetter.subset_otf(data, gids), :FontFile3]
|
|
491
|
+
else
|
|
492
|
+
ttf = Pdfrb::Font::TrueType::File.new(data)
|
|
493
|
+
subsetter = Pdfrb::Font::TrueType::Subsetter.new(ttf)
|
|
494
|
+
[subsetter.subset(codepoints.to_a), :FontFile2]
|
|
495
|
+
end
|
|
496
|
+
dict = @font_dicts[resource]
|
|
497
|
+
return unless dict
|
|
498
|
+
|
|
499
|
+
desc_ref = dict.value[:FontDescriptor]
|
|
500
|
+
return unless desc_ref
|
|
501
|
+
|
|
502
|
+
desc = desc_ref.is_a?(Pdfrb::Model::Reference) ? document.object(desc_ref) : desc_ref
|
|
503
|
+
return unless desc
|
|
504
|
+
|
|
505
|
+
fd_stream = document.add({ Length: subset.bytesize }, type: Pdfrb::Model::Cos::Stream)
|
|
506
|
+
fd_stream.stream = subset
|
|
507
|
+
if font_file_key == :FontFile3
|
|
508
|
+
fd_stream.value[:Subtype] = :OpenType
|
|
509
|
+
fd_stream.value[:Length1] = subset.bytesize
|
|
510
|
+
end
|
|
511
|
+
desc.value[font_file_key] = Pdfrb::Model::Reference.new(fd_stream.oid, fd_stream.gen)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Fonts attach to the page-tree ROOT's /Resources so every page
|
|
515
|
+
# inherits them (s7.7.3.2 resource inheritance). Attaching to
|
|
516
|
+
# the Catalog instead — as this used to — produced a stray key
|
|
517
|
+
# no viewer honours, leaving written pages without fonts.
|
|
484
518
|
def attach_to_resources(resource, font_dict)
|
|
485
519
|
ref = Pdfrb::Model::Reference.new(font_dict.oid, font_dict.gen)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
520
|
+
root = document.pages.pages_root
|
|
521
|
+
root.value[:Resources] ||= {}
|
|
522
|
+
root.value[:Resources][:Font] ||= {}
|
|
523
|
+
root.value[:Resources][:Font][resource] = ref
|
|
490
524
|
end
|
|
491
525
|
end
|
|
492
526
|
end
|
|
@@ -47,10 +47,11 @@ module Pdfrb
|
|
|
47
47
|
|
|
48
48
|
def attach_to_resources(name, image)
|
|
49
49
|
ref = Pdfrb::Model::Reference.new(image.oid, image.gen)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
# Page-tree root so every page inherits it (s7.7.3.2).
|
|
51
|
+
root = document.pages.pages_root
|
|
52
|
+
root.value[:Resources] ||= {}
|
|
53
|
+
root.value[:Resources][:XObject] ||= {}
|
|
54
|
+
root.value[:Resources][:XObject][name] = ref
|
|
54
55
|
end
|
|
55
56
|
end
|
|
56
57
|
end
|
data/lib/pdfrb/document/pages.rb
CHANGED
|
@@ -26,11 +26,13 @@ module Pdfrb
|
|
|
26
26
|
media_box ||= (width && height ? [0, 0, width, height] : [0, 0, 612, 792])
|
|
27
27
|
root = pages_root
|
|
28
28
|
contents = document.add({}, type: Pdfrb::Model::Cos::Stream)
|
|
29
|
+
# No eager /Resources: an empty page-level dict would override
|
|
30
|
+
# the inheritable root resources (s7.7.3.2). The canvas seeds
|
|
31
|
+
# one lazily when it attaches XObjects.
|
|
29
32
|
page_hash = {
|
|
30
33
|
Type: :Page,
|
|
31
34
|
Parent: Pdfrb::Model::Reference.new(root.oid, root.gen),
|
|
32
35
|
MediaBox: media_box,
|
|
33
|
-
Resources: {},
|
|
34
36
|
Contents: Pdfrb::Model::Reference.new(contents.oid, 0)
|
|
35
37
|
}
|
|
36
38
|
page_hash[:BleedBox] = bleed_box if bleed_box
|
|
@@ -134,8 +136,9 @@ module Pdfrb
|
|
|
134
136
|
end
|
|
135
137
|
end
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
# The page-tree root (Catalog /Pages), seeding an empty tree if
|
|
140
|
+
# needed. Resources placed here are inherited by every page
|
|
141
|
+
# (s7.7.3.2).
|
|
139
142
|
def pages_root
|
|
140
143
|
catalog = document.catalog
|
|
141
144
|
raise Pdfrb::Error, "Document has no Catalog" unless catalog
|
|
@@ -150,6 +153,8 @@ module Pdfrb
|
|
|
150
153
|
root
|
|
151
154
|
end
|
|
152
155
|
|
|
156
|
+
private
|
|
157
|
+
|
|
153
158
|
def walk(node, &block)
|
|
154
159
|
kids = node.is_a?(Pdfrb::Model::Cos::Dictionary) ? node.value[:Kids] : node[:Kids]
|
|
155
160
|
return unless kids
|
|
@@ -142,10 +142,13 @@ module Pdfrb
|
|
|
142
142
|
|
|
143
143
|
def register(name, shading_obj)
|
|
144
144
|
@registry[name] = shading_obj
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
document.catalog
|
|
146
|
+
# Attach to the page-tree root so every page inherits it
|
|
147
|
+
# (s7.7.3.2); the Catalog has no /Resources key in PDF.
|
|
148
|
+
root = document.pages.pages_root
|
|
149
|
+
root.value[:Resources] ||= {}
|
|
150
|
+
root.value[:Resources][:Shading] ||= {}
|
|
151
|
+
root.value[:Resources][:Shading][name] =
|
|
149
152
|
Pdfrb::Model::Reference.new(shading_obj.oid, shading_obj.gen)
|
|
150
153
|
end
|
|
151
154
|
|
data/lib/pdfrb/document.rb
CHANGED
|
@@ -223,6 +223,7 @@ module Pdfrb
|
|
|
223
223
|
target = io || (path && File.open(path, "wb"))
|
|
224
224
|
raise ArgumentError, "write needs a path or io:" unless target
|
|
225
225
|
|
|
226
|
+
fonts.subset_fonts! if config["writer.subset_fonts"] != false
|
|
226
227
|
Pdfrb::Writer.write(self, target)
|
|
227
228
|
target.close if path && io.nil? && target.is_a?(IO)
|
|
228
229
|
self
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pdfrb
|
|
4
|
+
module Font
|
|
5
|
+
module CFF
|
|
6
|
+
# DICT data parsing (TN5176 s4): a sequence of operands (from
|
|
7
|
+
# the b0 byte ranges) followed by an operator byte.
|
|
8
|
+
#
|
|
9
|
+
# Used both to READ the Top DICT (CharStrings offset, Private
|
|
10
|
+
# [size offset], charset) and to locate operator positions so a
|
|
11
|
+
# subsetter can patch offsets in place.
|
|
12
|
+
class Dict
|
|
13
|
+
# One operator plus its trailing operands, with the byte
|
|
14
|
+
# range it occupied in the source DICT.
|
|
15
|
+
Entry = Struct.new(:operator, :operands, :start, :finish) do
|
|
16
|
+
def int_operand(i = 0)
|
|
17
|
+
v = operands[i]
|
|
18
|
+
v.is_a?(Integer) ? v : nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :entries, :raw
|
|
23
|
+
|
|
24
|
+
def self.parse(data)
|
|
25
|
+
entries = []
|
|
26
|
+
operands = []
|
|
27
|
+
pos = 0
|
|
28
|
+
entry_start = 0
|
|
29
|
+
while pos < data.bytesize
|
|
30
|
+
b0 = data.getbyte(pos)
|
|
31
|
+
case b0
|
|
32
|
+
when 28 then operands << data.byteslice(pos + 1, 2).unpack1("n")
|
|
33
|
+
pos += 3
|
|
34
|
+
when 29 then operands << data.byteslice(pos + 1, 4).unpack1("N")
|
|
35
|
+
pos += 5
|
|
36
|
+
when 30
|
|
37
|
+
real, pos = parse_real(data, pos + 1)
|
|
38
|
+
operands << real
|
|
39
|
+
when 32..246
|
|
40
|
+
operands << (b0 - 139)
|
|
41
|
+
pos += 1
|
|
42
|
+
when 247..250
|
|
43
|
+
operands << (((b0 - 247) * 256) + data.getbyte(pos + 1) + 108)
|
|
44
|
+
pos += 2
|
|
45
|
+
when 251..254
|
|
46
|
+
operands << ((-(b0 - 251) * 256) - data.getbyte(pos + 1) - 108)
|
|
47
|
+
pos += 2
|
|
48
|
+
else
|
|
49
|
+
# Operator (0..21, with 12 x for two-byte ops).
|
|
50
|
+
if b0 == 12
|
|
51
|
+
op = [12, data.getbyte(pos + 1)]
|
|
52
|
+
pos += 2
|
|
53
|
+
else
|
|
54
|
+
op = b0
|
|
55
|
+
pos += 1
|
|
56
|
+
end
|
|
57
|
+
entries << Entry.new(op, operands, entry_start, pos)
|
|
58
|
+
operands = []
|
|
59
|
+
entry_start = pos
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
new(data, entries)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
NIBBLE_CHARS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "E", "E-", nil, "-"].freeze
|
|
66
|
+
private_constant :NIBBLE_CHARS
|
|
67
|
+
|
|
68
|
+
# Real operand (b0 30): nibble-encoded decimal terminated by
|
|
69
|
+
# 0xF. Returns [Float, next_pos].
|
|
70
|
+
def self.parse_real(data, pos)
|
|
71
|
+
buf = +""
|
|
72
|
+
while pos < data.bytesize
|
|
73
|
+
nibble_pair = data.getbyte(pos)
|
|
74
|
+
pos += 1
|
|
75
|
+
hi = nibble_pair >> 4
|
|
76
|
+
lo = nibble_pair & 0xF
|
|
77
|
+
break if hi == 0xF
|
|
78
|
+
|
|
79
|
+
buf << NIBBLE_CHARS[hi]
|
|
80
|
+
break if lo == 0xF
|
|
81
|
+
|
|
82
|
+
buf << NIBBLE_CHARS[lo]
|
|
83
|
+
end
|
|
84
|
+
[buf.to_f, pos]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def initialize(raw, entries)
|
|
88
|
+
@raw = raw
|
|
89
|
+
@entries = entries
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Entry for a one-byte Integer operator or a two-byte
|
|
93
|
+
# [12, x] operator.
|
|
94
|
+
def entry_for(operator)
|
|
95
|
+
entries.find { |e| e.operator == operator }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Top DICT helpers (operator numbers per TN5176 Table 6):
|
|
99
|
+
# 15 charset, 17 CharStrings, 18 Private [size offset].
|
|
100
|
+
def charset_offset
|
|
101
|
+
entry_for(15)&.int_operand
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def charstrings_offset
|
|
105
|
+
entry_for(17)&.int_operand
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns [size, offset].
|
|
109
|
+
def private_size_offset
|
|
110
|
+
e = entry_for(18)
|
|
111
|
+
return nil unless e
|
|
112
|
+
|
|
113
|
+
[e.int_operand(0), e.int_operand(1)]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pdfrb
|
|
4
|
+
module Font
|
|
5
|
+
module CFF
|
|
6
|
+
# Parsed CFF table (TN5176). Exposes the pieces a subsetter
|
|
7
|
+
# needs: header span, Name/String/Global-Subr INDEXes, Top DICT,
|
|
8
|
+
# charset (format 0), CharStrings INDEX, Private DICT span, and
|
|
9
|
+
# the Local Subr INDEX when present.
|
|
10
|
+
class File
|
|
11
|
+
attr_reader :data, :header_size, :name_index, :top_dict,
|
|
12
|
+
:string_index, :global_subrs, :charset_format,
|
|
13
|
+
:charset_sids, :charstrings, :private_span,
|
|
14
|
+
:local_subrs
|
|
15
|
+
|
|
16
|
+
def initialize(data)
|
|
17
|
+
@data = data.b
|
|
18
|
+
parse
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def num_glyphs
|
|
22
|
+
charstrings&.size || 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def charstring(gid)
|
|
26
|
+
charstrings[gid] if charstrings
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Charset (format 0 only): gid -> SID, with .notdef implicit
|
|
30
|
+
# at gid 0.
|
|
31
|
+
def sid_for_gid(gid)
|
|
32
|
+
return 0 if gid.zero?
|
|
33
|
+
|
|
34
|
+
@charset_sids[gid - 1]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Reverse charset: SID -> gid (first wins).
|
|
38
|
+
def gid_for_sid(sid)
|
|
39
|
+
@gid_by_sid ||= begin
|
|
40
|
+
map = { 0 => 0 }
|
|
41
|
+
@charset_sids.each_with_index { |s, i| map[s] ||= i + 1 }
|
|
42
|
+
map
|
|
43
|
+
end
|
|
44
|
+
@gid_by_sid[sid]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def parse
|
|
50
|
+
@header_size = @data.getbyte(2)
|
|
51
|
+
pos = @header_size
|
|
52
|
+
|
|
53
|
+
@name_index, pos = Index.parse(@data, pos)
|
|
54
|
+
top_index, pos = Index.parse(@data, pos)
|
|
55
|
+
@top_dict = Dict.parse(top_index.items.first || "".b)
|
|
56
|
+
@string_index, pos = Index.parse(@data, pos)
|
|
57
|
+
@global_subrs, = Index.parse(@data, pos)
|
|
58
|
+
|
|
59
|
+
# CharStrings first: the format-0 charset reader needs the
|
|
60
|
+
# glyph count.
|
|
61
|
+
parse_charstrings
|
|
62
|
+
parse_charset
|
|
63
|
+
parse_private
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_charset
|
|
67
|
+
off = @top_dict.charset_offset
|
|
68
|
+
@charset_sids = []
|
|
69
|
+
if off.nil? || off.zero?
|
|
70
|
+
@charset_format = :iso_glyph_order
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
format = @data.getbyte(off)
|
|
75
|
+
@charset_format = format
|
|
76
|
+
case format
|
|
77
|
+
when 0
|
|
78
|
+
(@charstrings&.size || 0).times do |i|
|
|
79
|
+
@charset_sids << @data.byteslice(off + 1 + (i * 2), 2).unpack1("n")
|
|
80
|
+
end
|
|
81
|
+
when 1, 2
|
|
82
|
+
parse_range_charset(off, format)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Formats 1/2 (TN5176 s18): [first(n2) nLeft(C1|n2)]* ranges;
|
|
87
|
+
# SIDs run consecutively from first for nLeft+1 entries.
|
|
88
|
+
def parse_range_charset(off, format)
|
|
89
|
+
pos = off + 1
|
|
90
|
+
count = @charstrings&.size || 0
|
|
91
|
+
while @charset_sids.size < count && pos < @data.bytesize
|
|
92
|
+
first = @data.byteslice(pos, 2).unpack1("n")
|
|
93
|
+
pos += 2
|
|
94
|
+
n_left = format == 1 ? @data.getbyte(pos) : @data.byteslice(pos, 2).unpack1("n")
|
|
95
|
+
pos += format == 1 ? 1 : 2
|
|
96
|
+
(0..n_left).each do |i|
|
|
97
|
+
@charset_sids << (first + i)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def parse_charstrings
|
|
103
|
+
off = @top_dict.charstrings_offset
|
|
104
|
+
return if off.nil? || off.zero?
|
|
105
|
+
|
|
106
|
+
@charstrings, = Index.parse(@data, off)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_private
|
|
110
|
+
span = @top_dict.private_size_offset
|
|
111
|
+
return if span.nil?
|
|
112
|
+
|
|
113
|
+
size, offset = span
|
|
114
|
+
@private_span = [offset, size]
|
|
115
|
+
|
|
116
|
+
priv = Dict.parse(@data.byteslice(offset, size))
|
|
117
|
+
subrs_entry = priv.entry_for(19)
|
|
118
|
+
return unless subrs_entry
|
|
119
|
+
|
|
120
|
+
subrs_off = subrs_entry.int_operand
|
|
121
|
+
@local_subrs, = Index.parse(@data, offset + subrs_off) if subrs_off&.positive?
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pdfrb
|
|
4
|
+
module Font
|
|
5
|
+
module CFF
|
|
6
|
+
# CFF INDEX structure (TN5176 s5): count (Card16), offSize
|
|
7
|
+
# (OffSize), offsets (count+1 × offSize bytes, 1-based), then
|
|
8
|
+
# the data items.
|
|
9
|
+
class Index
|
|
10
|
+
attr_reader :items
|
|
11
|
+
|
|
12
|
+
# Parse an INDEX at +offset+ in +data+; returns [index,
|
|
13
|
+
# next_offset].
|
|
14
|
+
def self.parse(data, offset)
|
|
15
|
+
count = data.byteslice(offset, 2).unpack1("n")
|
|
16
|
+
return [new([]), offset + 2] if count.zero?
|
|
17
|
+
|
|
18
|
+
off_size = data.getbyte(offset + 2)
|
|
19
|
+
offsets_base = offset + 3
|
|
20
|
+
offsets = (0..count).map do |i|
|
|
21
|
+
read_off(data, offsets_base + (i * off_size), off_size)
|
|
22
|
+
end
|
|
23
|
+
data_base = offsets_base + ((count + 1) * off_size)
|
|
24
|
+
items = (0...count).map do |i|
|
|
25
|
+
start = data_base + offsets[i] - 1
|
|
26
|
+
len = offsets[i + 1] - offsets[i]
|
|
27
|
+
data.byteslice(start, len)
|
|
28
|
+
end
|
|
29
|
+
[new(items), data_base + offsets[count] - 1]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.read_off(data, pos, size)
|
|
33
|
+
value = 0
|
|
34
|
+
size.times { |i| value = (value * 256) + data.getbyte(pos + i) }
|
|
35
|
+
value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(items)
|
|
39
|
+
@items = items
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def each(&)
|
|
43
|
+
@items.each(&)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def size
|
|
47
|
+
@items.size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def [](i)
|
|
51
|
+
@items[i]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Serialize back to INDEX bytes.
|
|
55
|
+
def serialize
|
|
56
|
+
return [0].pack("n") if @items.empty?
|
|
57
|
+
|
|
58
|
+
offsets = [1]
|
|
59
|
+
@items.each { |item| offsets << (offsets.last + item.bytesize) }
|
|
60
|
+
max = offsets.last
|
|
61
|
+
off_size = if max < 0x100
|
|
62
|
+
1
|
|
63
|
+
else
|
|
64
|
+
(if max < 0x10000
|
|
65
|
+
2
|
|
66
|
+
else
|
|
67
|
+
(max < 0x1000000 ? 3 : 4)
|
|
68
|
+
end)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
buf = +"".b
|
|
72
|
+
buf << [@items.size].pack("n")
|
|
73
|
+
buf << off_size.chr
|
|
74
|
+
offsets.each do |o|
|
|
75
|
+
off_size.downto(1) { |i| buf << ((o >> (8 * (i - 1))) & 0xFF).chr }
|
|
76
|
+
end
|
|
77
|
+
@items.each { |item| buf << item.b }
|
|
78
|
+
buf
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pdfrb
|
|
4
|
+
module Font
|
|
5
|
+
# CFF (Compact Font Format, ISO 32000-2 Annex / Adobe CFF spec
|
|
6
|
+
# TN5176) parsing and safe subsetting for OTF fonts embedded via
|
|
7
|
+
# /FontFile3 /Subtype /OpenType.
|
|
8
|
+
#
|
|
9
|
+
# Layout: header, Name INDEX, Top DICT INDEX, String INDEX,
|
|
10
|
+
# Global Subr INDEX, then (at offsets recorded in the Top DICT)
|
|
11
|
+
# charset, CharStrings INDEX, Private DICT (+ Local Subr INDEX).
|
|
12
|
+
module CFF
|
|
13
|
+
autoload :Index, "pdfrb/font/cff/index"
|
|
14
|
+
autoload :Dict, "pdfrb/font/cff/dict"
|
|
15
|
+
autoload :File, "pdfrb/font/cff/file"
|
|
16
|
+
autoload :Subsetter, "pdfrb/font/cff/subsetter"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/pdfrb/font.rb
CHANGED
|
@@ -9,6 +9,7 @@ module Pdfrb
|
|
|
9
9
|
autoload :AFMParser, "pdfrb/font/afm_parser"
|
|
10
10
|
autoload :CMap, "pdfrb/font/cmap"
|
|
11
11
|
autoload :TrueType, "pdfrb/font/true_type"
|
|
12
|
+
autoload :CFF, "pdfrb/font/cff"
|
|
12
13
|
autoload :Type1, "pdfrb/font/type1"
|
|
13
14
|
autoload :Metrics, "pdfrb/font/metrics_helper"
|
|
14
15
|
autoload :MetricsHelper, "pdfrb/font/metrics_helper"
|
data/lib/pdfrb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pdfrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.46
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -865,6 +865,11 @@ files:
|
|
|
865
865
|
- lib/pdfrb/filter/run_length_decode.rb
|
|
866
866
|
- lib/pdfrb/font.rb
|
|
867
867
|
- lib/pdfrb/font/afm_parser.rb
|
|
868
|
+
- lib/pdfrb/font/cff.rb
|
|
869
|
+
- lib/pdfrb/font/cff/dict.rb
|
|
870
|
+
- lib/pdfrb/font/cff/file.rb
|
|
871
|
+
- lib/pdfrb/font/cff/index.rb
|
|
872
|
+
- lib/pdfrb/font/cff/subsetter.rb
|
|
868
873
|
- lib/pdfrb/font/cmap.rb
|
|
869
874
|
- lib/pdfrb/font/cmap/parser.rb
|
|
870
875
|
- lib/pdfrb/font/cmap/writer.rb
|