hexapdf 0.37.2 → 0.39.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 +41 -0
- data/README.md +17 -13
- data/data/hexapdf/sRGB2014.icc +0 -0
- data/data/hexapdf/sRGB2014.icc.LICENSE +7 -0
- data/examples/030-pdfa.rb +89 -0
- data/lib/hexapdf/configuration.rb +11 -0
- data/lib/hexapdf/document/layout.rb +8 -4
- data/lib/hexapdf/document/metadata.rb +50 -13
- data/lib/hexapdf/layout/column_box.rb +2 -2
- data/lib/hexapdf/layout/container_box.rb +3 -2
- data/lib/hexapdf/layout/frame.rb +40 -6
- data/lib/hexapdf/layout/inline_box.rb +11 -4
- data/lib/hexapdf/layout/list_box.rb +17 -14
- data/lib/hexapdf/layout/style.rb +20 -0
- data/lib/hexapdf/layout/table_box.rb +2 -1
- data/lib/hexapdf/layout/text_box.rb +11 -2
- data/lib/hexapdf/layout/text_layouter.rb +12 -3
- data/lib/hexapdf/task/pdfa.rb +87 -0
- data/lib/hexapdf/task.rb +1 -0
- data/lib/hexapdf/type/optional_content_properties.rb +2 -2
- data/lib/hexapdf/type/output_intent.rb +85 -0
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/document/test_layout.rb +13 -0
- data/test/hexapdf/document/test_metadata.rb +60 -5
- data/test/hexapdf/layout/test_frame.rb +31 -2
- data/test/hexapdf/layout/test_inline_box.rb +8 -1
- data/test/hexapdf/layout/test_list_box.rb +20 -0
- data/test/hexapdf/layout/test_style.rb +1 -0
- data/test/hexapdf/layout/test_text_box.rb +22 -5
- data/test/hexapdf/task/test_pdfa.rb +41 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/test_optional_content_properties.rb +2 -2
- metadata +8 -2
@@ -227,12 +227,12 @@ module HexaPDF
|
|
227
227
|
remove_indent_from_frame_shape(shape) unless shape.polygons.empty?
|
228
228
|
end
|
229
229
|
|
230
|
-
item_frame =
|
231
|
-
|
230
|
+
item_frame = frame.child_frame(item_frame_left, top - height, item_frame_width, height,
|
231
|
+
shape: shape, box: self)
|
232
232
|
|
233
233
|
if index != 0 || !split_box? || @split_box == :show_first_marker
|
234
234
|
box = item_marker_box(frame.document, index)
|
235
|
-
marker_frame =
|
235
|
+
marker_frame = frame.child_frame(0, 0, content_indentation, height, box: self)
|
236
236
|
break unless box.fit(content_indentation, height, marker_frame)
|
237
237
|
item_result.marker = box
|
238
238
|
item_result.marker_pos_x = item_frame.x - content_indentation
|
@@ -325,27 +325,30 @@ module HexaPDF
|
|
325
325
|
return @marker_type.call(document, self, index) if @marker_type.kind_of?(Proc)
|
326
326
|
return @item_marker_box if defined?(@item_marker_box)
|
327
327
|
|
328
|
+
marker_style = {
|
329
|
+
font: style.font? ? style.font : document.fonts.add("Times"),
|
330
|
+
font_size: style.font_size || 10, fill_color: style.fill_color
|
331
|
+
}
|
328
332
|
fragment = case @marker_type
|
329
333
|
when :disc
|
330
|
-
TextFragment.create("•",
|
331
|
-
font_size: style.font_size, fill_color: style.fill_color)
|
334
|
+
TextFragment.create("•", marker_style)
|
332
335
|
when :circle
|
333
|
-
|
336
|
+
unless marker_style[:font].decode_codepoint("❍".ord).valid?
|
337
|
+
marker_style[:font] = document.fonts.add("ZapfDingbats")
|
338
|
+
end
|
339
|
+
TextFragment.create("❍", **marker_style,
|
334
340
|
font_size: style.font_size / 2.0,
|
335
|
-
fill_color: style.fill_color,
|
336
341
|
text_rise: -style.font_size / 1.8)
|
337
342
|
when :square
|
338
|
-
|
343
|
+
unless marker_style[:font].decode_codepoint("■".ord).valid?
|
344
|
+
marker_style[:font] = document.fonts.add("ZapfDingbats")
|
345
|
+
end
|
346
|
+
TextFragment.create("■", **marker_style,
|
339
347
|
font_size: style.font_size / 2.0,
|
340
|
-
fill_color: style.fill_color,
|
341
348
|
text_rise: -style.font_size / 1.8)
|
342
349
|
when :decimal
|
343
350
|
text = (@start_number + index).to_s << "."
|
344
|
-
|
345
|
-
font: (style.font? ? style.font : document.fonts.add("Times")),
|
346
|
-
font_size: style.font_size || 10, fill_color: style.fill_color
|
347
|
-
}
|
348
|
-
TextFragment.create(text, decimal_style)
|
351
|
+
TextFragment.create(text, marker_style)
|
349
352
|
else
|
350
353
|
raise HexaPDF::Error, "Unknown list marker type #{@marker_type.inspect}"
|
351
354
|
end
|
data/lib/hexapdf/layout/style.rb
CHANGED
@@ -1084,6 +1084,25 @@ module HexaPDF
|
|
1084
1084
|
# 'Centered',
|
1085
1085
|
# {text: "\u{00a0}", fill_horizontal: 1, overlays: overlays}])
|
1086
1086
|
|
1087
|
+
##
|
1088
|
+
# :method: text_overflow
|
1089
|
+
# :call-seq:
|
1090
|
+
# text_overflow(mode = nil)
|
1091
|
+
#
|
1092
|
+
# Specifies how text overflowing a box with a given initial height should be handled:
|
1093
|
+
#
|
1094
|
+
# Possible values:
|
1095
|
+
#
|
1096
|
+
# :error:: An error is raised (default).
|
1097
|
+
# :truncate:: Truncates the overflowing text.
|
1098
|
+
#
|
1099
|
+
# Examples:
|
1100
|
+
#
|
1101
|
+
# #>pdf-composer100
|
1102
|
+
# composer.text("This is some longer text that does appear in two lines.")
|
1103
|
+
# composer.text("This is some longer text that does not appear in two lines.",
|
1104
|
+
# height: 15, text_overflow: :truncate)
|
1105
|
+
|
1087
1106
|
##
|
1088
1107
|
# :method: background_color
|
1089
1108
|
# :call-seq:
|
@@ -1435,6 +1454,7 @@ module HexaPDF
|
|
1435
1454
|
extra_args: ", extra_arg = nil"}],
|
1436
1455
|
[:last_line_gap, false, {valid_values: [true, false]}],
|
1437
1456
|
[:fill_horizontal, nil],
|
1457
|
+
[:text_overflow, :error],
|
1438
1458
|
[:background_color, nil],
|
1439
1459
|
[:background_alpha, 1],
|
1440
1460
|
[:padding, "Quad.new(0)", {setter: "Quad.new(value)"}],
|
@@ -218,7 +218,7 @@ module HexaPDF
|
|
218
218
|
height = available_height - reserved_height
|
219
219
|
return false if width <= 0 || height <= 0
|
220
220
|
|
221
|
-
frame =
|
221
|
+
frame = frame.child_frame(0, 0, width, height, box: self)
|
222
222
|
case children
|
223
223
|
when Box
|
224
224
|
fit_result = frame.fit(children)
|
@@ -607,6 +607,7 @@ module HexaPDF
|
|
607
607
|
columns = calculate_column_widths(width)
|
608
608
|
return false if columns.empty?
|
609
609
|
|
610
|
+
frame = frame.child_frame(box: self)
|
610
611
|
@special_cells_fit_not_successful = false
|
611
612
|
[@header_cells, @footer_cells].each do |special_cells|
|
612
613
|
next unless special_cells
|
@@ -79,6 +79,7 @@ module HexaPDF
|
|
79
79
|
return false if (@initial_width > 0 && @initial_width > available_width) ||
|
80
80
|
(@initial_height > 0 && @initial_height > available_height)
|
81
81
|
|
82
|
+
frame = frame.child_frame(box: self)
|
82
83
|
@width = @height = 0
|
83
84
|
@result = if style.position == :flow
|
84
85
|
@tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
|
@@ -104,7 +105,8 @@ module HexaPDF
|
|
104
105
|
@height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
|
105
106
|
end
|
106
107
|
|
107
|
-
@result.status == :success
|
108
|
+
@result.status == :success ||
|
109
|
+
(@result.status == :height && @initial_height > 0 && style.text_overflow == :truncate)
|
108
110
|
end
|
109
111
|
|
110
112
|
# Splits the text box into two boxes if necessary and possible.
|
@@ -132,7 +134,14 @@ module HexaPDF
|
|
132
134
|
|
133
135
|
# Draws the text into the box.
|
134
136
|
def draw_content(canvas, x, y)
|
135
|
-
return unless @result
|
137
|
+
return unless @result
|
138
|
+
|
139
|
+
if @result.status == :height && @initial_height > 0 && style.text_overflow == :error
|
140
|
+
raise HexaPDF::Error, "Text doesn't fit into box with limited height and " \
|
141
|
+
"style property text_overflow is set to :error"
|
142
|
+
end
|
143
|
+
|
144
|
+
return if @result.lines.empty?
|
136
145
|
@result.draw(canvas, x, y + content_height)
|
137
146
|
end
|
138
147
|
|
@@ -307,10 +307,15 @@ module HexaPDF
|
|
307
307
|
class SimpleLineWrapping
|
308
308
|
|
309
309
|
# :call-seq:
|
310
|
-
# SimpleLineWrapping.call(items, width_block) {|line, item| block } -> rest
|
310
|
+
# SimpleLineWrapping.call(items, width_block, frame = nil) {|line, item| block } -> rest
|
311
311
|
#
|
312
312
|
# Arranges the items into lines.
|
313
313
|
#
|
314
|
+
# The optional +frame+ argument needs to be a Frame object that is used when fitting inline
|
315
|
+
# boxes. If not provided, a custom Frame object is used. However, if the items contain
|
316
|
+
# inline boxes that need to access a frame's context object, it is mandatory to provide an
|
317
|
+
# appropriate Frame object.
|
318
|
+
#
|
314
319
|
# The +width_block+ argument has to be a callable object that returns the width of the line:
|
315
320
|
#
|
316
321
|
# * If the line width doesn't depend on the height or the vertical position of the line
|
@@ -340,7 +345,7 @@ module HexaPDF
|
|
340
345
|
# current start of the line index should be stored for later use.
|
341
346
|
#
|
342
347
|
# After the algorithm is finished, it returns the unused items.
|
343
|
-
def self.call(items, width_block, frame, &block)
|
348
|
+
def self.call(items, width_block, frame = nil, &block)
|
344
349
|
obj = new(items, width_block, frame)
|
345
350
|
if width_block.arity == 1
|
346
351
|
obj.variable_width_wrapping(&block)
|
@@ -507,7 +512,7 @@ module HexaPDF
|
|
507
512
|
#
|
508
513
|
# Returns +true+ if the item could be added and +false+ otherwise.
|
509
514
|
def add_box_item(item)
|
510
|
-
item.fit_wrapped_box(@frame
|
515
|
+
item.fit_wrapped_box(@frame) if item.kind_of?(InlineBox)
|
511
516
|
return false unless @width + item.width <= @available_width
|
512
517
|
@line_items.concat(@glue_items).push(item)
|
513
518
|
@width += item.width
|
@@ -718,6 +723,10 @@ module HexaPDF
|
|
718
723
|
# Specifies whether style.text_indent should be applied to the first line. This should be
|
719
724
|
# set to +false+ if the items start with a continuation of a paragraph instead of starting
|
720
725
|
# a new paragraph (e.g. after a page break).
|
726
|
+
#
|
727
|
+
# +frame+::
|
728
|
+
# If used with the document layout functionality, this should be the frame into which the
|
729
|
+
# text is laid out.
|
721
730
|
def fit(items, width, height, apply_first_text_indent: true, frame: nil)
|
722
731
|
unless items.empty? || items[0].respond_to?(:type)
|
723
732
|
items = style.text_segmentation_algorithm.call(items)
|
@@ -0,0 +1,87 @@
|
|
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-2024 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 'set'
|
38
|
+
require 'hexapdf/serializer'
|
39
|
+
require 'hexapdf/content/parser'
|
40
|
+
require 'hexapdf/content/operator'
|
41
|
+
require 'hexapdf/type/xref_stream'
|
42
|
+
require 'hexapdf/type/object_stream'
|
43
|
+
|
44
|
+
module HexaPDF
|
45
|
+
module Task
|
46
|
+
|
47
|
+
# Task for creating a PDF/A compliant document.
|
48
|
+
#
|
49
|
+
# It automatically
|
50
|
+
#
|
51
|
+
# * prevents the Standard 14 PDF fonts to be used.
|
52
|
+
# * adds an appropriate output intent if none is set.
|
53
|
+
# * adds the necessary PDF/A metadata properties.
|
54
|
+
module PDFA
|
55
|
+
|
56
|
+
# Performs the necessary tasks to make the document PDF/A compatible.
|
57
|
+
#
|
58
|
+
# +level+::
|
59
|
+
# Specifies the PDF/A conformance level that should be used. Can be one of the following
|
60
|
+
# strings: 2b, 2u, 3b, 3u.
|
61
|
+
def self.call(doc, level: '3u')
|
62
|
+
unless level.match?(/\A[23][bu]\z/)
|
63
|
+
raise ArgumentError, "The given PDF/A conformance level '#{level}' is not supported"
|
64
|
+
end
|
65
|
+
doc.config['font_loader'].delete('HexaPDF::FontLoader::Standard14')
|
66
|
+
doc.register_listener(:complete_objects) do
|
67
|
+
part, conformance = level.chars
|
68
|
+
doc.metadata.property('pdfaid', 'part', part)
|
69
|
+
doc.metadata.property('pdfaid', 'conformance', conformance.upcase)
|
70
|
+
add_srgb_icc_output_intent(doc) unless doc.catalog.key?(:OutputIntents)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
SRGB_ICC = 'sRGB2014.icc' # :nodoc:
|
75
|
+
|
76
|
+
def self.add_srgb_icc_output_intent(doc) # :nodoc:
|
77
|
+
icc = doc.add({N: 3}, stream: File.binread(File.join(HexaPDF.data_dir, SRGB_ICC)))
|
78
|
+
doc.catalog[:OutputIntents] = [
|
79
|
+
doc.add({S: :GTS_PDFA1, OutputConditionIdentifier: SRGB_ICC, Info: SRGB_ICC,
|
80
|
+
RegistryName: 'https://www.color.org', DestOutputProfile: icc}),
|
81
|
+
]
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
data/lib/hexapdf/task.rb
CHANGED
@@ -136,7 +136,7 @@ module HexaPDF
|
|
136
136
|
if hash
|
137
137
|
self[:D] = hash
|
138
138
|
else
|
139
|
-
self[:D] ||= {Creator: 'HexaPDF'}
|
139
|
+
self[:D] ||= {Name: 'Default', Creator: 'HexaPDF'}
|
140
140
|
end
|
141
141
|
self[:D]
|
142
142
|
end
|
@@ -146,7 +146,7 @@ module HexaPDF
|
|
146
146
|
def perform_validation(&block) # :nodoc:
|
147
147
|
unless key?(:D)
|
148
148
|
yield('The OptionalContentProperties dictionary needs a default configuration', true)
|
149
|
-
self[:D] = {Creator: 'HexaPDF'}
|
149
|
+
self[:D] = {Name: 'Default', Creator: 'HexaPDF'}
|
150
150
|
end
|
151
151
|
super
|
152
152
|
end
|
@@ -0,0 +1,85 @@
|
|
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-2024 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/dictionary'
|
38
|
+
|
39
|
+
module HexaPDF
|
40
|
+
module Type
|
41
|
+
|
42
|
+
# Represents an output intent dictionary.
|
43
|
+
#
|
44
|
+
# Such a dictionary may be referenced from the catalog's /OutputIntents entry or from the
|
45
|
+
# /OutputIntents entry of a page object.
|
46
|
+
#
|
47
|
+
# See: PDF2.0 s14.11.5, Catalog
|
48
|
+
class OutputIntent < Dictionary
|
49
|
+
|
50
|
+
# Represents a destination output profile reference dictionary.
|
51
|
+
#
|
52
|
+
# Such a dictionary is referenced from the /DestOutputProfileRef entry of an OutputIntent
|
53
|
+
# dictionary.
|
54
|
+
#
|
55
|
+
# See: PDF2.0 s14.11.5
|
56
|
+
class DestOutputProfileRef < Dictionary
|
57
|
+
|
58
|
+
define_type :XXDestOutputProfileRef
|
59
|
+
|
60
|
+
define_field :CheckSum, type: String, version: "2.0"
|
61
|
+
define_field :ColorantTable, type: PDFArray, version: "2.0"
|
62
|
+
define_field :ICCVersion, type: String, version: "2.0"
|
63
|
+
define_field :ProfileCS, type: String, version: "2.0"
|
64
|
+
define_field :ProfileName, type: String, version: "2.0"
|
65
|
+
define_field :URLs, type: PDFArray, version: "2.0"
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
define_type :OutputIntent
|
70
|
+
|
71
|
+
define_field :Type, type: Symbol, required: false, default: type
|
72
|
+
define_field :S, type: Symbol, required: true
|
73
|
+
define_field :OutputCondition, type: String
|
74
|
+
define_field :OutputConditionIdentifier, type: String, required: true
|
75
|
+
define_field :RegistryName, type: String
|
76
|
+
define_field :Info, type: String
|
77
|
+
define_field :DestOutputProfile, type: Stream
|
78
|
+
define_field :DestOutputProfileRef, type: :XXDestOutputProfileRef, version: "2.0"
|
79
|
+
define_field :MixingHints, type: Dictionary, version: "2.0"
|
80
|
+
define_field :SpectralData, type: Dictionary, version: "2.0"
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
data/lib/hexapdf/type.rb
CHANGED
@@ -81,6 +81,7 @@ module HexaPDF
|
|
81
81
|
autoload(:OptionalContentProperties, 'hexapdf/type/optional_content_properties')
|
82
82
|
autoload(:OptionalContentConfiguration, 'hexapdf/type/optional_content_configuration')
|
83
83
|
autoload(:Metadata, 'hexapdf/type/metadata')
|
84
|
+
autoload(:OutputIntent, 'hexapdf/type/output_intent')
|
84
85
|
|
85
86
|
end
|
86
87
|
|
data/lib/hexapdf/version.rb
CHANGED
@@ -104,6 +104,13 @@ describe HexaPDF::Document::Layout::CellArgumentCollector do
|
|
104
104
|
@args[5, 6] = {e: :f}
|
105
105
|
assert_equal({key: :value, a: :c, e: :f}, @args.retrieve_arguments_for(5, 6))
|
106
106
|
end
|
107
|
+
|
108
|
+
it "deep merges the :cell keys" do
|
109
|
+
@args[] = {cell: {a: :b, c: :d}}
|
110
|
+
@args[3..7] = {cell: {a: :y, e: :f}}
|
111
|
+
@args[5, 6] = {cell: {a: :z}}
|
112
|
+
assert_equal({cell: {a: :z, c: :d, e: :f}}, @args.retrieve_arguments_for(5, 6))
|
113
|
+
end
|
107
114
|
end
|
108
115
|
end
|
109
116
|
|
@@ -229,6 +236,12 @@ describe HexaPDF::Document::Layout do
|
|
229
236
|
assert_equal(20, box.style.font_size)
|
230
237
|
|
231
238
|
box = @layout.text_box("Test", style: {font_size: 20})
|
239
|
+
assert_same(@doc.fonts.add("Times"), box.style.font)
|
240
|
+
assert_equal(20, box.style.font_size)
|
241
|
+
|
242
|
+
@layout.style(:base, font: ['Times', {variant: :bold}])
|
243
|
+
box = @layout.text_box("Test", style: {font_size: 20})
|
244
|
+
assert_same(@doc.fonts.add("Times", variant: :bold), box.style.font)
|
232
245
|
assert_equal(20, box.style.font_size)
|
233
246
|
|
234
247
|
@layout.style(:named, font_size: 20)
|
@@ -27,8 +27,8 @@ describe HexaPDF::Document::Metadata do
|
|
27
27
|
assert_equal("de", HexaPDF::Document::Metadata.new(@doc).default_language)
|
28
28
|
end
|
29
29
|
|
30
|
-
it "falls back to
|
31
|
-
assert_equal('
|
30
|
+
it "falls back to the default language if the document doesn't have a default language set" do
|
31
|
+
assert_equal('x-default', @metadata.default_language)
|
32
32
|
end
|
33
33
|
|
34
34
|
it "allows changing the default language" do
|
@@ -80,6 +80,25 @@ describe HexaPDF::Document::Metadata do
|
|
80
80
|
refute(@metadata.instance_variable_get(:@metadata)[@metadata.namespace('dc')].key?('title'))
|
81
81
|
end
|
82
82
|
|
83
|
+
describe "delete" do
|
84
|
+
it "deletes all properties" do
|
85
|
+
@metadata.delete
|
86
|
+
assert(@metadata.instance_variable_get(:@metadata).empty?)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "deletes all properties of a single namespace" do
|
90
|
+
@metadata.creator('Test')
|
91
|
+
@metadata.delete('dc')
|
92
|
+
assert_equal('Test', @metadata.creator)
|
93
|
+
refute(@metadata.instance_variable_get(:@metadata).key?(@metadata.namespace('dc')))
|
94
|
+
end
|
95
|
+
|
96
|
+
it "deletes a specific property" do
|
97
|
+
@metadata.delete('dc', 'title')
|
98
|
+
assert_nil(@metadata.title)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
83
102
|
it "allows reading and setting all info dictionary properties" do
|
84
103
|
[['title', 'dc', 'title'], ['author', 'dc', 'creator'], ['subject', 'dc', 'description'],
|
85
104
|
['keywords', 'pdf', 'Keywords'], ['creator', 'xmp', 'CreatorTool'],
|
@@ -120,6 +139,17 @@ describe HexaPDF::Document::Metadata do
|
|
120
139
|
assert_equal(:True, info[:Trapped])
|
121
140
|
end
|
122
141
|
|
142
|
+
it "omits values in the info dictionary that are not set" do
|
143
|
+
@metadata.delete('pdf', 'Trapped')
|
144
|
+
@metadata.delete('dc', 'title')
|
145
|
+
@metadata.delete('dc', 'creator')
|
146
|
+
@doc.write(StringIO.new, update_fields: false)
|
147
|
+
info = @doc.trailer.info
|
148
|
+
refute(info.key?(:Title))
|
149
|
+
refute(info.key?(:Author))
|
150
|
+
refute(info.key?(:Trapped))
|
151
|
+
end
|
152
|
+
|
123
153
|
it "uses a correctly updated modification date if set so by Document#write" do
|
124
154
|
info = @doc.trailer.info
|
125
155
|
sleep(0.1)
|
@@ -140,6 +170,23 @@ describe HexaPDF::Document::Metadata do
|
|
140
170
|
assert_equal('Subject', info[:Subject])
|
141
171
|
end
|
142
172
|
|
173
|
+
it "omits rdf:Description elements without values" do
|
174
|
+
@metadata.delete
|
175
|
+
@doc.write(StringIO.new, update_fields: false)
|
176
|
+
metadata = <<~XMP
|
177
|
+
<?xpacket begin="" id=""?>
|
178
|
+
<x:xmpmeta xmlns:x="adobe:ns:meta/">
|
179
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
180
|
+
<rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
|
181
|
+
<pdf:Producer>HexaPDF version #{HexaPDF::VERSION}</pdf:Producer>
|
182
|
+
</rdf:Description>
|
183
|
+
</rdf:RDF>
|
184
|
+
</x:xmpmeta>
|
185
|
+
<?xpacket end="r"?>
|
186
|
+
XMP
|
187
|
+
assert_equal(metadata, @doc.catalog[:Metadata].stream.sub(/(?<=id=")\w+/, ''))
|
188
|
+
end
|
189
|
+
|
143
190
|
it "writes the XMP metadata" do
|
144
191
|
title = HexaPDF::Document::Metadata::LocalizedString.new('Der Titel')
|
145
192
|
title.language = 'de'
|
@@ -147,13 +194,16 @@ describe HexaPDF::Document::Metadata do
|
|
147
194
|
@metadata.author(['Author 1', 'Author 2'])
|
148
195
|
@metadata.register_property_type('dc', 'other', 'URI')
|
149
196
|
@metadata.property('dc', 'other', 'https://test.org/example')
|
197
|
+
@metadata.property('pdfaid', 'part', 3)
|
198
|
+
@metadata.property('pdfaid', 'conformance', 'b')
|
150
199
|
@doc.write(StringIO.new, update_fields: false)
|
151
200
|
metadata = <<~XMP
|
152
201
|
<?xpacket begin="" id=""?>
|
202
|
+
<x:xmpmeta xmlns:x="adobe:ns:meta/">
|
153
203
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
154
204
|
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
155
205
|
<dc:title><rdf:Alt>
|
156
|
-
<rdf:li xml:lang="
|
206
|
+
<rdf:li xml:lang="x-default">Title</rdf:li>
|
157
207
|
<rdf:li xml:lang="de">Der Titel</rdf:li>
|
158
208
|
</rdf:Alt></dc:title>
|
159
209
|
<dc:creator><rdf:Seq>
|
@@ -161,13 +211,13 @@ describe HexaPDF::Document::Metadata do
|
|
161
211
|
<rdf:li>Author 2</rdf:li>
|
162
212
|
</rdf:Seq></dc:creator>
|
163
213
|
<dc:description><rdf:Alt>
|
164
|
-
<rdf:li xml:lang="
|
214
|
+
<rdf:li xml:lang="x-default">Subject</rdf:li>
|
165
215
|
</rdf:Alt></dc:description>
|
166
216
|
<dc:other rdf:resource="https://test.org/example" />
|
167
217
|
</rdf:Description>
|
168
218
|
<rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
|
169
219
|
<pdf:Keywords>Keywords</pdf:Keywords>
|
170
|
-
<pdf:Producer>
|
220
|
+
<pdf:Producer>HexaPDF version #{HexaPDF::VERSION}</pdf:Producer>
|
171
221
|
<pdf:Trapped>True</pdf:Trapped>
|
172
222
|
</rdf:Description>
|
173
223
|
<rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
|
@@ -175,7 +225,12 @@ describe HexaPDF::Document::Metadata do
|
|
175
225
|
<xmp:CreateDate>#{@metadata.send(:xmp_date, @time)}</xmp:CreateDate>
|
176
226
|
<xmp:ModifyDate>#{@metadata.send(:xmp_date, @time)}</xmp:ModifyDate>
|
177
227
|
</rdf:Description>
|
228
|
+
<rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
|
229
|
+
<pdfaid:part>3</pdfaid:part>
|
230
|
+
<pdfaid:conformance>b</pdfaid:conformance>
|
231
|
+
</rdf:Description>
|
178
232
|
</rdf:RDF>
|
233
|
+
</x:xmpmeta>
|
179
234
|
<?xpacket end="r"?>
|
180
235
|
XMP
|
181
236
|
assert_equal(metadata, @doc.catalog[:Metadata].stream.sub(/(?<=id=")\w+/, ''))
|
@@ -10,7 +10,8 @@ describe HexaPDF::Layout::Frame::FitResult do
|
|
10
10
|
doc = HexaPDF::Document.new(config: {'debug' => true})
|
11
11
|
canvas = doc.pages.add.canvas
|
12
12
|
box = HexaPDF::Layout::Box.create(width: 20, height: 20) {}
|
13
|
-
|
13
|
+
frame = HexaPDF::Layout::Frame.new(5, 10, 100, 150)
|
14
|
+
result = HexaPDF::Layout::Frame::FitResult.new(frame, box)
|
14
15
|
result.mask = Geom2D::Rectangle(0, 0, 20, 20)
|
15
16
|
result.x = result.y = 0
|
16
17
|
result.draw(canvas, dx: 10, dy: 15)
|
@@ -30,7 +31,8 @@ describe HexaPDF::Layout::Frame::FitResult do
|
|
30
31
|
Q
|
31
32
|
CONTENTS
|
32
33
|
ocg = doc.optional_content.ocgs.first
|
33
|
-
assert_equal([['Debug', ocg]], doc.optional_content.default_configuration[:Order])
|
34
|
+
assert_equal([['Debug', ['Page 1', ocg]]], doc.optional_content.default_configuration[:Order])
|
35
|
+
assert_match(/10,15-20x20/, ocg.name)
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
@@ -61,6 +63,11 @@ describe HexaPDF::Layout::Frame do
|
|
61
63
|
assert_equal(150, @frame.available_height)
|
62
64
|
end
|
63
65
|
|
66
|
+
it "allows access to the frame's parent boxes" do
|
67
|
+
frame = HexaPDF::Layout::Frame.new(5, 10, 100, 150, parent_boxes: [:a])
|
68
|
+
assert_equal([:a], frame.parent_boxes)
|
69
|
+
end
|
70
|
+
|
64
71
|
it "allows setting the shape of the frame on initialization" do
|
65
72
|
shape = Geom2D::Polygon([50, 10], [55, 100], [105, 100], [105, 10])
|
66
73
|
frame = HexaPDF::Layout::Frame.new(5, 10, 100, 150, shape: shape)
|
@@ -71,6 +78,27 @@ describe HexaPDF::Layout::Frame do
|
|
71
78
|
assert_equal(90, frame.available_height)
|
72
79
|
end
|
73
80
|
|
81
|
+
describe "child_frame" do
|
82
|
+
before do
|
83
|
+
@frame = HexaPDF::Layout::Frame.new(10, 10, 100, 100, parent_boxes: [:a])
|
84
|
+
end
|
85
|
+
|
86
|
+
it "duplicates the frame setting the parent boxes appropriately" do
|
87
|
+
assert_same(@frame.parent_boxes, @frame.child_frame.parent_boxes)
|
88
|
+
frame = @frame.child_frame(box: :b)
|
89
|
+
assert_equal([:a, :b], frame.parent_boxes)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "creates a new frame, optionally adding a parent box" do
|
93
|
+
shape = Geom2D::Rectangle(0, 0, 20, 20)
|
94
|
+
frame = @frame.child_frame(0, 0, 20, 20, shape: shape)
|
95
|
+
assert_same(@frame.parent_boxes, frame.parent_boxes)
|
96
|
+
assert_equal(shape, frame.shape)
|
97
|
+
frame = @frame.child_frame(0, 0, 20, 20, box: :b)
|
98
|
+
assert_equal([:a, :b], frame.parent_boxes)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
74
102
|
it "returns an appropriate width specification object" do
|
75
103
|
ws = @frame.width_specification(10)
|
76
104
|
assert_kind_of(HexaPDF::Layout::WidthFromPolygon, ws)
|
@@ -97,6 +125,7 @@ describe HexaPDF::Layout::Frame do
|
|
97
125
|
@canvas.expect(:translate, nil, pos)
|
98
126
|
fit_result = @frame.fit(@box)
|
99
127
|
refute_nil(fit_result)
|
128
|
+
assert_same(@frame, fit_result.frame)
|
100
129
|
@frame.draw(@canvas, fit_result)
|
101
130
|
assert_equal(mask, fit_result.mask.bbox.to_a)
|
102
131
|
if @frame.shape.respond_to?(:polygons)
|
@@ -31,7 +31,14 @@ describe HexaPDF::Layout::InlineBox do
|
|
31
31
|
end
|
32
32
|
|
33
33
|
describe "fit_wrapped_box" do
|
34
|
-
it "automatically fits the provided box into
|
34
|
+
it "automatically fits the provided box into the given frame" do
|
35
|
+
ibox = inline_box(HexaPDF::Document.new.layout.text("test is going good", width: 20))
|
36
|
+
ibox.fit_wrapped_box(HexaPDF::Layout::Frame.new(0, 0, 50, 50))
|
37
|
+
assert_equal(20, ibox.width)
|
38
|
+
assert_equal(45, ibox.height)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "automatically fits the provided box into a custom frame" do
|
35
42
|
ibox = inline_box(HexaPDF::Document.new.layout.text("test is going good", width: 20))
|
36
43
|
ibox.fit_wrapped_box(nil)
|
37
44
|
assert_equal(20, ibox.width)
|