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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -1
- data/CONTRIBUTERS +1 -1
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/examples/018-composer.rb +44 -0
- data/lib/hexapdf/cli.rb +2 -0
- data/lib/hexapdf/cli/command.rb +2 -2
- data/lib/hexapdf/cli/optimize.rb +1 -1
- data/lib/hexapdf/cli/split.rb +82 -0
- data/lib/hexapdf/composer.rb +303 -0
- data/lib/hexapdf/configuration.rb +2 -2
- data/lib/hexapdf/content/canvas.rb +3 -6
- data/lib/hexapdf/dictionary.rb +0 -3
- data/lib/hexapdf/document.rb +30 -22
- data/lib/hexapdf/document/files.rb +1 -1
- data/lib/hexapdf/document/images.rb +1 -1
- data/lib/hexapdf/filter/predictor.rb +8 -8
- data/lib/hexapdf/layout.rb +1 -0
- data/lib/hexapdf/layout/box.rb +55 -12
- data/lib/hexapdf/layout/frame.rb +143 -46
- data/lib/hexapdf/layout/image_box.rb +96 -0
- data/lib/hexapdf/layout/inline_box.rb +10 -0
- data/lib/hexapdf/layout/line.rb +1 -1
- data/lib/hexapdf/layout/style.rb +55 -3
- data/lib/hexapdf/layout/text_box.rb +38 -8
- data/lib/hexapdf/layout/text_layouter.rb +66 -52
- data/lib/hexapdf/object.rb +3 -2
- data/lib/hexapdf/parser.rb +6 -1
- data/lib/hexapdf/rectangle.rb +6 -0
- data/lib/hexapdf/reference.rb +4 -6
- data/lib/hexapdf/revision.rb +34 -8
- data/lib/hexapdf/revisions.rb +12 -2
- data/lib/hexapdf/stream.rb +18 -0
- data/lib/hexapdf/task.rb +1 -1
- data/lib/hexapdf/task/dereference.rb +1 -1
- data/lib/hexapdf/task/optimize.rb +2 -2
- data/lib/hexapdf/type/form.rb +10 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +25 -5
- data/man/man1/hexapdf.1 +17 -0
- data/test/hexapdf/layout/test_box.rb +7 -1
- data/test/hexapdf/layout/test_frame.rb +38 -1
- data/test/hexapdf/layout/test_image_box.rb +73 -0
- data/test/hexapdf/layout/test_inline_box.rb +8 -0
- data/test/hexapdf/layout/test_line.rb +1 -1
- data/test/hexapdf/layout/test_style.rb +58 -1
- data/test/hexapdf/layout/test_text_box.rb +57 -12
- data/test/hexapdf/layout/test_text_layouter.rb +14 -4
- data/test/hexapdf/task/test_optimize.rb +3 -3
- data/test/hexapdf/test_composer.rb +258 -0
- data/test/hexapdf/test_document.rb +29 -3
- data/test/hexapdf/test_importer.rb +8 -2
- data/test/hexapdf/test_object.rb +3 -1
- data/test/hexapdf/test_parser.rb +6 -0
- data/test/hexapdf/test_rectangle.rb +7 -0
- data/test/hexapdf/test_reference.rb +3 -1
- data/test/hexapdf/test_revision.rb +13 -0
- data/test/hexapdf/test_revisions.rb +1 -0
- data/test/hexapdf/test_stream.rb +13 -0
- data/test/hexapdf/test_writer.rb +13 -2
- data/test/hexapdf/type/test_annotation.rb +1 -1
- data/test/hexapdf/type/test_form.rb +13 -2
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba9272055e9cb48ac9f8af80aa239f39853130b62c1b46753b4534b8ccc94918
|
4
|
+
data.tar.gz: 9915555ad5a90ef3e5badc66cedd7d33ab2922a58c1785247b6949b3d61e6352
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 811ec430adb953546ed75c368c40f1dfa7f19e7645f7a05847bd329e9e84b4da481482ed846b1f98a6a4b452e2518eba7083daec56d67c185e2ff7c60a85492d
|
7
|
+
data.tar.gz: 563bd9f126355785ba877d1e9245c794b584f50cddb8fc634aecef9f6795fc05f1480938bf14e2f440de02282af86af1aa871b86bdc734fade81ed304d34e345
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
*
|
100
|
+
* HexaPDF::Layout::TextLayouter#draw to return result of #fit if possible
|
50
101
|
|
51
102
|
### Removed
|
52
103
|
|
data/CONTRIBUTERS
CHANGED
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.
|
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.
|
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
|
data/lib/hexapdf/cli.rb
CHANGED
@@ -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)
|
data/lib/hexapdf/cli/command.rb
CHANGED
@@ -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(
|
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(
|
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)
|
data/lib/hexapdf/cli/optimize.rb
CHANGED
@@ -87,7 +87,7 @@ module HexaPDF
|
|
87
87
|
end
|
88
88
|
doc.catalog[:Pages] = page_tree
|
89
89
|
|
90
|
-
doc.each(
|
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
|