hexapdf 0.32.2 → 0.34.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 (221) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -1
  3. data/README.md +9 -0
  4. data/examples/002-graphics.rb +15 -17
  5. data/examples/003-arcs.rb +9 -9
  6. data/examples/009-text_layouter_alignment.rb +1 -1
  7. data/examples/010-text_layouter_inline_boxes.rb +2 -2
  8. data/examples/011-text_layouter_line_wrapping.rb +1 -1
  9. data/examples/012-text_layouter_styling.rb +7 -7
  10. data/examples/013-text_layouter_shapes.rb +1 -1
  11. data/examples/014-text_in_polygon.rb +1 -1
  12. data/examples/015-boxes.rb +8 -7
  13. data/examples/016-frame_automatic_box_placement.rb +2 -2
  14. data/examples/017-frame_text_flow.rb +2 -1
  15. data/examples/018-composer.rb +1 -1
  16. data/examples/020-column_box.rb +2 -1
  17. data/examples/025-table_box.rb +46 -0
  18. data/examples/026-optional_content.rb +55 -0
  19. data/examples/027-composer_optional_content.rb +83 -0
  20. data/lib/hexapdf/cli/command.rb +12 -3
  21. data/lib/hexapdf/cli/fonts.rb +1 -1
  22. data/lib/hexapdf/cli/form.rb +5 -5
  23. data/lib/hexapdf/cli/inspect.rb +5 -7
  24. data/lib/hexapdf/composer.rb +106 -53
  25. data/lib/hexapdf/configuration.rb +65 -40
  26. data/lib/hexapdf/content/canvas.rb +445 -267
  27. data/lib/hexapdf/content/color_space.rb +72 -25
  28. data/lib/hexapdf/content/graphic_object/arc.rb +57 -24
  29. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +66 -23
  30. data/lib/hexapdf/content/graphic_object/geom2d.rb +47 -6
  31. data/lib/hexapdf/content/graphic_object/solid_arc.rb +58 -36
  32. data/lib/hexapdf/content/graphic_object.rb +6 -7
  33. data/lib/hexapdf/content/graphics_state.rb +54 -45
  34. data/lib/hexapdf/content/operator.rb +54 -54
  35. data/lib/hexapdf/content/parser.rb +2 -2
  36. data/lib/hexapdf/content/processor.rb +15 -15
  37. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  38. data/lib/hexapdf/content.rb +5 -0
  39. data/lib/hexapdf/dictionary.rb +7 -5
  40. data/lib/hexapdf/dictionary_fields.rb +43 -16
  41. data/lib/hexapdf/digital_signature/cms_handler.rb +2 -2
  42. data/lib/hexapdf/digital_signature/handler.rb +1 -1
  43. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +2 -3
  44. data/lib/hexapdf/digital_signature/signature.rb +6 -6
  45. data/lib/hexapdf/digital_signature/signatures.rb +13 -12
  46. data/lib/hexapdf/digital_signature/signing/default_handler.rb +14 -5
  47. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +2 -4
  48. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +4 -4
  49. data/lib/hexapdf/digital_signature/signing.rb +4 -0
  50. data/lib/hexapdf/digital_signature/verification_result.rb +3 -4
  51. data/lib/hexapdf/digital_signature.rb +7 -2
  52. data/lib/hexapdf/document/destinations.rb +12 -11
  53. data/lib/hexapdf/document/files.rb +1 -1
  54. data/lib/hexapdf/document/fonts.rb +1 -1
  55. data/lib/hexapdf/document/layout.rb +170 -39
  56. data/lib/hexapdf/document/pages.rb +4 -3
  57. data/lib/hexapdf/document.rb +96 -55
  58. data/lib/hexapdf/encryption/aes.rb +5 -5
  59. data/lib/hexapdf/encryption/arc4.rb +1 -1
  60. data/lib/hexapdf/encryption/fast_aes.rb +2 -2
  61. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  62. data/lib/hexapdf/encryption/identity.rb +1 -1
  63. data/lib/hexapdf/encryption/ruby_aes.rb +11 -21
  64. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  65. data/lib/hexapdf/encryption/security_handler.rb +31 -24
  66. data/lib/hexapdf/encryption/standard_security_handler.rb +45 -36
  67. data/lib/hexapdf/encryption.rb +7 -2
  68. data/lib/hexapdf/error.rb +18 -0
  69. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  70. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  71. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  72. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  73. data/lib/hexapdf/filter/pass_through.rb +1 -1
  74. data/lib/hexapdf/filter/predictor.rb +1 -1
  75. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  76. data/lib/hexapdf/filter.rb +55 -6
  77. data/lib/hexapdf/font/cmap/parser.rb +2 -2
  78. data/lib/hexapdf/font/cmap.rb +1 -1
  79. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  80. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +2 -2
  82. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  83. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  84. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +3 -3
  85. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  86. data/lib/hexapdf/font/invalid_glyph.rb +3 -0
  87. data/lib/hexapdf/font/true_type_wrapper.rb +17 -4
  88. data/lib/hexapdf/font/type1_wrapper.rb +19 -4
  89. data/lib/hexapdf/font_loader/from_configuration.rb +5 -2
  90. data/lib/hexapdf/font_loader/from_file.rb +5 -5
  91. data/lib/hexapdf/font_loader/standard14.rb +3 -3
  92. data/lib/hexapdf/font_loader.rb +3 -0
  93. data/lib/hexapdf/image_loader/jpeg.rb +2 -2
  94. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  95. data/lib/hexapdf/image_loader/png.rb +2 -2
  96. data/lib/hexapdf/image_loader.rb +1 -1
  97. data/lib/hexapdf/importer.rb +13 -0
  98. data/lib/hexapdf/layout/box.rb +32 -5
  99. data/lib/hexapdf/layout/box_fitter.rb +2 -2
  100. data/lib/hexapdf/layout/column_box.rb +20 -5
  101. data/lib/hexapdf/layout/frame.rb +53 -18
  102. data/lib/hexapdf/layout/image_box.rb +5 -0
  103. data/lib/hexapdf/layout/inline_box.rb +21 -9
  104. data/lib/hexapdf/layout/list_box.rb +50 -20
  105. data/lib/hexapdf/layout/page_style.rb +6 -5
  106. data/lib/hexapdf/layout/style.rb +64 -9
  107. data/lib/hexapdf/layout/table_box.rb +684 -0
  108. data/lib/hexapdf/layout/text_box.rb +12 -3
  109. data/lib/hexapdf/layout/text_fragment.rb +29 -3
  110. data/lib/hexapdf/layout/text_layouter.rb +32 -8
  111. data/lib/hexapdf/layout.rb +1 -0
  112. data/lib/hexapdf/name_tree_node.rb +1 -1
  113. data/lib/hexapdf/number_tree_node.rb +1 -1
  114. data/lib/hexapdf/object.rb +18 -7
  115. data/lib/hexapdf/parser.rb +7 -7
  116. data/lib/hexapdf/pdf_array.rb +1 -1
  117. data/lib/hexapdf/rectangle.rb +1 -1
  118. data/lib/hexapdf/reference.rb +1 -1
  119. data/lib/hexapdf/revision.rb +1 -1
  120. data/lib/hexapdf/revisions.rb +3 -3
  121. data/lib/hexapdf/serializer.rb +15 -15
  122. data/lib/hexapdf/stream.rb +5 -4
  123. data/lib/hexapdf/tokenizer.rb +14 -14
  124. data/lib/hexapdf/type/acro_form/appearance_generator.rb +22 -22
  125. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  126. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  127. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  128. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  129. data/lib/hexapdf/type/acro_form/signature_field.rb +4 -4
  130. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  131. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  132. data/lib/hexapdf/type/acro_form.rb +1 -1
  133. data/lib/hexapdf/type/action.rb +1 -1
  134. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  135. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  136. data/lib/hexapdf/type/actions/launch.rb +1 -1
  137. data/lib/hexapdf/type/actions/set_ocg_state.rb +86 -0
  138. data/lib/hexapdf/type/actions/uri.rb +1 -1
  139. data/lib/hexapdf/type/actions.rb +2 -1
  140. data/lib/hexapdf/type/annotation.rb +3 -3
  141. data/lib/hexapdf/type/annotations/link.rb +1 -1
  142. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  143. data/lib/hexapdf/type/annotations/text.rb +2 -3
  144. data/lib/hexapdf/type/annotations/widget.rb +2 -2
  145. data/lib/hexapdf/type/annotations.rb +1 -1
  146. data/lib/hexapdf/type/catalog.rb +11 -2
  147. data/lib/hexapdf/type/cid_font.rb +18 -4
  148. data/lib/hexapdf/type/embedded_file.rb +1 -1
  149. data/lib/hexapdf/type/file_specification.rb +2 -2
  150. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  151. data/lib/hexapdf/type/font_simple.rb +2 -2
  152. data/lib/hexapdf/type/font_type0.rb +3 -3
  153. data/lib/hexapdf/type/font_type3.rb +1 -1
  154. data/lib/hexapdf/type/form.rb +76 -6
  155. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  156. data/lib/hexapdf/type/icon_fit.rb +1 -1
  157. data/lib/hexapdf/type/image.rb +1 -1
  158. data/lib/hexapdf/type/info.rb +1 -1
  159. data/lib/hexapdf/type/mark_information.rb +1 -1
  160. data/lib/hexapdf/type/names.rb +2 -2
  161. data/lib/hexapdf/type/object_stream.rb +2 -1
  162. data/lib/hexapdf/type/optional_content_configuration.rb +170 -0
  163. data/lib/hexapdf/type/optional_content_group.rb +370 -0
  164. data/lib/hexapdf/type/optional_content_membership.rb +63 -0
  165. data/lib/hexapdf/type/optional_content_properties.rb +158 -0
  166. data/lib/hexapdf/type/outline.rb +1 -1
  167. data/lib/hexapdf/type/outline_item.rb +1 -1
  168. data/lib/hexapdf/type/page.rb +46 -21
  169. data/lib/hexapdf/type/page_label.rb +5 -9
  170. data/lib/hexapdf/type/page_tree_node.rb +1 -1
  171. data/lib/hexapdf/type/resources.rb +1 -1
  172. data/lib/hexapdf/type/trailer.rb +2 -2
  173. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  174. data/lib/hexapdf/type/xref_stream.rb +2 -2
  175. data/lib/hexapdf/type.rb +4 -0
  176. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -2
  177. data/lib/hexapdf/version.rb +1 -1
  178. data/lib/hexapdf/writer.rb +4 -4
  179. data/lib/hexapdf/xref_section.rb +2 -2
  180. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +11 -1
  181. data/test/hexapdf/content/graphic_object/test_geom2d.rb +7 -0
  182. data/test/hexapdf/content/test_canvas.rb +49 -1
  183. data/test/hexapdf/digital_signature/test_signatures.rb +22 -0
  184. data/test/hexapdf/document/test_files.rb +2 -2
  185. data/test/hexapdf/document/test_layout.rb +105 -2
  186. data/test/hexapdf/document/test_pages.rb +6 -6
  187. data/test/hexapdf/encryption/test_security_handler.rb +12 -11
  188. data/test/hexapdf/encryption/test_standard_security_handler.rb +35 -23
  189. data/test/hexapdf/font/test_true_type_wrapper.rb +18 -1
  190. data/test/hexapdf/font/test_type1_wrapper.rb +15 -1
  191. data/test/hexapdf/layout/test_box.rb +14 -5
  192. data/test/hexapdf/layout/test_column_box.rb +65 -21
  193. data/test/hexapdf/layout/test_frame.rb +27 -15
  194. data/test/hexapdf/layout/test_image_box.rb +4 -0
  195. data/test/hexapdf/layout/test_inline_box.rb +17 -3
  196. data/test/hexapdf/layout/test_list_box.rb +84 -33
  197. data/test/hexapdf/layout/test_page_style.rb +3 -2
  198. data/test/hexapdf/layout/test_style.rb +60 -0
  199. data/test/hexapdf/layout/test_table_box.rb +728 -0
  200. data/test/hexapdf/layout/test_text_box.rb +26 -0
  201. data/test/hexapdf/layout/test_text_fragment.rb +33 -0
  202. data/test/hexapdf/layout/test_text_layouter.rb +36 -5
  203. data/test/hexapdf/test_composer.rb +10 -0
  204. data/test/hexapdf/test_dictionary.rb +10 -0
  205. data/test/hexapdf/test_dictionary_fields.rb +4 -1
  206. data/test/hexapdf/test_document.rb +5 -0
  207. data/test/hexapdf/test_filter.rb +8 -0
  208. data/test/hexapdf/test_importer.rb +9 -0
  209. data/test/hexapdf/test_object.rb +16 -5
  210. data/test/hexapdf/test_stream.rb +7 -0
  211. data/test/hexapdf/test_writer.rb +3 -3
  212. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +13 -5
  213. data/test/hexapdf/type/acro_form/test_form.rb +4 -3
  214. data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
  215. data/test/hexapdf/type/test_catalog.rb +11 -0
  216. data/test/hexapdf/type/test_form.rb +119 -0
  217. data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
  218. data/test/hexapdf/type/test_optional_content_group.rb +158 -0
  219. data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
  220. data/test/hexapdf/type/test_page.rb +20 -6
  221. metadata +28 -8
