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
@@ -0,0 +1,83 @@
1
+ # # Composer - Optional Content
2
+ #
3
+ # This example shows how to use the optional content feature to create a quiz
4
+ # where the answers can be individually shown and hidden. There is also a link
5
+ # after the questions to toggle all answers.
6
+ #
7
+ # Note: To provide the "All answers" layer switch functionality we need to make
8
+ # use of optional content membership dictionaries. However, this PDF feature is
9
+ # not supported by all PDF viewers. To enable the "All answers" switch in this
10
+ # example, use `a1m`, `a2m`, and `a3m` instead of `a1`, `a2`, and `a3` when
11
+ # defining the optional content for a box.
12
+ #
13
+ # Usage:
14
+ # : `ruby composer_optional_content.rb`
15
+ #
16
+ require 'hexapdf'
17
+
18
+ HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
19
+ composer.style(:question, font_size: 16, margin: [0, 0, 16], fill_color: 'hp-blue')
20
+ composer.style(:answer, font: 'ZapfDingbats', fill_color: "green")
21
+
22
+ all = composer.document.optional_content.ocg('All answers')
23
+ a1 = composer.document.optional_content.ocg('Answer 1')
24
+ a1m = composer.document.optional_content.create_ocmd([a1, all], policy: :any_on)
25
+ a2 = composer.document.optional_content.ocg('Answer 2')
26
+ a2m = composer.document.optional_content.create_ocmd([a2, all], policy: :any_on)
27
+ a3 = composer.document.optional_content.ocg('Answer 3')
28
+ a3m = composer.document.optional_content.create_ocmd([a3, all], policy: :any_on)
29
+
30
+ composer.text('The Great Ruby Quiz', align: :center, margin: [0, 0, 24],
31
+ font: ['Helvetica', variant: :bold], font_size: 24)
32
+
33
+ composer.list(item_type: :decimal, item_spacing: 32, style: :question) do |listing|
34
+ listing.multiple do |item|
35
+ item.text('Who created Ruby?', style: :question)
36
+ item.column(columns: 3, gaps: 5) do |cols|
37
+ cols.list(item_type: :decimal) do |answers|
38
+ answers.text('Guido van Rossum')
39
+ answers.multiple do |answer|
40
+ answer.text('Yukihiro “Matz” Matsumoto', position: :float)
41
+ answer.text("\u{a0}\u{a0}4", style: :answer,
42
+ properties: {'optional_content' => a1})
43
+ end
44
+ answers.text('Rob Pike')
45
+ end
46
+ end
47
+ end
48
+
49
+ listing.multiple do |item|
50
+ item.text('When was Ruby created?', style: :question)
51
+ item.column(columns: 3, gaps: 5) do |cols|
52
+ cols.list(item_type: :decimal) do |answers|
53
+ answers.text('1991')
54
+ answers.text('1992')
55
+ answers.multiple do |answer|
56
+ answer.text('1993', position: :float)
57
+ answer.text("\u{a0}\u{a0}4", style: :answer,
58
+ properties: {'optional_content' => a2})
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ listing.multiple do |item|
65
+ item.text('What is the best PDF library for Ruby?', style: :question)
66
+ answer = composer.document.layout.text('There are several PDF libraries for ' \
67
+ 'Ruby but the best is HexaPDF! :)',
68
+ width: 400,
69
+ properties: {'optional_content' => a3})
70
+ item.formatted_text([{box: answer}], border: {width: [0, 0, 1]})
71
+ end
72
+ end
73
+
74
+ action = composer.document.wrap({Type: :Action, S: :SetOCGState})
75
+ action.add_state_change(:toggle, [a1, a2, a3])
76
+ composer.text("Click to toggle answers", border: {width: 1, color: "red"},
77
+ position_hint: :right, padding: 2, overlays: [[:link, action: action]])
78
+
79
+ composer.document.optional_content.default_configuration(
80
+ BaseState: :OFF,
81
+ Order: [all, a1, a2, a3],
82
+ )
83
+ end
@@ -43,6 +43,9 @@ require 'hexapdf/font/true_type'
43
43
  module HexaPDF
