hexapdf 0.26.1 → 0.26.2

Sign up to get free protection for your applications and to get access to all the features.
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