hexapdf 0.32.2 → 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
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)