44
44
  module CLI
45
45
 
46
+ # Raised when problems occur on the CLI side of things.
47
+ class Error < HexaPDF::Error; end
48
+
46
49
  # Base class for all hexapdf commands. It provides utility methods needed by the individual
47
50
  # commands.
48
51
  class Command < CmdParse::Command
@@ -50,9 +53,15 @@ module HexaPDF
50
53
  module Extensions #:nodoc:
51
54
  def help_banner #:nodoc:
52
55
  "hexapdf #{HexaPDF::VERSION} - Versatile PDF Manipulation Tool\n" \
53
- "Copyright (c) 2014-2021 Thomas Leitner; licensed under the AGPLv3\n\n" \
56
+ "Copyright (c) 2014-2023 Thomas Leitner; licensed under the AGPLv3\n\n" \
54
57
  "#{format(usage, indent: 7)}\n\n"
55
58
  end
59
+
60
+ def help #:nodoc:
61
+ super << format("See https://hexapdf.gettalong.org/documentation/hexapdf.1.html " \
62
+ "for the full manual page with examples.", indent: 0)
63
+ end
64
+
56
65
  end
57
66
 
58
67
  include Extensions
@@ -134,7 +143,7 @@ module HexaPDF
134
143
  doc.trailer.update_id
135
144
  doc.validate(auto_correct: true) do |msg, correctable, object|
136
145
  if command_parser.strict && !correctable
137
- raise "Validation error for object (#{object.oid},#{object.gen}): #{msg}"
146
+ raise Error, "Validation error for object (#{object.oid},#{object.gen}): #{msg}"
138
147
  elsif command_parser.verbosity_info?
139
148
  $stderr.puts "#{correctable ? 'Corrected' : 'Ignored'} validation problem " \
140
149
  "for object (#{object.oid},#{object.gen}): #{msg}"
@@ -151,7 +160,7 @@ module HexaPDF
151
160
  # HexaPDF::CLI#force is not set.
152
161
  def maybe_raise_on_existing_file(filename)
153
162
  if !command_parser.force && File.exist?(filename)
154
- raise "Output file '#{filename}' already exists, not overwriting. Use --force to " \
163
+ raise Error, "Output file '#{filename}' already exists, not overwriting. Use --force to " \
155
164
  "force writing"
156
165
  end
157
166
  end
@@ -127,7 +127,7 @@ module HexaPDF
127
127
  page.resources[:Font]&.each(&font_proc)
128
128
  page.resources[:XObject]&.each do |_, xobj|
129
129
  next unless xobj[:Subtype] == :Form
130
- xobj.ressources[:Font]&.each(&font_proc)
130
+ xobj.resources[:Font]&.each(&font_proc)
131
131
  end
132
132
  page.each_annotation do |annotation|
133
133
  appearance = annotation.appearance
@@ -97,7 +97,7 @@ module HexaPDF
97
97
  def execute(in_file, out_file = nil) #:nodoc:
98
98
  maybe_raise_on_existing_file(out_file) if out_file
99
99
  if (@fill || @flatten) && !out_file
100
- raise "Output file missing"
100
+ raise Error, "Output file missing"
101
101
  end
102
102
  with_document(in_file, password: @password, out_file: out_file,
103
103
  incremental: @incremental) do |doc|
@@ -106,7 +106,7 @@ module HexaPDF
106
106
  end
107
107
 
108
108
  if !doc.acro_form
109
- raise "This PDF doesn't contain an interactive form"
109
+ raise Error, "This PDF doesn't contain an interactive form"
110
110
  elsif out_file
111
111
  doc.acro_form[:NeedAppearances] = @need_appearances unless @need_appearances.nil?
112
112
  if @fill || !@flatten
@@ -220,7 +220,7 @@ module HexaPDF
220
220
  form = doc.acro_form
221
221
  data.each do |name, value|
222
222
  field = form.field_by_name(name)
223
- raise "Field '#{name}' not found in input PDF" unless field
223
+ raise Error, "Field '#{name}' not found in input PDF" unless field
224
224
  apply_field_value(field, value)
