hexapdf 1.2.0 → 1.4.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/README.md +1 -1
  4. data/lib/hexapdf/cli/form.rb +9 -4
  5. data/lib/hexapdf/cli/inspect.rb +13 -4
  6. data/lib/hexapdf/composer.rb +14 -0
  7. data/lib/hexapdf/configuration.rb +15 -0
  8. data/lib/hexapdf/dictionary_fields.rb +1 -1
  9. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -2
  10. data/lib/hexapdf/document/annotations.rb +107 -2
  11. data/lib/hexapdf/document/layout.rb +94 -15
  12. data/lib/hexapdf/document/metadata.rb +10 -3
  13. data/lib/hexapdf/document.rb +9 -0
  14. data/lib/hexapdf/encryption/standard_security_handler.rb +7 -2
  15. data/lib/hexapdf/error.rb +11 -3
  16. data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
  17. data/lib/hexapdf/layout/box.rb +5 -0
  18. data/lib/hexapdf/layout/container_box.rb +63 -28
  19. data/lib/hexapdf/layout/style.rb +129 -20
  20. data/lib/hexapdf/layout/table_box.rb +20 -2
  21. data/lib/hexapdf/object.rb +2 -2
  22. data/lib/hexapdf/pdf_array.rb +25 -3
  23. data/lib/hexapdf/tokenizer.rb +4 -1
  24. data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
  25. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  26. data/lib/hexapdf/type/acro_form/form.rb +7 -6
  27. data/lib/hexapdf/type/annotation.rb +12 -0
  28. data/lib/hexapdf/type/annotations/appearance_generator.rb +169 -16
  29. data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
  30. data/lib/hexapdf/type/annotations/circle.rb +65 -0
  31. data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
  32. data/lib/hexapdf/type/annotations/line.rb +5 -192
  33. data/lib/hexapdf/type/annotations/line_ending_styling.rb +208 -0
  34. data/lib/hexapdf/type/annotations/markup_annotation.rb +0 -1
  35. data/lib/hexapdf/type/annotations/polygon.rb +64 -0
  36. data/lib/hexapdf/type/annotations/polygon_polyline.rb +109 -0
  37. data/lib/hexapdf/type/annotations/polyline.rb +64 -0
  38. data/lib/hexapdf/type/annotations/square.rb +65 -0
  39. data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
  40. data/lib/hexapdf/type/annotations/widget.rb +50 -20
  41. data/lib/hexapdf/type/annotations.rb +9 -0
  42. data/lib/hexapdf/type/measure.rb +57 -0
  43. data/lib/hexapdf/type.rb +1 -0
  44. data/lib/hexapdf/version.rb +1 -1
  45. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +0 -1
  46. data/test/hexapdf/document/test_annotations.rb +42 -0
  47. data/test/hexapdf/document/test_layout.rb +38 -10
  48. data/test/hexapdf/document/test_metadata.rb +13 -1
  49. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -1
  50. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  51. data/test/hexapdf/layout/test_box.rb +8 -0
  52. data/test/hexapdf/layout/test_container_box.rb +34 -6
  53. data/test/hexapdf/layout/test_page_style.rb +1 -1
  54. data/test/hexapdf/layout/test_style.rb +46 -2
  55. data/test/hexapdf/layout/test_table_box.rb +14 -1
  56. data/test/hexapdf/test_composer.rb +7 -0
  57. data/test/hexapdf/test_dictionary_fields.rb +1 -0
  58. data/test/hexapdf/test_object.rb +1 -1
  59. data/test/hexapdf/test_pdf_array.rb +36 -3
  60. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
  61. data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
  62. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  63. data/test/hexapdf/type/acro_form/test_form.rb +17 -1
  64. data/test/hexapdf/type/annotations/test_appearance_generator.rb +210 -0
  65. data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
  66. data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
  67. data/test/hexapdf/type/annotations/test_line.rb +0 -45
  68. data/test/hexapdf/type/annotations/test_line_ending_styling.rb +42 -0
  69. data/test/hexapdf/type/annotations/test_polygon_polyline.rb +29 -0
  70. data/test/hexapdf/type/annotations/test_widget.rb +35 -0
  71. metadata +16 -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: 82d0430964f9f4c6925af5bb076a2e641908c3a4cb6796acc64dbe1303bc3407
4
+ data.tar.gz: 689a86b637a86331ca203d7d7ac90a9fb3c50c275f07b21d8109013faa509f07
5
5
  SHA512:
6
- metadata.gz: 508fa118a26d5825f9ae2a105cdf4717b02901fd3095c49cf5855caae7648bf60b9cea414bb6670dd3bd0c82f99b076a8df7ffb79efa60990003468d7f3a9db9
7
- data.tar.gz: 613323b8b2a93a01e31ec9e35664e6dc2939b210bbe7d7c055cb861eac6cd530978b14256a55a8e51f2e3804be06808440d5816c73e808df4af3f111f325b9c9
6
+ metadata.gz: 4cfe8038379e5dc7f3bebeb38b44525676b934cb2b2355ac7575fa7a4466a6c4ec9ab9e0a80a184aebab32b2aa87e06dff735c10915abebe11541ada95d8129c
7
+ data.tar.gz: 62562b4bae557ad3dac03cb51913a8d1d6796d6cad9ce0626bc901cc15afeb301e42d061c345a1ebcb30e09a018dbe4b7c26fc4c567423c262f67607d04b95c9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,93 @@
1
+ ## 1.4.0 - 2025-08-03
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Type::Annotations::Polygon] for polygon annotations as well as
6
+ [HexaPDF::Document::Annotations#create_polygon]
7
+ * [HexaPDF::Type::Annotations::Polyline] for polyline annotations as well as
8
+ [HexaPDF::Document::Annotations#create_polyline]
9
+ * [HexaPDF::Layout::ContainerBox#splitable] for specifying whether the container
10
+ box may be split
11
+ * [HexaPDF::Layout::Style::Layers#layers] for retrieving the list of defined
12
+ layers
13
+ * [HexaPDF::Document::Layout#resolve_font] for resolving the font style property
14
+ * [HexaPDF::Type::Measure] for representing the measure dictionary
15
+ * [HexaPDF::Layout::Box::FitResult#failure!] for setting the result status to
16
+ failure
17
+
18
+ ### Changed
19
+
20
+ * **Breaking change**: Extracted `#line_ending_style` and associated data class
21
+ from [HexaPDF::Type::Annotations::Line] into
22
+ [HexaPDF::Type::Annotations::LineEndingStyling]
23
+ * [HexaPDF::Layout::TableBox] implementation to allow setting the minimum height
24
+ of a table cell
25
+ * [HexaPDF::Layout::Style::Quad#set] to allow setting a subset of values using a
26
+ hash
27
+ * CLI command `hp form` to show the names of radio button widgets
28
+ * CLI command `hp form` to show position and size of widgets in easier to
29
+ understand form
30
+ * Default signing handler to not set /DigestMethod entry on signature reference
31
+ dictionary anymore
32
+
33
+ ### Fixed
34
+
35
+ * Parsing and writing the /ModDate and /CreationDate trailer info fields in case
36
+ of string values when using the XMP metadata handler
37
+ * [HexaPDF::Layout::Style] to not accidentally set subscript or superscript
38
+ values
39
+ * [HexaPDF::DictionaryFields::DateConverter] to handle invalid dates with two
40
+ trailing apostrophes
41
+ * [HexaPDF::Document::Layout::CellArgumentCollector#retrieve_arguments_for] to
42
+ not change the stored data
43
+ * Encryption when using AES with 256bits and an owner password
44
+
45
+
46
+ ## 1.3.0 - 2025-04-23
47
+
48
+ ### Added
49
+
50
+ * [HexaPDF::Type::Annotations::Square] for rectangle annotations as well as
51
+ [HexaPDF::Document::Annotations#create_rectangle]
52
+ * [HexaPDF::Type::Annotations::Circle] for ellipse annotations as well as
53
+ [HexaPDF::Document::Annotations#create_ellipse]
54
+ * Basic appearance generation for push button fields
55
+ * [HexaPDF::Type::Annotation::BorderEffect] type class
56
+ * [HexaPDF::Type::Annotations::BorderEffect] module that provides convenience
57
+ access to the border effect dictionary
58
+ * [HexaPDF::Document::Layout#style?] and [HexaPDF::Composer#style?] for checking
59
+ whether a given style (name) exists
60
+ * [HexaPDF::Layout::Style#each_property] for iterating over all set properties
61
+ * [HexaPDF::Layout::Style#merge] for merging another style instance
62
+ * [HexaPDF::Layout::Style#box_options] for specifying box initialization options
63
+ * [HexaPDF::Layout::Style#font_bold] and [HexaPDF::Layout::Style#font_italic]
64
+ for setting bold and/or italic variants independently of the font name
65
+ * [HexaPDF::PDFArray#map!] for mapping elements in-place
66
+ * [HexaPDF::PDFArray#compact!] for removing `nil` elements
67
+
68
+ ### Changed
69
+
70
+ * **Breaking change**: [HexaPDF::Type::Annotations::Widget::MarkerStyle::new]
71
+ got a new positional argument
72
+ * [HexaPDF::Type::Annotations::Widget#marker_style] to allow setting and
73
+ retrieving the font for push buttons
74
+ * Extracted `#interior_color` from [HexaPDF::Type::Annotations::Line] into
75
+ [HexaPDF::Type::Annotations::InteriorColor]
76
+ * CLI command `hexapdf inspect` to support decoding Form XObject streams
77
+ * [HexaPDF::Layout::Style#line_spacing] to accept a `LineSpacing` object when
78
+ setting the value
79
+
80
+ ### Fixed
81
+
82
+ * Text extraction with macOS Preview due a bug in Preview
83
+ * [HexaPDF::PDFArray#reject!] to work according to documented method signature
84
+ * [HexaPDF::Type::AcroForm::Field#create_widget] to ensure the proper type
85
+ class is stored in the document in case an embedded widget is extracted
86
+ * [HexaPDF::Type::AcroForm::Form] validation to ensure that all field objects in
87
+ the field hierarchy are using a field type class
88
+ * [HexaPDF::Type::AcroForm::Form] validation to delete merged fields
89
+
90
+
1
91
  ## 1.2.0 - 2025-02-10
2
92
 
3
93
  ### 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
@@ -161,8 +161,8 @@ module HexaPDF
161
161
  (field.alternate_field_name ? " (#{field.alternate_field_name})" : '')
162
162
  concrete_field_type = field.concrete_field_type
163
163
  nice_field_type = concrete_field_type.to_s.split('_').map(&:capitalize).join(' ')
164
- size = "(#{widget[:Rect].width.round(3)}x#{widget[:Rect].height.round(3)})"
165
- position = "(#{widget[:Rect].left}, #{widget[:Rect].bottom})"
164
+ size = "#{widget[:Rect].width.round(3)}x#{widget[:Rect].height.round(3)}"
165
+ position = "x=#{widget[:Rect].left}, y=#{widget[:Rect].bottom}"
166
166
  field_value = if !field.field_value || concrete_field_type != :signature_field
167
167
  field.field_value.inspect
168
168
  else
@@ -172,10 +172,15 @@ module HexaPDF
172
172
  temp
173
173
  end
174
174
 
175
+ if concrete_field_type == :radio_button
176
+ rb_name = ((widget.appearance_dict&.normal_appearance&.value&.keys || []) - [:Off]).first
177
+ rb_name = " (#{rb_name.inspect})"
178
+ end
179
+
175
180
  flags = field_flags(field)
176
- puts " #{field_name}" << (flags.empty? ? '' : " (#{flags.join(', ')})")
181
+ puts " #{field_name}#{rb_name}" << (flags.empty? ? '' : " (#{flags.join(', ')})")
177
182
  if command_parser.verbosity_info?
178
- printf(" └─ %-22s | %-20s\n", nice_field_type, "#{size} #{position}")
183
+ printf(" └─ %-22s | %-20s\n", nice_field_type, "#{position}, #{size} ")
179
184
  end
180
185
  puts " └─ #{field_value}"
181
186
  if command_parser.verbosity_info?
@@ -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
@@ -374,6 +374,11 @@ module HexaPDF
374
374
  # The default implementation returns an object of class HexaPDF::Font::InvalidGlyph which, when
375
375
  # not removed before encoding, will raise a HexaPDF::MissingGlyphError.
376
376
  #
377
+ # Note: The 'font.on_invalid_glyph' configuration option does something similar but is used
378
+ # later and only by the layout engine. If this callback hook returns an invalid glyph instance,
379
+ # the 'font.on_invalid_glyph' callback hook is invoked when using the layout engine and it can
380
+ # return a substitute glyph in any font.
381
+ #
377
382
  # If a replacement glyph should be displayed instead of an error, the following provides a good
378
383
  # starting implementation:
379
384
  #
@@ -709,6 +714,7 @@ module HexaPDF
709
714
  XXAcroFormField: 'HexaPDF::Type::AcroForm::Field',
710
715
  XXAppearanceDictionary: 'HexaPDF::Type::Annotation::AppearanceDictionary',
711
716
  Border: 'HexaPDF::Type::Annotation::Border',
717
+ XXBorderEffect: 'HexaPDF::Type::Annotation::BorderEffect',
712
718
  SigFieldLock: 'HexaPDF::Type::AcroForm::SignatureField::LockDictionary',
713
719
  SV: 'HexaPDF::Type::AcroForm::SignatureField::SeedValueDictionary',
714
720
  SVCert: 'HexaPDF::Type::AcroForm::SignatureField::CertificateSeedValueDictionary',
@@ -747,6 +753,7 @@ module HexaPDF
747
753
  Namespace: 'HexaPDF::Type::Namespace',
748
754
  MCR: 'HexaPDF::Type::MarkedContentReference',
749
755
  OBJR: 'HexaPDF::Type::ObjectReference',
756
+ Measure: 'HexaPDF::Type::Measure',
750
757
  },
751
758
  'object.subtype_map' => {
752
759
  nil => {
@@ -766,6 +773,10 @@ module HexaPDF
766
773
  Link: 'HexaPDF::Type::Annotations::Link',
767
774
  Widget: 'HexaPDF::Type::Annotations::Widget',
768
775
  Line: 'HexaPDF::Type::Annotations::Line',
776
+ Square: 'HexaPDF::Type::Annotations::Square',
777
+ Circle: 'HexaPDF::Type::Annotations::Circle',
778
+ Polygon: 'HexaPDF::Type::Annotations::Polygon',
779
+ PolyLine: 'HexaPDF::Type::Annotations::Polyline',
769
780
  XML: 'HexaPDF::Type::Metadata',
770
781
  GTS_PDFX: 'HexaPDF::Type::OutputIntent',
771
782
  GTS_PDFA1: 'HexaPDF::Type::OutputIntent',
@@ -795,6 +806,10 @@ module HexaPDF
795
806
  Link: 'HexaPDF::Type::Annotations::Link',
796
807
  Widget: 'HexaPDF::Type::Annotations::Widget',
797
808
  Line: 'HexaPDF::Type::Annotations::Line',
809
+ Square: 'HexaPDF::Type::Annotations::Square',
810
+ Circle: 'HexaPDF::Type::Annotations::Circle',
811
+ Polygon: 'HexaPDF::Type::Annotations::Polygon',
812
+ PolyLine: 'HexaPDF::Type::Annotations::Polyline',
798
813
  },
799
814
  XXAcroFormField: {
800
815
  Tx: 'HexaPDF::Type::AcroForm::TextField',
@@ -313,7 +313,7 @@ module HexaPDF
313
313
  [String, Time, Date, DateTime]
314
314
  end
315
315
 
316
- DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d+)(?:'|'(\d+)'?|\z)?)?\z/n # :nodoc:
316
+ DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d+)(?:'|'(\d+)'?'?|\z)?)?\z/n # :nodoc:
317
317
 
318
318
  # Checks if the given object is a string and converts into a Time object if possible.
319
319
  # Otherwise returns +nil+.
@@ -297,8 +297,7 @@ module HexaPDF
297
297
  raise HexaPDF::Error, "Can set DocMDP access permissions only on first signature"
298
298
  end
299
299
  params = doc.add({Type: :TransformParams, V: :'1.2', P: doc_mdp_permissions})
300
- sigref = doc.add({Type: :SigRef, TransformMethod: :DocMDP, DigestMethod: :SHA256,
301
- TransformParams: params})
300
+ sigref = doc.add({Type: :SigRef, TransformMethod: :DocMDP, TransformParams: params})
302
301
  signature[:Reference] = [sigref]
303
302
  (doc.catalog[:Perms] ||= {})[:DocMDP] = signature
304
303
  end
@@ -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,111 @@ 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
+
161
+ # :call-seq:
162
+ # annotations.create_polyline(page, *points) -> annotation
163
+ #
164
+ # Creates a polyline annotation for the given +points+ (alternating horizontal and vertical
165
+ # coordinates) on the given page and returns it.
166
+ #
167
+ # The polyline uses a black color and a width of 1pt. It can be further styled using the
168
+ # convenience methods on the returned annotation object.
169
+ #
170
+ # Example:
171
+ #
172
+ # #>pdf-small
173
+ # doc.annotations.create_polyline(doc.pages[0], 20, 20, 30, 70, 80, 60, 40, 30).
174
+ # border_style(color: "hp-blue", width: 2, style: [3, 1]).
175
+ # regenerate_appearance
176
+ #
177
+ # See: Type::Annotations::Polyline
178
+ def create_polyline(page, *points)
179
+ create_and_add_to_page(:PolyLine, page).
180
+ vertices(*points).
181
+ border_style(color: 0, width: 1)
182
+ end
183
+
184
+ # :call-seq:
185
+ # annotations.create_polygon(page, *points) -> annotation
186
+ #
187
+ # Creates a polygon annotation for the given +points+ (alternating horizontal and vertical
188
+ # coordinates) on the given page and returns it.
189
+ #
190
+ # The polygon uses a black color and a width of 1pt for the border and no interior color. It
191
+ # can be further styled using the convenience methods on the returned annotation object.
192
+ #
193
+ # Example:
194
+ #
195
+ # #>pdf-small
196
+ # doc.annotations.create_polygon(doc.pages[0], 20, 20, 30, 70, 80, 60, 40, 30).
197
+ # border_style(color: "hp-blue", width: 2, style: [3, 1]).
198
+ # interior_color("hp-orange").
199
+ # regenerate_appearance
200
+ #
201
+ # See: Type::Annotations::Polygon
202
+ def create_polygon(page, *points)
203
+ create_and_add_to_page(:Polygon, page).
204
+ vertices(*points).
205
+ border_style(color: 0, width: 1)
206
+ end
207
+
103
208
  private
104
209
 
105
210
  # Returns the root of the destinations name tree.
@@ -92,13 +92,23 @@ module HexaPDF
92
92
  #
93
93
  # style.font = ['Helvetica', variant: :bold]
94
94
  #
95
- # Helvetica in bold could also be set the conventional way:
95
+ # Helvetica in bold could also be set in the following ways:
96
96
  #
97
97
  # style.font = 'Helvetica bold'
98
+ # # or
99
+ # style.font_bold = true
100
+ # style.font = 'Helvetica'
101
+ #
102
+ # The font_bold and font_italic style properties are always taken into account. For example,
103
+ # if the font is set to 'Helvetica italic' and font_bold to +true+, the actual font would be
104
+ # the bold _and_ italic Helvetica font.
98
105
  #
99
106
  # However, using an array it is also possible to specify other options when setting a font,
100
107
  # like the :subset option.
101
108
  #
109
+ # * It is possible to resolve the font of a style object manually by using the #resolve_font
110
+ # method.
111
+ #
102
112
  class Layout
103
113
 
104
114
  # This class is used when a box can contain child boxes and the creation of such boxes should
@@ -218,6 +228,77 @@ module HexaPDF
218
228
  style
219
229
  end
220
230
 
231
+ # Returns +true+ if a style with the given +name+ exists, else +false+.
232
+ #
233
+ # Example:
234
+ #
235
+ # layout.style(:header, font: 'Helvetica')
236
+ # layout.style?(:header) # => true
237
+ # layout.style?(:paragraph) # => false
238
+ #
239
+ # See: #style
240
+ def style?(name)
241
+ @styles.key?(name)
242
+ end
243
+
244
+ FONT_BOLD_VARIANT_MAPPER = { #:nodoc:
245
+ nil => {true => :bold, false: :none},
246
+ none: {true => :bold, false: :none},
247
+ bold: {true => :bold, false: :none},
248
+ italic: {true => :bold_italic, false: :italic},
249
+ bold_italic: {true => :bold_italic, false: :italic},
250
+ }
251
+
252
+ FONT_ITALIC_VARIANT_MAPPER = { #:nodoc:
253
+ nil => {true => :italic, false: :none},
254
+ none: {true => :italic, false: :none},
255
+ italic: {true => :italic, false: :none},
256
+ bold: {true => :bold_italic, false: :bold},
257
+ bold_italic: {true => :bold_italic, false: :bold},
258
+ }
259
+
260
+ # Resolves the font object for the given +style+ and applies the result to it.
261
+ #
262
+ # The Layout::Style#font property is the only one without a default value but is needed for
263
+ # many operations. This method ensures that the +style+ has a valid font object for the font
264
+ # property by resolving the font name.
265
+ #
266
+ # The font object is resolved in the following way:
267
+ #
268
+ # * If the font property is not set, the font value of the :base style is used and if that is
269
+ # also not set, the 'font.default' configuration value is used.
270
+ #
271
+ # * Afterwards, if the font property is a valid font object, nothing needs to be done.
272
+ #
273
+ # * Otherwise, if the font property is a single font name or a [font name, options hash]
274
+ # array, it is resolved to a font object, also taking the font_bold and font_italic style
275
+ # properties into account.
276
+ #
277
+ # Example:
278
+ #
279
+ # style = layout.style(:header, font: 'Helvetica')
280
+ # style.font # => 'Helvetica'
281
+ # layout.resolve_font(style)
282
+ # style.font # => #<HexaPDF::Font::Type1Wrapper>
283
+ #
284
+ # See: The "Box Styles" section in Layout for more details.
285
+ def resolve_font(style)
286
+ unless style.font?
287
+ style.font(@styles[:base].font? && @styles[:base].font || @document.config['font.default'])
288
+ end
289
+ unless style.font.respond_to?(:pdf_object)
290
+ name, options = *style.font
291
+ options ||= {}
292
+ if style.font_bold?
293
+ options[:variant] = FONT_BOLD_VARIANT_MAPPER.dig(options[:variant], style.font_bold)
294
+ end
295
+ if style.font_italic?
296
+ options[:variant] = FONT_ITALIC_VARIANT_MAPPER.dig(options[:variant], style.font_italic)
297
+ end
298
+ style.font(@document.fonts.add(name, **options))
299
+ end
300
+ end
301
+
221
302
  # :call-seq:
222
303
  # layout.styles -> styles
223
304
  # layout.styles(**mapping) -> styles
@@ -286,8 +367,9 @@ module HexaPDF
286
367
  box_options[:children] = ChildrenCollector.collect(self, &block)
287
368
  end
288
369
  end
370
+ style = retrieve_style(style)
289
371
  box_class_for_name(name).new(width: width, height: height,
290
- style: retrieve_style(style), **box_options, &box_block)
372
+ style: style, **style.box_options, **box_options, &box_block)
291
373
  end
292
374
 
293
375
  # Creates an array of HexaPDF::Layout::TextFragment objects for the given +text+.
@@ -354,7 +436,7 @@ module HexaPDF
354
436
  box_style = (box_style ? retrieve_style(box_style) : style)
355
437
  box_class_for_name(:text).new(items: text_fragments(text, style: style),
356
438
  width: width, height: height, properties: properties,
357
- style: box_style)
439
+ style: box_style, **box_style.box_options)
358
440
  end
359
441
  alias text text_box
360
442
 
@@ -457,7 +539,8 @@ module HexaPDF
457
539
  end
458
540
  end
459
541
  box_class_for_name(:text).new(items: data, width: width, height: height,
460
- properties: properties, style: box_style)
542
+ properties: properties, style: box_style,
543
+ **box_style.box_options)
461
544
  end
462
545
  alias formatted_text formatted_text_box
463
546
 
@@ -479,7 +562,7 @@ module HexaPDF
479
562
  style = retrieve_style(style, style_properties)
480
563
  image = file.kind_of?(HexaPDF::Stream) ? file : @document.images.add(file)
481
564
  box_class_for_name(:image).new(image: image, width: width, height: height,
482
- properties: properties, style: style)
565
+ properties: properties, style: style, **style.box_options)
483
566
  end
484
567
  alias image image_box
485
568
 
@@ -533,9 +616,10 @@ module HexaPDF
533
616
  @argument_infos.each_with_object({}) do |arg_info, result|
534
617
  next unless arg_info.rows.include?(row) && arg_info.cols.include?(col)
535
618
  if arg_info.args[:cell]
536
- arg_info.args[:cell] = (result[:cell] || {}).merge(arg_info.args[:cell])
619
+ result.update(arg_info.args, cell: (result[:cell] || {}).merge(arg_info.args[:cell]))
620
+ else
621
+ result.update(arg_info.args)
537
622
  end
538
- result.update(arg_info.args)
539
623
  end
540
624
  end
541
625
 
@@ -608,7 +692,8 @@ module HexaPDF
608
692
  end
609
693
  box_class_for_name(:table).new(cells: cells, column_widths: column_widths, header: header,
610
694
  footer: footer, cell_style: cell_style, width: width,
611
- height: height, properties: properties, style: style)
695
+ height: height, properties: properties, style: style,
696
+ **style.box_options)
612
697
  end
613
698
  alias table table_box
614
699
 
@@ -685,13 +770,7 @@ module HexaPDF
685
770
  end
686
771
  style = HexaPDF::Layout::Style.create(@styles[style] || style || @styles[:base])
687
772
  style = style.dup.update(**properties) unless properties.nil? || properties.empty?
688
- unless style.font?
689
- style.font(@styles[:base].font? && @styles[:base].font || @document.config['font.default'])
690
- end
691
- unless style.font.respond_to?(:pdf_object)
692
- name, options = *style.font
693
- style.font(@document.fonts.add(name, **(options || {})))
694
- end
773
+ resolve_font(style)
695
774
  style
696
775
  end
697
776
 
@@ -430,8 +430,12 @@ module HexaPDF
430
430
  @metadata[ns_dc]['creator'] = info_dict[:Author] if info_dict.key?(:Author)
431
431
  @metadata[ns_dc]['description'] = info_dict[:Subject] if info_dict.key?(:Subject)
432
432
  @metadata[ns_xmp]['CreatorTool'] = info_dict[:Creator] if info_dict.key?(:Creator)
433
- @metadata[ns_xmp]['CreateDate'] = info_dict[:CreationDate] if info_dict.key?(:CreationDate)
434
- @metadata[ns_xmp]['ModifyDate'] = info_dict[:ModDate] if info_dict.key?(:ModDate)
433
+ if info_dict.key?(:CreationDate) && !info_dict[:CreationDate].kind_of?(String)
434
+ @metadata[ns_xmp]['CreateDate'] = info_dict[:CreationDate]
435
+ end
436
+ if info_dict.key?(:ModDate) && !info_dict[:ModDate].kind_of?(String)
437
+ @metadata[ns_xmp]['ModifyDate'] = info_dict[:ModDate] if info_dict.key?(:ModDate)
438
+ end
435
439
  @metadata[ns_pdf]['Keywords'] = info_dict[:Keywords] if info_dict.key?(:Keywords)
436
440
  @metadata[ns_pdf]['Producer'] = info_dict[:Producer] if info_dict.key?(:Producer)
437
441
  if info_dict.key?(:Trapped) && info_dict[:Trapped] != :Unknown
@@ -528,7 +532,10 @@ module HexaPDF
528
532
  # Formats the given date-time object (Time, Date, or DateTime) to be a valid XMP date-time
529
533
  # value.
530
534
  def xmp_date(date)
531
- date.strftime("%Y-%m-%dT%H:%M:%S%:z")
535
+ case date
536
+ when Time, Date, DateTime then date.strftime("%Y-%m-%dT%H:%M:%S%:z")
537
+ else ''
538
+ end
532
539
  end
533
540
 
534
541
  end
@@ -743,6 +743,15 @@ module HexaPDF
743
743
  # Before the document is written, it is validated using #validate and an error is raised if the
744
744
  # document is not valid. However, this step can be skipped if needed.
745
745
  #
746
+ # The method dispatches two messages:
747
+ #
748
+ # :complete_objects::
749
+ # This message is dispatched before anything is done and should be used to finalize objects.
750
+ #
751
+ # :before_write::
752
+ # This message is dispatched directly before the document gets serialized and allows, for
753
+ # example, overriding automatic HexaPDF changes (e.g. forcefully setting a document version).
754
+ #
746
755
  # Options:
747
756
  #
748
757
  # incremental::
@@ -325,8 +325,13 @@ module HexaPDF
325
325
  options.user_password = prepare_password(options.user_password)
326
326
  options.owner_password = prepare_password(options.owner_password)
327
327
 
328
- dict[:O] = compute_o_field(options.owner_password, options.user_password)
329
- dict[:U] = compute_u_field(options.user_password)
328
+ if dict[:R] <= 4
329
+ dict[:O] = compute_o_field(options.owner_password, options.user_password)
330
+ dict[:U] = compute_u_field(options.user_password)
331
+ else
332
+ dict[:U] = compute_u_field(options.user_password)
333
+ dict[:O] = compute_o_field(options.owner_password, options.user_password)
334
+ end
330
335
 
331
336
  if dict[:R] <= 4
332
337
  encryption_key = compute_user_encryption_key(options.user_password)
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