@@ -54,7 +54,7 @@ module HexaPDF
54
54
  # Field inheritance means that if a field is not set on the page object itself, the value is
55
55
  # taken from the nearest page tree ancestor that has this value set.
56
56
  #
57
- # See: PDF1.7 s7.7.3.3, s7.7.3.4, Pages
57
+ # See: PDF2.0 s7.7.3.3, s7.7.3.4, Pages
58
58
  class Page < Dictionary
59
59
 
60
60
  # The predefined paper sizes in points (1/72 inch):
@@ -223,7 +223,7 @@ module HexaPDF
223
223
  # The art box defines the region of the page's meaningful content as intended by the
224
224
  # author. The default is the crop box.
225
225
  #
226
- # See: PDF1.7 s14.11.2
226
+ # See: PDF2.0 s14.11.2
227
227
  def box(type = :crop, rectangle = nil)
228
228
  if rectangle
229
229
  case type
@@ -261,12 +261,23 @@ module HexaPDF
261
261
  # Rotates the page +angle+ degrees counterclockwise where +angle+ has to be a multiple of 90.
262
262
  #
263
263
  # Positive values rotate the page to the left, negative values to the right. If +flatten+ is
264
- # +true+, the rotation is not done via the page's meta data but by "rotating" the canvas
265
- # itself.
264
+ # +true+, the rotation is not done via the page's meta (i.e. the /Rotate key) data but by
265
+ # rotating the canvas itself and all other necessary objects like the various page boxes and
266
+ # annotations.
266
267
  #
