hexapdf 0.20.4 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +5 -3
  4. data/Rakefile +10 -1
  5. data/examples/018-composer.rb +10 -10
  6. data/lib/hexapdf/cli/batch.rb +4 -6
  7. data/lib/hexapdf/cli/info.rb +5 -1
  8. data/lib/hexapdf/cli/inspect.rb +59 -0
  9. data/lib/hexapdf/cli/split.rb +1 -1
  10. data/lib/hexapdf/composer.rb +147 -53
  11. data/lib/hexapdf/configuration.rb +7 -3
  12. data/lib/hexapdf/content/canvas.rb +1 -1
  13. data/lib/hexapdf/content/color_space.rb +1 -1
  14. data/lib/hexapdf/content/operator.rb +7 -7
  15. data/lib/hexapdf/content/parser.rb +3 -3
  16. data/lib/hexapdf/content/processor.rb +9 -9
  17. data/lib/hexapdf/document/signatures.rb +5 -4
  18. data/lib/hexapdf/document.rb +7 -0
  19. data/lib/hexapdf/font/true_type/font.rb +7 -7
  20. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  21. data/lib/hexapdf/font/true_type/subsetter.rb +1 -1
  22. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +3 -3
  23. data/lib/hexapdf/font/true_type_wrapper.rb +9 -14
  24. data/lib/hexapdf/font/type1/font.rb +10 -12
  25. data/lib/hexapdf/font/type1_wrapper.rb +1 -2
  26. data/lib/hexapdf/layout/box.rb +12 -9
  27. data/lib/hexapdf/layout/image_box.rb +1 -1
  28. data/lib/hexapdf/layout/style.rb +28 -8
  29. data/lib/hexapdf/layout/text_fragment.rb +10 -9
  30. data/lib/hexapdf/parser.rb +5 -0
  31. data/lib/hexapdf/tokenizer.rb +3 -3
  32. data/lib/hexapdf/type/acro_form/appearance_generator.rb +6 -4
  33. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -2
  34. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  35. data/lib/hexapdf/type/font_type0.rb +1 -1
  36. data/lib/hexapdf/type/font_type3.rb +1 -1
  37. data/lib/hexapdf/type/resources.rb +4 -4
  38. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +1 -1
  39. data/lib/hexapdf/type/signature.rb +1 -1
  40. data/lib/hexapdf/type/trailer.rb +3 -3
  41. data/lib/hexapdf/version.rb +1 -1
  42. data/lib/hexapdf/xref_section.rb +1 -1
  43. data/test/hexapdf/common_tokenizer_tests.rb +5 -5
  44. data/test/hexapdf/content/test_graphics_state.rb +1 -0
  45. data/test/hexapdf/content/test_operator.rb +2 -2
  46. data/test/hexapdf/content/test_processor.rb +1 -1
  47. data/test/hexapdf/encryption/test_standard_security_handler.rb +23 -29
  48. data/test/hexapdf/filter/test_predictor.rb +16 -20
  49. data/test/hexapdf/font/test_type1_wrapper.rb +1 -1
  50. data/test/hexapdf/font/true_type/table/common.rb +1 -1
  51. data/test/hexapdf/font/true_type/table/test_cmap.rb +1 -1
  52. data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +1 -1
  53. data/test/hexapdf/image_loader/test_pdf.rb +6 -8
  54. data/test/hexapdf/image_loader/test_png.rb +2 -2
  55. data/test/hexapdf/layout/test_box.rb +11 -1
  56. data/test/hexapdf/layout/test_style.rb +23 -0
  57. data/test/hexapdf/layout/test_text_fragment.rb +21 -21
  58. data/test/hexapdf/test_composer.rb +115 -52
  59. data/test/hexapdf/test_dictionary.rb +2 -2
  60. data/test/hexapdf/test_document.rb +11 -9
  61. data/test/hexapdf/test_object.rb +1 -1
  62. data/test/hexapdf/test_parser.rb +13 -7
  63. data/test/hexapdf/test_serializer.rb +20 -22
  64. data/test/hexapdf/test_stream.rb +7 -9
  65. data/test/hexapdf/test_writer.rb +2 -2
  66. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +1 -2
  67. data/test/hexapdf/type/acro_form/test_choice_field.rb +1 -1
  68. data/test/hexapdf/type/signature/common.rb +1 -1
  69. data/test/hexapdf/type/test_font_type0.rb +1 -1
  70. data/test/hexapdf/type/test_font_type1.rb +7 -7
  71. data/test/hexapdf/type/test_image.rb +13 -17
  72. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f88aacce2ba9f2df02fdeaf17019871c9f8e1f7ab9202aa4dd8af89b86394c5e