225
225
  end
226
226
  end
@@ -268,10 +268,10 @@ module HexaPDF
268
268
  when :radio_button
269
269
  field.field_value = value.to_sym
270
270
  else
271
- raise "Field type #{field.concrete_field_type} not yet supported"
271
+ raise Error, "Field type #{field.concrete_field_type} not yet supported"
272
272
  end
273
273
  rescue StandardError
274
- raise "Error while setting '#{field.full_field_name}': #{$!.message}"
274
+ raise Error, "Error while setting '#{field.full_field_name}': #{$!.message}"
275
275
  end
276
276
 
277
277
  # Iterates over all non-push button fields in page order. If a field appears on multiple
@@ -93,11 +93,9 @@ module HexaPDF
93
93
 
94
94
  private
95
95
 
96
- # :nodoc:
97
- COMMAND_LIST = %w[object recursive stream raw-stream xref catalog trailer pages
96
+ COMMAND_LIST = %w[object recursive stream raw-stream xref catalog trailer pages # :nodoc:
98
97
  page-count search quit help]
99
- # :nodoc:
100
- RELINE_COMPLETION_PROC = proc do |s|
98
+ RELINE_COMPLETION_PROC = proc do |s| # :nodoc:
101
99
  if s.empty?
102
100
  COMMAND_DESCRIPTIONS.map {|cmd, desc| cmd.ljust(35) << desc }
103
101
  else
@@ -266,11 +264,11 @@ module HexaPDF
266
264
  # Resolves the PDF object from the given string reference and returns it.
267
265
  def pdf_object_from_string_reference(str)
268
266
  if str.nil?
269
- raise "Error: Missing argument object identifier OID[,GEN]"
267
+ raise Error, "Error: Missing argument object identifier OID[,GEN]"
270
268
  elsif !str.match?(/^\d+(,\d+)?$/)
271
- raise "Error: Invalid argument: Must be of form OID[,GEN], not '#{str}'"
269
+ raise Error, "Error: Invalid argument: Must be of form OID[,GEN], not '#{str}'"
272
270
  elsif !(obj = @doc.object(pdf_reference_from_string(str)))
273
- raise "Error: No object with the given object identifier '#{str}' found"
271
+ raise Error, "Error: No object with the given object identifier '#{str}' found"
274
272
  else
275
273
  obj
276
274
  end
@@ -51,17 +51,18 @@ module HexaPDF
51
51
  # On creation a HexaPDF::Document object is created as well the first page and an accompanying
52
52
  # HexaPDF::Layout::Frame object. The frame is used by the various methods for general document
53
53
  # layout tasks, like positioning of text, images, and so on. By default, it covers the whole page
54
- # except the margin area. How the frame gets created can be customized by overriding the
55
- # #create_frame method.
54
+ # except the margin area. How the frame gets created can be customized by defining a custom page
55
+ # style, see #page_style. Use the +skip_page_creation+ argument to avoid the initial page
56
+ # creation when creating a Composer instance.
56
57
  #
57
58
  # Once the Composer object is created, its methods can be used to draw text, images, ... on the
58
- # page. Behind the scenes HexaPDF::Layout::Box (and subclass) objects are created and drawn on the
59
- # page via the frame.
59
+ # page. Behind the scenes HexaPDF::Layout::Box (and subclass) objects are created using the
60
+ # HexaPDF::Document::Layout methods and drawn on the page via the frame.
60
61
  #
61
62
  # If the frame of a page is full and a box doesn't fit anymore, a new page is automatically
62
63
  # created. The box is either split into two boxes where one fits on the first page and the other
63
64
  # on the new page, or it is drawn completely on the new page. A new page can also be created by
64
- # calling the #new_page method.
65
+ # calling the #new_page method, optionally providing a page style.
65
66
  #
66
67
  # The #x and #y methods provide the point where the next box would be drawn if it fits the
67
68
  # available space. This information can be used, for example, for custom drawing operations
@@ -75,30 +76,36 @@ module HexaPDF
75
76
  #
76
77
  # == Example
77
78
  #
78
- # HexaPDF::Composer.create('output.pdf', margin: 36) do |pdf|
79
- # pdf.base_style.font_size(20).align(:center)
79
+ # #>pdf-full
80
+ # HexaPDF::Composer.create('out.pdf', page_size: :A6, margin: 36) do |pdf|
81
+ # pdf.style(:base, font_size: 20, align: :center)
80
82
  # pdf.text("Hello World", valign: :center)
81
83
  # end
84
+ #
85
+ # See: HexaPDF::Document::Layout, HexaPDF::Layout::Frame, HexaPDF::Layout::Box
82
86
  class Composer
83
87
 
84
- # Creates a new PDF document and writes it to +output+. The +options+ are passed to ::new.
88
+ # Creates a new PDF document and writes it to +output+. The argument +options+ and +block+ are
89
+ # passed to ::new.
85
90
  #
86
91
  # Example:
87
92
  #
88
- # HexaPDF::Composer.create('output.pdf', margin: 36) do |pdf|
93
+ # HexaPDF::Composer.create('out.pdf', margin: 36) do |pdf|
89
94
  # ...
90
95
  # end
91
96
  def self.create(output, **options, &block)
92
97
  new(**options, &block).write(output)
93
98
  end
94
99
 
95
- # The PDF document that is created.
100
+ # The PDF document (HexaPDF::Document) that is created.
96
101
  attr_reader :document
97
102
 
98
103
  # The current page (a HexaPDF::Type::Page object).
99
104
  attr_reader :page
100
105
 
101
- # The Content::Canvas of the current page. Can be used to perform arbitrary drawing operations.
106
+ # The canvas instance (a Content::Canvas object) of the current page.
107
+ #
108
+ # Can be used to perform arbitrary drawing operations.
102
109
  attr_reader :canvas
103
110
 
104
111
  # The HexaPDF::Layout::Frame for automatic box placement.
@@ -107,43 +114,53 @@ module HexaPDF
107
114
  # Creates a new Composer object and optionally yields it to the given block.
108
115
  #
109
116
  # skip_page_creation::
110
- # If this argument is +true+ (the default), the arguments +page_size+, +page_orientation+
111
- # and +margin+ are used to create a page style with the name :default and an initial page is
112
- # created as well.
117
+ # If this argument is +false+ (the default), the arguments +page_size+, +page_orientation+
118
+ # and +margin+ are used to create a page style with the name :default. Additionally, an
119
+ # initial page/frame is created using this page style.
113
120
  #
114
- # Otherwise, i.e. when this argument is +false+, no initial page or default page style is
115
- # created. This has to be done manually using the #page_style and #new_page methods.
121
+ # Otherwise, i.e. when this argument is +true+, no initial page or default page style is
122
+ # created. This is useful when the first page needs a custom page style. The #page_style
123
+ # method needs to be used to define a page style which is then used with the #new_page
124
+ # method to create the initial page/frame.
116
125
  #
117
126
  # page_size::
118
127
  # Can be any valid predefined page size (see Type::Page::PAPER_SIZE) or an array [llx, lly,
119
128
  # urx, ury] specifying a custom page size.
120
129
  #
130
+ # Only used if +skip_page_creation+ is +false+.
131
+ #
121
132
  # page_orientation::
122
- # Specifies the orientation of the page, either +:portrait+ or +:landscape+. Only used if
123
- # +page_size+ is one of the predefined page sizes.
133
+ # Specifies the orientation of the page, either +:portrait+ or +:landscape+, if +page_size+
134
+ # is one of the predefined page sizes.
135
+ #
136
+ # Only used if +skip_page_creation+ is +false+.
124
137
  #
125
138
  # margin::
126
139
  # The margin to use. See HexaPDF::Layout::Style::Quad#set for possible values.
127
140
  #
141
+ # Only used if +skip_page_creation+ is +false+.
142
+ #
128
143
  # Example:
129
144
  #
130
- # composer = HexaPDF::Composer.new # uses the default values
145
+ # # Uses the default values
146
+ # composer = HexaPDF::Composer.new
131
147
  #
132
148
  # HexaPDF::Composer.new(page_size: :Letter, margin: 72) do |composer|
133
149
  # #...
134
150
  # end
135
151
  #
136
152
  # HexaPDF::Composer.new(skip_page_creation: true) do |composer|
137
- # page_template = lambda {|canvas, style| style.create_frame(canvas.context, 36) }
138
- # page_style(:default, template: page_template)
139
- # new_page
153
+ # composer.page_style(:default) do |canvas, style|
154
+ # style.frame = style.create_frame(canvas.context, 36)
155
+ # end
156
+ # composer.new_page
140
157
  # # ...
141
158
  # end
142
159
  def initialize(skip_page_creation: false, page_size: :A4, page_orientation: :portrait,
143
160
  margin: 36) #:yields: composer
144
161
  @document = HexaPDF::Document.new
145
162
  @page_styles = {}
146
- @page_style = :default
163
+ @next_page_style = :default
147
164
  unless skip_page_creation
148
165
  page_style(:default, page_size: page_size, orientation: page_orientation) do |canvas, style|
149
166
  style.frame = style.create_frame(canvas.context, margin)
@@ -155,41 +172,61 @@ module HexaPDF
155
172
 
156
173
  # Creates a new page, making it the current one.
157
174
  #
158
- # The page style to use for the new page can be set via the +style+ argument. If not provided,
159
- # the currently set page style is used.
175
+ # The page style (see #page_style) to use for the new page can be set via the +style+ argument.
176
+ # If not provided, the currently set page style is used (:default is the initial value for
177
+ # @next_page_style).
160
178
  #
161
- # The used page style determines the page style that should be used for the following new pages.
162
- # If this information is not provided, the used page style is used again.
179
+ # The applied page style determines the page style that should be used for the following new
180
+ # pages (see Layout::PageStyle#next_style). If this information is not provided by the applied
181
+ # page style, that page style is used again.
163
182
  #
164
183
  # Examples:
165
184
  #
166
- # composer.page_style(:cover, page_size: :A4).next_style = :content
185
+ # # Define two page styles
186
+ # composer.page_style(:cover, page_size: :A4, next_style: :content)
167
187
  # composer.page_style(:content, page_size: :A4)
188
+ #
168
189
  # composer.new_page(:cover) # uses the :cover style, set next style to :content
169
190
  # composer.new_page # uses the :content style, next style again :content
170
- def new_page(style = @page_style)
191
+ def new_page(style = @next_page_style)
171
192
  page_style = @page_styles.fetch(style) do |key|
172
193
  raise ArgumentError, "Page style #{key} has not been defined"
173
194
  end
174
195
  @page = @document.pages.add(page_style.create_page(@document))
175
196
  @canvas = @page.canvas
176
197
  @frame = page_style.frame
177
- @page_style = page_style.next_style || style
198
+ @next_page_style = page_style.next_style || style
178
199
  end
179
200
 
180
- # The x-position of the cursor inside the current frame.
201
+ # The x-position inside the current frame where the next box (provided it fits) will be placed.
202
+ #
203
+ # Example:
204
+ #
205
+ # #>pdf-composer
206
+ # composer.text("Hello", position: :float)
207
+ # composer.canvas.stroke_color("hp-blue").
208
+ # circle(composer.x, composer.y, 0.5).fill.
209
+ # circle(composer.x, composer.y, 5).stroke
181
210
  def x
182
211
  @frame.x
183
212
  end
184
213
 
185
- # The y-position of the cursor inside the current frame.
214
+ # The y-position inside the current frame.where the next box (provided it fits) will be placed.
215
+ #
216
+ # Example:
217
+ #
218
+ # #>pdf-composer
219
+ # composer.text("Hello", position: :float)
220
+ # composer.canvas.stroke_color("hp-blue").
221
+ # circle(composer.x, composer.y, 0.5).fill.
222
+ # circle(composer.x, composer.y, 5).stroke
186
223
  def y
187
224
  @frame.y
188
225
  end
189
226
 
190
- # Writes the PDF document to the given output.
227
+ # Writes the created PDF document to the given output.
191
228
  #
192
- # See Document#write for details.
229
+ # See HexaPDF::Document#write for details.
193
230
  def write(output, optimize: true, **options)
194
231
  @document.write(output, optimize: optimize, **options)
195
232
  end
@@ -201,6 +238,8 @@ module HexaPDF
201
238
  # Creates or updates the HexaPDF::Layout::Style object called +name+ with the given property
202
239
  # values and returns it.
203
240
  #
241
+ # If neither +base+ nor any style properties are specified, the style +name+ is just returned.
242
+ #
204
243
  # See HexaPDF::Document::Layout#style for details; this method is just a thin wrapper around
205
244
  # that method.
206
245
  #
@@ -225,15 +264,15 @@ module HexaPDF
225
264
  # +nil+ is returned.
226
265
  #
227
266
  # If one or more page style attributes are given, a new HexaPDF::Layout::PageStyle object with
228
- # those attribute values is created, stored under +name+ and returned. If a block is provided,
229
- # it is used to define the page template.
267
+ # those attribute values is created, stored under +name+ and returned. Additionally, if a block
268
+ # is provided, it is used to define the page template.
230
269
  #
231
270
  # Example:
232
271
  #
233
272
  # composer.page_style(:default)
234
273
  # composer.page_style(:cover, page_size: :A4) do |canvas, style|
235
274
  # page_box = canvas.context.box
236
- # canvas.fill_color("fd0") do
275
+ # canvas.fill_color("green") do
237
276
  # canvas.rectangle(0, 0, page_box.width, page_box.height).
238
277
  # fill
239
278
  # end
@@ -251,9 +290,9 @@ module HexaPDF
251
290
 
252
291
  # Draws the given text at the current position into the current frame.
253
292
  #
254
- # The text will be positioned at the current position if possible. Otherwise the next best
255
- # position is used. If the text doesn't fit onto the current page or only partially, new pages
256
- # are created automatically.
293
+ # The text will be positioned at the current position (see #x and #y) if possible. Otherwise the
294
+ # next best position is used. If the text doesn't fit onto the current page or only partially,
295
+ # one or more new pages are created automatically.
257
296
  #
258
297
  # This method is of the two main methods for creating text boxes, the other being
259
298
  # #formatted_text. It uses HexaPDF::Document::Layout#text_box behind the scenes to create the
@@ -264,18 +303,21 @@ module HexaPDF
264
303
  # Examples:
265
304
  #
266
305
  # #>pdf-composer
267
- # composer.text("Test " * 15)
306
+ # composer.text("Test it now " * 15)
268
307
  # composer.text("Now " * 7, width: 100)
269
- # composer.text("Another test", font_size: 15, fill_color: "green")
308
+ # composer.text("Another test", font_size: 15, fill_color: "hp-blue")
270
309
  # composer.text("Different box style", fill_color: 'white', box_style: {
271
310
  # underlays: [->(c, b) { c.rectangle(0, 0, b.content_width, b.content_height).fill }]
272
311
  # })
312
+ #
313
+ # See: #formatted_text, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment
273
314
  def text(str, width: 0, height: 0, style: nil, box_style: nil, **style_properties)
274
315
  draw_box(@document.layout.text_box(str, width: width, height: height, style: style,
275
316
  box_style: box_style, **style_properties))
276
317
  end
277
318
 
278
- # Draws text like #text but allows parts of the text to be formatted differently.
319
+ # Draws text like #text but allows parts of the text to be formatted differently and
320
+ # interspersing with inline boxes.
279
321
  #
280
322
  # It uses HexaPDF::Document::Layout#formatted_text_box behind the scenes to create the
281
323
  # HexaPDF::Layout::TextBox that does the actual work. See that method for details on the
@@ -285,10 +327,13 @@ module HexaPDF
285
327
  #
286
328
  # #>pdf-composer
287
329
  # composer.formatted_text(["Some string"])
288
- # composer.formatted_text(["Some ", {text: "string", fill_color: 128}])
330
+ # composer.formatted_text(["Some ", {text: "string", fill_color: "hp-orange"}])
289
331
  # composer.formatted_text(["Some ", {link: "https://example.com",
290
- # fill_color: 'blue', text: "Example"}])
332
+ # fill_color: 'hp-blue', text: "Example"}])
291
333
  # composer.formatted_text(["Some ", {text: "string", style: {font_size: 20}}])
334
+ # block = lambda {|list| list.text("First item"); list.text("Second item") }
335
+ # composer.formatted_text(["Some ", {box: :list, width: 50,
336
+ # valign: :bottom, block: block}])
292
337
  #
293
338
  # See: #text, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment
294
339
  def formatted_text(data, width: 0, height: 0, style: nil, box_style: nil, **style_properties)
@@ -296,7 +341,7 @@ module HexaPDF
296
341
  box_style: box_style, **style_properties))
