hexapdf 0.26.1 → 0.26.2

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: '071877a8fbc781ac3d35f5432d6b7d83b80d5d7f981d3fd7110551e0e09bc18b'
4
- data.tar.gz: a0ac1aa3be4d720561b9d58d197f547e9c3958fb0c252d8da27d9e9a356c0b7b
3
+ metadata.gz: 5f7ff4db8b6417cf6f0101a2e72e2481571a40c9542713be82753376d58c047c
4
+ data.tar.gz: 0b62a42fe5b91bdd8f11c4c31105e560cd4cf2f27259a86b9fd07db2f2280d6e
5
5
  SHA512:
6
- metadata.gz: 7528b421de6360e09bf4caeb72caec3b71f317314c8e0389fca287ee00bfab0ee952d8cecced2df937ff2e8198655fe81ebd12a494fac94847ae28ca20169384
7
- data.tar.gz: 52a0ab62632122772162fa1dbbb80e05b11956d187385c813f97f0a6c6c8b23c83f568cef2df81e93caf2ae558cda005ef0923de45f16d2c943cc9d86f92c868
6
+ metadata.gz: 0ef60dd2da6c1c233768e564c5afb9330fb07065a33f293a2fbca6c71b8948d2ab754b2c9383ed4a84400e944e98f6c5a3638bf0c970c2b819eb53a0e024a2ca
7
+ data.tar.gz: 9c0190529f0d25d9ec655bef27d25054b9bfb5dc2ec980742807e73e0eb953550033f5362249cad1ec83ff8e78a1f1ad91785614cecdbaed113ad6365a77ade5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 0.26.2 - 2022-10-22
2
+
3
+ ### Added
4
+
5
+ * Support for setting custom properties on [HexaPDF::Layout::Box] and
6
+ [HexaPDF::Layout::TextFragment]
7
+
8
+ ### Changed
9
+
10
+ * [HexaPDF::Layout::Style::LinkLayer] to use the 'link' custom box property if
11
+ no target is set
12
+
13
+ ### Fixed
14
+
15
+ * [HexaPDF::Layout::Style::Layers] to allow named layers without options
16
+ * [HexaPDF::Revision#each_modified_object] to not yield signature objects
17
+ * [HexaPDF::Revision#each_modified_object] to force comparison of direct objects
18
+ * [HexaPDF::Type::ObjectStream] to work for encrypted documents again
19
+
20
+
1
21
  ## 0.26.1 - 2022-10-14
2
22
 
3
23
  ### Changed
@@ -249,6 +249,10 @@ module HexaPDF
249
249
  # +style_properties+ are specified, the style is duplicated and the additional styles are
250
250
  # applied.
251
251
  #
252
+ # +properties+::
253
+ # This can be used to set custom properties on the created text box. See Box#properties
254
+ # for details and usage.
255
+ #
252
256
  # +box_style+::
253
257
  # Sometimes it is necessary for the box to have a different style than the text, e.g. when
254
258
  # using overlays. In such a case use +box_style+ for specifiying the style of the box (a
@@ -266,11 +270,13 @@ module HexaPDF
266
270
  # })
267
271
  #
268
272
  # See: #formatted_text_box, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment
269
- def text_box(text, width: 0, height: 0, style: nil, box_style: nil, **style_properties)
273
+ def text_box(text, width: 0, height: 0, style: nil, properties: nil, box_style: nil,
274
+ **style_properties)
270
275
  style = retrieve_style(style, style_properties)
271
276
  box_style = (box_style ? retrieve_style(box_style) : style)
272
277
  box_class_for_name(:text).new(items: [HexaPDF::Layout::TextFragment.create(text, style)],
273
- width: width, height: height, style: box_style)
278
+ width: width, height: height, properties: properties,
279
+ style: box_style)
274
280
  end
275
281
 
276
282
  # Creates a HexaPDF::Layout::TextBox like #text_box but allows parts of the text to be
@@ -294,7 +300,8 @@ module HexaPDF
294
300
  # If any style properties are set, the used style is duplicated and the additional
295
301
  # properties applied.
296
302
  #
297
- # See #text_box for details on +width+, +height+, +style+, +style_properties+ and +box_style+.
303
+ # See #text_box for details on +width+, +height+, +style+, +style_properties+, +properties+
304
+ # and +box_style+.
298
305
  #
299
306
  # Examples:
300
307
  #
@@ -305,7 +312,8 @@ module HexaPDF
305
312
  # layout.formatted_text_box(["Some ", {text: "string", style: {font_size: 20}}])
306
313
  #
307
314
  # See: #text_box, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment
308
- def formatted_text_box(data, width: 0, height: 0, style: nil, box_style: nil, **style_properties)
315
+ def formatted_text_box(data, width: 0, height: 0, style: nil, properties: nil, box_style: nil,
316
+ **style_properties)
309
317
  style = retrieve_style(style, style_properties)
310
318
  box_style = (box_style ? retrieve_style(box_style) : style)
311
319
  data.map! do |hash|
@@ -315,10 +323,15 @@ module HexaPDF
315
323
  link = hash.delete(:link)
316
324
  (hash[:overlays] ||= []) << [:link, {uri: link}] if link
317
325
  text = hash.delete(:text) || link || ""
318
- HexaPDF::Layout::TextFragment.create(text, retrieve_style(hash.delete(:style) || style, hash))
326
+ properties = hash.delete(:properties)
327
+ frag_style = retrieve_style(hash.delete(:style) || style, hash)
328
+ fragment = HexaPDF::Layout::TextFragment.create(text, frag_style)
329
+ fragment.properties.update(properties) if properties
330
+ fragment
319
331
  end
320
332
  end
321
- box_class_for_name(:text).new(items: data, width: width, height: height, style: box_style)
333
+ box_class_for_name(:text).new(items: data, width: width, height: height,
334
+ properties: properties, style: box_style)
322
335
  end
323
336
 
324
337
  # Creates a HexaPDF::Layout::ImageBox for the given image.
@@ -326,7 +339,8 @@ module HexaPDF
326
339
  # The +file+ argument can be anything that is accepted by HexaPDF::Document::Images#add or a
327
340
  # HexaPDF::Type::Form object.
328
341
  #
329
- # See #text_box for details on +width+, +height+, +style+ and +style_properties+.
342
+ # See #text_box for details on +width+, +height+, +style+, +style_properties+ and
343
+ # +properties+.
330
344
  #
331
345
  # Examples:
332
346
  #
@@ -334,10 +348,11 @@ module HexaPDF
334
348
  # layout.image_box(machu_picchu, height: 30)
335
349
  #
336
350
  # See: HexaPDF::Layout::ImageBox
337
- def image_box(file, width: 0, height: 0, style: nil, **style_properties)
351
+ def image_box(file, width: 0, height: 0, properties: nil, style: nil, **style_properties)
338
352
  style = retrieve_style(style, style_properties)
339
353
  image = file.kind_of?(HexaPDF::Stream) ? file : @document.images.add(file)
340
- box_class_for_name(:image).new(image: image, width: width, height: height, style: style)
354
+ box_class_for_name(:image).new(image: image, width: width, height: height,
355
+ properties: properties, style: style)
341
356
  end
342
357
 
343
358
  # :nodoc:
@@ -116,8 +116,14 @@ module HexaPDF
116
116
  # * Style#underlays
117
117
  attr_reader :style
118
118
 
119
+ # Hash with custom properties. The keys should be strings and can be arbitrary.
120
+ #
121
+ # This can be used to store arbitrary information on boxes for later use. For example, a
122
+ # generic style layer could use one or more custom properties for its work.
123
+ attr_reader :properties
124
+
119
125
  # :call-seq:
120
- # Box.new(width: 0, height: 0, style: nil) {|canv, box| block} -> box
126
+ # Box.new(width: 0, height: 0, style: nil, properties: {}) {|canv, box| block} -> box
121
127
  #
122
128
  # Creates a new Box object with the given width and height that uses the provided block when
123
129
  # it is asked to draw itself on a canvas (see #draw).
@@ -125,10 +131,11 @@ module HexaPDF
125
131
  # Since the final location of the box is not known beforehand, the drawing operations inside
126
132
  # the block should draw inside the rectangle (0, 0, content_width, content_height) - note that
127
133
  # the width and height of the box may not be known beforehand.
128
- def initialize(width: 0, height: 0, style: nil, &block)
134
+ def initialize(width: 0, height: 0, style: nil, properties: {}, &block)
129
135
  @width = @initial_width = width
130
136
  @height = @initial_height = height
131
137
  @style = Style.create(style)
138
+ @properties = properties
132
139
  @draw_block = block
133
140
  @fit_successful = false
134
141
  @split_box = false
@@ -192,13 +192,14 @@ module HexaPDF
192
192
 
193
193
  # Adds the given item at the end of the item list.
194
194
  #
195
- # If both the item and the last item in the item list are TextFragment objects and they have
196
- # the same style, they are combined.
195
+ # If both the item and the last item in the item list are TextFragment objects with the same
196
+ # attributes, they are combined.
197
197
  #
198
198
  # Note: The cache is not cleared!
199
199
  def add(item)
200
200
  last = @items.last
201
- if last.instance_of?(item.class) && item.kind_of?(TextFragment) && last.style == item.style
201
+ if last.instance_of?(item.class) && item.kind_of?(TextFragment) &&
202
+ last.attributes_hash == item.attributes_hash
202
203
  if last.items.frozen?
203
204
  @items[-1] = last = last.dup
204
205
  last.items = last.items.dup
@@ -382,8 +382,9 @@ module HexaPDF
382
382
  class Layers
383
383
 
384
384
  # Creates a new Layers object popuplated with the given +layers+.
385
- def initialize(layers = [])
386
- @layers = layers
385
+ def initialize(layers = nil)
386
+ @layers = []
387
+ layers&.each {|name, options| add(name, **(options || {})) }
387
388
  end
388
389
 
389
390
  # Duplicates the array holding the layers.
@@ -402,8 +403,8 @@ module HexaPDF
402
403
  # object in 'style.layers_map'. In this case +name+ is used as the reference and the options
403
404
  # are passed to layer object if it needs initialization.
404
405
  def add(name = nil, **options, &block)
405
- if block_given?
406
- @layers << block
406
+ if block_given? || name.kind_of?(Proc)
407
+ @layers << (block || name)
407
408
  elsif name
408
409
  @layers << [name, options]
409
410
  else
@@ -452,7 +453,9 @@ module HexaPDF
452
453
  # be specified):
453
454
  #
454
455
  # +dest+::
455
- # The destination array or a name of a named destination for in-document links.
456
+ # The destination array or a name of a named destination for in-document links. If neither
457
+ # +dest+ nor +uri+ nor +file+ is specified, it is assumed that the box has a custom
458
+ # property named 'link' which is used for the destination.
456
459
  #
457
460
  # +uri+::
458
461
  # The URI to link to.
@@ -473,6 +476,7 @@ module HexaPDF
473
476
  # Examples:
474
477
  # LinkLayer.new(dest: [page, :XYZ, nil, nil, nil], border: true)
475
478
  # LinkLayer.new(uri: "https://my.example.com/path", border: [5 5 2])
479
+ # LinkLayer.new # use 'link' custom box property for dest
476
480
  def initialize(dest: nil, uri: nil, file: nil, border: false, border_color: nil)
477
481
  if dest && (uri || file) || uri && file
478
482
  raise ArgumentError, "Only one of dest, uri and file is allowed"
@@ -496,6 +500,8 @@ module HexaPDF
496
500
  # page.
497
501
  def call(canvas, box)
498
502
  return unless canvas.context.type == :Page
503
+ @dest = box.properties['link'] unless @dest || @action
504
+
499
505
  page = canvas.context
500
506
  matrix = canvas.graphics_state.ctm
501
507
  quad_points = [*matrix.evaluate(0, 0), *matrix.evaluate(box.width, 0),
@@ -554,7 +560,7 @@ module HexaPDF
554
560
  # Duplicates the complex properties that can be modified, as well as the cache.
555
561
  def initialize_copy(other)
556
562
  super
557
- @scaled_item_widths = {}
563
+ @scaled_item_widths = {}.compare_by_identity
558
564
  clear_cache
559
565
 
560
566
  @font_features = @font_features.dup if defined?(@font_features)
@@ -105,9 +105,29 @@ module HexaPDF
105
105
  #
106
106
  # The argument +style+ can either be a Style object or a hash of style properties, see
107
107
  # Style::create for details.
108
- def initialize(items, style)
108
+ def initialize(items, style, properties: nil)
109
109
  @items = items
110
110
  @style = Style.create(style)
111
+ @properties = properties
112
+ end
113
+
114
+ # Creates a new TextFragment with the same style and custom properties as this one but with
115
+ # the given +items+.
116
+ def dup_attributes(items)
117
+ self.class.new(items, @style, properties: @properties.dup)
118
+ end
119
+
120
+ # Returns the custom properties hash for the text fragment.
121
+ #
122
+ # See Box#properties for usage details.
123
+ def properties
124
+ @properties ||= {}
125
+ end
126
+
127
+ # Returns the value that should be used as hash key when only the fragment's attributes -
128
+ # without the items - should play a role.
129
+ def attributes_hash
130
+ @style.hash ^ @properties.hash
111
131
  end
112
132
 
113
133
  # The precision used to determine whether two floats represent the same value.
@@ -243,43 +243,38 @@ module HexaPDF
243
243
  box_items << glyph if glyph && !glyph.kind_of?(Numeric) && glyph.str == '-'
244
244
 
245
245
  unless box_items.empty?
246
- result << Box.new(TextFragment.new(box_items.freeze, item.style))
246
+ result << Box.new(item.dup_attributes(box_items.freeze))
247
247
  end
248
248
 
249
249
  if glyph
250
250
  case glyph.str
251
251
  when ' '
252
- glues[item.style] ||=
253
- Glue.new(TextFragment.new([glyph].freeze, item.style))
254
- result << glues[item.style]
252
+ result << (glues[item.attributes_hash] ||=
253
+ Glue.new(item.dup_attributes([glyph].freeze)))
255
254
  when "\n", "\v", "\f", "\u{85}", "\u{2029}"
256
- penalties[item.style] ||=
257
- Penalty.new(Penalty::PARAGRAPH_BREAK, 0,
258
- item: TextFragment.new([].freeze, item.style))
259
- result << penalties[item.style]
255
+ result << (penalties[item.attributes_hash] ||=
256
+ Penalty.new(Penalty::PARAGRAPH_BREAK, 0,
257
+ item: item.dup_attributes([].freeze)))
260
258
  when "\u{2028}"
261
259
  result << Penalty.new(Penalty::LINE_BREAK, 0,
262
- item: TextFragment.new([].freeze, item.style))
260
+ item: item.dup_attributes([].freeze))
263
261
  when "\r"
264
262
  if !item.items[i + 1] || item.items[i + 1].kind_of?(Numeric) ||
265
263
  item.items[i + 1].str != "\n"
266
- penalties[item.style] ||=
267
- Penalty.new(Penalty::PARAGRAPH_BREAK, 0,
268
- item: TextFragment.new([].freeze, item.style))
269
- result << penalties[item.style]
264
+ result << (penalties[item.attributes_hash] ||=
265
+ Penalty.new(Penalty::PARAGRAPH_BREAK, 0,
266
+ item: item.dup_attributes([].freeze)))
270
267
  end
271
268
  when '-'
272
269
  result << Penalty::Standard
273
270
  when "\t"
274
271
  spaces = [item.style.font.decode_utf8(" ").first] * 8
275
- result << Glue.new(TextFragment.new(spaces.freeze, item.style))
272
+ result << Glue.new(item.dup_attributes(spaces.freeze))
276
273
  when "\u{00AD}"
277
- hyphen = item.style.font.decode_utf8("-").first
278
- frag = TextFragment.new([hyphen].freeze, item.style)
274
+ frag = item.dup_attributes([item.style.font.decode_utf8("-").first].freeze)
279
275
  result << Penalty.new(Penalty::Standard.penalty, frag.width, item: frag)
280
276
  when "\u{00A0}"
281
- space = item.style.font.decode_utf8(" ").first
282
- frag = TextFragment.new([space].freeze, item.style)
277
+ frag = item.dup_attributes([item.style.font.decode_utf8(" ").first].freeze)
283
278
  result << Penalty.new(Penalty::ProhibitedBreak.penalty, frag.width, item: frag)
284
279
  when "\u{200B}"
285
280
  result << Penalty.new(0)
@@ -835,7 +830,7 @@ module HexaPDF
835
830
  if too_wide_box && (too_wide_box.item.kind_of?(TextFragment) &&
836
831
  too_wide_box.item.items.size > 1)
837
832
  rest[0..rest.index(too_wide_box)] = too_wide_box.item.items.map do |item|
838
- Box.new(TextFragment.new([item].freeze, too_wide_box.item.style))
833
+ Box.new(too_wide_box.item.dup_attributes([item].freeze))
839
834
  end
840
835
  too_wide_box = nil
841
836
  else
@@ -939,8 +934,7 @@ module HexaPDF
939
934
  frag = line.items[indexes[i]]
940
935
  value = -frag.items[indexes[i + 1]].width * adjustment
941
936
  if frag.items.frozen?
942
- value = HexaPDF::Layout::TextFragment.new([value], frag.style)
943
- line.items.insert(indexes[i], value)
937
+ line.items.insert(indexes[i], frag.dup_attributes([value]))
944
938
  else
945
939
  frag.items.insert(indexes[i + 1], value)
946
940
  frag.clear_cache
@@ -243,14 +243,21 @@ module HexaPDF
243
243
  @objects.each do |oid, gen, obj|
244
244
  if @xref_section.entry?(oid, gen)
245
245
  stored_obj = @loader.call(@xref_section[oid, gen])
246
- next if (stored_obj.type == :ObjStm || stored_obj.type == :XRef) && obj.null?
246
+ next if (stored_obj.type == :ObjStm || stored_obj.type == :XRef) && obj.null? ||
247
+ stored_obj.type == :Sig || stored_obj.type == :DocTimeStamp
247
248
 
248
249
  streams_are_same = (obj.data.stream == stored_obj.data.stream)
249
250
  next if obj.value == stored_obj.value && streams_are_same
250
251
 
251
252
  if obj.value.kind_of?(Hash) && stored_obj.value.kind_of?(Hash)
252
253
  keys = obj.value.keys | stored_obj.value.keys
253
- next if keys.all? {|key| obj[key] == stored_obj[key] } && streams_are_same
254
+ values_unchanged = keys.all? do |key|
255
+ other = stored_obj[key]
256
+ # Force comparison of values if both are indirect objects
257
+ other = other.value if other.kind_of?(Object) && !other.indirect?
258
+ obj[key] == other
259
+ end
260
+ next if values_unchanged && streams_are_same
254
261
  end
255
262
  end
256
263
 
@@ -203,13 +203,6 @@ module HexaPDF
203
203
 
204
204
  private
205
205
 
206
- # Parses the stream data after the object is first initialized. Since the parsed stream data
207
- # is cached, it is only parsed on initialization and not again if e.g. the stream is changed.
208
- def after_data_change
209
- super
210
- parse_stream
211
- end
212
-
213
206
  # Parses the object numbers and their offsets from the start of the stream data.
214
207
  def parse_oids_and_offsets(data)
215
208
  oids = []
@@ -227,7 +220,12 @@ module HexaPDF
227
220
 
228
221
  # Returns the container with the to-be-stored objects.
229
222
  def objects
230
- @objects ||= {}
223
+ @objects ||=
224
+ begin
225
+ @objects = {}
226
+ parse_stream
227
+ @objects
228
+ end
231
229
  end
232
230
 
233
231
  # Validates that the generation number of the object stream is zero.
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.26.1'
40
+ VERSION = '0.26.2'
41
41
 
42
42
  end
@@ -90,12 +90,14 @@ describe HexaPDF::Document::Layout do
90
90
 
91
91
  describe "box" do
92
92
  it "creates the request box" do
93
- box = @layout.box(:column, columns: 3, gaps: 20, width: 15, height: 30, style: {font_size: 10})
93
+ box = @layout.box(:column, columns: 3, gaps: 20, width: 15, height: 30, style: {font_size: 10},
94
+ properties: {key: :value})
94
95
  assert_equal(15, box.width)
95
96
  assert_equal(30, box.height)
96
97
  assert_equal([-1, -1, -1], box.columns)
97
98
  assert_equal([20], box.gaps)
98
99
  assert_equal(10, box.style.font_size)
100
+ assert_equal({key: :value}, box.properties)
99
101
  end
100
102
 
101
103
  it "allows specifying the box's children via a provided block" do
@@ -113,13 +115,14 @@ describe HexaPDF::Document::Layout do
113
115
 
114
116
  describe "text_box" do
115
117
  it "creates a text box" do
116
- box = @layout.text_box("Test", width: 10, height: 15)
118
+ box = @layout.text_box("Test", width: 10, height: 15, properties: {key: :value})
117
119
  assert_equal(10, box.width)
118
120
  assert_equal(15, box.height)
119
121
  assert_same(@doc.fonts.add("Times"), box.style.font)
120
122
  items = box.instance_variable_get(:@items)
121
123
  assert_equal(1, items.length)
122
124
  assert_same(box.style, items.first.style)
125
+ assert_equal({key: :value}, box.properties)
123
126
  end
124
127
 
125
128
  it "allows setting of a custom style" do
@@ -162,6 +165,11 @@ describe HexaPDF::Document::Layout do
162
165
  assert_equal(1, box.instance_variable_get(:@items).length)
163
166
  end
164
167
 
168
+ it "allows setting custom properties on the whole box" do
169
+ box = @layout.formatted_text_box(["Test"], properties: {key: :value})
170
+ assert_equal({key: :value}, box.properties)
171
+ end
172
+
165
173
  it "allows using a hash with :text key instead of a simple string" do
166
174
  box = @layout.formatted_text_box([{text: "Test"}])
167
175
  items = box.instance_variable_get(:@items)
@@ -218,18 +226,26 @@ describe HexaPDF::Document::Layout do
218
226
  assert_equal([:link, {uri: 'URI'}], items[0].style.overlays.instance_variable_get(:@layers)[0])
219
227
  refute(items[2].style.overlays?)
220
228
  end
229
+
230
+ it "allows setting custom properties" do
231
+ box = @layout.formatted_text_box([{text: 'test', properties: {named_dest: 'test'}}])
232
+ items = box.instance_variable_get(:@items)
233
+ assert_equal({named_dest: 'test'}, items[0].properties)
234
+ end
221
235
  end
222
236
 
223
237
  describe "image_box" do
224
238
  it "creates an image box" do
225
239
  image_path = File.join(TEST_DATA_DIR, 'images', 'gray.jpg')
226
240
 
227
- box = @layout.image_box(image_path, width: 10, height: 15, style: {font_size: 20}, subscript: true)
241
+ box = @layout.image_box(image_path, width: 10, height: 15, style: {font_size: 20},
242
+ properties: {key: :value}, subscript: true)
228
243
  assert_equal(10, box.width)
229
244
  assert_equal(15, box.height)
230
245
  assert_equal(20, box.style.font_size)
231
246
  assert(box.style.subscript)
232
247
  assert_same(@doc.images.add(image_path), box.image)
248
+ assert_equal({key: :value}, box.properties)
233
249
  end
234
250
 
235
251
  it "allows using a form XObject" do
@@ -54,6 +54,11 @@ describe HexaPDF::Layout::Box do
54
54
  box = create_box(style: HexaPDF::Layout::Style.new(padding: 20))
55
55
  assert_equal(20, box.style.padding.top)
56
56
  end
57
+
58
+ it "allows setting custom properties" do
59
+ box = create_box(properties: {'key' => :value})
60
+ assert_equal({'key' => :value}, box.properties)
61
+ end
57
62
  end
58
63
 
59
64
  it "returns false when asking whether it is a split box by default" do
@@ -50,10 +50,12 @@ describe HexaPDF::Layout::Line do
50
50
 
51
51
  it "combines text fragments if possible" do
52
52
  frag1 = setup_fragment("Home")
53
- frag2 = HexaPDF::Layout::TextFragment.new(frag1.items.slice!(2, 2), frag1.style)
54
- @line << setup_fragment("o") << :other << frag1 << frag2
55
- assert_equal(3, @line.items.length)
56
- assert_equal(4, @line.items.last.items.length)
53
+ frag2 = HexaPDF::Layout::TextFragment.new(frag1.items[2, 2], frag1.style)
54
+ frag3 = HexaPDF::Layout::TextFragment.new(frag1.items[2, 2], frag1.style,
55
+ properties: {'key' => :value})
56
+ @line << setup_fragment("o") << :other << frag1 << frag2 << frag3
57
+ assert_equal(4, @line.items.length)
58
+ assert_equal(6, @line.items[-2].items.length)
57
59
  end
58
60
 
59
61
  it "duplicates the first of two combinable text fragments if its items are frozen" do
@@ -448,12 +448,16 @@ end
448
448
  describe HexaPDF::Layout::Style::Layers do
449
449
  before do
450
450
  @layers = HexaPDF::Layout::Style::Layers.new
451
+ value = Object.new
452
+ value.define_singleton_method(:new) {|*| :new }
453
+ @config = Object.new
454
+ @config.define_singleton_method(:constantize) {|*| value }
451
455
  end
452
456
 
453
457
  it "can be initialized with an array of layers" do
454
- data = [lambda {}]
458
+ data = [lambda {}, [:test]]
455
459
  layers = HexaPDF::Layout::Style::Layers.new(data)
456
- assert_equal(data, layers.enum_for(:each, {}).to_a)
460
+ assert_equal([data[0], :new], layers.enum_for(:each, @config).to_a)
457
461
  end
458
462
 
459
463
  it "can be duplicated" do
@@ -469,13 +473,15 @@ describe HexaPDF::Layout::Style::Layers do
469
473
  assert_equal([block], @layers.enum_for(:each, {}).to_a)
470
474
  end
471
475
 
476
+ it "can use a given proc" do
477
+ block = proc { true }
478
+ @layers.add(block)
479
+ assert_equal([block], @layers.enum_for(:each, {}).to_a)
480
+ end
481
+
472
482
  it "can store a reference" do
473
483
  @layers.add(:link, option: :value)
474
- value = Object.new
475
- value.define_singleton_method(:new) {|*| :new }
476
- config = Object.new
477
- config.define_singleton_method(:constantize) {|*| value }
478
- assert_equal([:new], @layers.enum_for(:each, config).to_a)
484
+ assert_equal([:new], @layers.enum_for(:each, @config).to_a)
479
485
  end
480
486
 
481
487
  it "fails if neither a block nor a name is given when adding a layer" do
@@ -590,6 +596,13 @@ describe HexaPDF::Layout::Style::LinkLayer do
590
596
  assert_equal({S: :Launch, F: "local-file.pdf", NewWindow: true}, annot[:A].value)
591
597
  assert_nil(annot[:Dest])
592
598
  end
599
+
600
+ it "works for destinations set via the 'link' custom box property" do
601
+ @box.properties['link'] = [@canvas.context, :FitH]
602
+ annot = call_link({})
603
+ assert_equal([@canvas.context, :FitH], annot[:Dest].value)
604
+ assert_nil(annot[:A])
605
+ end
593
606
  end
594
607
  end
595
608
 
@@ -46,11 +46,36 @@ describe HexaPDF::Layout::TextFragment do
46
46
  end
47
47
 
48
48
  it "can use style options" do
49
- frag = HexaPDF::Layout::TextFragment.new(@items, font: @font, font_size: 20)
49
+ frag = HexaPDF::Layout::TextFragment.new(@items, {font: @font, font_size: 20})
50
50
  assert_equal(20, frag.style.font_size)
51
51
  end
52
52
  end
53
53
 
54
+ it "allows duplicating with only its attributes while also setting new items" do
55
+ setup_fragment([20])
56
+ @fragment.properties['key'] = :value
57
+ frag = @fragment.dup_attributes([21])
58
+ assert_equal([21], frag.items)
59
+ assert_same(frag.style, @fragment.style)
60
+ assert_equal(:value, frag.properties['key'])
61
+ end
62
+
63
+ it "creates an attributes hash for storing the fragment based on the attributes without the items" do
64
+ setup_fragment([20])
65
+ hash = @fragment.attributes_hash
66
+ @fragment.properties['key'] = :value
67
+ new_hash = @fragment.attributes_hash
68
+ refute_equal(hash, new_hash)
69
+ @fragment.items << [21]
70
+ assert_equal(new_hash, @fragment.attributes_hash)
71
+ end
72
+
73
+ it "allows setting custom properties" do
74
+ setup_fragment([])
75
+ @fragment.properties[:key] = :value
76
+ assert_equal({key: :value}, @fragment.properties)
77
+ end
78
+
54
79
  it "returns :text for valign" do
55
80
  assert_equal(:text, setup_fragment([]).valign)
56
81
  end
@@ -31,12 +31,14 @@ module TestTextLayouterHelpers
31
31
  else
32
32
  assert_same(item.style, obj.item.style)
33
33
  assert_equal(item.items, obj.item.items)
34
+ assert_equal(item.properties, obj.item.properties)
34
35
  end
35
36
  end
36
37
 
37
38
  def assert_glue(obj, fragment)
38
39
  assert_kind_of(HexaPDF::Layout::TextLayouter::Glue, obj)
39
40
  assert_same(fragment.style, obj.item.style)
41
+ assert_equal(fragment.properties, obj.item.properties)
40
42
  end
41
43
 
42
44
  def assert_penalty(obj, penalty, item = nil)
@@ -45,6 +47,7 @@ module TestTextLayouterHelpers
45
47
  if item
46
48
  assert_same(item.style, obj.item.style)
47
49
  assert_equal(item.items, obj.item.items)
50
+ assert_equal(item.properties, obj.item.properties)
48
51
  end
49
52
  end
50
53
 
@@ -83,12 +86,10 @@ describe HexaPDF::Layout::TextLayouter::SimpleTextSegmentation do
83
86
  @obj = HexaPDF::Layout::TextLayouter::SimpleTextSegmentation
84
87
  end
85
88
 
86
- def setup_fragment(text, style = nil)
87
- if style
88
- HexaPDF::Layout::TextFragment.create(text, style)
89
- else
90
- HexaPDF::Layout::TextFragment.create(text, font: @font)
91
- end
89
+ def setup_fragment(text, style = {font: @font})
90
+ fragment = HexaPDF::Layout::TextFragment.create(text, style)
91
+ fragment.properties['key'] = :value
92
+ fragment
92
93
  end
93
94
 
94
95
  it "handles InlineBox objects" do
@@ -138,11 +139,13 @@ describe HexaPDF::Layout::TextLayouter::SimpleTextSegmentation do
138
139
  assert_equal([], result[index].item.items)
139
140
  assert(result[index].item.items.frozen?)
140
141
  assert_same(frag.style, result[index].item.style)
142
+ assert_equal(frag.properties, result[index].item.properties)
141
143
  end
142
144
  assert_penalty(result[15], HexaPDF::Layout::TextLayouter::Penalty::LINE_BREAK)
143
145
  assert_equal([], result[15].item.items)
144
146
  assert(result[15].item.items.frozen?)
145
147
  assert_same(frag.style, result[15].item.style)
148
+ assert_equal(frag.properties, result[15].item.properties)
146
149
  end
147
150
 
148
151
  it "insert a standard penalty after a hyphen" do
@@ -86,24 +86,15 @@ describe HexaPDF::Composer do
86
86
  end
87
87
 
88
88
  describe "style" do
89
- it "creates a new style if it does not exist based on the base argument" do
90
- @composer.style(:base, font_size: 20)
91
- assert_equal(20, @composer.style(:newstyle, subscript: true).font_size)
92
- refute(@composer.style(:base).subscript)
93
- assert_equal(10, @composer.style(:another_new, base: nil).font_size)
94
- assert(@composer.style(:yet_another_new, base: :newstyle).subscript)
95
- end
96
-
97
- it "returns the named style" do
98
- assert_kind_of(HexaPDF::Layout::Style, @composer.style(:base))
99
- end
100
-
101
- it "updates the style with the given properties" do
102
- assert_equal(20, @composer.style(:base, font_size: 20).font_size)
89
+ it "delegates to layout.style" do
90
+ @composer.document.layout.style(:base, font_size: 20)
91
+ assert_equal(20, @composer.style(:base).font_size)
92
+ @composer.style(:base, font_size: 30)
93
+ assert_equal(30, @composer.document.layout.style(:base).font_size)
103
94
  end
104
95
  end
105
96
 
106
- describe "text" do
97
+ describe "text/formatted_text/image/box" do
107
98
  before do
108
99
  test_self = self
109
100
  @composer.define_singleton_method(:draw_box) do |arg|
@@ -111,151 +102,36 @@ describe HexaPDF::Composer do
111
102
  end
112
103
  end
113
104
 
114
- it "creates a text box and draws it on the canvas" do
115
- @composer.text("Test", width: 10, height: 15)
105
+ it "delegates #text to layout.text" do
106
+ @composer.text("Test", width: 10, height: 15, style: {font_size: 20},
107
+ box_style: {font_size: 30}, line_spacing: 2)
116
108
  assert_equal(10, @box.width)
117
109
  assert_equal(15, @box.height)
118
- assert_same(@composer.document.fonts.add("Times"), @box.style.font)
110
+ assert_equal(30, @box.style.font_size)
119
111
  items = @box.instance_variable_get(:@items)
120
112
  assert_equal(1, items.length)
121
- assert_same(@box.style, items.first.style)
113
+ assert_same(20, items.first.style.font_size)
122
114
  end
123
115
 
124
- it "allows setting of a custom style" do
125
- style = HexaPDF::Layout::Style.new(font_size: 20, font: ['Times', {variant: :bold}])
126
- @composer.text("Test", style: style)
127
- assert_same(@box.style, style)
128
- assert_same(@composer.document.fonts.add("Times", variant: :bold), @box.style.font)
129
- assert_equal(20, @box.style.font_size)
130
-
131
- @composer.text("Test", style: {font_size: 20})
132
- assert_equal(20, @box.style.font_size)
133
-
134
- @composer.style(:named, font_size: 20)
135
- @composer.text("Test", style: :named)
136
- assert_equal(20, @box.style.font_size)
137
- end
138
-
139
- it "updates the used style with the provided options" do
140
- @composer.text("Test", style: {subscript: true}, font_size: 20)
141
- assert_equal(20, @box.style.font_size)
142
- end
143
-
144
- it "allows using a box style different from the text style" do
145
- style = HexaPDF::Layout::Style.new(font_size: 20)
146
- @composer.text("Test", box_style: style)
147
- refute_same(@box.instance_variable_get(:@items).first.style, style)
148
- assert_same(@box.style, style)
149
-
150
- @composer.style(:named, font_size: 20)
151
- @composer.text("Test", box_style: :named)
152
- assert_equal(20, @box.style.font_size)
153
- end
154
- end
155
-
156
- describe "formatted_text" do
157
- before do
158
- test_self = self
159
- @composer.define_singleton_method(:draw_box) do |arg|
160
- test_self.instance_variable_set(:@box, arg)
161
- end
162
- end
163
-
164
- it "creates a text box with the given text and draws it on the canvas" do
116
+ it "delegates #formatted_text to layout.formatted_text" do
165
117
  @composer.formatted_text(["Test"], width: 10, height: 15)
166
118
  assert_equal(10, @box.width)
167
119
  assert_equal(15, @box.height)
168
120
  assert_equal(1, @box.instance_variable_get(:@items).length)
169
121
  end
170
122
 
171
- it "allows using a hash with :text key instead of a simple string" do
172
- @composer.formatted_text([{text: "Test"}])
173
- items = @box.instance_variable_get(:@items)
174
- assert_equal(4, items[0].items.length)
175
- end
176
-
177
- it "uses an empty string if the :text key for a hash is not specified" do
178
- @composer.formatted_text([{font_size: "Test"}])
179
- items = @box.instance_variable_get(:@items)
180
- assert_equal(0, items[0].items.length)
181
- end
182
-
183
- it "allows setting a custom base style for all parts" do
184
- @composer.formatted_text(["Test", "other"], font_size: 20)
185
- items = @box.instance_variable_get(:@items)
186
- assert_equal(20, @box.style.font_size)
187
- assert_equal(20, items[0].style.font_size)
188
- assert_equal(20, items[1].style.font_size)
189
- end
190
-
191
- it "allows using custom style properties for a single part" do
192
- @composer.formatted_text([{text: "Test", font_size: 20}, "test"], align: :center)
193
- items = @box.instance_variable_get(:@items)
194
- assert_equal(10, @box.style.font_size)
195
-
196
- assert_equal(20, items[0].style.font_size)
197
- assert_equal(:center, items[0].style.align)
198
-
199
- assert_equal(10, items[1].style.font_size)
200
- assert_equal(:center, items[1].style.align)
201
- end
202
-
203
- it "allows using a custom style as basis for a single part" do
204
- @composer.formatted_text([{text: "Test", style: {font_size: 20}, subscript: true}, "test"],
205
- align: :center)
206
- items = @box.instance_variable_get(:@items)
207
- assert_equal(10, @box.style.font_size)
208
-
209
- assert_equal(20, items[0].style.font_size)
210
- assert_equal(:left, items[0].style.align)
211
- assert(items[0].style.subscript)
212
-
213
- assert_equal(10, items[1].style.font_size)
214
- assert_equal(:center, items[1].style.align)
215
- refute(items[1].style.subscript)
216
- end
217
-
218
- it "allows specifying a link to an URL via the :link key" do
219
- @composer.formatted_text([{text: "Test", link: "URI"}, {link: "URI"}, "test"])
220
- items = @box.instance_variable_get(:@items)
221
- assert_equal(3, items.length)
222
- assert_equal(4, items[0].items.length, "text should be Test")
223
- assert_equal(3, items[1].items.length, "text should be URI")
224
- assert_equal([:link, {uri: 'URI'}], items[0].style.overlays.instance_variable_get(:@layers)[0])
225
- refute(items[2].style.overlays?)
226
- end
227
- end
228
-
229
- describe "image" do
230
- it "creates an image box and draws it on the canvas" do
231
- box = nil
232
- @composer.define_singleton_method(:draw_box) {|arg| box = arg }
233
- image_path = File.join(TEST_DATA_DIR, 'images', 'gray.jpg')
234
-
235
- @composer.image(image_path, width: 10, height: 15, style: {font_size: 20}, subscript: true)
236
- assert_equal(10, box.width)
237
- assert_equal(15, box.height)
238
- assert_equal(20, box.style.font_size)
239
- assert(box.style.subscript)
240
- assert_same(@composer.document.images.add(image_path), box.image)
241
- end
242
-
243
- it "allows using a form XObject" do
123
+ it "delegates #image to layout.image" do
244
124
  form = @composer.document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 10, 10]})