4
- data.tar.gz: bca3c34f6f71da1ae6df8f81c98c69e3bdda09f71f195b46979e16be91a7e1b7
3
+ metadata.gz: b666a69330b87a3ad7a5c937a1113a66a3b5ed3d0cb4f4478ee24f97bed0e411
4
+ data.tar.gz: 460c3cd90b8f76d3e4d32fdd6f1bc8fd71ddfef5d18799c6a207f562773371f8
5
5
  SHA512:
6
- metadata.gz: 2ac8a3205b04705614dfa42fb0e73994aa7ccca90f23c73a0e7b8ba9f9932d18059ced2a62ecefd68c8f5425e3a354f51d8101e3b1b642d11312dbd3510ff1a6
7
- data.tar.gz: 410c1cac0e07dfeda869d70a69370af74f255dcd17feb057064b39448b61d4494ea0bc7a05e99eefaa7a2459cc32bab4c01240b00dad9a6f63c83b32bc2176c2
6
+ metadata.gz: a9135019912d8c3d1e282797054dfbc99c028f26aa28f8412a138ab3d8d67124c4e9c52ad104eeb4289487e47dd850e3ff4211b5e928cdb0a8aab2d8ea773930
7
+ data.tar.gz: 070d0facdb6576fb6a0377a7139609ee648a580be879dabfd10e943bd2f62d66d9d5077058edcecf9ca3eb4e7969a2427c7a4af26bbb4d8f734e238f7022c7cd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## 0.21.0 - 2022-03-04
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Parser#reconstructed?] which returns true if the cross-reference
6
+ table was reconstructed
7
+ - [HexaPDF::Layout::Style::create] for easier creation of style objects
8
+ * The ability to view revisions of a PDF document or extract a single revision
9
+ via `hexapdf inspect`
10
+
11
+ ### Changed
12
+
13
+ * **Breaking change**: Refactored [HexaPDF::Composer] for better and more
14
+ consistent style support
15
+ * **Breaking change**: Arguments for configuration option
16
+ 'font.on_missing_glyph' have changed to allow access to the document instance
17
+
18
+ ### Fixed
19
+
20
+ * Setter for [HexaPDF::Layout::Style#line_spacing] to allow usage of numeric
21
+ arguments
22
+ * Digital Signature validation for 'adbe.pkcs7.detached' certifiates in case no
23
+ key usage was defined
24
+ * Removed caching of configuration 'font.on_missing_glyph' in font wrappers to
25
+ avoid problems
26
+
27
+
1
28
  ## 0.20.4 - 2022-01-26
2
29
 
3
30
  ### Fixed
data/README.md CHANGED
@@ -7,13 +7,12 @@ short, it allows
7
7
  * **manipulating** existing PDF files,
8
8
  * **merging** multiple PDF files into one,
9
9
  * **extracting** meta information, text, images and files from PDF files,
10
- * **securing** PDF files by encrypting them and
10
+ * **securing** PDF files by encrypting or signing them and
11
11
  * **optimizing** PDF files for smaller file size or other criteria.
12
12
 
13
13
  HexaPDF was designed with ease of use and performance in mind. It uses lazy loading and lazy
14
14
  computing when possible and tries to produce small PDF files by default.
15
15
 
16
-
17
16
  ## Usage
18
17
 
19
18
  The HexaPDF distribution provides the library as well as the `hexapdf` application. The application
@@ -46,9 +45,12 @@ with example graphics and PDF files and tightly integrated into the rest of the
46
45
  ## Requirements and Installation
47
46
 
48
47
  Since HexaPDF is written in Ruby, a working Ruby installation is needed - see the
49
- [official installation documentation][rbinstall] for details. Note that you need Ruby version 2.4 or
48
+ [official installation documentation][rbinstall] for details. Note that you need Ruby version 2.5 or
50
49
  higher as prior versions are not supported!
51
50
 
51
+ HexaPDF works on all Ruby implementations that are CRuby compatible, e.g. TruffleRuby, and on any
52
+ platform supported by Ruby (Linux, macOS, Windows, ...).
53
+
52
54
  Apart from Ruby itself the HexaPDF library has only one external dependency `geom2d` which is
53
55
  written and provided by the HexaPDF authors. The `hexapdf` application has an additional dependency
54
56
  on `cmdparse`, a command line parsing library.
data/Rakefile CHANGED
@@ -46,8 +46,17 @@ namespace :dev do
46
46
  puts 'done'
47
47
  end
48
48
 
49
+ task :test_all do
50
+ versions = `rbenv versions --bare | grep -i 2.[567]\\\\\\|3.`.split("\n")
51
+ versions.each do |version|
52
+ sh "rbenv shell #{version} &>/dev/null && rake test"
53
+ end
54
+ puts "Looks okay? (enter to continue, Ctrl-c to abort)"
55
+ $stdin.gets
56
+ end
57
+
49
58
  desc 'Release HexaPDF version ' + HexaPDF::VERSION
50
- task release: [:clobber, :package, :publish_files]
59
+ task release: [:clobber, :test_all, :package, :publish_files]
51
60
 
52
61
  desc "Set-up everything for development"
53
62
  task :setup do
@@ -3,9 +3,10 @@
3
3
  # This example shows how [HexaPDF::Composer] simplifies the creation of PDF
4
4
  # documents by providing a high-level interface to the box layouting engine.
5
5
  #
6
- # Basic style properties can be set on the [HexaPDF::Composer#base_style] style.
7
- # These properties are reused by every box and can be adjusted on a box-by-box
8
- # basis.
6
+ # Basic style properties can be set using the [HexaPDF::Composer#style] method
7
+ # and the style name `:basic`. These properties are reused by every box and can
8
+ # be adjusted on a box-by-box basis. Newly defined styles also inherit the
9
+ # properties from the `:basic` style.
9
10
  #
10
11
  # Various methods allow the easy creation of boxes, for example, text and image
11
12
  # boxes. All these boxes are automatically drawn on the page. If the page has
@@ -24,21 +25,20 @@ dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exer\u{00AD}citation
24
25
  ullamco laboris nisi ut aliquip ex ea commodo consequat. ".tr("\n", " ")
25
26
 
26
27
  HexaPDF::Composer.create('composer.pdf') do |pdf|
27
- pdf.base_style.update(line_spacing: {type: :proportional, value: 1.5},
28
- last_line_gap: true, align: :justify)
29
- image_style = pdf.base_style.dup.update(border: {width: 1}, padding: 5, margin: 10)
30
- link_style = pdf.base_style.dup.update(fill_color: [6, 158, 224], underline: true)
28
+ pdf.style(:base, line_spacing: 1.5, last_line_gap: true, align: :justify)
29
+ pdf.style(:image, border: {width: 1}, padding: 5, margin: 10)
30
+ pdf.style(:link, fill_color: [6, 158, 224], underline: true)
31
31
  image = File.join(__dir__, 'machupicchu.jpg')
32
32
 
33
33
  pdf.text(lorem_ipsum * 2)
34
- pdf.image(image, style: image_style, width: 200, position: :float)
35
- pdf.image(image, style: image_style, width: 200, position: :absolute,
34
+ pdf.image(image, style: :image, width: 200, position: :float)
35
+ pdf.image(image, style: :image, width: 200, position: :absolute,
36
36
  position_hint: [200, 300])
37
37
  pdf.text(lorem_ipsum * 20, position: :flow)
38
38
 
39
39
  pdf.formatted_text(["Produced by ",
40
40
  {link: "https://hexapdf.gettalong.org", text: "HexaPDF",
41
- style: link_style},
41
+ style: :link},
42
42
  " via HexaPDF::Composer"],
43
43
  font_size: 15, align: :center, padding: 15)
44
44
  end
@@ -59,12 +59,10 @@ module HexaPDF
59
59
  def execute(command, *files) #:nodoc:
60
60
  args = Shellwords.split(command)
61
61
  files.each do |file|
62
- begin
63
- HexaPDF::CLI::Application.new.parse(args.map {|a| a.gsub(/{}/, file) })
64
- rescue StandardError
65
- if command_parser.verbosity_warning?
66
- $stderr.puts "Error processing '#{file}': #{$!.message}"
67
- end
62
+ HexaPDF::CLI::Application.new.parse(args.map {|a| a.gsub(/{}/, file) })
63
+ rescue StandardError
64
+ if command_parser.verbosity_warning?
65
+ $stderr.puts "Error processing '#{file}': #{$!.message}"
68
66
  end
69
67
  end
70
68
  end
@@ -112,7 +112,8 @@ module HexaPDF
112
112
  output_line("File name", file)
113
113
  output_line("File size", File.stat(file).size.to_s << " bytes")
114
114
  @auto_decrypt && INFO_KEYS.each do |name|
115
- next unless doc.trailer.info.key?(name)
115
+ value = doc.trailer.info[name]
116
+ next if !value || (value.kind_of?(String) && value.empty?)
116
117
  output_line(name.to_s, doc.trailer.info[name].to_s)
117
118
  end
118
119
 
@@ -152,6 +153,9 @@ module HexaPDF
152
153
 
153
154
  output_line("Pages", doc.pages.count.to_s)
154
155
  output_line("Version", doc.version)
156
+ if doc.revisions.parser.reconstructed?
157
+ output_line("Reconstructed", "yes (use --check for details)")
158
+ end
155
159
  end
156
160
  rescue HexaPDF::EncryptionError
157
161
  if @auto_decrypt
@@ -216,6 +216,28 @@ module HexaPDF
216
216
  end
217
217
  end
218
218
 
219
+ when 'rev', 'revision'
220
+ if (rev_index = data.shift)
221
+ rev_index = rev_index.to_i - 1
222
+ if rev_index < 0 || rev_index >= @doc.revisions.count
223
+ $stderr.puts("Error: Invalid revision numer specified")
224
+ next
225
+ end
226
+ length = 0
227
+ revision_information do |_, index, _, _, end_offset|
228
+ length = end_offset if index == rev_index
229
+ end
230
+ IO.copy_stream(@doc.revisions.parser.io, $stdout, length, 0)
231
+ else
232
+ puts "Document has #{@doc.revisions.size} revision#{@doc.revisions.size == 1 ? '' : 's'}"
233
+ revision_information do |_, index, count, signature, end_offset|
234
+ puts "Revision #{index + 1}"
235
+ puts " Objects : #{count}"
236
+ puts " Signed : yes" if signature
237
+ puts " Byte range: 0-#{end_offset}"
238
+ end
239
+ end
240
+
219
241
  when 'q', 'quit'
220
242
  return true
221
243
 
@@ -294,11 +316,48 @@ module HexaPDF
294
316
  puts if indent == 0
295
317
  end
296
318
 
319
+ # Yields information about the document's revisions.
320
+ #
321
+ # Returns an array of arrays that include the following information:
322
+ #
323
+ # - The revision object itself
324
+ # - The index of the revision in terms of all revisions of the document
325
+ # - The number of objects in the revision
326
+ # - The signature dictionary if this revision was signed
327
+ # - The byte offset from the start of the file to the end of the revision
328
+ def revision_information
329
+ signatures = @doc.signatures.map do |sig|
330
+ [@doc.revisions.find {|rev| rev.object(sig) == sig }, sig]
331
+ end.to_h
332
+ io = @doc.revisions.parser.io
333
+
334
+ startxrefs = @doc.revisions.map {|rev| rev.trailer[:Prev] }
335
+ io.seek(0, IO::SEEK_END)
336
+ startxrefs.push(@doc.revisions.parser.startxref_offset, io.pos).shift
337
+
338
+ @doc.revisions.each_with_index.map do |rev, index|
339
+ end_index = 0
340
+ sig = signatures[rev]
341
+ if sig
342
+ end_index = sig[:ByteRange][-2] + sig[:ByteRange][-1]
343
+ else
344
+ io.seek(startxrefs[index], IO::SEEK_SET)
345
+ while io.pos < startxrefs[index + 1]
346
+ if io.gets =~ /^\s*%%EOF\s*$/
347
+ end_index = io.pos
348
+ end
349
+ end
350
+ end
351
+ yield(rev, index, rev.next_free_oid - 1, sig, end_index)
352
+ end
353
+ end
354
+
297
355
  COMMAND_DESCRIPTIONS = [ #:nodoc:
298
356
  ["OID[,GEN] | o[bject] OID[,GEN]", "Print object"],
299
357
  ["r[ecursive] OID[,GEN]", "Print object recursively"],
300
358
  ["s[tream] OID[,GEN]", "Print filtered stream"],
301
359
  ["raw[-stream] OID[,GEN]", "Print raw stream"],
360
+ ["rev[ision] [NUMBER]", "Print or extract revision"],
302
361
  ["x[ref] OID[,GEN]", "Print the cross-reference entry"],
303
362
  ["c[atalog]", "Print the catalog dictionary"],
304
363
  ["t[railer]", "Print the trailer dictionary"],
@@ -136,7 +136,7 @@ module HexaPDF
136
136
  end
137
137
 
138
138
  @page_name_cache[media_box] =
139
- paper_size ? paper_size[0] : "%.0fx%.0f" % media_box.values_at(2, 3)
139
+ paper_size ? paper_size[0] : sprintf("%.0fx%.0f", *media_box.values_at(2, 3))
140
140
  end
141
141
 
142
142
  end
@@ -39,8 +39,9 @@ require 'hexapdf/layout'
39
39
 
40
40
  module HexaPDF
41
41
 
42
- # The composer class can be used to create PDF documents from scratch. It uses Frame and Box
43
- # objects underneath.
42
+ # The composer class can be used to create PDF documents from scratch. It uses
43
+ # HexaPDF::Layout::Frame and HexaPDF::Layout::Box objects underneath and binds them together to
44
+ # provide a convenient interface for working with them.
44
45
  #
45
46
  # == Usage
46
47
  #
@@ -57,10 +58,23 @@ module HexaPDF
57
58
  # page. Behind the scenes HexaPDF::Layout::Box (and subclass) objects are created and drawn on the
58
59
  # page via the frame.
59
60
  #
60
- # The base style that is used by all these boxes can be defined using the #base_style method which
61
- # returns a HexaPDF::Layout::Style object. The only style property that is set by default is the
62
- # font (Times) because otherwise there would be problems with text drawing operations (font is the
63
- # only style property that has no valid default value).
61
+ # All drawing methods accept HexaPDF::Layout::Style objects or names for style objects (defined
62
+ # via #style). The HexaPDF::Layout::Style#font is handled specially:
63
+ #
64
+ # * If no font is set on a style, the font "Times" is automatically set because otherwise there
65
+ # would be problems with text drawing operations (font is the only style property that has no
66
+ # valid default value).
67
+ #
68
+ # * Standard style objects only allow font wrapper objects to be set via the
69
+ # HexaPDF::Layout::Style#font method. Composer makes usage easier by allowing strings or an
70
+ # array [name, options_hash] to be used, like with e.g Content::Canvas. So using Helvetica as
71
+ # font, one could just do this by saying
72
+ #
73
+ # style.font = 'Helvetica'
74
+ #
75
+ # And if Helvetica bold should be used it would be
76
+ #
77
+ # style.font = ['Helvetica', variant: :bold]
64
78
  #
65
79
  # If the frame of a page is full and a box doesn't fit anymore, a new page is automatically
66
80
  # created. The box is either split into two boxes where one fits on the first page and the other
@@ -105,12 +119,9 @@ module HexaPDF
105
119
  # The Content::Canvas of the current page. Can be used to perform arbitrary drawing operations.
106
120
  attr_reader :canvas
107
121
 
108
- # The Layout::Frame for automatic box placement.
122
+ # The HexaPDF::Layout::Frame for automatic box placement.
109
123
  attr_reader :frame
110
124
 
111
- # The base style which is used when no explicit style is provided to methods (e.g. to #text).
112
- attr_reader :base_style
113
-
114
125
  # Creates a new Composer object and optionally yields it to the given block.
115
126
  #
116
127
  # page_size::
@@ -122,15 +133,22 @@ module HexaPDF
122
133
  # +page_size+ is one of the predefined page sizes.
123
134
  #
124
135
  # margin::
125
- # The margin to use. See Layout::Style::Quad#set for possible values.
136
+ # The margin to use. See HexaPDF::Layout::Style::Quad#set for possible values.
137
+ #
138
+ # Example:
139
+ #
140
+ # composer = HexaPDF::Composer.new # uses the default values
141
+ # HexaPDF::Composer.new(page_size: :Letter, margin: 72) do |composer|
142
+ # #...
143
+ # end
126
144
  def initialize(page_size: :A4, page_orientation: :portrait, margin: 36) #:yields: composer
127
145
  @document = HexaPDF::Document.new
128
146
  @page_size = page_size
129
147
  @page_orientation = page_orientation
130
148
  @margin = Layout::Style::Quad.new(margin)
149
+ @styles = {base: Layout::Style.new}
131
150
 
132
151
  new_page
133
- @base_style = Layout::Style.new(font: 'Times')
134
152
  yield(self) if block_given?
135
153
  end
136
154
 
@@ -170,32 +188,85 @@ module HexaPDF
170
188
  @document.write(output, optimize: optimize, **options)
171
189
  end
172
190
 
191
+ # :call-seq:
192
+ # composer.style(:header) -> style
193
+ # composer.style(:header, base: :base, **properties) -> style
194
+ #
195
+ # Creates or updates the HexaPDF::Layout::Style object called +name+ with the given property
196
+ # values and returns it. Such a style can then be used by name in the various box drawing
197
+ # methods, e.g. #text or #image.
198
+ #
199
+ # If neither +base+ nor any style properties are specified, the style +name+ is just returned.
200
+ #
201
+ # If the style +name+ does not exist yet and the argument +base+ specifies the name of another
202
+ # style, that style is duplicated and used as basis for the style.
203
+ #
204
+ # The special name :base should be used for setting the base style which is used when no
205
+ # specific style is set. It is best to fully initialize the base style before creating any
206
+ # other styles.
207
+ #
208
+ # Note that the style property 'font' is handled specially by Composer, see the class
209
+ # documentation for details.
210
+ #
211
+ # Example:
212
+ #
213
+ # composer.style(:base, font_size: 12, leading: 1.2)
214
+ # composer.style(:header, font: 'Helvetica', fill_color: "008")
215
+ # composer.style(:header1, base: :header, font_size: 30)
216
+ #
217
+ # See: HexaPDF::Layout::Style
218
+ def style(name, base: :base, **properties)
219
+ style = @styles[name] ||= (@styles.key?(base) ? @styles[base].dup : Layout::Style.new)
220
+ style.update(**properties) unless properties.empty?
221
+ style
222
+ end
223
+
173
224
  # Draws the given text at the current position into the current frame.
174
225
  #
175
- # This method is the main method for displaying text on a PDF page. It uses a Layout::TextBox
176
- # behind the scenes to do the actual work.
226
+ # This method is the main method for displaying text on a PDF page. It uses a
227
+ # HexaPDF::Layout::TextBox behind the scenes to do the actual work.
177
228
  #
178
229
  # The text will be positioned at the current position if possible. Otherwise the next best
179
230
  # position is used. If the text doesn't fit onto the current page or only partially, new pages
180
231
  # are created automatically.
181
232
  #
182
- # The arguments +width+ and +height+ are used as constraints and are respected when fitting the
183
- # box.
233
+ # +width+, +height+::
234
+ # The arguments +width+ and +height+ are used as constraints and are respected when fitting
235
+ # the box. The default value of 0 means that no constraints are set.
184
236
  #
185
- # The text is styled using the given +style+ object (see Layout::Style) or, if no style object
186
- # is specified, the base style (see #base_style). If any additional style +options+ are
187
- # specified, the used style is copied and the additional styles are applied.
237
+ # +style+, +style_properties+::
238
+ # The box and the text are styled using the given +style+. This can either be a style name
239
+ # set via #style or anything HexaPDF::Layout::Style::create accepts. If any additional
240
+ # +style_properties+ are specified, the style is duplicated and the additional styles are
241
+ # applied.
188
242
  #
189
- # See HexaPDF::Layout::TextBox for details.
190
- def text(str, width: 0, height: 0, style: nil, **options)
191
- style = update_style(style, options)
243
+ # +box_style+::
244
+ # Sometimes it is necessary for the box to have a different style than the text, e.g. when
245
+ # using overlays. In such a case use +box_style+ for specifiying the style of the box (a
246
+ # style name set via #style or anything HexaPDF::Layout::Style::create accepts). The +style+
247
+ # together with the +style_properties+ will be used for the text style.
248
+ #
249
+ # Examples:
250
+ #
251
+ # #>pdf-composer
252
+ # composer.text("Test " * 15)
253
+ # composer.text("Now " * 7, width: 100)
254
+ # composer.text("Another test", font_size: 15, fill_color: "green")
255
+ # composer.text("Different box style", fill_color: 'white', box_style: {
256
+ # underlays: [->(c, b) { c.rectangle(0, 0, b.content_width, b.content_height).fill }]
257
+ # })
258
+ #
259
+ # See HexaPDF::HexaPDF::Layout::TextBox for details.
260
+ def text(str, width: 0, height: 0, style: nil, box_style: nil, **style_properties)
261
+ style = retrieve_style(style, style_properties)
262
+ box_style = (box_style ? retrieve_style(box_style) : style)
192
263
  draw_box(Layout::TextBox.new([Layout::TextFragment.create(str, style)],
193
- width: width, height: height, style: style))
264
+ width: width, height: height, style: box_style))
194
265
  end