297
342
  end
298
343
 
299
- # Draws the given image at the current position.
344
+ # Draws the given image at the current position (see #x and #y).
300
345
  #
301
346
  # It uses HexaPDF::Document::Layout#image_box behind the scenes to create the
302
347
  # HexaPDF::Layout::ImageBox that does the actual work. See that method for details on the
@@ -314,7 +359,7 @@ module HexaPDF
314
359
  style: style, **style_properties))
315
360
  end
316
361
 
317
- # Draws the named box at the current position.
362
+ # Draws the named box at the current position (see #x and #y).
318
363
  #
319
364
  # It uses HexaPDF::Document::Layout#box behind the scenes to create the named box. See that
320
365
  # method for details on the arguments.
@@ -331,11 +376,19 @@ module HexaPDF
331
376
 
332
377
  # Draws any custom box that can be created using HexaPDF::Document::Layout.
333
378
  #
379
+ # This includes all named boxes defined in the 'layout.boxes.map' configuration option.
380
+ #
334
381
  # Examples:
335
382
  #
336
383
  # #>pdf-composer
337
- # composer.lorem_ipsum
338
- # composer.column {|column| column.lorem_ipsum }
384
+ # composer.lorem_ipsum(sentences: 1, margin: [0, 0, 5])
385
+ # composer.list(item_spacing: 2) do |list|
386
+ # composer.document.config['layout.boxes.map'].each do |name, klass|
387
+ # list.formatted_text([{text: name.to_s, fill_color: "hp-blue-dark"}, "\n#{klass}"])
388
+ # end
389
+ # end
390
+ #
391
+ # See: HexaPDF::Document::Layout#box
339
392
  def method_missing(name, *args, **kwargs, &block)
