hexapdf 0.8.0 → 0.9.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -1
  3. data/CONTRIBUTERS +1 -1
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -1
  6. data/examples/018-composer.rb +44 -0
  7. data/lib/hexapdf/cli.rb +2 -0
  8. data/lib/hexapdf/cli/command.rb +2 -2
  9. data/lib/hexapdf/cli/optimize.rb +1 -1
  10. data/lib/hexapdf/cli/split.rb +82 -0
  11. data/lib/hexapdf/composer.rb +303 -0
  12. data/lib/hexapdf/configuration.rb +2 -2
  13. data/lib/hexapdf/content/canvas.rb +3 -6
  14. data/lib/hexapdf/dictionary.rb +0 -3
  15. data/lib/hexapdf/document.rb +30 -22
  16. data/lib/hexapdf/document/files.rb +1 -1
  17. data/lib/hexapdf/document/images.rb +1 -1
  18. data/lib/hexapdf/filter/predictor.rb +8 -8
  19. data/lib/hexapdf/layout.rb +1 -0
  20. data/lib/hexapdf/layout/box.rb +55 -12
  21. data/lib/hexapdf/layout/frame.rb +143 -46
  22. data/lib/hexapdf/layout/image_box.rb +96 -0
  23. data/lib/hexapdf/layout/inline_box.rb +10 -0
  24. data/lib/hexapdf/layout/line.rb +1 -1
  25. data/lib/hexapdf/layout/style.rb +55 -3
  26. data/lib/hexapdf/layout/text_box.rb +38 -8
  27. data/lib/hexapdf/layout/text_layouter.rb +66 -52
  28. data/lib/hexapdf/object.rb +3 -2
  29. data/lib/hexapdf/parser.rb +6 -1
  30. data/lib/hexapdf/rectangle.rb +6 -0
  31. data/lib/hexapdf/reference.rb +4 -6
  32. data/lib/hexapdf/revision.rb +34 -8
  33. data/lib/hexapdf/revisions.rb +12 -2
  34. data/lib/hexapdf/stream.rb +18 -0
  35. data/lib/hexapdf/task.rb +1 -1
  36. data/lib/hexapdf/task/dereference.rb +1 -1
  37. data/lib/hexapdf/task/optimize.rb +2 -2
  38. data/lib/hexapdf/type/form.rb +10 -0
  39. data/lib/hexapdf/version.rb +1 -1
  40. data/lib/hexapdf/writer.rb +25 -5
  41. data/man/man1/hexapdf.1 +17 -0
  42. data/test/hexapdf/layout/test_box.rb +7 -1
  43. data/test/hexapdf/layout/test_frame.rb +38 -1
  44. data/test/hexapdf/layout/test_image_box.rb +73 -0
  45. data/test/hexapdf/layout/test_inline_box.rb +8 -0
  46. data/test/hexapdf/layout/test_line.rb +1 -1
  47. data/test/hexapdf/layout/test_style.rb +58 -1
  48. data/test/hexapdf/layout/test_text_box.rb +57 -12
  49. data/test/hexapdf/layout/test_text_layouter.rb +14 -4
  50. data/test/hexapdf/task/test_optimize.rb +3 -3
  51. data/test/hexapdf/test_composer.rb +258 -0
  52. data/test/hexapdf/test_document.rb +29 -3
  53. data/test/hexapdf/test_importer.rb +8 -2
  54. data/test/hexapdf/test_object.rb +3 -1
  55. data/test/hexapdf/test_parser.rb +6 -0
  56. data/test/hexapdf/test_rectangle.rb +7 -0
  57. data/test/hexapdf/test_reference.rb +3 -1
  58. data/test/hexapdf/test_revision.rb +13 -0
  59. data/test/hexapdf/test_revisions.rb +1 -0
  60. data/test/hexapdf/test_stream.rb +13 -0
  61. data/test/hexapdf/test_writer.rb +13 -2
  62. data/test/hexapdf/type/test_annotation.rb +1 -1
  63. data/test/hexapdf/type/test_form.rb +13 -2
  64. metadata +10 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a7cfa4a446d0ec5c87bc204de8040e8c9671e299592901644a1e1c64b5e69af