267
- # Note that the :Rotate key of a page object describes the angle in a clockwise orientation
268
- # but this method uses counterclockwise rotation to be consistent with other rotation methods
269
- # (e.g. HexaPDF::Content::Canvas#rotate).
268
+ # Notes:
269
+ #
270
+ # * The given +angle+ is applied in addition to a possibly already existing rotation
271
+ # (specified via the /Rotate key) and does not replace it.
272
+ #
273
+ # * Specifying 0 for +angle+ is valid and means that no additional rotation should be applied.
274
+ # The only meaningful usage of 0 for +angle+ is when +flatten+ is set to +true+ (so that the
275
+ # /Rotate key is removed and the existing rotation information incorporated into the canvas,
276
+ # page boxes and annotations).
277
+ #
278
+ # * The /Rotate key of a page object describes the angle in a clockwise orientation but this
279
+ # method uses counterclockwise rotation to be consistent with other rotation methods (e.g.
280
+ # HexaPDF::Content::Canvas#rotate).
270
281
  def rotate(angle, flatten: false)
271
282
  if angle % 90 != 0
272
283
  raise ArgumentError, "Page rotation has to be multiple of 90 degrees"
@@ -423,7 +434,7 @@ module HexaPDF
423
434
  #
424
435
  # To check whether the origin has been translated or not, use
425
436
  #
426
- # canvas.graphics_state.ctm.evaluate(0, 0)
437
+ # canvas.pos(0, 0)
427
438
  #
428
439
  # and check whether the result is [0, 0]. If it is, then the origin has not been
429
440
  # translated.
@@ -522,8 +533,8 @@ module HexaPDF
522
533
  # Yields each annotation of this page.
523
534
  def each_annotation
524
535
  return to_enum(__method__) unless block_given?
525
- self[:Annots]&.each do |annotation|
526
- next unless annotation
536
+ Array(self[:Annots]).each do |annotation|
537
+ next unless annotation&.key?(:Subtype) && annotation&.key?(:Rect)
527
538
  yield(document.wrap(annotation, type: :Annot))
528
539
  end
529
540
  self