245
125
  @composer.image(form, width: 10)
246
- assert_equal(796, @composer.y)
247
- assert_equal(36, @composer.x)
126
+ assert_equal(10, @box.width)
127
+ assert_equal(0, @box.height)
248
128
  end
249
- end
250
129
 
251
- describe "box" do
252
- it "creates the named box and draws it on the canvas" do
253
- box = nil
254
- @composer.define_singleton_method(:draw_box) {|arg| box = arg }
130
+ it "delegates #box to layout.box" do
255
131
  image = @composer.document.images.add(File.join(TEST_DATA_DIR, 'images', 'gray.jpg'))
256
132
  @composer.box(:list, width: 20) {|list| list.image(image) }
257
- assert_equal(20, box.width)
258
- assert_same(image, box.children[0].image)
133
+ assert_equal(20, @box.width)
134
+ assert_same(image, @box.children[0].image)
259
135
  end
260
136
  end
261
137
 
@@ -25,10 +25,13 @@ describe HexaPDF::Revision do
25
25
  HexaPDF::Object.new(nil, oid: entry.oid, gen: entry.gen)
26
26
  else
27
27
  case entry.oid
28
+ when 2 then HexaPDF::Dictionary.new({Type: :Sig}, oid: entry.oid, gen: entry.gen)
28
29
  when 4 then HexaPDF::Dictionary.new({Type: :XRef}, oid: entry.oid, gen: entry.gen)
