hexapdf 1.1.1 → 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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +1 -1
  4. data/lib/hexapdf/cli/command.rb +63 -63
  5. data/lib/hexapdf/cli/inspect.rb +14 -5
  6. data/lib/hexapdf/cli/modify.rb +0 -1
  7. data/lib/hexapdf/cli/optimize.rb +5 -5
  8. data/lib/hexapdf/composer.rb +14 -0
  9. data/lib/hexapdf/configuration.rb +26 -0
  10. data/lib/hexapdf/content/graphics_state.rb +1 -1
  11. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
  12. data/lib/hexapdf/document/annotations.rb +173 -0
  13. data/lib/hexapdf/document/layout.rb +45 -6
  14. data/lib/hexapdf/document.rb +28 -7
  15. data/lib/hexapdf/error.rb +11 -3
  16. data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
  17. data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
  18. data/lib/hexapdf/font/type1_wrapper.rb +1 -0
  19. data/lib/hexapdf/layout/style.rb +101 -7
  20. data/lib/hexapdf/object.rb +2 -2
  21. data/lib/hexapdf/pdf_array.rb +25 -3
  22. data/lib/hexapdf/tokenizer.rb +4 -1
  23. data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
  24. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  25. data/lib/hexapdf/type/acro_form/form.rb +7 -6
  26. data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
  27. data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
  28. data/lib/hexapdf/type/annotation.rb +71 -1
  29. data/lib/hexapdf/type/annotations/appearance_generator.rb +348 -0
  30. data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
  31. data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
  32. data/lib/hexapdf/type/annotations/circle.rb +65 -0
  33. data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
  34. data/lib/hexapdf/type/annotations/line.rb +490 -0
  35. data/lib/hexapdf/type/annotations/square.rb +65 -0
  36. data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
  37. data/lib/hexapdf/type/annotations/widget.rb +52 -116
  38. data/lib/hexapdf/type/annotations.rb +8 -0
  39. data/lib/hexapdf/type/form.rb +2 -2
  40. data/lib/hexapdf/version.rb +1 -1
  41. data/lib/hexapdf/writer.rb +0 -1
  42. data/lib/hexapdf/xref_section.rb +7 -4
  43. data/test/hexapdf/content/test_graphics_state.rb +2 -3
  44. data/test/hexapdf/content/test_operator.rb +4 -5
  45. data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
  46. data/test/hexapdf/digital_signature/test_handler.rb +2 -3
  47. data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
  48. data/test/hexapdf/document/test_annotations.rb +55 -0
  49. data/test/hexapdf/document/test_layout.rb +24 -2
  50. data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
  51. data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
  52. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  53. data/test/hexapdf/layout/test_style.rb +27 -2
  54. data/test/hexapdf/task/test_optimize.rb +1 -1
  55. data/test/hexapdf/test_composer.rb +7 -0
  56. data/test/hexapdf/test_document.rb +11 -3
  57. data/test/hexapdf/test_object.rb +1 -1
  58. data/test/hexapdf/test_pdf_array.rb +36 -3
  59. data/test/hexapdf/test_stream.rb +1 -2
  60. data/test/hexapdf/test_xref_section.rb +1 -1
  61. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
  62. data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
  63. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  64. data/test/hexapdf/type/acro_form/test_form.rb +17 -1
  65. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
  66. data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
  67. data/test/hexapdf/type/annotations/test_appearance_generator.rb +482 -0
  68. data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
  69. data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
  70. data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
  71. data/test/hexapdf/type/annotations/test_line.rb +169 -0
  72. data/test/hexapdf/type/annotations/test_widget.rb +35 -81
  73. data/test/hexapdf/type/test_annotation.rb +55 -0
  74. data/test/hexapdf/type/test_form.rb +6 -0
  75. metadata +17 -2
@@ -0,0 +1,173 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2025 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'hexapdf/dictionary'
38
+ require 'hexapdf/error'
39
+
40
+ module HexaPDF
41
+ class Document
42
+
43
+ # This class provides methods for creating and managing the annotations of a PDF file.
44
+ #
45
+ # An annotation is an object that can be added to a certain location on a page, provides a
46
+ # visual appearance and allows for interaction with the user via keyboard and mouse.
47
+ #
48
+ # == Usage
49
+ #
50
+ # To create an annotation either call the general #create method or a specific creation method
51
+ # for an annotation type. After the annotation has been created customize it using the
52
+ # convenience methods on the annotation object. The last step should be the call to
53
+ # +regenerate_appearance+ so that the appearance is generated.
54
+ #
55
+ # See: PDF2.0 s12.5
56
+ class Annotations
57
+
58
+ include Enumerable
59
+
60
+ # Creates a new Annotations object for the given PDF document.
61
+ def initialize(document)
62
+ @document = document
63
+ end
64
+
65
+ # :call-seq:
66
+ # annotations.create(type, page, **options) -> annotation
67
+ #
68
+ # Creates a new annotation object with the given +type+ and +page+ by calling the respective
69
+ # +create_type+ method.
70
+ #
71
+ # The +options+ are passed on the specific annotation creation method.
72
+ def create(type, page, *args, **options)
73
+ method_name = "create_#{type}"
74
+ unless respond_to?(method_name)
75
+ raise ArgumentError, "Invalid type specified"
76
+ end
77
+ send("create_#{type}", page, *args, **options)
78
+ end
79
+
80
+ # :call-seq:
81
+ # annotations.create_line(page, start_point:, end_point:) -> annotation
82
+ #
83
+ # Creates a line annotation from +start_point+ to +end_point+ on the given page and returns
84
+ # it.
85
+ #
86
+ # The line uses a black color and a width of 1pt. It can be further styled using the
87
+ # convenience methods on the returned annotation object.
88
+ #
89
+ # Example:
90
+ #
91
+ # doc.annotations.create_line(doc.pages[0], start_point: [100, 100], end_point: [130, 180]).
92
+ # border_style(color: "blue", width: 2).
93
+ # leader_line_length(10).
94
+ # regenerate_appearance
95
+ #
96
+ # See: Type::Annotations::Line
97
+ def create_line(page, start_point:, end_point:)
98
+ create_and_add_to_page(:Line, page).
99
+ line(*start_point, *end_point).
100
+ border_style(color: 0, width: 1)
101
+ end
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
+
161
+ private
162
+
163
+ # Returns the root of the destinations name tree.
164
+ def create_and_add_to_page(subtype, page)
165
+ annot = @document.add({Type: :Annot, Subtype: subtype})
166
+ (page[:Annots] ||= []) << annot
167
+ annot
168
+ end
169
+
170
+ end
171
+
172
+ end
173
+ end
@@ -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
@@ -123,6 +123,7 @@ module HexaPDF
123
123
  autoload(:Destinations, 'hexapdf/document/destinations')
