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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06cc47c628b9d603882cbfc10b2b5fc0beb6b2e1eabf672fa215c6ee3ae94429
4
- data.tar.gz: a9da7630cee4f6ada3c6a7c06260c7416cf9de6cb3efac7d844361c557d8df6e
3
+ metadata.gz: 263ee1cee601c0c5b86775ad6bbeb97777f4e7e01c95f63074281d00fbec3d54
4
+ data.tar.gz: 5d157430c7657ae19b8954975721863a68db7dbd1a5e3439b8a830da3bc7db4a
5
5
  SHA512:
6
- metadata.gz: 91c9ecb34465b5a6c1f8c440028638fd2d9e33ef58e2b89306c7290f94492965d9f8891a883d043bc5981b502eafedeed659580351b3e6d2608feac135d4fbbe
7
- data.tar.gz: 52cc28480e19f62104658d4b730fcf11b913005c6ecedd9d65b0ec9b28272c70bb7656300935ab968dbeff3e97b13609c306679cf993ee5f13c3ec8207bc58d0
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, First: nil, Last: nil, Count: entries.length },
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
- prev_ref = nil
34
+ prev_dict = nil
46
35
  first_ref = nil
47
- entries.each_with_index do |entry, _i|
48
- ref = entry.build!(@document)
49
- entry.value[:Parent] = Pdfrb::Model::Reference.new(root.oid, root.gen)
50
- entry.value[:Prev] = prev_ref if prev_ref
51
- entry.value[:Next] = nil
52
- prev_ref&.value&.[]=(:Next, ref)
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
- prev_ref = ref
46
+ prev_dict = dict
55
47
  end
56
48
 
57
49
  root.value[:First] = first_ref
58
- root.value[:Last] = prev_ref
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, :value
69
- attr_accessor :oid
59
+ attr_reader :title, :dest, :children
70
60
 
71
- def initialize(title:, dest:, document:)
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
- first_child = nil
100
- prev_child = nil
77
+ prev_child_dict = nil
78
+ first_ref = nil
101
79
  @children.each do |child|
102
- child_ref = child.build!(document)
103
- child.value[:Parent] =
104
- Pdfrb::Model::Reference.new(dict.oid, dict.gen)
105
- child.value[:Prev] = prev_child.value ? Pdfrb::Model::Reference.new(prev_child.oid, 0) : nil if prev_child
106
- prev_child&.value&.[]=(:Next, Pdfrb::Model::Reference.new(child_ref.oid, 0))
107
- first_child ||= child_ref
108
- prev_child = child
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
- dict.value[:First] = Pdfrb::Model::Reference.new(first_child.oid, 0)
111
- dict.value[:Last] = Pdfrb::Model::Reference.new(prev_child.oid, 0)
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
 
@@ -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
- # This is a stub — full subsetting (table rewriting, composite
9
- # glyph handling, CMap format 4 rewrite) is complex.
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
- attr_reader :source_file, :glyph_ids
17
+ NOTDEF_GID = 0
18
+ SFNT_VERSION = [0x00010000].freeze
19
+
20
+ attr_reader :ttf, :glyph_ids
12
21
 
13
- def initialize(source_file, glyph_ids)
14
- @source_file = source_file
15
- @glyph_ids = glyph_ids.sort.uniq
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. Currently a stub that
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
- @source_file.instance_variable_get(:@data)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfrb
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
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.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.