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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9283c2555bc64be8606a06fb6fc62c9498382499c30c7eea8afd36c4dcfc6a82
4
- data.tar.gz: e93caaff66db1dcf3944873ad5b41fe9a8876623d33ae0c53918461a6f0a4c49
3
+ metadata.gz: 28f2fc6a03f24e2ad53f50a61e50221057c15d88d3db91a3c0d8487fa1e47186
4
+ data.tar.gz: 50fa314624a65f648a4e190f6464e335bdfa14f06301d4a280bbaf94c45a01f6
5
5
  SHA512:
6
- metadata.gz: 16b7e2394f0275d631d4ba0946e3b0d5f2973240f9fe1d4f5eebfbb4998e666e6e0ff9e62dd50fd43eebb5ecb7b418846d6ded5be052b1c3462f0d3033cbd1a3
7
- data.tar.gz: 4219346da2b075bb87024963f248c1a8e26852f9e928c24f0155291e242bd8d4ff28175f74ef1719c05812fc34aa0782d4f81d2a226c108e996d5940d48ab6fe
6
+ metadata.gz: f00c0cc7abf3e0caebf3c47095e6bedc8c8c1c884565a14243df2a59fbec04f1731b478253471045ff872aa83024949ff17d4eea13d08b8118d71288f9a6c268
7
+ data.tar.gz: d68c928edc696a330b782321c104a99d876510bb2b1723a30b4f42cc9956f5ac82ca3e5a9d02aff9ac29c4501501088a8c242b10a5e1e9dadff9b843bd85ca98
@@ -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])
@@ -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
- ttf = Pdfrb::Font::TrueType::File.new(data)
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
- catalog = document.catalog
487
- catalog.value[:Resources] ||= {}
488
- catalog.value[:Resources][:Font] ||= {}
489
- catalog.value[:Resources][:Font][resource] = ref
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
- catalog = document.catalog
51
- catalog.value[:Resources] ||= {}
52
- catalog.value[:Resources][:XObject] ||= {}
53
- catalog.value[:Resources][:XObject][name] = ref
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
@@ -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
- private
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
- catalog = document.catalog
146
- catalog.value[:Resources] ||= {}
147
- catalog.value[:Resources][:Shading] ||= {}
148
- catalog.value[:Resources][:Shading][name] =
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfrb
4
- VERSION = "0.7.45"
4
+ VERSION = "0.7.46"
5
5
  end
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.45
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