hexapdf 0.34.1 → 0.35.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -0
- data/examples/009-text_layouter_alignment.rb +7 -7
- data/examples/010-text_layouter_inline_boxes.rb +1 -1
- data/examples/011-text_layouter_line_wrapping.rb +2 -4
- data/examples/013-text_layouter_shapes.rb +9 -11
- data/examples/014-text_in_polygon.rb +2 -2
- data/examples/016-frame_automatic_box_placement.rb +6 -7
- data/examples/017-frame_text_flow.rb +2 -2
- data/examples/018-composer.rb +5 -6
- data/examples/020-column_box.rb +2 -2
- data/examples/021-list_box.rb +1 -1
- data/examples/027-composer_optional_content.rb +5 -5
- data/examples/028-frame_mask_mode.rb +23 -0
- data/examples/029-composer_fallback_fonts.rb +22 -0
- data/lib/hexapdf/cli/info.rb +1 -0
- data/lib/hexapdf/cli/inspect.rb +55 -2
- data/lib/hexapdf/composer.rb +4 -3
- data/lib/hexapdf/configuration.rb +61 -1
- data/lib/hexapdf/content/canvas.rb +63 -0
- data/lib/hexapdf/content/canvas_composer.rb +142 -0
- data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +2 -8
- data/lib/hexapdf/content.rb +1 -0
- data/lib/hexapdf/dictionary.rb +14 -3
- data/lib/hexapdf/document/layout.rb +35 -13
- data/lib/hexapdf/document.rb +1 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +15 -0
- data/lib/hexapdf/error.rb +2 -1
- data/lib/hexapdf/font/invalid_glyph.rb +22 -6
- data/lib/hexapdf/font/true_type_wrapper.rb +48 -20
- data/lib/hexapdf/font/type1_wrapper.rb +48 -24
- data/lib/hexapdf/layout/box.rb +14 -10
- data/lib/hexapdf/layout/column_box.rb +5 -3
- data/lib/hexapdf/layout/frame.rb +78 -40
- data/lib/hexapdf/layout/image_box.rb +3 -3
- data/lib/hexapdf/layout/list_box.rb +20 -19
- data/lib/hexapdf/layout/style.rb +173 -68
- data/lib/hexapdf/layout/table_box.rb +3 -3
- data/lib/hexapdf/layout/text_box.rb +5 -5
- data/lib/hexapdf/layout/text_fragment.rb +50 -0
- data/lib/hexapdf/layout/text_layouter.rb +7 -6
- data/lib/hexapdf/layout/width_from_polygon.rb +1 -1
- data/lib/hexapdf/object.rb +5 -2
- data/lib/hexapdf/pdf_array.rb +5 -0
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +16 -11
- data/lib/hexapdf/type/icon_fit.rb +2 -2
- data/lib/hexapdf/type/page.rb +35 -35
- data/lib/hexapdf/utils/sorted_tree_node.rb +0 -10
- data/lib/hexapdf/utils.rb +66 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_canvas.rb +37 -0
- data/test/hexapdf/content/test_canvas_composer.rb +112 -0
- data/test/hexapdf/document/test_layout.rb +40 -12
- data/test/hexapdf/document/test_pages.rb +5 -5
- data/test/hexapdf/encryption/test_standard_security_handler.rb +43 -0
- data/test/hexapdf/font/test_invalid_glyph.rb +13 -1
- data/test/hexapdf/font/test_true_type_wrapper.rb +15 -2
- data/test/hexapdf/font/test_type1_wrapper.rb +21 -2
- data/test/hexapdf/layout/test_box.rb +7 -0
- data/test/hexapdf/layout/test_column_box.rb +18 -4
- data/test/hexapdf/layout/test_frame.rb +181 -95
- data/test/hexapdf/layout/test_list_box.rb +7 -7
- data/test/hexapdf/layout/test_page_style.rb +5 -5
- data/test/hexapdf/layout/test_style.rb +14 -10
- data/test/hexapdf/layout/test_table_box.rb +3 -3
- data/test/hexapdf/layout/test_text_box.rb +2 -2
- data/test/hexapdf/layout/test_text_fragment.rb +37 -0
- data/test/hexapdf/layout/test_text_layouter.rb +10 -10
- data/test/hexapdf/test_composer.rb +13 -13
- data/test/hexapdf/test_configuration.rb +49 -0
- data/test/hexapdf/test_dictionary.rb +1 -1
- data/test/hexapdf/test_object.rb +13 -12
- data/test/hexapdf/test_pdf_array.rb +9 -0
- data/test/hexapdf/test_utils.rb +20 -0
- data/test/hexapdf/test_writer.rb +3 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +41 -13
- data/test/hexapdf/type/test_page.rb +3 -3
- data/test/hexapdf/type/test_page_tree_node.rb +1 -1
- data/test/hexapdf/utils/test_sorted_tree_node.rb +1 -1
- metadata +9 -3
@@ -36,6 +36,7 @@
|
|
36
36
|
|
37
37
|
require 'hexapdf/content/graphics_state'
|
38
38
|
require 'hexapdf/content/operator'
|
39
|
+
require 'hexapdf/content/canvas_composer'
|
39
40
|
require 'hexapdf/serializer'
|
40
41
|
require 'hexapdf/utils/math_helpers'
|
41
42
|
require 'hexapdf/utils/graphics_helpers'
|
@@ -1450,6 +1451,43 @@ module HexaPDF
|
|
1450
1451
|
self
|
1451
1452
|
end
|
1452
1453
|
|
1454
|
+
# :call-seq:
|
1455
|
+
# canvas.form {|form_canvas| block } => form
|
1456
|
+
# canvas.form(width, height) {|form_canvas| block } => form
|
1457
|
+
#
|
1458
|
+
# Creates a reusable Form XObject, yields its canvas and then returns it.
|
1459
|
+
#
|
1460
|
+
# If no arguments are provided, the bounding box of the form is the same as that of the
|
1461
|
+
# context object of this canvas. Otherwise you need to provide the +width+ and +height+ for
|
1462
|
+
# the form.
|
1463
|
+
#
|
1464
|
+
# Once the form has been created, it can be used like an image and drawn mulitple times with
|
1465
|
+
# the #xobject method. Note that the created form object is independent of this canvas and its
|
1466
|
+
# context object. This means it can also be used with other canvases.
|
1467
|
+
#
|
1468
|
+
# Examples:
|
1469
|
+
#
|
1470
|
+
# #>pdf
|
1471
|
+
# form = canvas.form do |form_canvas|
|
1472
|
+
# form_canvas.fill_color("hp-blue").line_width(5).
|
1473
|
+
# rectangle(10, 10, 80, 80).fill_stroke
|
1474
|
+
# end
|
1475
|
+
# canvas.xobject(form, at: [0, 0])
|
1476
|
+
# canvas.xobject(form, width: 50, at: [100, 100])
|
1477
|
+
#
|
1478
|
+
# See: HexaPDF::Type::Form
|
1479
|
+
def form(width = nil, height = nil) # :yield: canvas
|
1480
|
+
obj = if width && height
|
1481
|
+
context.document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height]})
|
1482
|
+
elsif width || height
|
1483
|
+
raise ArgumentError, "Both arguments width and height need to be provided"
|
1484
|
+
else
|
1485
|
+
context.document.add({Type: :XObject, Subtype: :Form, BBox: context.box.value.dup})
|
1486
|
+
end
|
1487
|
+
yield(obj.canvas) if block_given?
|
1488
|
+
obj
|
1489
|
+
end
|
1490
|
+
|
1453
1491
|
# :call-seq:
|
1454
1492
|
# canvas.graphic_object(obj, **options) => obj
|
1455
1493
|
# canvas.graphic_object(name, **options) => graphic_object
|
@@ -2535,6 +2573,31 @@ module HexaPDF
|
|
2535
2573
|
# See: PDF2.0 s8.11
|
2536
2574
|
alias :end_optional_content :end_marked_content_sequence
|
2537
2575
|
|
2576
|
+
# :call-seq:
|
2577
|
+
# canvas.composer(margin: 0) {|composer| block } -> composer
|
2578
|
+
#
|
2579
|
+
# Creates a CanvasComposer object for composing content using high-level document layout
|
2580
|
+
# features, yields it, if a block is given, and returns it.
|
2581
|
+
#
|
2582
|
+
# The +margin+ can be any value allowed by HexaPDF::Layout::Style::Quad#set and defines the
|
2583
|
+
# margin that should not be used during composition. For the remaining area of the canvas a
|
2584
|
+
# frame object will be created.
|
2585
|
+
#
|
2586
|
+
# Examples:
|
2587
|
+
#
|
2588
|
+
# #>pdf
|
2589
|
+
# canvas.composer(margin: [10, 30]) do |composer|
|
2590
|
+
# composer.image(machu_picchu, height: 30, position: :float)
|
2591
|
+
# composer.lorem_ipsum(position: :flow)
|
2592
|
+
# end
|
2593
|
+
#
|
2594
|
+
# See: CanvasComposer, HexaPDF::Document::Layout
|
2595
|
+
def composer(margin: 0)
|
2596
|
+
composer = CanvasComposer.new(self, margin: margin)
|
2597
|
+
yield(composer) if block_given?
|
2598
|
+
composer
|
2599
|
+
end
|
2600
|
+
|
2538
2601
|
# Creates and returns a color object from the given color specification. See #stroke_color for
|
2539
2602
|
# details on the possible color specifications.
|
2540
2603
|
#
|
@@ -0,0 +1,142 @@
|
|
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-2023 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
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
|
+
#++
|
36
|
+
|
37
|
+
require 'hexapdf/layout'
|
38
|
+
|
39
|
+
module HexaPDF
|
40
|
+
module Content
|
41
|
+
|
42
|
+
# The CanvasComposer class allows using the document layout functionality for a single canvas.
|
43
|
+
# It works in a similar manner as the HexaPDF::Composer class.
|
44
|
+
#
|
45
|
+
# See: HexaPDF::Composer, HexaPDF::Document::Layout
|
46
|
+
class CanvasComposer
|
47
|
+
|
48
|
+
# The associated canvas.
|
49
|
+
attr_reader :canvas
|
50
|
+
|
51
|
+
# The associated HexaPDF::Document instance.
|
52
|
+
attr_reader :document
|
53
|
+
|
54
|
+
# The HexaPDF::Layout::Frame instance into which the boxes are laid out.
|
55
|
+
attr_reader :frame
|
56
|
+
|
57
|
+
# Creates a new CanvasComposer instance for the given +canvas+.
|
58
|
+
#
|
59
|
+
# The +margin+ can be any value allowed by HexaPDF::Layout::Style::Quad#set and defines the
|
60
|
+
# margin that should not be used during composition. For the remaining area of the canvas a
|
61
|
+
# frame object will be created.
|
62
|
+
def initialize(canvas, margin: 0)
|
63
|
+
@canvas = canvas
|
64
|
+
@document = @canvas.context.document
|
65
|
+
|
66
|
+
box = @canvas.context.box
|
67
|
+
margin = Layout::Style::Quad.new(margin)
|
68
|
+
@frame = Layout::Frame.new(box.left + margin.left,
|
69
|
+
box.bottom + margin.bottom,
|
70
|
+
box.width - margin.left - margin.right,
|
71
|
+
box.height - margin.bottom - margin.top,
|
72
|
+
context: @canvas.context)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Invokes HexaPDF::Document::Layout#style with the given arguments to create/update and return
|
76
|
+
# a style object.
|
77
|
+
def style(name, base: :base, **properties)
|
78
|
+
@document.layout.style(name, base: base, **properties)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Draws the given HexaPDF::Layout::Box and returns the last drawn box.
|
82
|
+
#
|
83
|
+
# The box is drawn into the frame. If it doesn't fit, the box is split. If it still doesn't
|
84
|
+
# fit, a new region of the frame is determined and then the process starts again.
|
85
|
+
#
|
86
|
+
# If none or only some parts of the box fit into the frame, an exception is thrown.
|
87
|
+
def draw_box(box)
|
88
|
+
while true
|
89
|
+
result = @frame.fit(box)
|
90
|
+
if result.success?
|
91
|
+
@frame.draw(@canvas, result)
|
92
|
+
break
|
93
|
+
elsif @frame.full?
|
94
|
+
raise HexaPDF::Error, "Frame for canvas composer is full and box doesn't fit anymore"
|
95
|
+
else
|
96
|
+
draw_box, box = @frame.split(result)
|
97
|
+
if draw_box
|
98
|
+
@frame.draw(@canvas, result)
|
99
|
+
elsif !@frame.find_next_region
|
100
|
+
raise HexaPDF::Error, "Frame for canvas composer is full and box doesn't fit anymore"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
box
|
105
|
+
end
|
106
|
+
|
107
|
+
# Draws any box that can be created using HexaPDF::Document::Layout.
|
108
|
+
#
|
109
|
+
# This includes all named boxes defined in the 'layout.boxes.map' configuration option.
|
110
|
+
#
|
111
|
+
# Examples:
|
112
|
+
#
|
113
|
+
# #>pdf
|
114
|
+
# canvas.composer(margin: 10) do |composer|
|
115
|
+
# composer.text("Some text", position: :float)
|
116
|
+
# composer.image(machu_picchu, height: 30, align: :right)
|
117
|
+
# composer.lorem_ipsum(sentences: 1, margin: [0, 0, 5])
|
118
|
+
# composer.list(item_spacing: 2) do |list|
|
119
|
+
# composer.document.config['layout.boxes.map'].each do |name, klass|
|
120
|
+
# list.formatted_text([{text: name.to_s, fill_color: "hp-blue-dark"},
|
121
|
+
# {text: "\n#{klass}"}, font_size: 7])
|
122
|
+
# end
|
123
|
+
# end
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# See: HexaPDF::Document::Layout#box
|
127
|
+
def method_missing(name, *args, **kwargs, &block)
|
128
|
+
if @document.layout.box_creation_method?(name)
|
129
|
+
draw_box(@document.layout.send(name, *args, **kwargs, &block))
|
130
|
+
else
|
131
|
+
super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def respond_to_missing?(name, _private) # :nodoc:
|
136
|
+
@document.layout.box_creation_method?(name) || super
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
@@ -62,8 +62,7 @@ module HexaPDF
|
|
62
62
|
# https://web.archive.org/web/20160310153722/https://www.w3.org/TR/SVG/implnote.html).
|
63
63
|
class EndpointArc
|
64
64
|
|
65
|
-
|
66
|
-
|
65
|
+
include Utils
|
67
66
|
include Utils::MathHelpers
|
68
67
|
|
69
68
|
# Creates and configures a new endpoint arc object.
|
@@ -283,7 +282,7 @@ module HexaPDF
|
|
283
282
|
|
284
283
|
# F.6.5.2
|
285
284
|
sqrt = (rxs * rys - rxs * y1ps - rys * x1ps) / (rxs * y1ps + rys * x1ps)
|
286
|
-
sqrt = 0 if sqrt.abs < EPSILON
|
285
|
+
sqrt = 0 if sqrt.abs < Utils::EPSILON
|
287
286
|
sqrt = Math.sqrt(sqrt)
|
288
287
|
sqrt *= -1 unless @large_arc == @clockwise
|
289
288
|
cxp = sqrt * rx * y1p / ry
|
@@ -303,11 +302,6 @@ module HexaPDF
|
|
303
302
|
inclination: @inclination, clockwise: @clockwise, max_curves: @max_curves}
|
304
303
|
end
|
305
304
|
|
306
|
-
# Compares two float numbers if they are within a certain delta.
|
307
|
-
def float_equal(a, b)
|
308
|
-
(a - b).abs < EPSILON
|
309
|
-
end
|
310
|
-
|
311
305
|
# Computes the angle in degrees between the x-axis and the vector.
|
312
306
|
def compute_angle_to_x_axis(vx, vy)
|
313
307
|
(vy < 0 ? -1 : 1) * rad_to_deg(Math.acos(vx / Math.sqrt(vx**2 + vy**2)))
|
data/lib/hexapdf/content.rb
CHANGED
data/lib/hexapdf/dictionary.rb
CHANGED
@@ -257,14 +257,25 @@ module HexaPDF
|
|
257
257
|
end
|
258
258
|
end
|
259
259
|
|
260
|
+
# Iterates over all currently set entries and all fields that are required.
|
261
|
+
def each_set_key_or_required_field #:yields: name, field
|
262
|
+
value.keys.each {|name| yield(name, self.class.field(name)) }
|
263
|
+
self.class.each_field do |name, field|
|
264
|
+
yield(name, field) if field.required? && !value.key?(name)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
260
268
|
# Performs validation tasks based on the currently set keys and defined fields.
|
261
269
|
def perform_validation(&block)
|
262
270
|
super
|
263
|
-
|
264
|
-
next unless field.required? || value.key?(name)
|
265
|
-
|
271
|
+
each_set_key_or_required_field do |name, field|
|
266
272
|
obj = key?(name) ? self[name] : nil
|
267
273
|
|
274
|
+
validate_nested(obj, &block)
|
275
|
+
|
276
|
+
# The checks below need associated field information
|
277
|
+
next unless field
|
278
|
+
|
268
279
|
# Check that required fields are set
|
269
280
|
if field.required? && obj.nil?
|
270
281
|
yield("Required field #{name} is not set", field.default?)
|
@@ -260,6 +260,28 @@ module HexaPDF
|
|
260
260
|
style: retrieve_style(style), **box_options)
|
261
261
|
end
|
262
262
|
|
263
|
+
# Creates an array of HexaPDF::Layout::TextFragment objects for the given +text+.
|
264
|
+
#
|
265
|
+
# This method uses the configuration option 'font.on_invalid_glyph' to map Unicode characters
|
266
|
+
# without a valid glyph in the given font to zero, one or more glyphs in a fallback font.
|
267
|
+
#
|
268
|
+
# +style+, +style_properties+::
|
269
|
+
# The text is styled using the given +style+. This can either be a style name set via
|
270
|
+
# #style or anything Layout::Style::create accepts. If any additional +style_properties+
|
271
|
+
# are specified, the style is duplicated and the additional styles are applied.
|
272
|
+
#
|
273
|
+
# +properties+::
|
274
|
+
# This can be used to set custom properties on the created text fragments. See
|
275
|
+
# Layout::Box#properties for details and usage.
|
276
|
+
def text_fragments(text, style: nil, properties: nil, **style_properties)
|
277
|
+
style = retrieve_style(style, style_properties)
|
278
|
+
fragments = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs(
|
279
|
+
text, style, &@document.config['font.on_invalid_glyph']
|
280
|
+
)
|
281
|
+
fragments.each {|f| f.properties.update(properties) } if properties
|
282
|
+
fragments
|
283
|
+
end
|
284
|
+
|
263
285
|
# Creates a HexaPDF::Layout::TextBox for the given text.
|
264
286
|
#
|
265
287
|
# This method is of the two main methods for creating text boxes, the other being
|
@@ -300,7 +322,7 @@ module HexaPDF
|
|
300
322
|
**style_properties)
|
301
323
|
style = retrieve_style(style, style_properties)
|
302
324
|
box_style = (box_style ? retrieve_style(box_style) : style)
|
303
|
-
box_class_for_name(:text).new(items:
|
325
|
+
box_class_for_name(:text).new(items: text_fragments(text, style: style),
|
304
326
|
width: width, height: height, properties: properties,
|
305
327
|
style: box_style)
|
306
328
|
end
|
@@ -319,11 +341,11 @@ module HexaPDF
|
|
319
341
|
# * Hashes can contain any style properties and the following special keys:
|
320
342
|
#
|
321
343
|
# text:: The text to be formatted. If this is set and :box is not, the hash will be
|
322
|
-
# transformed into
|
344
|
+
# transformed into text fragments.
|
323
345
|
#
|
324
346
|
# link:: A URL that should be linked to. If no text is provided but a link, the link is used
|
325
|
-
# for the text. If this is set and :box is not, the hash will be transformed into
|
326
|
-
# text
|
347
|
+
# for the text. If this is set and :box is not, the hash will be transformed into
|
348
|
+
# text fragments with an appropriate link overlay.
|
327
349
|
#
|
328
350
|
# style:: The style to use as base style instead of the style created from the +style+ and
|
329
351
|
# +style_properties+ arguments. This can either be a style name set via #style or
|
@@ -334,6 +356,8 @@ module HexaPDF
|
|
334
356
|
#
|
335
357
|
# The final style is used for a created text fragment.
|
336
358
|
#
|
359
|
+
# properties:: The custom properties that should be set on the created text fragments.
|
360
|
+
#
|
337
361
|
# box:: An inline box to be used. If this is set, the hash will be transformed into an
|
338
362
|
# inline box.
|
339
363
|
#
|
@@ -379,26 +403,24 @@ module HexaPDF
|
|
379
403
|
**style_properties)
|
380
404
|
style = retrieve_style(style, style_properties)
|
381
405
|
box_style = (box_style ? retrieve_style(box_style) : style)
|
382
|
-
data.
|
406
|
+
data = data.inject([]) do |result, item|
|
383
407
|
case item
|
384
408
|
when String
|
385
|
-
|
409
|
+
result.concat(text_fragments(item, style: style))
|
386
410
|
when Hash
|
387
411
|
if (args = item.delete(:box))
|
388
412
|
block = item.delete(:block)
|
389
|
-
inline_box(*args, **item, &block)
|
413
|
+
result << inline_box(*args, **item, &block)
|
390
414
|
else
|
391
415
|
link = item.delete(:link)
|
392
416
|
(item[:overlays] ||= []) << [:link, {uri: link}] if link
|
393
417
|
text = item.delete(:text) || link || ""
|
394
|
-
|
418
|
+
item_properties = item.delete(:properties)
|
395
419
|
frag_style = retrieve_style(item.delete(:style) || style, item)
|
396
|
-
|
397
|
-
fragment.properties.update(properties) if properties
|
398
|
-
fragment
|
420
|
+
result.concat(text_fragments(text, style: frag_style, properties: item_properties))
|
399
421
|
end
|
400
422
|
when HexaPDF::Layout::InlineBox
|
401
|
-
item
|
423
|
+
result << item
|
402
424
|
else
|
403
425
|
raise ArgumentError, "Invalid item of class #{item.class} in data array"
|
404
426
|
end
|
@@ -506,7 +528,7 @@ module HexaPDF
|
|
506
528
|
# # row 0 has a grey background and bold text
|
507
529
|
# args[0] = {font: ['Helvetica', variant: :bold], cell: {background_color: 'eee'}}
|
508
530
|
# # text in last column is right aligned
|
509
|
-
# args[0..-1, -1] = {
|
531
|
+
# args[0..-1, -1] = {text_align: :right}
|
510
532
|
# end
|
511
533
|
#
|
512
534
|
# See: HexaPDF::Layout::TableBox
|
data/lib/hexapdf/document.rb
CHANGED
@@ -250,6 +250,18 @@ module HexaPDF
|
|
250
250
|
end
|
251
251
|
end
|
252
252
|
|
253
|
+
# Returns the type of password used for decrypting the PDF document.
|
254
|
+
#
|
255
|
+
# The return value is one of the following:
|
256
|
+
#
|
257
|
+
# :none:: No password was needed for decryption.
|
258
|
+
# :user:: The provided user password was used for decryption.
|
259
|
+
# :owner:: The provided owner password was used for decryption.
|
260
|
+
# :unknown:: The document was not decrypted, only encrypted.
|
261
|
+
def decryption_password_type
|
262
|
+
@decryption_password_type || :unknown
|
263
|
+
end
|
264
|
+
|
253
265
|
def decrypt(obj) #:nodoc:
|
254
266
|
if dict[:V] >= 4 && obj.type == :Metadata && obj[:Subtype] == :XML && !dict[:EncryptMetadata]
|
255
267
|
obj
|
@@ -345,10 +357,13 @@ module HexaPDF
|
|
345
357
|
password = prepare_password(password)
|
346
358
|
|
347
359
|
if user_password_valid?(prepare_password(''))
|
360
|
+
@decryption_password_type = :none
|
348
361
|
encryption_key = compute_user_encryption_key(prepare_password(''))
|
349
362
|
elsif user_password_valid?(password)
|
363
|
+
@decryption_password_type = :user
|
350
364
|
encryption_key = compute_user_encryption_key(password)
|
351
365
|
elsif owner_password_valid?(password)
|
366
|
+
@decryption_password_type = :owner
|
352
367
|
encryption_key = compute_owner_encryption_key(password)
|
353
368
|
else
|
354
369
|
raise HexaPDF::EncryptionError, "Invalid password specified"
|
data/lib/hexapdf/error.rb
CHANGED
@@ -94,7 +94,8 @@ module HexaPDF
|
|
94
94
|
end
|
95
95
|
|
96
96
|
def message # :nodoc:
|
97
|
-
"No glyph for #{glyph.str.inspect} in font '#{glyph.
|
97
|
+
"No glyph for #{glyph.str.inspect} in font '#{glyph.font_wrapper.wrapped_font.full_name}' " \
|
98
|
+
"found. \n\n" \
|
98
99
|
"Use the configuration option 'font.on_missing_glyph' to customize missing glyph handling."
|
99
100
|
end
|
100
101
|
|
@@ -34,6 +34,8 @@
|
|
34
34
|
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
35
|
#++
|
36
36
|
|
37
|
+
require 'set'
|
38
|
+
|
37
39
|
module HexaPDF
|
38
40
|
module Font
|
39
41
|
|
@@ -41,21 +43,21 @@ module HexaPDF
|
|
41
43
|
# font.
|
42
44
|
class InvalidGlyph
|
43
45
|
|
44
|
-
# The associated font object.
|
45
|
-
attr_reader :
|
46
|
+
# The associated font wrapper object, either a Type1Wrapper or a TrueTypeWrapper.
|
47
|
+
attr_reader :font_wrapper
|
46
48
|
|
47
49
|
# The string that could not be represented as a glyph.
|
48
50
|
attr_reader :str
|
49
51
|
|
50
52
|
# Creates a new Glyph object.
|
51
|
-
def initialize(
|
52
|
-
@
|
53
|
+
def initialize(font_wrapper, str)
|
54
|
+
@font_wrapper = font_wrapper
|
53
55
|
@str = str
|
54
56
|
end
|
55
57
|
|
56
58
|
# Returns the appropriate missing glyph id based on the used font.
|
57
59
|
def id
|
58
|
-
@
|
60
|
+
@font_wrapper.wrapped_font.missing_glyph_id
|
59
61
|
end
|
60
62
|
alias name id
|
61
63
|
|
@@ -73,9 +75,23 @@ module HexaPDF
|
|
73
75
|
false
|
74
76
|
end
|
75
77
|
|
78
|
+
# Returns +false+ since this is an invalid glyph.
|
79
|
+
def valid?
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
# Set of codepoints for text control characters, like tabulator, line separators, non-breaking
|
84
|
+
# space etc.
|
85
|
+
CONTROL_CHARS = Set.new([9, 10, 11, 12, 13, 133, 8232, 8233, 8203, 173, 160]) #:nodoc:
|
86
|
+
|
87
|
+
# Returns +true+ if this glyph represents a control character like tabulator or newline.
|
88
|
+
def control_char?
|
89
|
+
CONTROL_CHARS.include?(str.ord)
|
90
|
+
end
|
91
|
+
|
76
92
|
#:nodoc:
|
77
93
|
def inspect
|
78
|
-
"#<#{self.class.name} font=#{@
|
94
|
+
"#<#{self.class.name} font=#{@font_wrapper.wrapped_font.full_name.inspect} id=#{id} #{@str.inspect}>"
|
79
95
|
end
|
80
96
|
|
81
97
|
end
|
@@ -59,8 +59,8 @@ module HexaPDF
|
|
59
59
|
# Represents a single glyph of the wrapped font.
|
60
60
|
class Glyph
|
61
61
|
|
62
|
-
# The associated
|
63
|
-
attr_reader :
|
62
|
+
# The associated TrueTypeWrapper object.
|
63
|
+
attr_reader :font_wrapper
|
64
64
|
|
65
65
|
# The glyph ID.
|
66
66
|
attr_reader :id
|
@@ -69,35 +69,40 @@ module HexaPDF
|
|
69
69
|
attr_reader :str
|
70
70
|
|
71
71
|
# Creates a new Glyph object.
|
72
|
-
def initialize(
|
73
|
-
@
|
72
|
+
def initialize(font_wrapper, id, str)
|
73
|
+
@font_wrapper = font_wrapper
|
74
74
|
@id = id
|
75
75
|
@str = str
|
76
76
|
end
|
77
77
|
|
78
78
|
# Returns the glyph's minimum x coordinate.
|
79
79
|
def x_min
|
80
|
-
@x_min ||= @
|
80
|
+
@x_min ||= @font_wrapper.wrapped_font[:glyf][id].x_min * 1000.0 /
|
81
|
+
@font_wrapper.wrapped_font[:head].units_per_em
|
81
82
|
end
|
82
83
|
|
83
84
|
# Returns the glyph's maximum x coordinate.
|
84
85
|
def x_max
|
85
|
-
@x_max ||= @
|
86
|
+
@x_max ||= @font_wrapper.wrapped_font[:glyf][id].x_max * 1000.0 /
|
87
|
+
@font_wrapper.wrapped_font[:head].units_per_em
|
86
88
|
end
|
87
89
|
|
88
90
|
# Returns the glyph's minimum y coordinate.
|
89
91
|
def y_min
|
90
|
-
@y_min ||= @
|
92
|
+
@y_min ||= @font_wrapper.wrapped_font[:glyf][id].y_min * 1000.0 /
|
93
|
+
@font_wrapper.wrapped_font[:head].units_per_em
|
91
94
|
end
|
92
95
|
|
93
96
|
# Returns the glyph's maximum y coordinate.
|
94
97
|
def y_max
|
95
|
-
@y_max ||= @
|
98
|
+
@y_max ||= @font_wrapper.wrapped_font[:glyf][id].y_max * 1000.0 /
|
99
|
+
@font_wrapper.wrapped_font[:head].units_per_em
|
96
100
|
end
|
97
101
|
|
98
102
|
# Returns the width of the glyph.
|
99
103
|
def width
|
100
|
-
@width ||= @
|
104
|
+
@width ||= @font_wrapper.wrapped_font[:hmtx][id].advance_width * 1000.0 /
|
105
|
+
@font_wrapper.wrapped_font[:head].units_per_em
|
101
106
|
end
|
102
107
|
|
103
108
|
# Returns +false+ since the word spacing parameter is never applied for multibyte font
|
@@ -106,9 +111,14 @@ module HexaPDF
|
|
106
111
|
false
|
107
112
|
end
|
108
113
|
|
114
|
+
# Returns +true+ since this is a valid glyph.
|
115
|
+
def valid?
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
109
119
|
#:nodoc:
|
110
120
|
def inspect
|
111
|
-
"#<#{self.class.name} font=#{@
|
121
|
+
"#<#{self.class.name} font=#{@font_wrapper.wrapped_font.full_name.inspect} id=#{id} #{str.inspect}>"
|
112
122
|
end
|
113
123
|
|
114
124
|
end
|
@@ -154,6 +164,16 @@ module HexaPDF
|
|
154
164
|
@scaling_factor ||= 1000.0 / @wrapped_font[:head].units_per_em
|
155
165
|
end
|
156
166
|
|
167
|
+
# Returns +true+ if the font contains bold glyphs.
|
168
|
+
def bold?
|
169
|
+
@wrapped_font.weight > 500
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns +true+ if the font contains glyphs with an incline (italic or slant).
|
173
|
+
def italic?
|
174
|
+
@wrapped_font.italic_angle.to_i != 0
|
175
|
+
end
|
176
|
+
|
157
177
|
# Returns +true+ if the wrapped TrueType font will be subset.
|
158
178
|
def subset?
|
159
179
|
!@subsetter.nil?
|
@@ -168,7 +188,7 @@ module HexaPDF
|
|
168
188
|
def glyph(id, str = nil)
|
169
189
|
@id_to_glyph[id] ||=
|
170
190
|
if id >= 0 && id < @wrapped_font[:maxp].num_glyphs
|
171
|
-
Glyph.new(
|
191
|
+
Glyph.new(self, id, str || (+'' << (@cmap.gid_to_code(id) || 0xFFFD)))
|
172
192
|
else
|
173
193
|
@pdf_object.document.config['font.on_missing_glyph'].call("\u{FFFD}", self)
|
174
194
|
end
|
@@ -183,19 +203,27 @@ module HexaPDF
|
|
183
203
|
if id < 0 || id >= @wrapped_font[:maxp].num_glyphs
|
184
204
|
raise HexaPDF::Error, "Glyph ID #{id} is invalid for font '#{@wrapped_font.full_name}'"
|
185
205
|
end
|
186
|
-
Glyph.new(
|
206
|
+
Glyph.new(self, id, string)
|
187
207
|
end
|
188
208
|
|
189
209
|
# Returns an array of glyph objects representing the characters in the UTF-8 encoded string.
|
210
|
+
#
|
211
|
+
# See #decode_codepoint for details.
|
190
212
|
def decode_utf8(str)
|
191
|
-
str.codepoints.map!
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
213
|
+
str.codepoints.map! {|c| @codepoint_to_glyph[c] || decode_codepoint(c) }
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns a glyph object for the given Unicode codepoint.
|
217
|
+
#
|
218
|
+
# The configuration option 'font.on_missing_glyph' is invoked if no glyph for a given
|
219
|
+
# codepoint is available.
|
220
|
+
def decode_codepoint(codepoint)
|
221
|
+
@codepoint_to_glyph[codepoint] ||=
|
222
|
+
if (gid = @cmap[codepoint])
|
223
|
+
glyph(gid, +'' << codepoint)
|
224
|
+
else
|
225
|
+
@pdf_object.document.config['font.on_missing_glyph'].call(+'' << codepoint, self)
|
226
|
+
end
|
199
227
|
end
|
200
228
|
|
201
229
|
# Encodes the glyph and returns the code string.
|