195
266
 
196
- # Draws text like #text but where parts of it can be formatted differently.
267
+ # Draws text like #text but allows parts of the text to be formatted differently.
197
268
  #
198
- # The argument +data+ needs to be an array of String or Hash objects:
269
+ # The argument +data+ needs to be an array of String and/or Hash objects:
199
270
  #
200
271
  # * A String object is treated like {text: data}.
201
272
  #
@@ -206,48 +277,58 @@ module HexaPDF
206
277
  # link:: A URL that should be linked to. If no text is provided but a link, the link is used
207
278
  # as text.
208
279
  #
209
- # style:: A Layout::Style object to use as basis instead of the style created from the +style+
210
- # and +options+ arguments.
280
+ # style:: The style to be use as basis instead of the style created from the +style+ and
281
+ # +style_properties+ arguments. See HexaPDF::Layout::Style::create for allowed values.
211
282
  #
212
283
  # If any style properties are set, the used style is copied and the additional properties
213
284
  # applied.
214
285
  #
286
+ # See #text for details on +width+, +height+, +style+, +style_properties+ and +box_style+.
287
+ #
215
288
  # Examples:
216
289
  #
217
- # composer.formatted_text(["Some string"]) # The same as #text
218
- # composer.formatted_text(["Some ", {text: "string", fill_color: 128}]
219
- # composer.formatted_text(["Some ", {link: "https://example.com", text: "Example"}])
220
- # composer.formatted_text(["Some ", {text: "string", style: my_style}])
221
- def formatted_text(data, width: 0, height: 0, style: nil, **options)
222
- style = update_style(style, options)
290
+ # #>pdf-composer
291
+ # composer.formatted_text(["Some string"])
292
+ # composer.formatted_text(["Some ", {text: "string", fill_color: 128}])
293
+ # composer.formatted_text(["Some ", {link: "https://example.com",
294
+ # fill_color: 'blue', text: "Example"}])
295
+ # composer.formatted_text(["Some ", {text: "string", style: {font_size: 20}}])
296
+ #
297
+ # See: #text, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment
298
+ def formatted_text(data, width: 0, height: 0, style: nil, box_style: nil, **style_properties)
299
+ style = retrieve_style(style, style_properties)
300
+ box_style = (box_style ? retrieve_style(box_style) : style)
223
301
  data.map! do |hash|