4
- data.tar.gz: 14e0e42f0d49eb0483be23446b8507d3f3d6e0b724b2c71530ff997bc97f33b3
3
+ metadata.gz: ba9272055e9cb48ac9f8af80aa239f39853130b62c1b46753b4534b8ccc94918
4
+ data.tar.gz: 9915555ad5a90ef3e5badc66cedd7d33ab2922a58c1785247b6949b3d61e6352
5
5
  SHA512:
6
- metadata.gz: c9e582a53eb0a946287a23449a563d7c7b1d29994b7ad799a199fdd23aaca6e01a76c15e35f6c07aaad69407247e859f7ab9501df076c58e9ed515ef6333e43a
7
- data.tar.gz: 6c0bf3a0e42a9b7228d2a8939056b3aceb037ff66540e4b6d8b917b1e4eb51d6c9213440278da9980a163067542f73ab874e6c798d0750b938a8afd8977707fd
6
+ metadata.gz: 811ec430adb953546ed75c368c40f1dfa7f19e7645f7a05847bd329e9e84b4da481482ed846b1f98a6a4b452e2518eba7083daec56d67c185e2ff7c60a85492d
7
+ data.tar.gz: 563bd9f126355785ba877d1e9245c794b584f50cddb8fc634aecef9f6795fc05f1480938bf14e2f440de02282af86af1aa871b86bdc734fade81ed304d34e345
@@ -1,3 +1,54 @@
1
+ ## 0.9.0 - 2018-12-31
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Composer] for composing PDF documents in a high-level way
6
+ * Incremental writing support (i.e. appending a single revision with all the
7
+ changes to an existing document) to [HexaPDF::Writer] and [HexaPDF::Document]
8
+ * CLI command `hexapdf split` to split a PDF file into individual pages
9
+ * [HexaPDF::Revisions#parser] for accessing the parser object that is created
10
+ when a document is read from an IO stream
11
+ * [HexaPDF::Document#each] argument `only_loaded` for iteration over loaded
12
+ objects only
13
+ * [HexaPDF::Document#validate] argument `only_loaded` for validating only loaded
14
+ objects
15
+ * [HexaPDF::Revision#each_modified_object] for iterating over all modified
16
+ objects of a revision
17
+ * [HexaPDF::Layout::Box#split] and [HexaPDF::Layout::TextBox#split] for
18
+ splitting a box into two parts
19
+ * [HexaPDF::Layout::Frame#full?] for testing whether the frame has any space
20
+ left
21
+ * [HexaPDF::Layout::Style] property `last_line_gap` for controlling the spacing
22
+ after the last line of text
23
+ * HexaPDF::Layout::Box#draw_content for use by subclasses
24
+ * [HexaPDF::Type::Form#width] and [HexaPDF::Type::Form#height] for compatibility
25
+ with [HexaPDF::Type::Image]
26
+ * [HexaPDF::Layout::ImageBox] for displaying an image inside a frame
27
+
28
+ ### Changed
29
+
30
+ * [HexaPDF::Revision#each] to allow iteration over loaded objects only
31
+ * [HexaPDF::Document#each] method argument from `current` to `only_current`
32
+ * [HexaPDF::Object#==] and [HexaPDF::Reference#==] so that Object and Reference
33
+ objects can be compared
34
+ * Refactored [HexaPDF::Layout::Frame] to allow separate fitting, splitting and
35
+ drawing of boxes
36
+ * [HexaPDF::Layout::Style::LineSpacing::new] to allow setting of line spacing
37
+ via a single hash argument
38
+ * Made [HexaPDF::Layout::Style] copyable
39
+
40
+ ### Fixed
41
+
42
+ * Configuration so that annotation objects are correctly mapped to classes
43
+ * Fix problem with [HexaPDF::Filter::Predictor] due to behaviour change of Ruby
44
+ 2.6.0 in `String#setbyte`
45
+ * Fitting of [HexaPDF::Layout::TextBox] when the box has padding and/or borders
46
+ * Fitting of [HexaPDF::Layout::TextBox] when width and/or height has been set
47
+ * Fitting of absolutely positioned boxes in [HexaPDF::Layout::Frame]
48
+ * Fix bug in variable width line wrapping due to not considering line spacing
49
+ correctly ([HexaPDF::Layout::Line::HeightCalculator#simulate_height] return
50
+ value needed to be changed for this fix)
51
+
1
52
  ## 0.8.0 - 2018-10-26
2
53
 
3
54
  ### Added
@@ -46,7 +97,7 @@
46
97
  [HexaPDF::Layout::TextFragment::create] method signatures
47
98
  * [HexaPDF::Encryption::SecurityHandler#set_up_encryption] argument `force_V4`
48
99
  to `force_v4`
49
- * [HexaPDF::Layout::TextLayouter#draw] to return result of #fit if possible
100
+ * HexaPDF::Layout::TextLayouter#draw to return result of #fit if possible
50
101
 
51
102
  ### Removed
52
103
 
@@ -1,3 +1,3 @@
1
1
  Count Name
2
2
  ======= ====
3
- 980 Thomas Leitner <t_leitner@gmx.at>
3
+ 1029 Thomas Leitner <t_leitner@gmx.at>
data/Rakefile CHANGED
@@ -65,7 +65,7 @@ namespace :dev do
65
65
  s.executables = ['hexapdf']
66
66
  s.default_executable = 'hexapdf'
67
67
  s.add_dependency('cmdparse', '~> 3.0', '>= 3.0.3')
68
- s.add_dependency('geom2d', '~> 0.1')
68
+ s.add_dependency('geom2d', '~> 0.2')
69
69
  s.add_development_dependency('kramdown', '~> 1.0', '>= 1.13.0')
70
70
  s.add_development_dependency('rubocop', '~> 0.58', '>= 0.58.2')
71
71
  s.required_ruby_version = '>= 2.4'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.0
1
+ 0.9.0
@@ -0,0 +1,44 @@
1
+ # ## Composer
2
+ #
3
+ # This example shows how [HexaPDF::Composer] simplifies the creation of PDF
4
+ # documents by providing a high-level interface to the box layouting engine.
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.
9
+ #
10
+ # Various methods allow the easy creation of boxes, for example, text and image
11
+ # boxes. All these boxes are automatically drawn on the page. If the page has
12
+ # not enough room left for a box, the box is split across pages (which are
13
+ # automatically created) if possible or just drawn on the new page.
14
+ #
15
+ # Usage:
16
+ # : `ruby composer.rb`
17
+ #
18
+
19
+ require 'hexapdf'
20
+
21
+ lorem_ipsum = "Lorem ipsum dolor sit amet, con\u{00AD}sectetur
22
+ adipis\u{00AD}cing elit, sed do eiusmod tempor incidi\u{00AD}dunt ut labore et
23
+ dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exer\u{00AD}citation
24
+ ullamco laboris nisi ut aliquip ex ea commodo consequat. ".tr("\n", " ")
25
+
26
+ 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)
31
+ image = File.join(__dir__, 'machupicchu.jpg')
32
+
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,
36
+ position_hint: [200, 300])
37
+ pdf.text(lorem_ipsum * 20, position: :flow)
38
+
39
+ pdf.formatted_text(["Produced by ",
40
+ {link: "https://hexapdf.gettalong.org", text: "HexaPDF",
41
+ style: link_style},
42
+ " via HexaPDF::Composer"],
43
+ font_size: 15, align: :center, padding: 15)
44
+ end
@@ -40,6 +40,7 @@ require 'hexapdf/cli/merge'
40
40
  require 'hexapdf/cli/optimize'
41
41
  require 'hexapdf/cli/images'
42
42
  require 'hexapdf/cli/batch'
43
+ require 'hexapdf/cli/split'
43
44
  require 'hexapdf/version'
44
45
  require 'hexapdf/document'
45
46
 
@@ -90,6 +91,7 @@ module HexaPDF
90
91
  add_command(HexaPDF::CLI::Optimize.new)
91
92
  add_command(HexaPDF::CLI::Merge.new)
92
93
  add_command(HexaPDF::CLI::Batch.new)
94
+ add_command(HexaPDF::CLI::Split.new)
93
95
  add_command(CmdParse::HelpCommand.new)
94
96
  version_command = CmdParse::VersionCommand.new(add_switches: false)
95
97
  add_command(version_command)
@@ -230,7 +230,7 @@ module HexaPDF
230
230
  xref_streams: @out_options.xref_streams,
231
231
  compress_pages: @out_options.compress_pages)
232
232
  if @out_options.streams != :preserve || @out_options.optimize_fonts
233
- doc.each(current: false) do |obj|
233
+ doc.each(only_current: false) do |obj|
234
234
  optimize_stream(obj)
235
235
  optimize_font(obj)
236
236
  end
@@ -324,7 +324,7 @@ module HexaPDF
324
324
  def remove_unused_pages(doc)
325
325
  retained = doc.pages.each_with_object({}) {|page, h| h[page.data] = true }
326
326
  retained[doc.pages.root.data] = true
327
- doc.each(current: false) do |obj|
327
+ doc.each(only_current: false) do |obj|
328
328
  next unless obj.kind_of?(HexaPDF::Dictionary)
329
329
  if (obj.type == :Pages || obj.type == :Page) && !retained.key?(obj.data)
330
330
  doc.delete(obj)
@@ -87,7 +87,7 @@ module HexaPDF
87
87
  end
88
88
  doc.catalog[:Pages] = page_tree
89
89
 
90
- doc.each(current: false) do |obj, revision|
90
+ doc.each(only_current: false) do |obj, revision|
91
91
  next unless obj.kind_of?(HexaPDF::Dictionary)
92
92
  if (obj.type == :Pages || obj.type == :Page) && !retained.key?(obj.data)
93
93
  revision.delete(obj)
@@ -0,0 +1,82 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2018 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #++
33
+
34
+ require 'hexapdf/cli/command'
35
+
36
+ module HexaPDF
37
+ module CLI
38
+
39
+ # Splits a PDF file, putting each page into a separate file.
40
+ class Split < Command
41
+
42
+ def initialize #:nodoc:
43
+ super('split', takes_commands: false)
44
+ short_desc("Split a PDF file into individual pages")
45
+ long_desc(<<~EOF)
46
+ If no OUTPUT_SPEC is specified, the pages are named <PDF>_0001.pdf, <PDF>_0002.pdf, ...
47
+ and so on. To specify a custom name, provide the OUTPUT_SPEC argument. It can contain a
48
+ prinft-style format definition like '%04d' to specify the place where the page number
49
+ should be inserted.
50
+
51
+ The optimization and encryption options are applied to each created output file.
52
+ EOF
53
+
54
+ options.on("--password PASSWORD", "-p", String,
55
+ "The password for decryption. Use - for reading from standard input.") do |pwd|
56
+ @password = (pwd == '-' ? read_password : pwd)
57
+ end
58
+ define_optimization_options
59
+ define_encryption_options
60
+
61
+ @password = nil
62
+ end
63
+
64
+ def execute(pdf, output_spec = pdf.sub(/\.pdf$/i, '_%04d.pdf')) #:nodoc:
65
+ output_spec = output_spec.sub('%', '%<page>')
66
+ with_document(pdf, password: @password) do |doc|
67
+ doc.pages.each_with_index do |page, index|
68
+ output_file = sprintf(output_spec, page: index + 1)
69
+ maybe_raise_on_existing_file(output_file)
70
+ out = HexaPDF::Document.new
71
+ out.pages.add(out.import(page))
72
+ apply_encryption_options(out)
73
+ apply_optimization_options(out)
74
+ write_document(out, output_file)
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,303 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2017 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #++
33
+
34
+ require 'hexapdf/document'
35
+ require 'hexapdf/layout'
36
+
37
+ module HexaPDF
38
+
39
+ # The composer class can be used to create PDF documents from scratch. It uses Frame and Box
40
+ # objects underneath.
41
+ #
42
+ # == Usage
43
+ #
44
+ # First, a new Composer objects needs to be created, either using ::new or the utility method
45
+ # ::create.
46
+ #
47
+ # On creation a HexaPDF::Document object is created as well the first page and an accompanying
48
+ # HexaPDF::Layout::Frame object. The frame is used by the various methods for general document
49
+ # layout tasks, like positioning of text, images, and so on. By default, it covers the whole page
50
+ # except the margin area. How the frame gets created can be customized by overriding the
51
+ # #create_frame method.
52
+ #
53
+ # Once the Composer object is created, its methods can be used to draw text, images, ... on the
54
+ # page. Behind the scenes HexaPDF::Layout::Box (and subclass) objects are created and drawn on the
55
+ # page via the frame.
56
+ #
57
+ # The base style that is used by all these boxes can be defined using the #base_style method which
58
+ # returns a HexaPDF::Layout::Style object. The only style property that is set by default is the
59
+ # font (Times) because otherwise there would be problems with text drawing operations (font is the
60
+ # only style property that has no valid default value).
61
+ #
62
+ # If the frame of a page is full and a box doesn't fit anymore, a new page is automatically
63
+ # created. The box is either split into two boxes where one fits on the first page and the other
64
+ # on the new page, or it is drawn completely on the new page. A new page can also be created by
65
+ # calling the #new_page method.
66
+ #
67
+ # The #x and #y methods provide the point where the next box would be drawn if it fits the
68
+ # available space. This information can be used, for example, for custom drawing operations
69
+ # through #canvas which provides direct access to the HexaPDF::Content::Canvas object of the
70
+ # current page.
71
+ #
72
+ # When using #canvas and modifying the graphics state, care has to be taken to avoid problems with
73
+ # later box drawing operations since the graphics state cannot completely be reset (e.g.
74
+ # transformations of the canvas cannot always be undone). So it is best to save the graphics state
75
+ # before and restore it afterwards.
76
+ #
77
+ # == Example
78
+ #
79
+ # HexaPDF::Composer.create('output.pdf', margin: 36) do |pdf|
80
+ # pdf.base_style.font_size(20).align(:center)
81
+ # pdf.text("Hello World", valign: :center)
82
+ # end
83
+ class Composer
84
+
85
+ # Creates a new PDF document and writes it to +output+. The +options+ are passed to ::new.
86
+ #
87
+ # Example:
88
+ #
89
+ # HexaPDF::Composer.create('output.pdf', margin: 36) do |pdf|
90
+ # ...
91
+ # end
92
+ def self.create(output, **options, &block)
93
+ new(options, &block).write(output)
94
+ end
95
+
96
+ # The PDF document that is created.
97
+ attr_reader :document
98
+
99
+ # The current page (a HexaPDF::Type::Page object).
100
+ attr_reader :page
101
+
102
+ # The Content::Canvas of the current page. Can be used to perform arbitrary drawing operations.
103
+ attr_reader :canvas
104
+
105
+ # The Layout::Frame for automatic box placement.
106
+ attr_reader :frame
107
+
108
+ # The base style which is used when no explicit style is provided to methods (e.g. to #text).
109
+ attr_reader :base_style
110
+
111
+ # Creates a new Composer object and optionally yields it to the given block.
112
+ #
113
+ # page_size::
114
+ # Can be any valid predefined page size (see Type::Page::PAPER_SIZE) or an array [llx, lly,
115
+ # urx, ury] specifying a custom page size.
116
+ #
117
+ # page_orientation::
118
+ # Specifies the orientation of the page, either +:portrait+ or +:landscape+. Only used if
119
+ # +page_size+ is one of the predefined page sizes.
120
+ #
121
+ # margin::
122
+ # The margin to use. See Layout::Style::Quad#set for possible values.
123
+ def initialize(page_size: :A4, page_orientation: :portrait, margin: 36) #:yields: composer
124
+ @document = HexaPDF::Document.new
125
+ @page_size = page_size
126
+ @page_orientation = page_orientation
127
+ @margin = Layout::Style::Quad.new(margin)
128
+
129
+ new_page
130
+ @base_style = Layout::Style.new(font: 'Times')
131
+ yield(self) if block_given?
132
+ end
133
+
134
+ # Creates a new page, making it the current one.
135
+ #
136
+ # If any of +page_size+, +page_orientation+ or +margin+ are set, they will be used instead of
137
+ # the default values and will become the default values.
138
+ #
139
+ # Examples:
140
+ #
141
+ # composer.new_page # uses the default values
142
+ # composer.new_page(page_size: :A5, margin: [72, 36])
143
+ def new_page(page_size: nil, page_orientation: nil, margin: nil)
144
+ @page_size = page_size if page_size
145
+ @page_orientation = page_orientation if page_orientation
146
+ @margin = Layout::Style::Quad.new(margin) if margin
147
+
148
+ @page = @document.pages.add(@page_size, orientation: @page_orientation)
149
+ @canvas = @page.canvas
150
+ create_frame
151
+ end
152
+
153
+ # The x-position of the cursor inside the current frame.
154
+ def x
155
+ @frame.x
156
+ end
157
+
158
+ # The y-position of the cursor inside the current frame.
159
+ def y
160
+ @frame.y
161
+ end
162
+
163
+ # Writes the PDF document to the given output.
164
+ #
165
+ # See Document#write for details.
166
+ def write(output, optimize: true, **options)
167
+ @document.write(output, optimize: optimize, **options)
168
+ end
169
+
170
+ # Draws the given text at the current position into the current frame.
171
+ #
172
+ # This method is the main method for displaying text on a PDF page. It uses a Layout::TextBox
173
+ # behind the scenes to do the actual work.
174
+ #
175
+ # The text will be positioned at the current position if possible. Otherwise the next best
176
+ # position is used. If the text doesn't fit onto the current page or only partially, new pages
177
+ # are created automatically.
178
+ #
179
+ # The arguments +width+ and +height+ are used as constraints and are respected when fitting the
180
+ # box.
181
+ #
182
+ # The text is styled using the given +style+ object (see Layout::Style) or, if no style object
183
+ # is specified, the base style (see #base_style). If any additional style +options+ are
184
+ # specified, the used style is copied and the additional styles are applied.
185
+ #
186
+ # See HexaPDF::Layout::TextBox for details.
187
+ def text(str, width: 0, height: 0, style: nil, **options)
188
+ style = update_style(style, options)
189
+ draw_box(Layout::TextBox.new([Layout::TextFragment.create(str, style)],
190
+ width: width, height: height, style: style))
191
+ end
192
+
193
+ # Draws text like #text but where parts of it can be formatted differently.
194
+ #
195
+ # The argument +data+ needs to be an array of String or Hash objects:
196
+ #
197
+ # * A String object is treated like {text: data}.
198
+ #
199
+ # * Hashes can contain any style properties and the following special keys:
200
+ #
201
+ # text:: The text to be formatted.
202
+ #
203
+ # link:: A URL that should be linked to. If no text is provided but a link, the link is used
204
+ # as text.
205
+ #
206
+ # style:: A Layout::Style object to use as basis instead of the style created from the +style+
207
+ # and +options+ arguments.
208
+ #
209
+ # If any style properties are set, the used style is copied and the additional properties
210
+ # applied.
211
+ #
212
+ # Examples:
213
+ #
214
+ # composer.formatted_text(["Some string"]) # The same as #text
215
+ # composer.formatted_text(["Some ", {text: "string", fill_color: 128}]
216
+ # composer.formatted_text(["Some ", {link: "https://example.com", text: "Example"}])
217
+ # composer.formatted_text(["Some ", {text: "string", style: my_style}])
218
+ def formatted_text(data, width: 0, height: 0, style: nil, **options)
219
+ style = update_style(style, options)
220
+ data.map! do |hash|
221
+ if hash.kind_of?(String)
222
+ Layout::TextFragment.create(hash, style)
223
+ else
224
+ link = hash.delete(:link)
225
+ text = hash.delete(:text) || link || ""
226
+ used_style = update_style(hash.delete(:style), options) || style
227
+ if link || !hash.empty?
228
+ used_style = used_style.dup
229
+ hash.each {|key, value| used_style.send(key, value) }
230
+ used_style.overlays.add(:link, uri: link) if link
231
+ end
232
+ Layout::TextFragment.create(text, used_style)
233
+ end
234
+ end
235
+ draw_box(Layout::TextBox.new(data, width: width, height: height, style: style))
236
+ end
237
+
238
+ # Draws the given image file at the current position.
239
+ #
240
+ # See #text for details on +width+, +height+, +style+ and +options+.
241
+ def image(file, width: 0, height: 0, style: nil, **options)
242
+ style = update_style(style, options)
243
+ image = document.images.add(file)
244
+ draw_box(Layout::ImageBox.new(image, width: width, height: height, style: style))
245
+ end
246
+
247
+ # Draws the given Layout::Box.
248
+ #
249
+ # The box is drawn into the current frame if possible. If it doesn't fit, the box is split. If
250
+ # it still doesn't fit, a new region of the frame is determined and then the process starts
251
+ # again.
252
+ #
253
+ # If none or only some parts of the box fit into the current frame, one or more new pages are
254
+ # created for the rest of the box.
255
+ def draw_box(box)
256
+ drawn_on_page = true
257
+ while true
258
+ if @frame.fit(box)
259
+ @frame.draw(@canvas, box)
260
+ break
261
+ elsif @frame.full?
262
+ new_page
263
+ drawn_on_page = false
264
+ else
265
+ draw_box, box = @frame.split(box)
266
+ if draw_box
267
+ @frame.draw(@canvas, draw_box)
268
+ drawn_on_page = true
269
+ elsif !@frame.find_next_region
270
+ unless drawn_on_page
271
+ raise HexaPDF::Error, "Box doesn't fit on empty page"
272
+ end
273
+ new_page
274
+ drawn_on_page = false
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ private
281
+
282
+ # Creates the frame into which boxes are layed out when a new page is created.
283
+ def create_frame
284
+ media_box = @page.box
285
+ @frame = Layout::Frame.new(media_box.left + @margin.left,
286
+ media_box.bottom + @margin.bottom,
287
+ media_box.width - @margin.left - @margin.right,
288
+ media_box.height - @margin.bottom - @margin.top)
289
+ end
290
+
291
+ # Updates the Layout::Style object +style+ if one is provided, or the base style, with the style
292
+ # options to make it work in all cases.
293
+ def update_style(style, options = {})
294
+ style ||= base_style
295
+ style = style.dup.update(options) unless options.empty?
296
+ style.font(base_style.font) unless style.font?
297
+ style.font(@document.fonts.add(style.font)) unless style.font.respond_to?(:dict)
298
+ style
299
+ end
300
+
301
+ end
302
+
303
+ end