@@ -539,14 +550,18 @@ module HexaPDF
539
550
  # If an annotation is a form field widget, only the widget will be deleted but not the form
540
551
  # field itself.
541
552
  def flatten_annotations(annotations = self[:Annots])
542
- not_flattened = (annotations || []).to_ary
553
+ not_flattened = Array(annotations) || []
543
554
  return not_flattened unless key?(:Annots)
544
555
 
545
- annotations = not_flattened & self[:Annots] if annotations != self[:Annots]
556
+ annotations = if annotations == self[:Annots]
557
+ not_flattened
558
+ else
559
+ not_flattened & Array(self[:Annots])
560
+ end
546
561
  return not_flattened if annotations.empty?
547
562
 
548
563
  canvas = self.canvas(type: :overlay)
549
- if (pos = canvas.graphics_state.ctm.evaluate(0, 0)) != [0, 0]
564
+ if (pos = canvas.pos(0, 0)) != [0, 0]
550
565
  canvas.save_graphics_state
551
566
  canvas.translate(-pos[0], -pos[1])
552
567
  end
@@ -554,6 +569,11 @@ module HexaPDF
554
569
  to_delete = []
555
570
  not_flattened -= annotations
556
571
  annotations.each do |annotation|
572
+ unless annotation&.key?(:Subtype) && annotation&.key?(:Rect)
573
+ to_delete << annotation if annotation
574
+ next
575
+ end
576
+
557
577
  annotation = document.wrap(annotation, type: :Annot)
558
578
  appearance = annotation.appearance
559
579
  if annotation.flagged?(:hidden) || annotation.flagged?(:invisible)
@@ -567,8 +587,8 @@ module HexaPDF
567
587
  rect = annotation[:Rect]
568
588
  box = appearance.box
569
589
 
570
- # PDF1.7 12.5.5 algorithm
571
- # Step a) Calculate smallest rectangle containing transformed bounding box
590
+ # PDF2.0 12.5.5 algorithm
591
+ # Step 1) Calculate smallest rectangle containing transformed bounding box
572
592
  matrix = HexaPDF::Content::TransformationMatrix.new(*appearance[:Matrix].value)
573
593
  llx, lly = matrix.evaluate(box.left, box.bottom)
574
594
  ulx, uly = matrix.evaluate(box.left, box.top)
@@ -582,14 +602,19 @@ module HexaPDF
582
602
  next
583
603
  end
584
604
 
585
- # Step b) Fit calculated rectangle to annotation rectangle by translating/scaling
586
- a = HexaPDF::Content::TransformationMatrix.new
587
- a.translate(rect.left - left, rect.bottom - bottom)
588
- a.scale(rect.width.fdiv(right - left), rect.height.fdiv(top - bottom))
605
+ # Step 2) Fit calculated rectangle to annotation rectangle by translating/scaling
606
+
607
+ # The final matrix is composed by translating the bottom-left corner of the transformed
608
+ # bounding box to the bottom-left corner of the annotation rectangle and scaling from the
609
+ # bottom-left corner of the transformed bounding box.
610
+ sx = rect.width.fdiv(right - left)
611
+ sy = rect.height.fdiv(top - bottom)
612
+ tx = rect.left - left + left - left * sx
613
+ ty = rect.bottom - bottom + bottom - bottom * sy
589
614
 
590
- # Step c) Premultiply form matrix - done implicitly when drawing the XObject
615
+ # Step 3) Premultiply form matrix - done implicitly when drawing the XObject
591
616
 
592
- canvas.transform(*a) do
617
+ canvas.transform(sx, 0, 0, sy, tx, ty) do
593
618
  # Use [box.left, box.bottom] to counter default translation in #xobject since that
594
619
  # is already taken care of in matrix a
595
620
  canvas.xobject(appearance, at: [box.left, box.bottom])
@@ -70,7 +70,7 @@ module HexaPDF
70
70
  #
71
71
  # "", "", "", ... (i.e. always the empty string)
72
72
  #
73
- # See: PDF1.7 s12.4.2, HexaPDF::Document::Pages, HexaPDF::Type::Catalog
73
+ # See: PDF2.0 s12.4.2, HexaPDF::Document::Pages, HexaPDF::Type::Catalog
74
74
  class PageLabel < Dictionary
75
75
 
76
76
  define_type :PageLabel
@@ -103,8 +103,7 @@ module HexaPDF
103
103
  end
104
104
  end
105
105
 