224
302
  if hash.kind_of?(String)
225
303
  Layout::TextFragment.create(hash, style)
226
304
  else
227
305
  link = hash.delete(:link)
306
+ (hash[:overlays] ||= []) << [:link, {uri: link}] if link
228
307
  text = hash.delete(:text) || link || ""
229
- used_style = update_style(hash.delete(:style), options) || style
230
- if link || !hash.empty?
231
- used_style = used_style.dup
232
- hash.each {|key, value| used_style.send(key, value) }
233
- used_style.overlays.add(:link, uri: link) if link
234
- end
235
- Layout::TextFragment.create(text, used_style)
308
+ Layout::TextFragment.create(text, retrieve_style(hash.delete(:style) || style, hash))
236
309
  end
237
310
  end
238
- draw_box(Layout::TextBox.new(data, width: width, height: height, style: style))
311
+ draw_box(Layout::TextBox.new(data, width: width, height: height, style: box_style))
239
312
  end
240
313
 
241
- # Draws the given image file at the current position.
314
+ # Draws the given image at the current position.
315
+ #
316
+ # The +file+ argument can be anything that is accepted by HexaPDF::Document::Images#add.
317
+ #
318
+ # See #text for details on +width+, +height+, +style+ and +style_properties+.
319
+ #
320
+ # Examples:
242
321
  #