340
393
  if @document.layout.box_creation_method?(name)
341
394
  draw_box(@document.layout.send(name, *args, **kwargs, &block))
@@ -344,12 +397,11 @@ module HexaPDF
344
397
  end
345
398
  end
346
399
 
347
- # :nodoc:
348
- def respond_to_missing?(name, _private)
400
+ def respond_to_missing?(name, _private) # :nodoc:
349
401
  @document.layout.box_creation_method?(name) || super
350
402
  end
351
403
 
352
- # Draws the given HexaPDF::Layout::Box.
404
+ # Draws the given HexaPDF::Layout::Box and returns the last drawn box.
353
405
  #
354
406
  # The box is drawn into the current frame if possible. If it doesn't fit, the box is split. If
355
407
  # it still doesn't fit, a new region of the frame is determined and then the process starts
@@ -381,6 +433,7 @@ module HexaPDF
381
433
  end
382
434
  end
383
435
  end
436
+ box
384
437
  end
385
438
 
386
439
  # Creates a stamp (Form XObject) which can be used like an image multiple times on a single page
@@ -393,7 +446,7 @@ module HexaPDF
393
446
  #
394
447
  # #>pdf-composer
395
448
  # stamp = composer.create_stamp(50, 50) do |canvas|
396
- # canvas.fill_color("red").line_width(5).
449
+ # canvas.fill_color("hp-blue").line_width(5).
397
450
  # rectangle(10, 10, 30, 30).fill_stroke
398
451
  # end
399
452
  # composer.image(stamp, width: 20, height: 20)