29
30
  when 5 then HexaPDF::Dictionary.new({Type: :ObjStm}, oid: entry.oid, gen: entry.gen)
30
31
  when 7 then HexaPDF::Type::Catalog.new({Type: :Catalog}, oid: entry.oid, gen: entry.gen,
31
32
  document: self)
33
+ when 6 then HexaPDF::Dictionary.new({Array: HexaPDF::PDFArray.new([1, 2])},
34
+ oid: entry.oid, gen: entry.gen)
32
35
  else HexaPDF::Object.new(:Test, oid: entry.oid, gen: entry.gen)
33
36
  end
34
37
  end
@@ -43,7 +46,7 @@ describe HexaPDF::Revision do
43
46
 
44
47
  it "takes an xref section and/or a parser on initialization" do
45
48
  rev = HexaPDF::Revision.new({}, loader: @loader, xref_section: @xref_section)
46
- assert_equal(:Test, rev.object(2).value)
49
+ assert_equal({Type: :Sig}, rev.object(2).value)
47
50
  end
48
51
 
49
52
  it "returns the next free object number" do
@@ -100,7 +103,7 @@ describe HexaPDF::Revision do
100
103
 
101
104
  it "loads an object that is defined in the cross-reference section" do
102
105
  obj = @rev.object(HexaPDF::Reference.new(2, 0))
