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