hexapdf 0.20.3 → 0.21.1
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 +42 -1
- data/README.md +5 -3
- data/Rakefile +10 -1
- data/examples/018-composer.rb +10 -10
- data/examples/020-column_box.rb +57 -0
- data/lib/hexapdf/cli/batch.rb +4 -6
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +59 -0
- data/lib/hexapdf/cli/split.rb +1 -1
- data/lib/hexapdf/composer.rb +147 -53
- data/lib/hexapdf/configuration.rb +7 -3
- data/lib/hexapdf/content/canvas.rb +1 -1
- data/lib/hexapdf/content/color_space.rb +1 -1
- data/lib/hexapdf/content/operator.rb +7 -7
- data/lib/hexapdf/content/parser.rb +3 -3
- data/lib/hexapdf/content/processor.rb +9 -9
- data/lib/hexapdf/document/signatures.rb +5 -4
- data/lib/hexapdf/document.rb +7 -0
- data/lib/hexapdf/encryption/aes.rb +9 -5
- data/lib/hexapdf/font/true_type/font.rb +7 -7
- data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
- data/lib/hexapdf/font/true_type/subsetter.rb +1 -1
- data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +3 -3
- data/lib/hexapdf/font/true_type_wrapper.rb +9 -14
- data/lib/hexapdf/font/type1/font.rb +10 -12
- data/lib/hexapdf/font/type1_wrapper.rb +15 -17
- data/lib/hexapdf/layout/box.rb +12 -9
- data/lib/hexapdf/layout/column_box.rb +168 -0
- data/lib/hexapdf/layout/image_box.rb +1 -1
- data/lib/hexapdf/layout/style.rb +28 -8
- data/lib/hexapdf/layout/text_fragment.rb +10 -9
- data/lib/hexapdf/parser.rb +5 -0
- data/lib/hexapdf/tokenizer.rb +3 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +6 -4
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -2
- data/lib/hexapdf/type/acro_form/field.rb +2 -2
- data/lib/hexapdf/type/font_type0.rb +1 -1
- data/lib/hexapdf/type/font_type3.rb +1 -1
- data/lib/hexapdf/type/resources.rb +4 -4
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +1 -1
- data/lib/hexapdf/type/signature.rb +1 -1
- data/lib/hexapdf/type/trailer.rb +3 -3
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/xref_section.rb +1 -1
- data/test/hexapdf/common_tokenizer_tests.rb +5 -5
- data/test/hexapdf/content/test_graphics_state.rb +1 -0
- data/test/hexapdf/content/test_operator.rb +2 -2
- data/test/hexapdf/content/test_processor.rb +1 -1
- data/test/hexapdf/encryption/test_aes.rb +8 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +23 -29
- data/test/hexapdf/filter/test_predictor.rb +16 -20
- data/test/hexapdf/font/test_type1_wrapper.rb +5 -1
- data/test/hexapdf/font/true_type/table/common.rb +1 -1
- data/test/hexapdf/font/true_type/table/test_cmap.rb +1 -1
- data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +1 -1
- data/test/hexapdf/image_loader/test_pdf.rb +6 -8
- data/test/hexapdf/image_loader/test_png.rb +2 -2
- data/test/hexapdf/layout/test_box.rb +11 -1
- data/test/hexapdf/layout/test_style.rb +23 -0
- data/test/hexapdf/layout/test_text_fragment.rb +21 -21
- data/test/hexapdf/test_composer.rb +115 -52
- data/test/hexapdf/test_dictionary.rb +2 -2
- data/test/hexapdf/test_document.rb +11 -9
- data/test/hexapdf/test_object.rb +1 -1
- data/test/hexapdf/test_parser.rb +13 -7
- data/test/hexapdf/test_serializer.rb +20 -22
- data/test/hexapdf/test_stream.rb +7 -9
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +1 -2
- data/test/hexapdf/type/acro_form/test_choice_field.rb +1 -1
- data/test/hexapdf/type/signature/common.rb +1 -1
- data/test/hexapdf/type/test_font_type0.rb +1 -1
- data/test/hexapdf/type/test_font_type1.rb +7 -7
- data/test/hexapdf/type/test_image.rb +13 -17
- metadata +4 -2
@@ -296,7 +296,7 @@ module HexaPDF
|
|
296
296
|
spec = if first_item.match?(/\A\h{6}\z/)
|
297
297
|
first_item.scan(/../).map!(&:hex)
|
298
298
|
elsif first_item.match?(/\A\h{3}\z/)
|
299
|
-
first_item.each_char.map {|x| (x*2).hex}
|
299
|
+
first_item.each_char.map {|x| (x * 2).hex }
|
300
300
|
elsif CSS_COLOR_NAMES.key?(first_item)
|
301
301
|
CSS_COLOR_NAMES[first_item]
|
302
302
|
else
|
@@ -1036,14 +1036,14 @@ module HexaPDF
|
|
1036
1036
|
s: EndPath.new('s'),
|
1037
1037
|
f: EndPath.new('f'),
|
1038
1038
|
F: EndPath.new('F'),
|
1039
|
-
'f*'
|
1039
|
+
'f*': EndPath.new('f*'),
|
1040
1040
|
B: EndPath.new('B'),
|
1041
|
-
'B*'
|
1041
|
+
'B*': EndPath.new('B*'),
|
1042
1042
|
b: EndPath.new('b'),
|
1043
|
-
'b*'
|
1043
|
+
'b*': EndPath.new('b*'),
|
1044
1044
|
n: EndPath.new('n'),
|
1045
1045
|
W: ClipPath.new('W'),
|
1046
|
-
'W*'
|
1046
|
+
'W*': ClipPath.new('W*'),
|
1047
1047
|
|
1048
1048
|
BI: InlineImage.new,
|
1049
1049
|
|
@@ -1059,10 +1059,10 @@ module HexaPDF
|
|
1059
1059
|
Td: MoveText.new,
|
1060
1060
|
TD: MoveTextAndSetLeading.new,
|
1061
1061
|
Tm: SetTextMatrix.new,
|
1062
|
-
'T*'
|
1062
|
+
'T*': MoveTextNextLine.new,
|
1063
1063
|
Tj: ShowText.new,
|
1064
|
-
'
|
1065
|
-
'"'
|
1064
|
+
"'": MoveTextNextLineAndShowText.new,
|
1065
|
+
'"': SetSpacingMoveTextNextLineAndShowText.new,
|
1066
1066
|
TJ: ShowTextWithPositioning.new,
|
1067
1067
|
}
|
1068
1068
|
DEFAULT_OPERATORS.default_proc = proc {|h, k| h[k] = BaseOperator.new(k.to_s) }
|
@@ -94,11 +94,11 @@ module HexaPDF
|
|
94
94
|
elsif byte == 40 # (
|
95
95
|
parse_literal_string
|
96
96
|
elsif byte == 60 # <
|
97
|
-
if @string.getbyte(@ss.pos + 1)
|
98
|
-
parse_hex_string
|
99
|
-
else
|
97
|
+
if @string.getbyte(@ss.pos + 1) == 60
|
100
98
|
@ss.pos += 2
|
101
99
|
TOKEN_DICT_START
|
100
|
+
else
|
101
|
+
parse_hex_string
|
102
102
|
end
|
103
103
|
elsif byte == 62 # >
|
104
104
|
unless @string.getbyte(@ss.pos + 1) == 62
|
@@ -187,7 +187,7 @@ module HexaPDF
|
|
187
187
|
|
188
188
|
# Returns the concatenated text of the boxes.
|
189
189
|
def string
|
190
|
-
@boxes.map(&:string).join
|
190
|
+
@boxes.map(&:string).join
|
191
191
|
end
|
192
192
|
|
193
193
|
# :call-seq:
|
@@ -260,14 +260,14 @@ module HexaPDF
|
|
260
260
|
s: :close_and_stroke_path,
|
261
261
|
f: :fill_path_non_zero,
|
262
262
|
F: :fill_path_non_zero,
|
263
|
-
'f*'
|
263
|
+
'f*': :fill_path_even_odd,
|
264
264
|
B: :fill_and_stroke_path_non_zero,
|
265
|
-
'B*'
|
265
|
+
'B*': :fill_and_stroke_path_even_odd,
|
266
266
|
b: :close_fill_and_stroke_path_non_zero,
|
267
|
-
'b*'
|
267
|
+
'b*': :close_fill_and_stroke_path_even_odd,
|
268
268
|
n: :end_path,
|
269
269
|
W: :clip_path_non_zero,
|
270
|
-
'W*'
|
270
|
+
'W*': :clip_path_even_odd,
|
271
271
|
BT: :begin_text,
|
272
272
|
ET: :end_text,
|
273
273
|
Tc: :set_character_spacing,
|
@@ -280,10 +280,10 @@ module HexaPDF
|
|
280
280
|
Td: :move_text,
|
281
281
|
TD: :move_text_and_set_leading,
|
282
282
|
Tm: :set_text_matrix,
|
283
|
-
'T*'
|
283
|
+
'T*': :move_text_next_line,
|
284
284
|
Tj: :show_text,
|
285
|
-
'
|
286
|
-
'"'
|
285
|
+
"'": :move_text_next_line_and_show_text,
|
286
|
+
'"': :set_spacing_move_text_next_line_and_show_text,
|
287
287
|
TJ: :show_text_with_positioning,
|
288
288
|
d0: :set_glyph_width, # only for Type 3 fonts
|
289
289
|
d1: :set_glyph_width_and_bounding_box, # only for Type 3 fonts
|
@@ -392,7 +392,7 @@ module HexaPDF
|
|
392
392
|
data = data.each_with_object(''.b) {|obj, result| result << obj if obj.kind_of?(String) }
|
393
393
|
end
|
394
394
|
font = graphics_state.font
|
395
|
-
font.decode(data).map {|code_point| font.to_utf8(code_point) }.join
|
395
|
+
font.decode(data).map {|code_point| font.to_utf8(code_point) }.join
|
396
396
|
end
|
397
397
|
|
398
398
|
# Decodes the given text object and returns it as a CompositeBox object.
|
@@ -93,12 +93,12 @@ module HexaPDF
|
|
93
93
|
|
94
94
|
# Returns the name to be set on the /Filter key when using this signing handler.
|
95
95
|
def filter_name
|
96
|
-
:
|
96
|
+
:'Adobe.PPKLite'
|
97
97
|
end
|
98
98
|
|
99
99
|
# Returns the name to be set on the /SubFilter key when using this signing handler.
|
100
100
|
def sub_filter_name
|
101
|
-
:
|
101
|
+
:'adbe.pkcs7.detached'
|
102
102
|
end
|
103
103
|
|
104
104
|
# Sets the DocMDP permissions that should be applied to the document.
|
@@ -171,7 +171,7 @@ module HexaPDF
|
|
171
171
|
# Creates a signing handler with the given options and returns it.
|
172
172
|
#
|
173
173
|
# A signing handler name is mapped to a class via the 'signature.signing_handler'
|
174
|
-
# configuration option.
|
174
|
+
# configuration option. The default signing handler is DefaultHandler.
|
175
175
|
def handler(name: :default, **options)
|
176
176
|
handler = @document.config.constantize('signature.signing_handler', name) do
|
177
177
|
raise HexaPDF::Error, "No signing handler named '#{name}' is available"
|
@@ -183,7 +183,8 @@ module HexaPDF
|
|
183
183
|
#
|
184
184
|
# This method will add a new signature to the document and write the updated document to the
|
185
185
|
# given file or IO stream. Afterwards the document can't be modified anymore and still retain
|
186
|
-
# a correct digital signature
|
186
|
+
# a correct digital signature. To modify the signed document (e.g. for adding another
|
187
|
+
# signature) create a new document based on the given file or IO stream instead.
|
187
188
|
#
|
188
189
|
# +signature+::
|
189
190
|
# Can either be a signature object (determined via the /Type key), a signature field or
|
data/lib/hexapdf/document.rb
CHANGED
@@ -638,6 +638,9 @@ module HexaPDF
|
|
638
638
|
# of the keyword arguments (see HexaPDF::Document::Signatures#handler for details).
|
639
639
|
#
|
640
640
|
# If not changed, the default signing handler is HexaPDF::Document::Signatures::DefaultHandler.
|
641
|
+
#
|
642
|
+
# *Note*: Once signing is done the document cannot be changed anymore since it was written. If a
|
643
|
+
# document needs to be signed multiple times, it needs to be loaded again after writing.
|
641
644
|
def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
|
642
645
|
handler = signatures.handler(name: handler, **handler_options)
|
643
646
|
signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
|
@@ -714,6 +717,10 @@ module HexaPDF
|
|
714
717
|
end
|
715
718
|
end
|
716
719
|
|
720
|
+
def inspect #:nodoc:
|
721
|
+
"<#{self.class.name}:#{object_id}>"
|
722
|
+
end
|
723
|
+
|
717
724
|
end
|
718
725
|
|
719
726
|
end
|
@@ -114,10 +114,12 @@ module HexaPDF
|
|
114
114
|
#
|
115
115
|
# See: PDF1.7 s7.6.2.
|
116
116
|
def decrypt(key, data)
|
117
|
-
if data.length % BLOCK_SIZE != 0 || data.length <
|
117
|
+
if data.length % BLOCK_SIZE != 0 || data.length < BLOCK_SIZE
|
118
118
|
raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
|
119
119
|
end
|
120
|
-
|
120
|
+
iv = data.slice!(0, BLOCK_SIZE)
|
121
|
+
# Handle invalid files with missing padding
|
122
|
+
data.empty? ? data : unpad(new(key, iv, :decrypt).process(data))
|
121
123
|
end
|
122
124
|
|
123
125
|
# Returns a Fiber object that decrypts the data from the given source fiber with the
|
@@ -140,11 +142,13 @@ module HexaPDF
|
|
140
142
|
Fiber.yield(algorithm.process(new_data))
|
141
143
|
end
|
142
144
|
|
143
|
-
if data.length
|
145
|
+
if data.length % BLOCK_SIZE != 0
|
144
146
|
raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
|
147
|
+
elsif data.empty?
|
148
|
+
data # Handle invalid files with missing padding
|
149
|
+
else
|
150
|
+
unpad(algorithm.process(data))
|
145
151
|
end
|
146
|
-
|
147
|
-
unpad(algorithm.process(data))
|
148
152
|
end
|
149
153
|
end
|
150
154
|
|
@@ -123,7 +123,7 @@ module HexaPDF
|
|
123
123
|
|
124
124
|
# Returns the weight of the font.
|
125
125
|
def weight
|
126
|
-
self[:
|
126
|
+
self[:'OS/2'].weight_class || 0
|
127
127
|
end
|
128
128
|
|
129
129
|
# Returns the bounding of the font.
|
@@ -133,22 +133,22 @@ module HexaPDF
|
|
133
133
|
|
134
134
|
# Returns the cap height of the font.
|
135
135
|
def cap_height
|
136
|
-
self[:
|
136
|
+
self[:'OS/2'].cap_height
|
137
137
|
end
|
138
138
|
|
139
139
|
# Returns the x-height of the font.
|
140
140
|
def x_height
|
141
|
-
self[:
|
141
|
+
self[:'OS/2'].x_height
|
142
142
|
end
|
143
143
|
|
144
144
|
# Returns the ascender of the font.
|
145
145
|
def ascender
|
146
|
-
self[:
|
146
|
+
self[:'OS/2'].typo_ascender || self[:hhea].ascent
|
147
147
|
end
|
148
148
|
|
149
149
|
# Returns the descender of the font.
|
150
150
|
def descender
|
151
|
-
self[:
|
151
|
+
self[:'OS/2'].typo_descender || self[:hhea].descent
|
152
152
|
end
|
153
153
|
|
154
154
|
# Returns the italic angle of the font, in degrees counter-clockwise from the vertical.
|
@@ -176,12 +176,12 @@ module HexaPDF
|
|
176
176
|
|
177
177
|
# Returns the distance from the baseline to the top of the strikeout line.
|
178
178
|
def strikeout_position
|
179
|
-
self[:
|
179
|
+
self[:'OS/2'].strikeout_position
|
180
180
|
end
|
181
181
|
|
182
182
|
# Returns the stroke width for the strikeout line.
|
183
183
|
def strikeout_thickness
|
184
|
-
self[:
|
184
|
+
self[:'OS/2'].strikeout_size
|
185
185
|
end
|
186
186
|
|
187
187
|
# Returns th glyph ID of the missing glyph, i.e. 0.
|
@@ -55,7 +55,7 @@ module HexaPDF
|
|
55
55
|
'hmtx' => font[:hmtx].raw_data,
|
56
56
|
}
|
57
57
|
tables['cmap'] = font[:cmap].raw_data if font[:cmap]
|
58
|
-
tables['cvt '] = font[:
|
58
|
+
tables['cvt '] = font[:'cvt '].raw_data if font[:'cvt ']
|
59
59
|
tables['fpgm'] = font[:fpgm].raw_data if font[:fpgm]
|
60
60
|
tables['prep'] = font[:prep].raw_data if font[:prep]
|
61
61
|
Builder.build(tables)
|
@@ -99,7 +99,7 @@ module HexaPDF
|
|
99
99
|
'loca' => loca,
|
100
100
|
'hmtx' => hmtx,
|
101
101
|
}
|
102
|
-
tables['cvt '] = @font[:
|
102
|
+
tables['cvt '] = @font[:'cvt '].raw_data if @font[:'cvt ']
|
103
103
|
tables['fpgm'] = @font[:fpgm].raw_data if @font[:fpgm]
|
104
104
|
tables['prep'] = @font[:prep].raw_data if @font[:prep]
|
105
105
|
|
@@ -282,12 +282,12 @@ module HexaPDF
|
|
282
282
|
def self.mapper(end_codes, start_codes, id_deltas, id_range_offsets, glyph_indexes)
|
283
283
|
compute_glyph_id = lambda do |index, code|
|
284
284
|
offset = id_range_offsets[index]
|
285
|
-
if offset
|
285
|
+
if offset == 0
|
286
|
+
glyph_id = (code + id_deltas[index]) % 65536
|
287
|
+
else
|
286
288
|
glyph_id = glyph_indexes[offset - end_codes.length + (code - start_codes[index])]
|
287
289
|
glyph_id ||= 0 # Handle invalid subtable entries
|
288
290
|
glyph_id = (glyph_id + id_deltas[index]) % 65536 if glyph_id != 0
|
289
|
-
else
|
290
|
-
glyph_id = (code + id_deltas[index]) % 65536
|
291
291
|
end
|
292
292
|
glyph_id
|
293
293
|
end
|
@@ -126,7 +126,6 @@ module HexaPDF
|
|
126
126
|
# If +subset+ is true, the font is subset.
|
127
127
|
def initialize(document, font, pdf_object: nil, subset: true)
|
128
128
|
@wrapped_font = font
|
129
|
-
@missing_glyph_callable = document.config['font.on_missing_glyph']
|
130
129
|
|
131
130
|
@subsetter = (subset ? HexaPDF::Font::TrueType::Subsetter.new(font) : nil)
|
132
131
|
|
@@ -165,12 +164,10 @@ module HexaPDF
|
|
165
164
|
# Note: Although this method is public, it should normally not be used by application code!
|
166
165
|
def glyph(id, str = nil)
|
167
166
|
@id_to_glyph[id] ||=
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
@missing_glyph_callable.call("\u{FFFD}", font_type, @wrapped_font)
|
173
|
-
end
|
167
|
+
if id >= 0 && id < @wrapped_font[:maxp].num_glyphs
|
168
|
+
Glyph.new(@wrapped_font, id, str || (+'' << (@cmap.gid_to_code(id) || 0xFFFD)))
|
169
|
+
else
|
170
|
+
@pdf_object.document.config['font.on_missing_glyph'].call("\u{FFFD}", self)
|
174
171
|
end
|
175
172
|
end
|
176
173
|
|
@@ -178,12 +175,10 @@ module HexaPDF
|
|
178
175
|
def decode_utf8(str)
|
179
176
|
str.codepoints.map! do |c|
|
180
177
|
@codepoint_to_glyph[c] ||=
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
@missing_glyph_callable.call(+'' << c, font_type, @wrapped_font)
|
186
|
-
end
|
178
|
+
if (gid = @cmap[c])
|
179
|
+
glyph(gid, +'' << c)
|
180
|
+
else
|
181
|
+
@pdf_object.document.config['font.on_missing_glyph'].call(+'' << c, self)
|
187
182
|
end
|
188
183
|
end
|
189
184
|
end
|
@@ -250,7 +245,7 @@ module HexaPDF
|
|
250
245
|
Supplement: 0},
|
251
246
|
CIDToGIDMap: :Identity})
|
252
247
|
dict = document.add({Type: :Font, Subtype: :Type0, BaseFont: cid_font[:BaseFont],
|
253
|
-
Encoding: :
|
248
|
+
Encoding: :'Identity-H', DescendantFonts: [cid_font]})
|
254
249
|
dict.font_wrapper = self
|
255
250
|
|
256
251
|
document.register_listener(:complete_objects) do
|
@@ -89,19 +89,17 @@ module HexaPDF
|
|
89
89
|
# Returns the built-in encoding of the font.
|
90
90
|
def encoding
|
91
91
|
@encoding ||=
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
encoding.code_to_name[key] = char_metric.name
|
102
|
-
end
|
103
|
-
encoding
|
92
|
+
if @metrics.encoding_scheme == 'AdobeStandardEncoding'
|
93
|
+
Encoding.for_name(:StandardEncoding)
|
94
|
+
elsif font_name == 'ZapfDingbats' || font_name == 'Symbol'
|
95
|
+
Encoding.for_name("#{font_name}Encoding".to_sym)
|
96
|
+
else
|
97
|
+
encoding = Encoding::Base.new
|
98
|
+
@metrics.character_metrics.each do |key, char_metric|
|
99
|
+
next unless key.kind_of?(Integer) && key >= 0
|
100
|
+
encoding.code_to_name[key] = char_metric.name
|
104
101
|
end
|
102
|
+
encoding
|
105
103
|
end
|
106
104
|
end
|
107
105
|
|
@@ -119,7 +119,6 @@ module HexaPDF
|
|
119
119
|
def initialize(document, font, pdf_object: nil, custom_encoding: false)
|
120
120
|
@wrapped_font = font
|
121
121
|
@pdf_object = pdf_object || create_pdf_object(document)
|
122
|
-
@missing_glyph_callable = document.config['font.on_missing_glyph']
|
123
122
|
|
124
123
|
if pdf_object
|
125
124
|
@encoding = pdf_object.encoding
|
@@ -160,7 +159,7 @@ module HexaPDF
|
|
160
159
|
if @wrapped_font.metrics.character_metrics.key?(name)
|
161
160
|
Glyph.new(@wrapped_font, name, str)
|
162
161
|
else
|
163
|
-
@
|
162
|
+
@pdf_object.document.config['font.on_missing_glyph'].call(str, self)
|
164
163
|
end
|
165
164
|
end
|
166
165
|
end
|
@@ -212,24 +211,23 @@ module HexaPDF
|
|
212
211
|
|
213
212
|
# Creates a PDF object representing the wrapped font for the given PDF document.
|
214
213
|
def create_pdf_object(document)
|
215
|
-
fd = document.
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
214
|
+
fd = document.add({Type: :FontDescriptor,
|
215
|
+
FontName: @wrapped_font.font_name.intern,
|
216
|
+
FontWeight: @wrapped_font.weight_class,
|
217
|
+
FontBBox: @wrapped_font.bounding_box,
|
218
|
+
ItalicAngle: @wrapped_font.italic_angle || 0,
|
219
|
+
Ascent: @wrapped_font.ascender || 0,
|
220
|
+
Descent: @wrapped_font.descender || 0,
|
221
|
+
CapHeight: @wrapped_font.cap_height,
|
222
|
+
XHeight: @wrapped_font.x_height,
|
223
|
+
StemH: @wrapped_font.dominant_horizontal_stem_width,
|
224
|
+
StemV: @wrapped_font.dominant_vertical_stem_width || 0})
|
226
225
|
fd.flag(:fixed_pitch) if @wrapped_font.metrics.is_fixed_pitch
|
227
226
|
fd.flag(@wrapped_font.metrics.character_set == 'Special' ? :symbolic : :nonsymbolic)
|
228
|
-
fd.must_be_indirect = true
|
229
227
|
|
230
|
-
dict = document.
|
231
|
-
|
232
|
-
|
228
|
+
dict = document.add({Type: :Font, Subtype: :Type1,
|
229
|
+
BaseFont: @wrapped_font.font_name.intern,
|
230
|
+
FontDescriptor: fd})
|
233
231
|
dict.font_wrapper = self
|
234
232
|
|
235
233
|
document.register_listener(:complete_objects) do
|
data/lib/hexapdf/layout/box.rb
CHANGED
@@ -51,13 +51,16 @@ module HexaPDF
|
|
51
51
|
# * If width or height is set to zero, they are determined automatically during layouting.
|
52
52
|
class Box
|
53
53
|
|
54
|
-
# Creates a new Box object, using the provided block as drawing block (see ::new).
|
55
|
-
# additional keyword arguments are used for creating the box's Style object.
|
54
|
+
# Creates a new Box object, using the provided block as drawing block (see ::new).
|
56
55
|
#
|
57
56
|
# If +content_box+ is +true+, the width and height are taken to mean the content width and
|
58
57
|
# height and the style's padding and border are removed from them appropriately.
|
59
|
-
|
60
|
-
|
58
|
+
#
|
59
|
+
# The +style+ argument defines the Style object (see Style::create for details) for the box.
|
60
|
+
# Any additional keyword arguments have to be style properties and are applied to the style
|
61
|
+
# object.
|
62
|
+
def self.create(width: 0, height: 0, content_box: false, style: nil, **style_properties, &block)
|
63
|
+
style = Style.create(style).update(**style_properties)
|
61
64
|
if content_box
|
62
65
|
width += style.padding.left + style.padding.right +
|
63
66
|
style.border.width.left + style.border.width.right
|
@@ -81,12 +84,12 @@ module HexaPDF
|
|
81
84
|
# * Style#background_alpha
|
82
85
|
# * Style#padding
|
83
86
|
# * Style#border
|
84
|
-
# * Style#
|
85
|
-
# * Style#
|
87
|
+
# * Style#overlays
|
88
|
+
# * Style#underlays
|
86
89
|
attr_reader :style
|
87
90
|
|
88
91
|
# :call-seq:
|
89
|
-
# Box.new(width: 0, height: 0, style:
|
92
|
+
# Box.new(width: 0, height: 0, style: nil) {|canv, box| block} -> box
|
90
93
|
#
|
91
94
|
# Creates a new Box object with the given width and height that uses the provided block when
|
92
95
|
# it is asked to draw itself on a canvas (see #draw).
|
@@ -94,10 +97,10 @@ module HexaPDF
|
|
94
97
|
# Since the final location of the box is not known beforehand, the drawing operations inside
|
95
98
|
# the block should draw inside the rectangle (0, 0, content_width, content_height) - note that
|
96
99
|
# the width and height of the box may not be known beforehand.
|
97
|
-
def initialize(width: 0, height: 0, style:
|
100
|
+
def initialize(width: 0, height: 0, style: nil, &block)
|
98
101
|
@width = @initial_width = width
|
99
102
|
@height = @initial_height = height
|
100
|
-
@style =
|
103
|
+
@style = Style.create(style)
|
101
104
|
@draw_block = block
|
102
105
|
end
|
103
106
|
|
@@ -0,0 +1,168 @@
|
|
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-2022 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
|
+
require 'hexapdf/layout/box'
|
37
|
+
|
38
|
+
module HexaPDF
|
39
|
+
module Layout
|
40
|
+
|
41
|
+
# A ColumnBox arranges boxes in one or more columns.
|
42
|
+
#
|
43
|
+
# The number of columns as well as the size of the gap between the columns can be modified.
|
44
|
+
class ColumnBox < Box
|
45
|
+
|
46
|
+
# The child boxes of this ColumnBox.
|
47
|
+
attr_reader :children
|
48
|
+
|
49
|
+
# The number of columns.
|
50
|
+
# TODO: allow array with column widths later like [100, :*, :*]; same for gaps
|
51
|
+
attr_reader :columns
|
52
|
+
|
53
|
+
# The size of the gap between the columns.
|
54
|
+
attr_reader :gap
|
55
|
+
|
56
|
+
# Creates a new ColumnBox object for the given +children+ boxes.
|
57
|
+
def initialize(children = [], columns = 2, gap: 36, **kwargs)
|
58
|
+
super(**kwargs)
|
59
|
+
@children = children
|
60
|
+
@columns = columns
|
61
|
+
@gap = gap
|
62
|
+
end
|
63
|
+
|
64
|
+
# Fits the column box into the available space.
|
65
|
+
def fit(available_width, available_height, frame)
|
66
|
+
last_height_difference = 1_000_000
|
67
|
+
height = if style.position == :flow
|
68
|
+
frame.height
|
69
|
+
else
|
70
|
+
(@initial_height > 0 ? @initial_height : available_height) - reserved_height
|
71
|
+
end
|
72
|
+
while true
|
73
|
+
p '-'*100
|
74
|
+
@frames = []
|
75
|
+
if style.position == :flow
|
76
|
+
column_width = (frame.width - gap * (@columns - 1)).to_f / @columns
|
77
|
+
@columns.times do |col_nr|
|
78
|
+
left = (column_width + gap) * col_nr + frame.left
|
79
|
+
bottom = frame.bottom
|
80
|
+
rect = Geom2D::Polygon([left, bottom],
|
81
|
+
[left + column_width, bottom],
|
82
|
+
[left + column_width, bottom + height],
|
83
|
+
[left, bottom + height])
|
84
|
+
shape = Geom2D::Algorithms::PolygonOperation.run(frame.shape, rect, :intersection)
|
85
|
+
col_frame = Frame.new(left, bottom, column_width, height)
|
86
|
+
col_frame.shape = shape
|
87
|
+
@frames << col_frame
|
88
|
+
end
|
89
|
+
@frame_index = 0
|
90
|
+
@results = @children.map {|child_box| fit_box(child_box) }
|
91
|
+
@width = frame.width
|
92
|
+
@height = frame.height - @frames.min_by(&:y).y
|
93
|
+
else
|
94
|
+
width = (@initial_width > 0 ? @initial_width : available_width) - reserved_width
|
95
|
+
column_width = (width - gap * (@columns - 1)).to_f / @columns
|
96
|
+
@columns.times do |col_nr|
|
97
|
+
@frames << Frame.new((column_width + gap) * col_nr, 0, column_width, height)
|
98
|
+
end
|
99
|
+
@frame_index = 0
|
100
|
+
@results = @children.map {|child_box| fit_box(child_box) }
|
101
|
+
@width = width
|
102
|
+
@height = height - @frames.min_by(&:y).y
|
103
|
+
end
|
104
|
+
min_y, max_y = @frames.minmax_by(&:y).map(&:y)
|
105
|
+
p [height, @frames.map(&:y), last_height_difference, min_y, max_y]
|
106
|
+
# TOOD: @result.any?(&:empty?) only for the first run!!!! if the first run fails, we
|
107
|
+
# cannot balance the columns because there is too much content.
|
108
|
+
# TODO: another break condition is if the @results didn't change since the last run
|
109
|
+
p [:maybe_redo, min_y, max_y, height, last_height_difference]
|
110
|
+
p [@results.map {|arr| arr.all? {|r| r.status }}]
|
111
|
+
break if max_y != height && @results.all? {|arr| !arr.empty? && arr.all? {|r| r.success? }} &&
|
112
|
+
(@results.any?(&:empty?) ||
|
113
|
+
max_y - min_y >= last_height_difference ||
|
114
|
+
max_y - min_y < 0.5)
|
115
|
+
if max_y == 0 && min_y == 0
|
116
|
+
height += last_height_difference / 4.0
|
117
|
+
else
|
118
|
+
last_height_difference = max_y - min_y
|
119
|
+
height -= last_height_difference / 2.0
|
120
|
+
end
|
121
|
+
end
|
122
|
+
@results.all? {|res| res.length == 1 }
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def fit_box(box)
|
128
|
+
cur_frame = @frames[@frame_index]
|
129
|
+
fit_results = []
|
130
|
+
while cur_frame
|
131
|
+
result = cur_frame.fit(box)
|
132
|
+
if result.success?
|
133
|
+
cur_frame.remove_area(result.mask)
|
134
|
+
fit_results << result
|
135
|
+
break
|
136
|
+
elsif cur_frame.full?
|
137
|
+
@frame_index += 1
|
138
|
+
break if @frame_index == @frames.length
|
139
|
+
cur_frame = @frames[@frame_index]
|
140
|
+
else
|
141
|
+
draw_box, box = cur_frame.split(result)
|
142
|
+
if draw_box
|
143
|
+
cur_frame.remove_area(result.mask)
|
144
|
+
fit_results << result
|
145
|
+
elsif !cur_frame.find_next_region
|
146
|
+
@frame_index += 1
|
147
|
+
break if @frame_index == @frames.length
|
148
|
+
cur_frame = @frames[@frame_index]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
fit_results
|
153
|
+
end
|
154
|
+
|
155
|
+
# Draws the child boxes onto the canvas at position [x, y].
|
156
|
+
def draw_content(canvas, x, y)
|
157
|
+
x = y = 0 if style.position == :flow
|
158
|
+
@results.each do |result_boxes|
|
159
|
+
result_boxes.each do |result|
|
160
|
+
result.box.draw(canvas, x + result.x, y + result.y)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|