103
- assert_equal(:Test, obj.value)
106
+ assert_equal({Type: :Sig}, obj.value)
104
107
  assert_equal(2, obj.oid)
105
108
  assert_equal(0, obj.gen)
106
109
  end
@@ -190,7 +193,7 @@ describe HexaPDF::Revision do
190
193
 
191
194
  describe "each_modified_object" do
192
195
  it "returns modified objects" do
193
- obj = @rev.object(2)
196
+ obj = @rev.object(3)
194
197
  obj.value = :Other
195
198
  @rev.add(@obj)
196
199
  deleted = @rev.object(6)
@@ -214,6 +217,18 @@ describe HexaPDF::Revision do
214
217
  obj.delete(:Type)
215
218
  assert_equal([], @rev.each_modified_object.to_a)
216
219
  end
220
+
221
+ it "doesn't return dictionaries that have direct HexaPDF::Object child objects" do
222
+ obj = @rev.object(6)
223
+ obj[:Array] = HexaPDF::PDFArray.new([1, 2]) # same value but differen #data instance
224
+ assert_equal([], @rev.each_modified_object.to_a)
225
+ end
226
+
227
+ it "doesn't return signature objects" do
228
+ obj = @rev.object(2)
229
+ obj[:x] = :y
230
+ assert_equal([], @rev.each_modified_object.to_a)
231
+ end
217
232
  end