243
- # See #text for details on +width+, +height+, +style+ and +options+.
244
- def image(file, width: 0, height: 0, style: nil, **options)
245
- style = update_style(style, options)
322
+ # #>pdf-composer
323
+ # composer.image(machu_picchu, border: {width: 3})
324
+ # composer.image(machu_picchu, height: 30)
325
+ def image(file, width: 0, height: 0, style: nil, **style_properties)
326
+ style = retrieve_style(style, style_properties)
246
327
  image = document.images.add(file)
247
328
  draw_box(Layout::ImageBox.new(image, width: width, height: height, style: style))
248
329
  end
249
330
 
250
- # Draws the given Layout::Box.
331
+ # Draws the given HexaPDF::Layout::Box.
251
332
  #
252
333
  # The box is drawn into the current frame if possible. If it doesn't fit, the box is split. If
253
334
  # it still doesn't fit, a new region of the frame is determined and then the process starts
@@ -291,13 +372,26 @@ module HexaPDF
291
372
  media_box.height - @margin.bottom - @margin.top)
292
373
  end
293
374
 
294
- # Updates the Layout::Style object +style+ if one is provided, or the base style, with the style
295
- # options to make it work in all cases.
296
- def update_style(style, options = {})
297
- style ||= base_style
298
- style = style.dup.update(**options) unless options.empty?
299
- style.font(base_style.font) unless style.font?
300
- style.font(@document.fonts.add(style.font)) unless style.font.respond_to?(:pdf_object)
375
+ # Retrieves the appropriate HexaPDF::Layout::Style object based on the +style+ and +properties+
376
+ # arguments.
377
+ #
378
+ # The +style+ argument specifies the style to retrieve. It can either be a registered style name
379
+ # (see #style), a hash with style properties or +nil+. In the latter case the registered style
380
+ # :base is used
381
+ #
382
+ # If the +properties+ hash is not empty, the retrieved style is duplicated and the properties
383
+ # hash is applied to it.
384
+ #
385
+ # Finally, a default font is set if necessary to ensure that the style object works in all
386
+ # cases.
387
+ def retrieve_style(style, properties = nil)
388
+ style = Layout::Style.create(@styles[style] || style || @styles[:base])
389
+ style = style.dup.update(**properties) unless properties.nil? || properties.empty?
390
+ style.font('Times') unless style.font?
391
+ unless style.font.respond_to?(:pdf_object)
392
+ name, options = *style.font
393
+ style.font(@document.fonts.add(name, **(options || {})))
394
+ end
301
395
  style