124
124
  autoload(:Layout, 'hexapdf/document/layout')
125
125
  autoload(:Metadata, 'hexapdf/document/metadata')
126
+ autoload(:Annotations, 'hexapdf/document/annotations')
126
127
 
127
128
  # :call-seq:
128
129
  # Document.open(filename, **docargs) -> doc
@@ -539,6 +540,12 @@ module HexaPDF
539
540
  @destinations ||= Destinations.new(self)
540
541
  end
541
542
 
543
+ # Returns the Annotations object that provides convenience methods for working with annotation
544
+ # objects.
545
+ def annotations
546
+ @annotations ||= Annotations.new(self)
547
+ end
548
+
542
549
  # Returns the Layout object that provides convenience methods for working with the
543
550
  # HexaPDF::Layout classes for document layout.
544
551
  def layout
@@ -726,8 +733,8 @@ module HexaPDF
726
733
  end
727
734
 
728
735
  # :call-seq:
729
- # doc.write(filename, incremental: false, validate: true, update_fields: true, optimize: false) -> [start_xref, section]
730
- # doc.write(io, incremental: false, validate: true, update_fields: true, optimize: false) -> [start_xref, section]
736
+ # doc.write(filename, incremental: false, validate: true, update_fields: true, optimize: false, compact: true) -> [start_xref, section]
737
+ # doc.write(io, incremental: false, validate: true, update_fields: true, optimize: false, compact: true) -> [start_xref, section]
731
738
  #
732
739
  # Writes the document to the given file (in case +io+ is a String) or IO stream. Returns the
733
740
  # file position of the start of the last cross-reference section and the last XRefSection object
@@ -755,7 +762,20 @@ module HexaPDF
755
762
  # optimize::
756
763
  # Optimize the file size by using object and cross-reference streams. This will raise the PDF
757
764
  # version to at least 1.5.
758
- def write(file_or_io, incremental: false, validate: true, update_fields: true, optimize: false)
765
+ #
766
+ # compact::
767
+ # Compact the document by reducing it to a single revision and removing null and unused
768
+ # objects.
769
+ #
770
+ # The initial revision of a document has to contain objects with continuous numbering. If some
771
+ # object numbers refer to free entries, other PDF libraries/viewers might not work
772
+ # correctly. So continuous object numbers are assigned to stay compliant with the
773
+ # specification.
774
+ #
775
+ # Only change this argument to +false+ if you run the optimization task with 'compact: true'
776
+ # beforehand or if you know exactly what you do and what not compacting implies.
777
+ def write(file_or_io, incremental: false, validate: true, update_fields: true, optimize: false,
778
+ compact: true)
759
779
  if update_fields
760
780
  trailer.update_id
761
781
  if @metadata
@@ -774,10 +794,11 @@ module HexaPDF
774
794
  end
775
795
  end
776
796
 
777
- if optimize
778
- task(:optimize, object_streams: :generate)
779
- self.version = '1.5' if version < '1.5'
780
- end
797
+ optimize_opts = {}
798
+ optimize_opts[:object_streams] = :generate if optimize
799
+ optimize_opts[:compact] = true if compact && !incremental
800
+ task(:optimize, **optimize_opts) unless optimize_opts.empty?
801
+ self.version = '1.5' if version < '1.5' if optimize
781
802
 
782
803
  dispatch_message(:before_write)
783
804
 
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
@@ -299,6 +299,7 @@ module HexaPDF
299
299
  dict.font_wrapper = self
300
300
 
301
301
  document.register_listener(:complete_objects) do
302
+ next if dict.null?
302
303
  update_font_name(dict)
303
304
  embed_font(dict, document)
304
305
  complete_width_information(dict)
@@ -270,6 +270,7 @@ module HexaPDF
270
270
  dict.font_wrapper = self
271
271
 
272
272
  document.register_listener(:complete_objects) do
273
+ next if dict.null?
273
274
  min, max = @encoding.code_to_name.keys.minmax
274
275
  dict[:FirstChar] = min
275
276
  dict[:LastChar] = max
@@ -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)