hexapdf 0.8.0 → 0.9.0

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