302
396
  end
303
397
 
@@ -266,10 +266,14 @@ module HexaPDF
266
266
  # font.on_missing_glyph::
267
267
  # Callback hook when an UTF-8 character cannot be mapped to a glyph of a font.
268
268
  #
269
- # The value needs to be an object that responds to \#call(character, font_type, font) where
269
+ # The value needs to be an object that responds to \#call(character, font_wrapper) where
270
270
  # +character+ is the Unicode character for the missing glyph and returns a substitute glyph to
271
271
  # be used instead.
272
272
  #
273
+ # The +font_wrapper+ argument is the used font wrapper object, e.g.
274
+ # HexaPDF::Font::TrueTypeWrapper. To access the HexaPDF::Document instance from which this hook
275
+ # was called, you can use +font_wrapper.pdf_object.document+.
276
+ #
273
277
  # The default implementation returns an object of class HexaPDF::Font::InvalidGlyph which, when
274
278
  # not removed before encoding, will raise an error.
275
279
  #
@@ -431,8 +435,8 @@ module HexaPDF
431
435
  Encryption: 'HexaPDF::Filter::Encryption',
432
436
  },
433
437
  'font.map' => {},
434
- 'font.on_missing_glyph' => proc do |char, _type, font|
435
- HexaPDF::Font::InvalidGlyph.new(font, char)
438
+ 'font.on_missing_glyph' => proc do |char, font_wrapper|
439
+ HexaPDF::Font::InvalidGlyph.new(font_wrapper.wrapped_font, char)
436
440
  end,