218
233
 
219
234
  describe "reset_objects" do
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.26.1)>>
43
+ <</Producer(HexaPDF version 0.26.2)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.26.1)>>
75
+ <</Producer(HexaPDF version 0.26.2)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -206,7 +206,7 @@ describe HexaPDF::Writer do
206
206
  <</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
207
207
  endobj
208
208
  5 0 obj
209
- <</Producer(HexaPDF version 0.26.1)>>
209
+ <</Producer(HexaPDF version 0.26.2)>>
210
210
  endobj
211
211
  4 0 obj
212
212
  <</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream
@@ -30,6 +30,12 @@ describe HexaPDF::Type::ObjectStream do
30
30
  stream: "1 0 5 2 5 [1 2]")
31
31
  end
32
32
 
33
+ it "parses an associated stream the first time the stored objects are accessed" do
34
+ assert_nil(@obj.instance_variable_get(:@objects))
35
+ assert_equal(0, @obj.object_index(HexaPDF::Reference.new(1, 0)))
36
+ assert_equal(1, @obj.object_index(HexaPDF::Reference.new(5, 0)))
37
+ end
38
+
33
39
  it "correctly parses stream data" do
34
40
  data = @obj.parse_stream
35
41
  assert_equal([5, 1], data.object_by_index(0))
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.1
4
+ version: 0.26.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-14 00:00:00.000000000 Z
11
+ date: 2022-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -706,7 +706,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
706
706
  - !ruby/object:Gem::Version
707
707
  version: '0'
708
708
  requirements: []
709
- rubygems_version: 3.3.3
709
+ rubygems_version: 3.2.32
710
710
  signing_key:
711
711
  specification_version: 4
712
712
  summary: HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby