hexapdf 1.2.0 → 1.3.0

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +1 -1
  4. data/lib/hexapdf/cli/inspect.rb +13 -4
  5. data/lib/hexapdf/composer.rb +14 -0
  6. data/lib/hexapdf/configuration.rb +5 -0
  7. data/lib/hexapdf/document/annotations.rb +60 -2
  8. data/lib/hexapdf/document/layout.rb +45 -6
  9. data/lib/hexapdf/error.rb +11 -3
  10. data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
  11. data/lib/hexapdf/layout/style.rb +101 -7
  12. data/lib/hexapdf/object.rb +2 -2
  13. data/lib/hexapdf/pdf_array.rb +25 -3
  14. data/lib/hexapdf/tokenizer.rb +4 -1
  15. data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
  16. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  17. data/lib/hexapdf/type/acro_form/form.rb +7 -6
  18. data/lib/hexapdf/type/annotation.rb +12 -0
  19. data/lib/hexapdf/type/annotations/appearance_generator.rb +75 -0
  20. data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
  21. data/lib/hexapdf/type/annotations/circle.rb +65 -0
  22. data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
  23. data/lib/hexapdf/type/annotations/line.rb +4 -35
  24. data/lib/hexapdf/type/annotations/square.rb +65 -0
  25. data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
  26. data/lib/hexapdf/type/annotations/widget.rb +50 -20
  27. data/lib/hexapdf/type/annotations.rb +5 -0
  28. data/lib/hexapdf/version.rb +1 -1
  29. data/test/hexapdf/document/test_annotations.rb +22 -0
  30. data/test/hexapdf/document/test_layout.rb +24 -2
  31. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  32. data/test/hexapdf/layout/test_style.rb +27 -2
  33. data/test/hexapdf/test_composer.rb +7 -0
  34. data/test/hexapdf/test_object.rb +1 -1
  35. data/test/hexapdf/test_pdf_array.rb +36 -3
  36. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
  37. data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
  38. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  39. data/test/hexapdf/type/acro_form/test_form.rb +17 -1
  40. data/test/hexapdf/type/annotations/test_appearance_generator.rb +84 -0
  41. data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
  42. data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
  43. data/test/hexapdf/type/annotations/test_line.rb +0 -20
  44. data/test/hexapdf/type/annotations/test_widget.rb +35 -0
  45. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 041330c9846091186ddeecd7900a7919bb96da26b3513b047f03c87888efedcd
4
- data.tar.gz: ff89d47f1eeac1d2dda4982224403318528401918ccfe7f1bb775d05d8f0e6f3
3
+ metadata.gz: e0e469ae650b98b48c88b662dab27cb405b676c706639c8c1cf358d6aefc8f53
4
+ data.tar.gz: d170526233e1e9aa37403c1bd8d2aa38697641c1b4fb4d4a5f6d8f02f299585b
5
5
  SHA512:
6
- metadata.gz: 508fa118a26d5825f9ae2a105cdf4717b02901fd3095c49cf5855caae7648bf60b9cea414bb6670dd3bd0c82f99b076a8df7ffb79efa60990003468d7f3a9db9
7
- data.tar.gz: 613323b8b2a93a01e31ec9e35664e6dc2939b210bbe7d7c055cb861eac6cd530978b14256a55a8e51f2e3804be06808440d5816c73e808df4af3f111f325b9c9
6
+ metadata.gz: fd18408ed2c2474e3395bf59b3a0281cb23005d16bf7d5b9d93f673dfa131fc215f342018d0cdce2de053db85c000a68c28a76ad980b0432b1a159bd51b1ed0c
7
+ data.tar.gz: c58b13ed27c980bb9670b9521a12b1640fc3960aea1188f6135951cfd1503a28e7633cb3f6be8c63d478d64a88e0bbdc9b84bdbc982aa3ec000aeac1a8627597
data/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## 1.3.0 - 2025-04-23
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Type::Annotations::Square] for rectangle annotations as well as
6
+ [HexaPDF::Document::Annotations#create_rectangle]
7
+ * [HexaPDF::Type::Annotations::Circle] for ellipse annotations as well as
8
+ [HexaPDF::Document::Annotations#create_ellipse]
9
+ * Basic appearance generation for push button fields
10
+ * [HexaPDF::Type::Annotation::BorderEffect] type class
11
+ * [HexaPDF::Type::Annotations::BorderEffect] module that provides convenience
12
+ access to the border effect dictionary
13
+ * [HexaPDF::Document::Layout#style?] and [HexaPDF::Composer#style?] for checking
14
+ whether a given style (name) exists
15
+ * [HexaPDF::Layout::Style#each_property] for iterating over all set properties
16
+ * [HexaPDF::Layout::Style#merge] for merging another style instance
17
+ * [HexaPDF::Layout::Style#box_options] for specifying box initialization options
18
+ * [HexaPDF::Layout::Style#font_bold] and [HexaPDF::Layout::Style#font_italic]
19
+ for setting bold and/or italic variants independently of the font name
20
+ * [HexaPDF::PDFArray#map!] for mapping elements in-place
21
+ * [HexaPDF::PDFArray#compact!] for removing `nil` elements
22
+
23
+ ### Changed
24
+
25
+ * **Breaking change**: [HexaPDF::Type::Annotations::Widget::MarkerStyle::new]
26
+ got a new positional argument
27
+ * [HexaPDF::Type::Annotations::Widget#marker_style] to allow setting and
28
+ retrieving the font for push buttons
29
+ * Extracted `#interior_color` from [HexaPDF::Type::Annotations::Line] into
30
+ [HexaPDF::Type::Annotations::InteriorColor]
31
+ * CLI command `hexapdf inspect` to support decoding Form XObject streams
32
+ * [HexaPDF::Layout::Style#line_spacing] to accept a `LineSpacing` object when
33
+ setting the value
34
+
35
+ ### Fixed
36
+
37
+ * Text extraction with macOS Preview due a bug in Preview
38
+ * [HexaPDF::PDFArray#reject!] to work according to documented method signature
39
+ * [HexaPDF::Type::AcroForm::Field#create_widget] to ensure the proper type
40
+ class is stored in the document in case an embedded widget is extracted
41
+ * [HexaPDF::Type::AcroForm::Form] validation to ensure that all field objects in
42
+ the field hierarchy are using a field type class
43
+ * [HexaPDF::Type::AcroForm::Form] validation to delete merged fields
44
+
45
+
1
46
  ## 1.2.0 - 2025-02-10
2
47
 
3
48
  ### Added
data/README.md CHANGED
@@ -82,7 +82,7 @@ canvas.text("Hello World!", at: [20, 400])
82
82
  doc.write("hello-world.pdf")
83
83
  ~~~
84
84
 
85
- For detailed information have a look at the [HexaPDF website][website] where you will the API
85
+ For detailed information have a look at the [HexaPDF website][website] where you will find the API
86
86
  documentation, example code and more.
87
87
 
88
88
  It is recommend to use the HTML API documentation provided by the HexaPDF website as it is enhanced
@@ -188,12 +188,20 @@ module HexaPDF
188
188
  end
189
189
  serialize(obj.value, recursive: true) if obj
190
190
 
191
- when 's', 'stream', 'raw', 'raw-stream'
191
+ when 's', 'stream', 'raw', 'raw-stream', 'sd'
192
192
  if (obj = pdf_object_from_string_reference(data.shift) rescue $stderr.puts($!.message)) &&
193
193
  obj.kind_of?(HexaPDF::Stream)
194
- source = (command.start_with?('raw') ? obj.stream_source : obj.stream_decoder)
195
- while source.alive? && (stream_data = source.resume)
196
- $stdout.write(stream_data)
194
+ if command == 'sd'
195
+ if obj.respond_to?(:process_contents)
196
+ obj.process_contents(ContentProcessor.new)
197
+ else
198
+ $stderr.puts("Error: The object is not a Form XObject or page")
199
+ end
200
+ else
201
+ source = (command.start_with?('raw') ? obj.stream_source : obj.stream_decoder)
202
+ while source.alive? && (stream_data = source.resume)
203
+ $stdout.write(stream_data)
204
+ end
197
205
  end
198
206
  elsif command_parser.verbosity_info?
199
207
  $stderr.puts("Note: Object has no stream data")
@@ -427,6 +435,7 @@ module HexaPDF
427
435
  ["OID[,GEN] | o[bject] OID[,GEN]", "Print object"],
428
436
  ["r[ecursive] OID[,GEN]", "Print object recursively"],
429
437
  ["s[tream] OID[,GEN]", "Print filtered stream"],
438
+ ["sd OID[,GEN]", "Print the decoded stream of a Form XObject or page"],
430
439
  ["raw[-stream] OID[,GEN]", "Print raw stream"],
431
440
  ["rev[ision] [NUMBER]", "Print or extract revision"],
432
441
  ["x[ref] OID[,GEN]", "Print the cross-reference entry"],
@@ -261,6 +261,20 @@ module HexaPDF
261
261
  @document.layout.style(name, base: base, **properties)
262
262
  end
263
263
 
264
+ # Returns +true+ if a style with the given +name+ exists, else +false+.
265
+ #
266
+ # See HexaPDF::Document::Layout#style for details; this method is just a thin wrapper around
267
+ # that method.
268
+ #
269
+ # Example:
270
+ #
271
+ # composer.style(:header, font: 'Helvetica')
272
+ # composer.style?(:header) # => true
273
+ # composer.style?(:paragraph) # => false
274
+ def style?(name)
275
+ @document.layout.style?(name)
276
+ end
277
+
264
278
  # :call-seq:
265
279
  # composer.styles -> styles
266
280
  # composer.styles(**mapping) -> styles
@@ -709,6 +709,7 @@ module HexaPDF
709
709
  XXAcroFormField: 'HexaPDF::Type::AcroForm::Field',
710
710
  XXAppearanceDictionary: 'HexaPDF::Type::Annotation::AppearanceDictionary',
711
711
  Border: 'HexaPDF::Type::Annotation::Border',
712
+ XXBorderEffect: 'HexaPDF::Type::Annotation::BorderEffect',
712
713
  SigFieldLock: 'HexaPDF::Type::AcroForm::SignatureField::LockDictionary',
713
714
  SV: 'HexaPDF::Type::AcroForm::SignatureField::SeedValueDictionary',
714
715
  SVCert: 'HexaPDF::Type::AcroForm::SignatureField::CertificateSeedValueDictionary',
@@ -766,6 +767,8 @@ module HexaPDF
766
767
  Link: 'HexaPDF::Type::Annotations::Link',
767
768
  Widget: 'HexaPDF::Type::Annotations::Widget',
768
769
  Line: 'HexaPDF::Type::Annotations::Line',
770
+ Square: 'HexaPDF::Type::Annotations::Square',
771
+ Circle: 'HexaPDF::Type::Annotations::Circle',
769
772
  XML: 'HexaPDF::Type::Metadata',
770
773
  GTS_PDFX: 'HexaPDF::Type::OutputIntent',
771
774
  GTS_PDFA1: 'HexaPDF::Type::OutputIntent',
@@ -795,6 +798,8 @@ module HexaPDF
795
798
  Link: 'HexaPDF::Type::Annotations::Link',
796
799
  Widget: 'HexaPDF::Type::Annotations::Widget',
797
800
  Line: 'HexaPDF::Type::Annotations::Line',
801
+ Square: 'HexaPDF::Type::Annotations::Square',
802
+ Circle: 'HexaPDF::Type::Annotations::Circle',
798
803
  },
799
804
  XXAcroFormField: {
800
805
  Tx: 'HexaPDF::Type::AcroForm::TextField',
@@ -69,12 +69,12 @@ module HexaPDF
69
69
  # +create_type+ method.
70
70
  #
71
71
  # The +options+ are passed on the specific annotation creation method.
72
- def create(type, page, **options)
72
+ def create(type, page, *args, **options)
73
73
  method_name = "create_#{type}"
74
74
  unless respond_to?(method_name)
75
75
  raise ArgumentError, "Invalid type specified"
76
76
  end
77
- send("create_#{type}", page, **options)
77
+ send("create_#{type}", page, *args, **options)
78
78
  end
79
79
 
80
80
  # :call-seq:
@@ -100,6 +100,64 @@ module HexaPDF
100
100
  border_style(color: 0, width: 1)
101
101
  end
102
102
 
103
+ # :call-seq:
104
+ # annotations.create_rectangle(page, x, y, width, height) -> annotation
105
+ #
106
+ # Creates a rectangle (called "square" in the PDF specification) annotation with the
107
+ # lower-left corner at (+x+, +y+) and the given +width+ and +height+.
108
+ #
109
+ # The rectangle uses a black stroke color, no interior color and a line width of 1pt by
110
+ # default. It can be further styled using the convenience methods on the returned annotation
111
+ # object.
112
+ #
113
+ # Example:
114
+ #
115
+ # #>pdf-small
116
+ # doc.annotations.create_rectangle(doc.pages[0], 20, 20, 20, 60).
117
+ # regenerate_appearance
118
+ #
119
+ # doc.annotations.create_rectangle(doc.pages[0], 60, 20, 20, 60).
120
+ # border_style(color: "hp-blue", width: 2).
121
+ # interior_color("hp-orange").
122
+ # regenerate_appearance
123
+ #
124
+ # See: Type::Annotations::Square
125
+ def create_rectangle(page, x, y, w, h)
126
+ annot = create_and_add_to_page(:Square, page)
127
+ annot[:Rect] = [x, y, x + w, y + h]
128
+ annot.border_style(color: 0, width: 1)
129
+ annot
130
+ end
131
+
132
+ # :call-seq:
133
+ # annotations.create_ellipse(page, cx, cy, a:, b:) -> annotation
134
+ #
135
+ # Creates an ellipse (called "circle" in the PDF specification) annotation with the center
136
+ # point at (+cx+, +cy+), the semi-major axis +a+ and the semi-minor axis +b+.
137
+ #
138
+ # The ellipse uses a black stroke color, no interior color and a line width of 1pt by
139
+ # default. It can be further styled using the convenience methods on the returned annotation
140
+ # object.
141
+ #
142
+ # Example:
143
+ #
144
+ # #>pdf-small
145
+ # doc.annotations.create_ellipse(doc.pages[0], 30, 50, a: 15, b: 20).
146
+ # regenerate_appearance
147
+ #
148
+ # doc.annotations.create_ellipse(doc.pages[0], 70, 50, a: 15, b: 20).
149
+ # border_style(color: "hp-blue", width: 2).
150
+ # interior_color("hp-orange").
151
+ # regenerate_appearance
152
+ #
153
+ # See: Type::Annotations::Circle
154
+ def create_ellipse(page, x, y, a:, b:)
155
+ annot = create_and_add_to_page(:Circle, page)
156
+ annot[:Rect] = [x - a, y - b, x + a, y + b]
157
+ annot.border_style(color: 0, width: 1)
158
+ annot
159
+ end
160
+
103
161
  private
104
162
 
105
163
  # Returns the root of the destinations name tree.
@@ -218,6 +218,19 @@ module HexaPDF
218
218
  style
219
219
  end
220
220
 
221
+ # Returns +true+ if a style with the given +name+ exists, else +false+.
222
+ #
223
+ # Example:
224
+ #
225
+ # layout.style(:header, font: 'Helvetica')
226
+ # layout.style?(:header) # => true
227
+ # layout.style?(:paragraph) # => false
228
+ #
229
+ # See: #style
230
+ def style?(name)
231
+ @styles.key?(name)
232
+ end
233
+
221
234
  # :call-seq:
222
235
  # layout.styles -> styles
223
236
  # layout.styles(**mapping) -> styles
@@ -286,8 +299,9 @@ module HexaPDF
286
299
  box_options[:children] = ChildrenCollector.collect(self, &block)
287
300
  end
288
301
  end
302
+ style = retrieve_style(style)
289
303
  box_class_for_name(name).new(width: width, height: height,
290
- style: retrieve_style(style), **box_options, &box_block)
304
+ style: style, **style.box_options, **box_options, &box_block)
291
305
  end
292
306
 
293
307
  # Creates an array of HexaPDF::Layout::TextFragment objects for the given +text+.
@@ -354,7 +368,7 @@ module HexaPDF
354
368
  box_style = (box_style ? retrieve_style(box_style) : style)
355
369
  box_class_for_name(:text).new(items: text_fragments(text, style: style),
356
370
  width: width, height: height, properties: properties,
357
- style: box_style)
371
+ style: box_style, **box_style.box_options)
358
372
  end
359
373
  alias text text_box
360
374
 
@@ -457,7 +471,8 @@ module HexaPDF
457
471
  end
458
472
  end
459
473
  box_class_for_name(:text).new(items: data, width: width, height: height,
460
- properties: properties, style: box_style)
474
+ properties: properties, style: box_style,
475
+ **box_style.box_options)
461
476
  end
462
477
  alias formatted_text formatted_text_box
463
478
 
@@ -479,7 +494,7 @@ module HexaPDF
479
494
  style = retrieve_style(style, style_properties)
480
495
  image = file.kind_of?(HexaPDF::Stream) ? file : @document.images.add(file)
481
496
  box_class_for_name(:image).new(image: image, width: width, height: height,
482
- properties: properties, style: style)
497
+ properties: properties, style: style, **style.box_options)
483
498
  end
484
499
  alias image image_box
485
500
 
@@ -608,7 +623,8 @@ module HexaPDF
608
623
  end
609
624
  box_class_for_name(:table).new(cells: cells, column_widths: column_widths, header: header,
610
625
  footer: footer, cell_style: cell_style, width: width,
611
- height: height, properties: properties, style: style)
626
+ height: height, properties: properties, style: style,
627
+ **style.box_options)
612
628
  end
613
629
  alias table table_box
614
630
 
@@ -666,6 +682,22 @@ module HexaPDF
666
682
  end
667
683
  end
668
684
 
685
+ FONT_BOLD_VARIANT_MAPPER = { #:nodoc:
686
+ nil => {true => :bold, false: :none},
687
+ none: {true => :bold, false: :none},
688
+ bold: {true => :bold, false: :none},
689
+ italic: {true => :bold_italic, false: :italic},
690
+ bold_italic: {true => :bold_italic, false: :italic},
691
+ }
692
+
693
+ FONT_ITALIC_VARIANT_MAPPER = { #:nodoc:
694
+ nil => {true => :italic, false: :none},
695
+ none: {true => :italic, false: :none},
696
+ italic: {true => :italic, false: :none},
697
+ bold: {true => :bold_italic, false: :bold},
698
+ bold_italic: {true => :bold_italic, false: :bold},
699
+ }
700
+
669
701
  # Retrieves the appropriate HexaPDF::Layout::Style object based on the +style+ and
670
702
  # +properties+ arguments.
671
703
  #
@@ -690,7 +722,14 @@ module HexaPDF
690
722
  end
691
723
  unless style.font.respond_to?(:pdf_object)
692
724
  name, options = *style.font
693
- style.font(@document.fonts.add(name, **(options || {})))
725
+ options ||= {}
726
+ if style.font_bold?
727
+ options[:variant] = FONT_BOLD_VARIANT_MAPPER.dig(options[:variant], style.font_bold)
728
+ end
729
+ if style.font_italic?
730
+ options[:variant] = FONT_ITALIC_VARIANT_MAPPER.dig(options[:variant], style.font_italic)
731
+ end
732
+ style.font(@document.fonts.add(name, **options))
694
733
  end
695
734
  style
696
735
  end
data/lib/hexapdf/error.rb CHANGED
@@ -94,9 +94,17 @@ module HexaPDF
94
94
  end
95
95
 
96
96
  def message # :nodoc:
97
- "No glyph for #{glyph.str.inspect} in font '#{glyph.font_wrapper.wrapped_font.full_name}' " \
98
- "found. \n\n" \
99
- "Use the configuration option 'font.on_missing_glyph' to customize missing glyph handling."
97
+ str = "No glyph for #{glyph.str.inspect} in font '#{glyph.font_wrapper.wrapped_font.full_name}' " \
98
+ "found. \n\n"
99
+ str << if glyph.font_wrapper.font_type == :Type1
100
+ "The used Type1 font only contains a very limited number of glyphs. TrueType " \
101
+ "fonts usually provide a much wider array of glyphs. Use the configuration option " \
102
+ "'font.map' to register appropriate font files. Also have a look at the " \
103
+ "'font.default' and 'font.fallback' options. "
104
+ else
105
+ "Maybe register another #{glyph.font_wrapper.font_type} font that contains the " \
106
+ "needed glyph and use it as fallback via the configuration option 'font.fallback'."
107
+ end
100
108
  end
101
109
 
102
110
  end
@@ -63,6 +63,16 @@ module HexaPDF
63
63
  def use_glyph(glyph_id)
64
64
  return @glyph_map[glyph_id] if @glyph_map.key?(glyph_id)
65
65
  @last_id += 1
66
+ # Handle codes for ASCII characters \r (13), (, ) (40, 41) and \ (92) specially so that
67
+ # they never appear in the output (PDF serialization would need to escape them)
68
+ if @last_id == 13 || @last_id == 40 || @last_id == 92
69
+ @glyph_map[:"s#{@last_id}"] = @last_id
70
+ if @last_id == 40
71
+ @last_id += 1
72
+ @glyph_map[:"s#{@last_id}"] = @last_id
73
+ end
74
+ @last_id += 1
75
+ end
66
76
  @glyph_map[glyph_id] = @last_id
67
77
  end
68
78
 
@@ -107,7 +117,7 @@ module HexaPDF
107
117
  locations = []
108
118
 
109
119
  @glyph_map.each_key do |old_gid|
110
- glyph = orig_glyf[old_gid]
120
+ glyph = orig_glyf[old_gid.kind_of?(Symbol) ? 0 : old_gid]
111
121
  locations << table.size
112
122
  data = glyph.raw_data
113
123
  if glyph.compound?
@@ -166,7 +176,10 @@ module HexaPDF
166
176
  # Adds the components of compound glyphs to the subset.
167
177
  def add_glyph_components
168
178
  glyf = @font[:glyf]
169
- @glyph_map.keys.each {|gid| glyf[gid].components&.each {|cgid| use_glyph(cgid) } }
179
+ @glyph_map.keys.each do |gid|
180
+ next if gid.kind_of?(Symbol)
181
+ glyf[gid].components&.each {|cgid| use_glyph(cgid) }
182
+ end
170
183
  end
171
184
 
172
185
  end
@@ -603,11 +603,36 @@ module HexaPDF
603
603
  # style.update(**properties) -> style
604
604
  #
605
605
  # Updates the style's properties using the key-value pairs specified by the +properties+ hash.
606
+ #
607
+ # Also see: #merge
606
608
  def update(**properties)
607
609
  properties.each {|key, value| send(key, value) }
608
610
  self
609
611
  end
610
612
 
613
+ # Yields all set properties.
614
+ def each_property # :yield: property, value
615
+ return to_enum(__method__) unless block_given?
616
+ instance_variables.each do |iv|
617
+ (val = PROPERTIES[iv]) && yield(val, instance_variable_get(iv))
618
+ end
619
+ end
620
+
621
+ # :call-seq:
622
+ # style.merge(other_style) -> style
623
+ #
624
+ # Merges the set properties of the +other_style+ object into this one.
625
+ #
626
+ # Note that merging is done on a per-property basis. So if a complex property is set on
627
+ # +other_style+ and also on +self+, the +other_style+ value completely overwrites the one from
628
+ # +self+.
629
+ #
630
+ # Also see: #update
631
+ def merge(other)
632
+ other.each_property {|property, value| send(property, value) }
633
+ self
634
+ end
635
+
611
636
  ##
612
637
  # :method: font
613
638
  # :call-seq:
@@ -615,8 +640,9 @@ module HexaPDF
615
640
  #
616
641
  # The font to be used, must be set to a valid font wrapper object before it can be used.
617
642
  #
618
- # HexaPDF::Composer handles this property specially in that it resolves a set string or array
619
- # to a font wrapper object before doing anything else with the style object.
643
+ # HexaPDF::Document::Layout handles this property - together with #font_bold and #font_italic
644
+ # - specially in that it resolves a set string or array to a font wrapper object before doing
645
+ # anything else with the style object.
620
646
  #
621
647
  # This is the only style property without a default value!
622
648
  #
@@ -633,6 +659,48 @@ module HexaPDF
633
659
  # composer.text("Courier Bold", font: "Courier bold")
634
660
  # composer.text("Courier Bold also", font: ["Courier", variant: :bold])
635
661
 
662
+ ##
663
+ # :method: font_bold
664
+ # :call-seq:
665
+ # font_bold(bold = false)
666
+ #
667
+ # Specifies whether the bold variant of the font is used.
668
+ #
669
+ # Note that this property only has affect if #font is not already set to a font wrapper
670
+ # object and if it is set explicitly (i.e. #font_bold? returns +true+).
671
+ #
672
+ # See #font, #font_italic
673
+ #
674
+ # Examples:
675
+ #
676
+ # #>pdf-composer100
677
+ # composer.text("Helvetica bold", font: "Helvetica", font_bold: true)
678
+ #
679
+ # helvetica_bold = composer.document.fonts.add("Helvetica", variant: :bold)
680
+ # composer.text("Helvetica bold", font: helvetica_bold, font_bold: false)
681
+ # composer.text("Helvetica", font: ["Helvetica", {variant: :bold}], font_bold: false)
682
+
683
+ ##
684
+ # :method: font_italic
685
+ # :call-seq:
686
+ # font_italic(bold = false)
687
+ #
688
+ # Specifies whether the italic variant of the font is used.
689
+ #
690
+ # Note that this property only has affect if #font is not already set to a font wrapper
691
+ # object and if it is set explicitly (i.e. #font_italic? returns +true+).
692
+ #
693
+ # See #font, #font_bold.
694
+ #
695
+ # Examples:
696
+ #
697
+ # #>pdf-composer100
698
+ # composer.text("Helvetica italic", font: "Helvetica", font_italic: true)
699
+ #
700
+ # helvetica_bold = composer.document.fonts.add("Helvetica", variant: :italic)
701
+ # composer.text("Helvetica italic", font: helvetica_bold, font_italic: false)
702
+ # composer.text("Helvetica", font: ["Helvetica", {variant: :italic}], font_italic: false)
703
+
636
704
  ##
637
705
  # :method: font_size
638
706
  # :call-seq:
@@ -1021,7 +1089,7 @@ module HexaPDF
1021
1089
  #
1022
1090
  # This method can set the line spacing in two ways:
1023
1091
  #
1024
- # * Using two positional arguments +type+ and +value+.
1092
+ # * Using the positional, mandatory argument +type+ and the optional +value+.
1025
1093
  # * Or a hash with the keys +type+ and +value+.
1026
1094
  #
1027
1095
  # Note that the last line has no additional spacing after it by default. Set #last_line_gap
@@ -1422,8 +1490,33 @@ module HexaPDF
1422
1490
  # composer.text("This is some longer text that does not appear in two lines.",
1423
1491
  # height: 15, overflow: :truncate)
1424
1492
 
1425
- [
1493
+ ##
1494
+ # :method: box_options
1495
+ # :call-seq:
1496
+ # box_options(**options)
1497
+ #
1498
+ # Contains initialization arguments for the box instance that is created with this
1499
+ # style. Together with the other style properties this allows the complete specification of a
1500
+ # box instance just via a Style instance.
1501
+ #
1502
+ # Note that this property is only used by the HexaPDF::Document::Layout methods when a box
1503
+ # instance is created. If a box instance is created directly, this property has no effect.
1504
+ #
1505
+ # Examples:
1506
+ #
1507
+ # #>pdf-composer100
1508
+ # composer.style(:my_list, box_options: {marker_type: :decimal, item_spacing: 15})
1509
+ # composer.list(style: :my_list) do |list|
1510
+ # list.text("This is some text.")
1511
+ # list.text("This is some other text.")
1512
+ # end
1513
+
1514
+
1515
+ # :nodoc:
1516
+ PROPERTIES = [
1426
1517
  [:font, "raise HexaPDF::Error, 'No font set'"],
1518
+ [:font_bold, false],
1519
+ [:font_italic, false],
1427
1520
  [:font_size, 10],
1428
1521
  [:line_height, nil],
1429
1522
  [:character_spacing, 0],
@@ -1457,8 +1550,8 @@ module HexaPDF
1457
1550
  [:text_valign, :top, {valid_values: [:top, :center, :bottom]}],
1458
1551
  [:text_indent, 0],
1459
1552
  [:line_spacing, "LineSpacing.new(type: :single)",
1460
- {setter: "LineSpacing.new(**(value.kind_of?(Symbol) || value.kind_of?(Numeric) ? " \
1461
- "{type: value, value: extra_arg} : value))",
1553
+ {setter: "LineSpacing.new(**(value.kind_of?(Symbol) || value.kind_of?(Numeric) || " \
1554
+ "value.kind_of?(LineSpacing) ? {type: value, value: extra_arg} : value))",
1462
1555
  extra_args: ", extra_arg = nil"}],
1463
1556
  [:last_line_gap, false, {valid_values: [true, false]}],
1464
1557
  [:fill_horizontal, nil],
@@ -1475,6 +1568,7 @@ module HexaPDF
1475
1568
  [:mask_mode, :default, {valid_values: [:default, :none, :box, :fill_horizontal,
1476
1569
  :fill_frame_horizontal, :fill_vertical, :fill]}],
1477
1570
  [:overflow, :error],
1571
+ [:box_options, {}],
1478
1572
  ].each do |name, default, options = {}|
1479
1573
  default = default.inspect unless default.kind_of?(String)
1480
1574
  setter = options.delete(:setter) || "value"
@@ -1500,7 +1594,7 @@ module HexaPDF
1500
1594
  end
1501
1595
  EOF
1502
1596
  alias_method("#{name}=", name)
1503
- end
1597
+ end.each_with_object({}) {|arr, hash| hash[:"@#{arr.first}"] = arr.first }
1504
1598
 
1505
1599
  ##
1506
1600
  # :method: text_segmentation_algorithm
@@ -305,8 +305,8 @@ module HexaPDF
305
305
  result
306
306
  rescue HexaPDF::Error
307
307
  raise
308
- rescue StandardError
309
- yield("Error: Unexpected value encountered", false, self) if block_given?
308
+ rescue StandardError => e
309
+ yield("Unexpected error encountered: #{e.message}", false, self) if block_given?
310
310
  false
311
311
  end
312
312
 
@@ -143,10 +143,32 @@ module HexaPDF
143
143
  # array.reject! {|item| block } -> array or nil
144
144
  # array.reject! -> Enumerator
145
145
  #
146
- # Deletes all elements from the array for which the block returns +true+. If no changes were
147
- # done, returns +nil+.
146
+ # Deletes all elements from the array for which the block returns +true+ and returns +self+. If
147
+ # no changes were done, returns +nil+.
148
148
  def reject!
149
- value.reject! {|item| yield(process_entry(item)) }
149
+ return to_enum(__method__) unless block_given?
150
+ value.reject! {|item| yield(process_entry(item)) } && self
151
+ end
152
+
153
+ # :call-seq:
154
+ # array.map! {|item| block } -> array
155
+ # array.map! -> Enumerator
156
+ #
157
+ # Maps all elements from the array in-place to the respective return value of the block+ and
158
+ # returns +self+.
159
+ def map!
160
+ return to_enum(__method__) unless block_given?
161
+ value.map! {|item| yield(process_entry(item)) }
162
+ self
163
+ end
164
+
165
+ # :call-seq:
166
+ # array.compact! -> array or nil
167
+ #
168
+ # Removes all +nil+ elements from the array. Returns +self+ if any elements were removed, +nil+
169
+ # otherwise.
170
+ def compact!
171
+ value.compact! && self
150
172
  end
151
173
 
152
174
  # :call-seq:
@@ -278,6 +278,9 @@ module HexaPDF
278
278
 
279
279
  REFERENCE_RE = /[#{WHITESPACE}]+([+]?\d+)[#{WHITESPACE}]+R#{WHITESPACE_OR_DELIMITER_RE}/ # :nodoc:
280
280
 
281
+ WHITESPACE_OR_DELIMITER_LUT = [] # :nodoc:
282
+ (WHITESPACE + DELIMITER).each_byte {|x| WHITESPACE_OR_DELIMITER_LUT[x] = true }
283
+
281
284
  # Parses the number (integer or real) at the current position.
282
285
  #
283
286
  # See: PDF2.0 s7.3.3
@@ -285,7 +288,7 @@ module HexaPDF
285
288
  prepare_string_scanner(40)
286
289
  pos = self.pos
287
290
  if (tmp = @ss.scan_integer)
288
- if @ss.eos? || @ss.match?(WHITESPACE_OR_DELIMITER_RE)
291
+ if @ss.eos? || WHITESPACE_OR_DELIMITER_LUT[@ss.peek_byte]
289
292
  # Handle object references, see PDF2.0 s7.3.10
290
293
  prepare_string_scanner(10)
291
294
  if @ss.scan(REFERENCE_RE)