437
441
  'font.on_missing_unicode_mapping' => proc do |code_point, font|
438
442
  raise HexaPDF::Error, "No Unicode mapping for code point #{code_point} " \
@@ -2015,7 +2015,7 @@ module HexaPDF
2015
2015
  invoke2(:Td, offset[0], offset[1])
2016
2016
  end
2017
2017
  else
2018
- invoke0(:"T*")
2018
+ invoke0(:'T*')
2019
2019
  end
2020
2020
  self
2021
2021
  end
@@ -296,7 +296,7 @@ module HexaPDF
296
296
  spec = if first_item.match?(/\A\h{6}\z/)
297
297
  first_item.scan(/../).map!(&:hex)
298
298
  elsif first_item.match?(/\A\h{3}\z/)
299
- first_item.each_char.map {|x| (x*2).hex}
299
+ first_item.each_char.map {|x| (x * 2).hex }
300
300
  elsif CSS_COLOR_NAMES.key?(first_item)
301
301
  CSS_COLOR_NAMES[first_item]
302
302
  else
@@ -1036,14 +1036,14 @@ module HexaPDF
1036
1036
  s: EndPath.new('s'),
1037
1037
  f: EndPath.new('f'),
1038
1038
  F: EndPath.new('F'),
1039
- 'f*'.to_sym => EndPath.new('f*'),
1039
+ 'f*': EndPath.new('f*'),
1040
1040
  B: EndPath.new('B'),
