hexapdf 1.3.0 → 1.4.1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/lib/hexapdf/cli/form.rb +10 -5
  4. data/lib/hexapdf/cli/images.rb +5 -1
  5. data/lib/hexapdf/cli.rb +3 -0
  6. data/lib/hexapdf/configuration.rb +10 -0
  7. data/lib/hexapdf/dictionary_fields.rb +1 -1
  8. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -2
  9. data/lib/hexapdf/document/annotations.rb +47 -0
  10. data/lib/hexapdf/document/layout.rb +73 -33
  11. data/lib/hexapdf/document/metadata.rb +10 -3
  12. data/lib/hexapdf/document.rb +10 -1
  13. data/lib/hexapdf/encryption/standard_security_handler.rb +7 -2
  14. data/lib/hexapdf/font/encoding/base.rb +27 -0
  15. data/lib/hexapdf/font/type1_wrapper.rb +1 -3
  16. data/lib/hexapdf/layout/box.rb +5 -0
  17. data/lib/hexapdf/layout/container_box.rb +63 -28
  18. data/lib/hexapdf/layout/style.rb +28 -13
  19. data/lib/hexapdf/layout/table_box.rb +20 -2
  20. data/lib/hexapdf/serializer.rb +7 -7
  21. data/lib/hexapdf/type/annotations/appearance_generator.rb +94 -16
  22. data/lib/hexapdf/type/annotations/interior_color.rb +1 -1
  23. data/lib/hexapdf/type/annotations/line.rb +1 -157
  24. data/lib/hexapdf/type/annotations/line_ending_styling.rb +208 -0
  25. data/lib/hexapdf/type/annotations/markup_annotation.rb +0 -1
  26. data/lib/hexapdf/type/annotations/polygon.rb +64 -0
  27. data/lib/hexapdf/type/annotations/polygon_polyline.rb +109 -0
  28. data/lib/hexapdf/type/annotations/polyline.rb +64 -0
  29. data/lib/hexapdf/type/annotations.rb +4 -0
  30. data/lib/hexapdf/type/font_type1.rb +12 -1
  31. data/lib/hexapdf/type/measure.rb +57 -0
  32. data/lib/hexapdf/type.rb +1 -0
  33. data/lib/hexapdf/utils/sorted_tree_node.rb +4 -1
  34. data/lib/hexapdf/version.rb +1 -1
  35. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +0 -1
  36. data/test/hexapdf/document/test_annotations.rb +20 -0
  37. data/test/hexapdf/document/test_layout.rb +16 -10
  38. data/test/hexapdf/document/test_metadata.rb +13 -1
  39. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -1
  40. data/test/hexapdf/font/encoding/test_base.rb +20 -0
  41. data/test/hexapdf/layout/test_box.rb +8 -0
  42. data/test/hexapdf/layout/test_container_box.rb +34 -6
  43. data/test/hexapdf/layout/test_page_style.rb +1 -1
  44. data/test/hexapdf/layout/test_style.rb +20 -1
  45. data/test/hexapdf/layout/test_table_box.rb +14 -1
  46. data/test/hexapdf/test_dictionary_fields.rb +1 -0
  47. data/test/hexapdf/test_document.rb +1 -0
  48. data/test/hexapdf/test_serializer.rb +2 -1
  49. data/test/hexapdf/type/annotations/test_appearance_generator.rb +126 -0
  50. data/test/hexapdf/type/annotations/test_line.rb +0 -25
  51. data/test/hexapdf/type/annotations/test_line_ending_styling.rb +42 -0
  52. data/test/hexapdf/type/annotations/test_polygon_polyline.rb +29 -0
  53. data/test/hexapdf/type/annotations/test_widget.rb +8 -0
  54. data/test/hexapdf/type/test_font_type1.rb +14 -0
  55. data/test/hexapdf/utils/test_sorted_tree_node.rb +11 -1
  56. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0e469ae650b98b48c88b662dab27cb405b676c706639c8c1cf358d6aefc8f53
4
- data.tar.gz: d170526233e1e9aa37403c1bd8d2aa38697641c1b4fb4d4a5f6d8f02f299585b
3
+ metadata.gz: b88ce85ee9bc603011b9f5a278829d588da10f53614c0b84a57e2d7fa38f52dc
4
+ data.tar.gz: ec2c8739ed69038e1297435550371bf329e516e9ef970fa0456502b15720d07b
5
5
  SHA512:
6
- metadata.gz: fd18408ed2c2474e3395bf59b3a0281cb23005d16bf7d5b9d93f673dfa131fc215f342018d0cdce2de053db85c000a68c28a76ad980b0432b1a159bd51b1ed0c
7
- data.tar.gz: c58b13ed27c980bb9670b9521a12b1640fc3960aea1188f6135951cfd1503a28e7633cb3f6be8c63d478d64a88e0bbdc9b84bdbc982aa3ec000aeac1a8627597
6
+ metadata.gz: 103edc366ef9f48ddd6579f7137b3ab23b4266dc2df0a77ee5b89cb4256419a00727776158a7c7570a0b10075e4f062506d524ec71ee801c00fdd9e4726c8232
7
+ data.tar.gz: f1f4a1af54445b2e7c3c9fc1adfa81fb9fad84f32461f58378fc550bd5aec16e16b276dadc7516ca5b0b6212394886d06f2cc2a3506dc2b2745b5a1f4c8136d1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,71 @@
1
+ ## 1.4.1 - 2025-09-23
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Font::Encoding::Base#to_compact_array] for creating a compact array
6
+ representation of the encoding
7
+
8
+ ### Changed
9
+
10
+ - CLI to handle missing file errors better
11
+
12
+ ### Fixed
13
+
14
+ * Serialization of strings that need to be UTF-16 encoded when using encryption
15
+ * [HexaPDF::Document#write_to_string] to pass on arguments to `#write`
16
+ * [HexaPDF::Type::FontType1] validation to handle PDFs with an invalid value of
17
+ /SymbolEncoding for the /Encoding key
18
+ * [HexaPDF::Type::FontType1] validation to handle PDFs with an invalid value of
19
+ /StandardEncoding for the /Encoding key
20
+ * CLI command `hexapdf form` to ignore widgets that don't belong to any field
21
+ * Validation of invalid sorted tree root nodes with odd number of direct entries
22
+
23
+
24
+ ## 1.4.0 - 2025-08-03
25
+
26
+ ### Added
27
+
28
+ * [HexaPDF::Type::Annotations::Polygon] for polygon annotations as well as
29
+ [HexaPDF::Document::Annotations#create_polygon]
30
+ * [HexaPDF::Type::Annotations::Polyline] for polyline annotations as well as
31
+ [HexaPDF::Document::Annotations#create_polyline]
32
+ * [HexaPDF::Layout::ContainerBox#splitable] for specifying whether the container
33
+ box may be split
34
+ * [HexaPDF::Layout::Style::Layers#layers] for retrieving the list of defined
35
+ layers
36
+ * [HexaPDF::Document::Layout#resolve_font] for resolving the font style property
37
+ * [HexaPDF::Type::Measure] for representing the measure dictionary
38
+ * [HexaPDF::Layout::Box::FitResult#failure!] for setting the result status to
39
+ failure
40
+
41
+ ### Changed
42
+
43
+ * **Breaking change**: Extracted `#line_ending_style` and associated data class
44
+ from [HexaPDF::Type::Annotations::Line] into
45
+ [HexaPDF::Type::Annotations::LineEndingStyling]
46
+ * [HexaPDF::Layout::TableBox] implementation to allow setting the minimum height
47
+ of a table cell
48
+ * [HexaPDF::Layout::Style::Quad#set] to allow setting a subset of values using a
49
+ hash
50
+ * CLI command `hexapdf form` to show the names of radio button widgets
51
+ * CLI command `hexapdf form` to show position and size of widgets in easier to
52
+ understand form
53
+ * Default signing handler to not set /DigestMethod entry on signature reference
54
+ dictionary anymore
55
+
56
+ ### Fixed
57
+
58
+ * Parsing and writing the /ModDate and /CreationDate trailer info fields in case
59
+ of string values when using the XMP metadata handler
60
+ * [HexaPDF::Layout::Style] to not accidentally set subscript or superscript
61
+ values
62
+ * [HexaPDF::DictionaryFields::DateConverter] to handle invalid dates with two
63
+ trailing apostrophes
64
+ * [HexaPDF::Document::Layout::CellArgumentCollector#retrieve_arguments_for] to
65
+ not change the stored data
66
+ * Encryption when using AES with 256bits and an owner password
67
+
68
+
1
69
  ## 1.3.0 - 2025-04-23
2
70
 
3
71
  ### Added
@@ -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?
@@ -285,7 +290,7 @@ module HexaPDF
285
290
  page.each_annotation do |annotation|
286
291
  next unless annotation[:Subtype] == :Widget
287
292
  field = annotation.form_field
288
- next if field.concrete_field_type == :push_button
293
+ next if !field.concrete_field_type || field.concrete_field_type == :push_button
289
294
  if with_seen || !seen[field.full_field_name]
290
295
  yield(page, page_index, field, annotation)
291
296
  seen[field.full_field_name] = true
@@ -132,7 +132,7 @@ module HexaPDF
132
132
  printf("%5s %5s %9s %6s %6s %5s %4s %3s %5s %5s %6s %5s %8s\n",
133
133
  "index", "page", "oid", "width", "height", "color", "comp", "bpc",
134
134
  "x-ppi", "y-ppi", "size", "type", "writable")
135
- puts("-" * 77)
135
+ puts("-" * 84)
136
136
  each_image(doc) do |image, index, pindex, (x_ppi, y_ppi)|
137
137
  info = image.info
138
138
  size = human_readable_file_size(image[:Length] + image[:SMask]&.[](:Length).to_i)
@@ -155,6 +155,10 @@ module HexaPDF
155
155
  puts "Extracting #{path}..." if command_parser.verbosity_info?
156
156
  image.write(path)
157
157
  done << index
158
+ if info.color_space == :cmyk && info.type == :jpeg
159
+ $stderr.puts "Note (image #{path}): JPEG uses CMYK colorspace and may " \
160
+ "need color post-processing"
161
+ end
158
162
  elsif command_parser.verbosity_warning?
159
163
  $stderr.puts "Warning (image #{index}): PDF image format not supported for writing"
160
164
  end
data/lib/hexapdf/cli.rb CHANGED
@@ -61,6 +61,9 @@ module HexaPDF
61
61
  # Runs the CLI application.
62
62
  def self.run(args = ARGV)
63
63
  Application.new.parse(args)
64
+ rescue Errno::ENOENT => e
65
+ path = e.message.scan(/(?<= - ).*?$/).first
66
+ $stderr.puts "Problem encountered: No such file - #{path}"
64
67
  rescue StandardError => e
65
68
  $stderr.puts "Problem encountered: #{e.message}"
66
69
  unless e.kind_of?(HexaPDF::Error)
@@ -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
  #
@@ -748,6 +753,7 @@ module HexaPDF
748
753
  Namespace: 'HexaPDF::Type::Namespace',
749
754
  MCR: 'HexaPDF::Type::MarkedContentReference',
750
755
  OBJR: 'HexaPDF::Type::ObjectReference',
756
+ Measure: 'HexaPDF::Type::Measure',
751
757
  },
752
758
  'object.subtype_map' => {
753
759
  nil => {
@@ -769,6 +775,8 @@ module HexaPDF
769
775
  Line: 'HexaPDF::Type::Annotations::Line',
770
776
  Square: 'HexaPDF::Type::Annotations::Square',
771
777
  Circle: 'HexaPDF::Type::Annotations::Circle',
778
+ Polygon: 'HexaPDF::Type::Annotations::Polygon',
779
+ PolyLine: 'HexaPDF::Type::Annotations::Polyline',
772
780
  XML: 'HexaPDF::Type::Metadata',
773
781
  GTS_PDFX: 'HexaPDF::Type::OutputIntent',
774
782
  GTS_PDFA1: 'HexaPDF::Type::OutputIntent',
@@ -800,6 +808,8 @@ module HexaPDF
800
808
  Line: 'HexaPDF::Type::Annotations::Line',
801
809
  Square: 'HexaPDF::Type::Annotations::Square',
802
810
  Circle: 'HexaPDF::Type::Annotations::Circle',
811
+ Polygon: 'HexaPDF::Type::Annotations::Polygon',
812
+ PolyLine: 'HexaPDF::Type::Annotations::Polyline',
803
813
  },
804
814
  XXAcroFormField: {
805
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
@@ -158,6 +158,53 @@ module HexaPDF
158
158
  annot
159
159
  end
160
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
+
161
208
  private
162
209
 
163
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
@@ -231,6 +241,64 @@ module HexaPDF
231
241
  @styles.key?(name)
232
242
  end
233
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
+
234
302
  # :call-seq:
235
303
  # layout.styles -> styles
236
304
  # layout.styles(**mapping) -> styles
@@ -548,9 +616,10 @@ module HexaPDF
548
616
  @argument_infos.each_with_object({}) do |arg_info, result|
549
617
  next unless arg_info.rows.include?(row) && arg_info.cols.include?(col)
550
618
  if arg_info.args[:cell]
551
- 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)
552
622
  end
553
- result.update(arg_info.args)
554
623
  end
555
624
  end
556
625
 
@@ -682,22 +751,6 @@ module HexaPDF
682
751
  end
683
752
  end
684
753
 
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
-
701
754
  # Retrieves the appropriate HexaPDF::Layout::Style object based on the +style+ and
702
755
  # +properties+ arguments.
703
756
  #
@@ -717,20 +770,7 @@ module HexaPDF
717
770
  end
718
771
  style = HexaPDF::Layout::Style.create(@styles[style] || style || @styles[:base])
719
772
  style = style.dup.update(**properties) unless properties.nil? || properties.empty?
720
- unless style.font?
721
- style.font(@styles[:base].font? && @styles[:base].font || @document.config['font.default'])
722
- end
723
- unless style.font.respond_to?(:pdf_object)
724
- name, options = *style.font
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))
733
- end
773
+ resolve_font(style)
734
774
  style
735
775
  end
736
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::
@@ -814,7 +823,7 @@ module HexaPDF
814
823
  # See #write for further information and details on the available arguments.
815
824
  def write_to_string(**args)
816
825
  io = StringIO.new(''.b)
817
- write(io)
826
+ write(io, **args)
818
827
  io.string
819
828
  end
820
829
 
@@ -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)
@@ -81,6 +81,33 @@ module HexaPDF
81
81
  @code_to_name.key(name)
82
82
  end
83
83
 
84
+ # Returns the encoding in a compact array form.
85
+ #
86
+ # If the optional +base_encoding+ argument is specified, all codes that have the same value
87
+ # in the base encoding are ignored.
88
+ #
89
+ # The returned array is of the form:
90
+ #
91
+ # code1 name1 name2 ... code2 name3 name4 ...
92
+ #
93
+ # This means that name1 is associated with code1, name2 with code1 + 1 and so on.
94
+ #
95
+ # See: PDF 2.0 s9.6.5.1
96
+ def to_compact_array(base_encoding: nil)
97
+ result = []
98
+ last_code = -3
99
+ @code_to_name.sort.each do |code, name|
100
+ next if base_encoding&.name(code) == name
101
+ if last_code + 1 == code
102
+ result << name
103
+ else
104
+ result << code << name
105
+ end
106
+ last_code = code
107
+ end
108
+ result
109
+ end
110
+
84
111
  end
85
112
 
86
113
  end
@@ -279,9 +279,7 @@ module HexaPDF
279
279
  if VALID_ENCODING_NAMES.include?(@encoding.encoding_name)
280
280
  dict[:Encoding] = @encoding.encoding_name
281
281
  elsif @encoding != @wrapped_font.encoding
282
- differences = [min]
283
- (min..max).each {|code| differences << @encoding.name(code) }
284
- dict[:Encoding] = {Differences: differences}
282
+ dict[:Encoding] = {Differences: @encoding.to_compact_array}
285
283
  end
286
284
  end
287
285
 
@@ -166,6 +166,11 @@ module HexaPDF
166
166
  @status == :overflow
167
167
  end
168
168
 
169
+ # Sets the result status to failure.
170
+ def failure!
171
+ @status = :failure
172
+ end
173
+
169
174
  # Returns +true+ if fitting was a failure.
170
175
  def failure?
171
176
  @status == :failure