106
- # :nodoc:
107
- NUMBERING_STYLE_MAPPING = {
106
+ NUMBERING_STYLE_MAPPING = { # :nodoc:
108
107
  decimal: :D, D: :D,
109
108
  uppercase_roman: :R, R: :R,
110
109
  lowercase_roman: :r, r: :r,
@@ -113,8 +112,7 @@ module HexaPDF
113
112
  none: nil
114
113
  }
115
114
 
116
- # :nodoc:
117
- REVERSE_NUMBERING_STYLE_MAPPING = Hash[*NUMBERING_STYLE_MAPPING.flatten.reverse]
115
+ REVERSE_NUMBERING_STYLE_MAPPING = Hash[*NUMBERING_STYLE_MAPPING.flatten.reverse] # :nodoc:
118
116
 
119
117
  # :call-seq:
120
118
  # page_label.numbering_style -> numbering_style
@@ -174,8 +172,7 @@ module HexaPDF
174
172
 
175
173
  private
176
174
 
177
- # :nodoc:
178
- ALPHABET = ('A'..'Z').to_a
175
+ ALPHABET = ('A'..'Z').to_a # :nodoc:
179
176
 
180
177
  # Maps the given number to uppercase (or, if +lowercase+ is +true+, lowercase) letters (e.g. 1
181
178
  # -> A, 27 -> AA, 28 -> AB, ...).
@@ -188,8 +185,7 @@ module HexaPDF
188
185
  lowercase ? result.downcase : result
189
186
  end
190
187
 
191
- # :nodoc:
192
- ROMAN_NUMERAL_MAPPING = {
188
+ ROMAN_NUMERAL_MAPPING = { # :nodoc:
193
189
  1000 => "M",
194
190
  900 => "CM",
195
191
  500 => "D",
@@ -60,7 +60,7 @@ module HexaPDF
60
60
  # value. If an inherited /Resources dictionary does *not* exist, an empty one is created for
61
61
  # the page.
62
62
  #
63
- # See: PDF1.7 s7.7.3.2, Page
63
+ # See: PDF2.0 s7.7.3.2, Page
64
64
  class PageTreeNode < Dictionary
65
65
 
66
66
  define_type :Pages
@@ -44,7 +44,7 @@ module HexaPDF
44
44
 
45
45
  # Represents the resources needed by a content stream.
46
46
  #
47
- # See: PDF1.7 s7.8.3
47
+ # See: PDF2.0 s7.8.3
48
48
  class Resources < Dictionary
49
49
 
50
50
  define_type :XXResources
@@ -54,7 +54,7 @@ module HexaPDF
54
54
  # HexaPDF::Revision object's trailer dictionary is always of this type. Only when a
55
55
  # cross-reference stream is written is the trailer integrated into the stream's dictionary.
56
56
  #
57
- # See: PDF1.7 s7.5.5, s14.4; XRefStream
57
+ # See: PDF2.0 s7.5.5, s14.4; XRefStream
58
58
  class Trailer < Dictionary
59
59
 
60
60
  define_type :XXTrailer
@@ -79,7 +79,7 @@ module HexaPDF
79
79
 
80
80
  # Sets the /ID field to an array of two copies of a random string and returns this array.
81
81
  #
82
- # See: PDF1.7 14.4
82
+ # See: PDF2.0 14.4
83
83
  def set_random_id
84
84
  value[:ID] = [Digest::MD5.digest(rand.to_s)] * 2
85
85
  end
@@ -44,7 +44,7 @@ module HexaPDF
44
44
  #
45
45
  # This dictionary is linked via the /ViewerPreferences entry from the Type::Catalog.
46
46
  #
47
- # See: PDF1.7 s12.2, Catalog
47
+ # See: PDF2.0 s12.2, Catalog
48
48
  class ViewerPreferences < Dictionary
49
49
 
50
50
  define_type :XXViewerPreferences
@@ -65,7 +65,7 @@ module HexaPDF
65
65
  # cross-reference section and trailer information and then written. Otherwise a normal
66
66
  # cross-reference section plus trailer are written.
67
67
  #
68
- # See: PDF1.7 s7.5.8
68
+ # See: PDF2.0 s7.5.8
69
69
  class XRefStream < HexaPDF::Stream
70
70
 
71
71
  define_type :XRef
@@ -161,7 +161,7 @@ module HexaPDF
161
161
  when TYPE_COMPRESSED
162
162
  xref.add_compressed_entry(oid, field2, field3)
163
163
  else
164
- nil # Ignore entry as per PDF1.7 s7.5.8.3
164
+ nil # Ignore entry as per PDF2.0 s7.5.8.3
165
165
  end
166
166
  start_pos = end_pos
167
167
  end
data/lib/hexapdf/type.rb CHANGED
@@ -76,6 +76,10 @@ module HexaPDF
76
76
  autoload(:OutlineItem, 'hexapdf/type/outline_item')
77
77
  autoload(:PageLabel, 'hexapdf/type/page_label')
78
78
  autoload(:MarkInformation, 'hexapdf/type/mark_information')
79
+ autoload(:OptionalContentGroup, 'hexapdf/type/optional_content_group')
80
+ autoload(:OptionalContentMembership, 'hexapdf/type/optional_content_membership')
81
+ autoload(:OptionalContentProperties, 'hexapdf/type/optional_content_properties')
82
+ autoload(:OptionalContentConfiguration, 'hexapdf/type/optional_content_configuration')
79
83
 
80
84
  end
81
85
 
@@ -49,10 +49,9 @@ module HexaPDF
49
49
  # When a PDF file is written, text strings are automatically encoded in either PDFDocEncoding
50
50
  # or UTF-16BE depending on the characters in the text string.
51
51
  #
52
- # See: PDF1.7 s7.9.2, D.1, D.3
52
+ # See: PDF2.0 s7.9.2, D.1, D.3
53
53
  module PDFDocEncoding
54
54
 
55
- # :nodoc:
56
55
  CHARACTER_MAP = %W[\uFFFD \uFFFD \uFFFD \uFFFD \uFFFD \uFFFD \uFFFD \uFFFD
57
56
  \uFFFD \u0009 \u000A \uFFFD \uFFFD \u000D \uFFFD \uFFFD
58
57
  \uFFFD \uFFFD \uFFFD \uFFFD \uFFFD \uFFFD \uFFFD \uFFFD
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.32.2'
40
+ VERSION = '0.34.0'
41
41
 
42
42
  end
@@ -124,7 +124,7 @@ module HexaPDF
124
124
 
125
125
  # Writes the PDF file header.
126
126
  #
127
- # See: PDF1.7 s7.5.2
127
+ # See: PDF2.0 s7.5.2
128
128
  def write_file_header
129
129
  @io << "%PDF-#{@document.version}\n%\xCF\xEC\xFF\xE8\xD7\xCB\xCD\n"
130
130
  end
@@ -225,7 +225,7 @@ module HexaPDF
225
225
 
226
226
  # Writes the cross-reference section.
227
227
  #
228
- # See: PDF1.7 s7.5.4
228
+ # See: PDF2.0 s7.5.4
229
229
  def write_xref_section(xref_section)
230
230
  @io << "xref\n"
231
231
  xref_section.each_subsection do |entries|
@@ -245,14 +245,14 @@ module HexaPDF
245
245
 
246
246
  # Writes the trailer dictionary.
247
247
  #
248
- # See: PDF1.7 s7.5.5
248
+ # See: PDF2.0 s7.5.5
249
249
  def write_trailer(trailer)
250
250
  @io << "trailer\n#{@serializer.serialize(trailer)}\n"
251
251
  end
252
252
 
253
253
  # Writes the startxref line needed for cross-reference sections and cross-reference streams.
254
254
  #
255
- # See: PDF1.7 s7.5.5, s7.5.8
255
+ # See: PDF2.0 s7.5.5, s7.5.8
256
256
  def write_startxref(startxref)
257
257
  @io << "startxref\n#{startxref}\n%%EOF\n"
258
258
  end
@@ -46,7 +46,7 @@ module HexaPDF
46
46
  #
47
47
  # Note that a cross-reference section may contain a single object number only once.
48
48
  #
49
- # See: HexaPDF::Revision, PDF1.7 s7.5.4, s7.5.8
49
+ # See: HexaPDF::Revision, PDF2.0 s7.5.4, s7.5.8
50
50
  class XRefSection < Utils::ObjectHash
51
51
 
52
52
  # One entry of a cross-reference section or stream.
@@ -69,7 +69,7 @@ module HexaPDF
69
69
  #
70
70
  # Objects in an object stream always have a generation number of 0!
71
71
  #
72
- # See: PDF1.7 s7.5.4, s7.5.8
72
+ # See: PDF2.0 s7.5.4, s7.5.8
73
73
  Entry = Struct.new(:type, :oid, :gen, :pos, :objstm) do
74
74
  def free?
75
75
  type == :free
@@ -16,13 +16,15 @@ describe HexaPDF::Content::GraphicObject::EndpointArc do
16
16
  assert_equal(0, arc.inclination)
17
17
  assert(arc.large_arc)
18
18
  refute(arc.clockwise)
19
+ assert_nil(arc.max_curves)
19
20
  end
20
21
  end
21
22
 
22
23
  describe "configure" do
23
24
  it "changes the values" do
24
25
  arc = HexaPDF::Content::GraphicObject::EndpointArc.new
25
- arc.configure(x: 1, y: 2, a: 3, b: 4, inclination: 5, large_arc: false, clockwise: true)
26
+ arc.configure(x: 1, y: 2, a: 3, b: 4, inclination: 5, large_arc: false, clockwise: true,
27
+ max_curves: 8)
26
28
  assert_equal(1, arc.x)
27
29
  assert_equal(2, arc.y)
28
30
  assert_equal(3, arc.a)
@@ -30,6 +32,7 @@ describe HexaPDF::Content::GraphicObject::EndpointArc do
30
32
  assert_equal(5, arc.inclination)
31
33
  refute(arc.large_arc)
32
34
  assert(arc.clockwise)
35
+ assert_equal(8, arc.max_curves)
33
36
  end
34
37
  end
35
38
 
@@ -94,5 +97,12 @@ describe HexaPDF::Content::GraphicObject::EndpointArc do
94
97
  clockwise: false)
95
98
  assert_equal(arc_data, @page.contents)
96
99
  end
100
+
101
+ it "assigns the max curves to the generated arc" do
102
+ arc = HexaPDF::Content::GraphicObject::EndpointArc.new
103
+ arc.configure(a: 1, b: 1, x: -1, y: 0, max_curves: 10)
104
+ hash = arc.send(:compute_arc_values, 1, 0)
105
+ assert_equal(10, hash[:max_curves])
106
+ end
97
107
  end
98
108
  end
@@ -51,6 +51,13 @@ describe HexaPDF::Content::GraphicObject::Geom2D do
51
51
  [[:move_to, [5, 6]], [:line_to, [10, 11]], [:stroke_path]])
52
52
  end
53
53
 
54
+ it "draws a Geom2D::Rectangle onto the canvas" do
55
+ @obj.object = Geom2D::Rectangle(5, 6, 20, 50)
56
+ @obj.draw(@canvas)
57
+ assert_operators(@canvas.contents,
58
+ [[:append_rectangle, [5, 6, 20, 50]], [:stroke_path]])
59
+ end
60
+
54
61
  it "draws a Geom2D::Polygon onto the canvas" do
55
62
  @obj.object = Geom2D::Polygon([5, 6], [10, 11], [7, 9])
56
63
  @obj.draw(@canvas)
@@ -71,6 +71,13 @@ describe HexaPDF::Content::Canvas do
71
71
  end
72
72
  end
73
73
 
74
+ describe "pos" do
75
+ it "returns the transformed position" do
76
+ @canvas.translate(9, 4)
77
+ assert_equal([10, 5], @canvas.pos(1, 1))
78
+ end
79
+ end
80
+
74
81
  describe "save_graphics_state" do
75
82
  it "invokes the operator implementation" do
76
83
  assert_operator_invoked(:q) { @canvas.save_graphics_state }
@@ -1193,7 +1200,6 @@ describe HexaPDF::Content::Canvas do
1193
1200
  assert_operators(@canvas.contents, [[:set_font_and_size, [:F1, 10]],
1194
1201
  [:begin_text],
1195
1202
  [:set_text_matrix, [1, 0, 0, 1, 100, 100]],
1196
- [:set_leading, [10]],
1197
1203
  [:show_text_with_positioning, [["Hallo"]]]])
1198
1204
  end
1199
1205
 
@@ -1281,6 +1287,48 @@ describe HexaPDF::Content::Canvas do
1281
1287
  end
1282
1288
  end
1283
1289
 
1290
+ describe "optional_content" do
1291
+ it "invokes the marked-sequence operator implementation" do
1292
+ assert_operator_invoked(:BDC, :OC, :P1) { @canvas.optional_content('Test') }
1293
+ end
1294
+
1295
+ it "is serialized correctly when no block is used" do
1296
+ @canvas.optional_content('Test')
1297
+ assert_operators(@canvas.contents, [[:begin_marked_content_with_property_list, [:OC, :P1]]])
1298
+ end
1299
+
1300
+ it "is serialized correctly when a block is used" do
1301
+ @canvas.optional_content('Test') {}
1302
+ assert_operators(@canvas.contents, [[:begin_marked_content_with_property_list, [:OC, :P1]],
1303
+ [:end_marked_content]])
1304
+ end
1305
+
1306
+ it "uses the provided OCG dictionary" do
1307
+ ocg = @doc.optional_content.add_ocg('Test')
1308
+ @canvas.optional_content(ocg)
1309
+ assert_equal(ocg, @page.resources.property_list(:P1))
1310
+ end
1311
+
1312
+ it "uses an existing OCG specified by name" do
1313
+ ocg = @doc.optional_content.add_ocg('Test')
1314
+ @canvas.optional_content('Test')
1315
+ assert_equal(ocg, @page.resources.property_list(:P1))
1316
+ end
1317
+
1318
+ it "creates an OCG if the named one doesn't yet exist" do
1319
+ @canvas.optional_content('Test')
1320
+ assert_equal(@doc.optional_content.ocg('Test'), @page.resources.property_list(:P1))
1321
+ end
1322
+
1323
+ it "always creates a new OCG if use_existing_ocg is false" do
1324
+ ocg = @doc.optional_content.add_ocg('Test')
1325
+ @canvas.optional_content('Test', use_existing_ocg: false)
1326
+ pl_item = @page.resources.property_list(:P1)
1327
+ refute_equal(ocg, pl_item)
1328
+ assert_equal(@doc.optional_content.ocgs.last, pl_item)
1329
+ end
1330
+ end
1331
+
1284
1332
  describe "color_from_specification "do
1285
1333
  it "accepts a color string" do
1286
1334
  assert_equal([1, 0, 0], @canvas.color_from_specification("red").components)
@@ -107,6 +107,28 @@ describe HexaPDF::DigitalSignature::Signatures do
107
107
  assert_equal(1, field.each_widget.count)
108
108
  end
109
109
 
110
+ it "creates an empty widget on the first page for the signature field if necessary" do
111
+ @doc.pages.add
112
+ field = @doc.acro_form(create: true).create_signature_field('Signature2')
113
+ field.field_value = sig = @doc.add({Type: :Sig, key: :value})
114
+ @doc.signatures.add(@io, @handler, signature: sig)
115
+ widgets = field.each_widget.to_a
116
+ assert_equal(1, widgets.size)
117
+ assert_equal(@doc.pages[0], widgets[0][:P])
118
+ assert_equal([0, 0, 0, 0], widgets[0][:Rect])
119
+ end
120
+
121
+ it "handles a bug in Adobe Acrobat related to images not showing without a /Resources entry" do
122
+ field = @doc.acro_form(create: true).create_signature_field('Signature')
123
+ image = @doc.add({Type: :XObject, Subtype: :Image, Width: 1, Height: 1, ColorSpace: :DeviceGray,
124
+ BitsPerComponent: 8}, stream: 'A')
125
+ field.create_widget(@doc.pages[0], Rect: [0, 0, 100, 100]).create_appearance.
126
+ canvas.xobject(image, at: [0, 0])
127
+ @doc.signatures.add(@io, @handler, signature: field)
128
+ assert(image.key?(:Resources))
129
+ assert_equal({}, image[:Resources])
130
+ end
131
+
110
132
  it "handles different xref section types correctly when determing the offsets" do
111
133
  @doc.delete(7)
112
134
  sig = @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
@@ -55,8 +55,8 @@ describe HexaPDF::Document::Files do
55
55
  spec2 = @doc.add({Type: :Filespec})
56
56
  @doc.pages.add # page without annot
57
57
  @doc.pages.add[:Annots] = [
58
- {Subtype: :FileAttachment, FS: HexaPDF::Reference.new(spec1.oid, spec1.gen)},
59
- {Subtype: :FileAttachment, FS: spec2},
58
+ {Subtype: :FileAttachment, Rect: [0, 0, 0, 0], FS: HexaPDF::Reference.new(spec1.oid, spec1.gen)},
59
+ {Subtype: :FileAttachment, Rect: [0, 0, 0, 0], FS: spec2},
60
60
  {},
61
61
  ]
62
62
  assert_equal([spec1, spec2], @doc.files.to_a)
@@ -59,6 +59,54 @@ describe HexaPDF::Document::Layout::ChildrenCollector do
59
59
  end
60
60
  end
61
61
 
62
+ describe HexaPDF::Document::Layout::CellArgumentCollector do
63
+ before do
64
+ @args = HexaPDF::Document::Layout::CellArgumentCollector.new(20, 10)
65
+ end
66
+
67
+ describe "[]" do
68
+ def check_argument_info(info, rows, cols, args)
69
+ assert_equal(rows, info.rows)
70
+ assert_equal(cols, info.cols)
71
+ assert_equal(args, info.args)
72
+ end
73
+
74
+ it "allows assigning to all cells" do
75
+ @args[] = {key: :value}
76
+ check_argument_info(@args.argument_infos.first, 0..19, 0..9, {key: :value})
77
+ end
78
+
79
+ it "allows assigning to all columns of a range of rows" do
80
+ @args[1..4] = {key: :value}
81
+ check_argument_info(@args.argument_infos.first, 1..4, 0..9, {key: :value})
82
+ end
83
+
84
+ it "allows assigning to the intersection of a range of rows with a range of columns" do
85
+ @args[1..4, 3..5] = {key: :value}
86
+ check_argument_info(@args.argument_infos.first, 1..4, 3..5, {key: :value})
87
+ end
88
+
89
+ it "allows selecting a single row or column" do
90
+ @args[1, 3] = {key: :value}
91
+ check_argument_info(@args.argument_infos.first, 1..1, 3..3, {key: :value})
92
+ end
93
+
94
+ it "allows using negative indices" do
95
+ @args[-3..-1, -5..-2] = {key: :value}
96
+ check_argument_info(@args.argument_infos.first, 17..19, 5..8, {key: :value})
97
+ end
98
+ end
99
+
100
+ describe "retrieve_arguments_for" do
101
+ it "merges all argument hashes, with later defined ones overridding prior ones" do
102
+ @args[] = {key: :value, a: :b}
103
+ @args[3..7] = {a: :c}
104
+ @args[5, 6] = {e: :f}
105
+ assert_equal({key: :value, a: :c, e: :f}, @args.retrieve_arguments_for(5, 6))
106
+ end
107
+ end
108
+ end
109
+
62
110
  describe HexaPDF::Document::Layout do
63
111
  before do
64
112
  @doc = HexaPDF::Document.new
@@ -171,6 +219,10 @@ describe HexaPDF::Document::Layout do
171
219
  box = @layout.text_box("Test", box_style: :named)
172
220
  assert_equal(20, box.style.font_size)
173
221
  end
222
+
223
+ it "raises an error if the to-be-used style doesn't exist" do
224
+ assert_raises(HexaPDF::Error) { @layout.text_box("Test", style: :unknown) }
225
+ end
174
226
  end
175
227
 
176
228
  describe "formatted_text" do
@@ -257,9 +309,10 @@ describe HexaPDF::Document::Layout do
257
309
 
258
310
  it "allows creating an inline box through a hash with a :box key" do
259
311
  block = lambda {|item| item.box(:base, width: 5, height: 15) }
260
- box = @layout.formatted_text_box([{box: :list, width: 10, block: block}])
312
+ box = @layout.formatted_text_box([{box: :column, columns: 1, width: 100, block: block}])
261
313
  ibox = box.instance_variable_get(:@items).first
262
- assert_equal(10, ibox.width)
314
+ ibox.fit_wrapped_box(nil)
315
+ assert_equal(100, ibox.width)
263
316
  assert_equal(15, ibox.height)
264
317
  end
265
318
 
@@ -290,6 +343,56 @@ describe HexaPDF::Document::Layout do
290
343
  end
291
344
  end
292
345
 
346
+ describe "table_box" do
347
+ it "creates a table box" do
348
+ box = @layout.table_box([['m']], column_widths: [100], header: proc { [['a']] },
349
+ footer: proc { [['b']] }, cell_style: {background_color: "red"},
350
+ width: 100, height: 300, style: {background_color: "blue"},
351
+ properties: {key: :value}, border: {width: 1})
352
+ assert_equal(100, box.width)
353
+ assert_equal(300, box.height)
354
+ assert_equal("blue", box.style.background_color)
355
+ assert_equal(1, box.style.border.width.left)
356
+ assert_equal({key: :value}, box.properties)
357
+ assert_equal(HexaPDF::Layout::TextBox, box.cells[0, 0].children.class)
358
+ assert_equal([100], box.column_widths)
359
+ assert_equal('a', box.header_cells[0, 0].children)
360
+ assert_equal('b', box.footer_cells[0, 0].children)
361
+ end
362
+
363
+ it "doesn't modify the children of cells if they are already in the correct form" do
364
+ image_path = File.join(TEST_DATA_DIR, 'images', 'gray.jpg')
365
+ cell0 = @layout.text('a')
366
+ cell1 = [@layout.text('b'), @layout.image(image_path)]
367
+ box = @layout.table_box([[cell0, cell1]])
368
+ assert_same(cell0, box.cells[0, 0].children)
369
+ assert_same(cell1, box.cells[0, 1].children)
370
+ end
371
+
372
+ it "converts cells containing other than Box and Array instances" do
373
+ box = @layout.table_box([['a', 5]])
374
+ assert_kind_of(HexaPDF::Layout::TextBox, box.cells[0, 0].children)
375
+ assert_kind_of(HexaPDF::Layout::TextBox, box.cells[0, 1].children)
376
+ end
377
+
378
+ it "allows customizing the creation arguments" do
379
+ box = @layout.table_box([['a']]) do |args|
380
+ args[] = {font_size: 20}
381
+ end
382
+ assert_equal(20, box.cells[0, 0].children.style.font_size)
383
+ refute_equal(20, box.cells[0, 0].style.font_size)
384
+ end
385
+
386
+ it "allows styling table cells themselves" do
387
+ box = @layout.table_box([['a', @layout.text('b')]]) do |args|
388
+ args[] = {cell: {background_color: "green"}}
389
+ end
390
+ assert_equal('green', box.cells[0, 0].style.background_color)
391
+ assert_nil(box.cells[0, 0].children.style.background_color)
392
+ assert_equal('green', box.cells[0, 1].style.background_color)
393
+ end
394
+ end
395
+
293
396
  describe "lorem_ipsum_box" do
294
397
  it "creates a standard lorem ipsum box" do
295
398
  box = @layout.lorem_ipsum_box(width: 10, height: 15, font_size: 15)