1041
- 'B*'.to_sym => EndPath.new('B*'),
1041
+ 'B*': EndPath.new('B*'),
1042
1042
  b: EndPath.new('b'),
1043
- 'b*'.to_sym => EndPath.new('b*'),
1043
+ 'b*': EndPath.new('b*'),
1044
1044
  n: EndPath.new('n'),
1045
1045
  W: ClipPath.new('W'),
1046
- 'W*'.to_sym => ClipPath.new('W*'),
1046
+ 'W*': ClipPath.new('W*'),
1047
1047
 
1048
1048
  BI: InlineImage.new,
1049
1049
 
@@ -1059,10 +1059,10 @@ module HexaPDF
1059
1059
  Td: MoveText.new,
1060
1060
  TD: MoveTextAndSetLeading.new,
1061
1061
  Tm: SetTextMatrix.new,
1062
- 'T*'.to_sym => MoveTextNextLine.new,
1062
+ 'T*': MoveTextNextLine.new,
1063
1063
  Tj: ShowText.new,
1064
- '\''.to_sym => MoveTextNextLineAndShowText.new,
1065
- '"'.to_sym => SetSpacingMoveTextNextLineAndShowText.new,
1064
+ "'": MoveTextNextLineAndShowText.new,
1065
+ '"': SetSpacingMoveTextNextLineAndShowText.new,
1066
1066
  TJ: ShowTextWithPositioning.new,
1067
1067
  }
1068
1068
  DEFAULT_OPERATORS.default_proc = proc {|h, k| h[k] = BaseOperator.new(k.to_s) }
@@ -94,11 +94,11 @@ module HexaPDF
94
94
  elsif byte == 40 # (
95
95
  parse_literal_string
96
96
  elsif byte == 60 # <
97
- if @string.getbyte(@ss.pos + 1) != 60
98
- parse_hex_string
99
- else
97
+ if @string.getbyte(@ss.pos + 1) == 60
100
98
  @ss.pos += 2
101
99
  TOKEN_DICT_START
100
+ else
101
+ parse_hex_string
102
102
  end
103
103
  elsif byte == 62 # >
104
104
  unless @string.getbyte(@ss.pos + 1) == 62