hexapdf 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -1
- data/examples/019-acro_form.rb +14 -3
- data/examples/023-images.rb +30 -0
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/cli/split.rb +2 -2
- data/lib/hexapdf/configuration.rb +1 -2
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +1 -5
- data/lib/hexapdf/document.rb +6 -10
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +32 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +13 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +4 -0
- data/lib/hexapdf/tokenizer.rb +14 -8
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +11 -5
- data/lib/hexapdf/type/acro_form/form.rb +33 -7
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -0
- data/lib/hexapdf/type/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +1 -1
- data/lib/hexapdf/type/page.rb +56 -46
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +2 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/document/test_pages.rb +2 -2
- data/test/hexapdf/encryption/test_aes.rb +1 -1
- data/test/hexapdf/filter/test_predictor.rb +0 -1
- data/test/hexapdf/layout/test_box.rb +2 -1
- data/test/hexapdf/layout/test_column_box.rb +1 -1
- data/test/hexapdf/layout/test_list_box.rb +1 -1
- data/test/hexapdf/test_document.rb +2 -8
- data/test/hexapdf/test_importer.rb +13 -6
- data/test/hexapdf/test_parser.rb +17 -0
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +43 -0
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +3 -4
- data/test/hexapdf/test_writer.rb +3 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
- data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_field.rb +4 -4
- data/test/hexapdf/type/acro_form/test_form.rb +18 -0
- data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
- data/test/hexapdf/type/signature/common.rb +3 -1
- data/test/hexapdf/type/test_font_true_type.rb +20 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -1
- data/test/hexapdf/type/test_outline.rb +3 -0
- data/test/hexapdf/type/test_page.rb +67 -30
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- metadata +46 -3
@@ -73,6 +73,8 @@ module HexaPDF
|
|
73
73
|
# See: PDF1.7 s12.7.4.3
|
74
74
|
class TextField < VariableTextField
|
75
75
|
|
76
|
+
define_type :XXAcroFormField
|
77
|
+
|
76
78
|
define_field :MaxLen, type: Integer
|
77
79
|
|
78
80
|
# All inheritable dictionary fields for text fields.
|
@@ -220,7 +222,15 @@ module HexaPDF
|
|
220
222
|
current_value = field_value
|
221
223
|
appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
|
222
224
|
each_widget do |widget|
|
223
|
-
|
225
|
+
is_cached = widget.cached?(:last_value)
|
226
|
+
unless force
|
227
|
+
if is_cached && widget.cache(:last_value) == current_value
|
228
|
+
next
|
229
|
+
elsif !is_cached && widget.appearance?
|
230
|
+
widget.cache(:last_value, current_value, update: true)
|
231
|
+
next
|
232
|
+
end
|
233
|
+
end
|
224
234
|
widget.cache(:last_value, current_value, update: true)
|
225
235
|
appearance_generator_class.new(widget).create_text_appearances
|
226
236
|
end
|
@@ -228,7 +238,7 @@ module HexaPDF
|
|
228
238
|
|
229
239
|
# Updates the widgets so that they reflect the current field value.
|
230
240
|
def update_widgets
|
231
|
-
create_appearances
|
241
|
+
create_appearances(force: true)
|
232
242
|
end
|
233
243
|
|
234
244
|
private
|
@@ -224,6 +224,9 @@ module HexaPDF
|
|
224
224
|
# The kind of marker that is shown inside the widget. Can either be one of the symbols
|
225
225
|
# +:check+, +:circle+, +:cross+, +:diamond+, +:square+ or +:star+, or a one character
|
226
226
|
# string. The latter is interpreted using the ZapfDingbats font.
|
227
|
+
#
|
228
|
+
# If an empty string is set, it is treated as if +nil+ was set, i.e. it shows the default
|
229
|
+
# marker for the field type.
|
227
230
|
attr_reader :style
|
228
231
|
|
229
232
|
# The size of the marker in PDF points that is shown inside the widget. The special value
|
@@ -35,6 +35,7 @@
|
|
35
35
|
#++
|
36
36
|
|
37
37
|
require 'hexapdf/type/font_simple'
|
38
|
+
require 'hexapdf/font/true_type_wrapper'
|
38
39
|
|
39
40
|
module HexaPDF
|
40
41
|
module Type
|
@@ -45,6 +46,19 @@ module HexaPDF
|
|
45
46
|
define_field :Subtype, type: Symbol, required: true, default: :TrueType
|
46
47
|
define_field :BaseFont, type: Symbol, required: true
|
47
48
|
|
49
|
+
# Overrides the default to provide a font wrapper in case none is set and a complete TrueType
|
50
|
+
# is embedded.
|
51
|
+
#
|
52
|
+
# See: Font#font_wrapper
|
53
|
+
def font_wrapper
|
54
|
+
if (tmp = super)
|
55
|
+
tmp
|
56
|
+
elsif (font_file = self.font_file) && self[:BaseFont].to_s !~ /\A[A-Z]{6}\+/
|
57
|
+
font = HexaPDF::Font::TrueType::Font.new(StringIO.new(font_file.stream))
|
58
|
+
@font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(document, font, subset: true)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
48
62
|
private
|
49
63
|
|
50
64
|
def perform_validation
|
@@ -179,7 +179,7 @@ module HexaPDF
|
|
179
179
|
# Due to a bug in Adobe Acrobat, the Catalog may not be in an object stream if the
|
180
180
|
# document is encrypted
|
181
181
|
if obj.nil? || obj.null? || obj.gen != 0 || obj.kind_of?(Stream) || obj == encrypt_dict ||
|
182
|
-
|
182
|
+
obj.type == :Catalog ||
|
183
183
|
obj.type == :Sig || obj.type == :DocTimeStamp ||
|
184
184
|
(obj.respond_to?(:key?) && obj.key?(:ByteRange) && obj.key?(:Contents))
|
185
185
|
delete_object(objects[index])
|
@@ -220,7 +220,7 @@ module HexaPDF
|
|
220
220
|
|
221
221
|
# Returns the container with the to-be-stored objects.
|
222
222
|
def objects
|
223
|
-
@objects ||=
|
223
|
+
@objects ||=
|
224
224
|
begin
|
225
225
|
@objects = {}
|
226
226
|
parse_stream
|
data/lib/hexapdf/type/outline.rb
CHANGED
@@ -128,7 +128,7 @@ module HexaPDF
|
|
128
128
|
node, dir = first ? [first, :Next] : [last, :Prev]
|
129
129
|
node = node[dir] while node.key?(dir)
|
130
130
|
self[dir == :Next ? :Last : :First] = node
|
131
|
-
elsif !first && !last && self[:Count]
|
131
|
+
elsif !first && !last && self[:Count] && self[:Count] != 0
|
132
132
|
yield('Outline dictionary key /Count set but no items exist', true)
|
133
133
|
delete(:Count)
|
134
134
|
end
|
data/lib/hexapdf/type/page.rb
CHANGED
@@ -267,6 +267,7 @@ module HexaPDF
|
|
267
267
|
raise ArgumentError, "Page rotation has to be multiple of 90 degrees"
|
268
268
|
end
|
269
269
|
|
270
|
+
# /Rotate and therefore cw_angle is angle in clockwise orientation
|
270
271
|
cw_angle = (self[:Rotate] - angle) % 360
|
271
272
|
|
272
273
|
if flatten
|
@@ -274,27 +275,41 @@ module HexaPDF
|
|
274
275
|
return if cw_angle == 0
|
275
276
|
|
276
277
|
matrix = case cw_angle
|
277
|
-
when 90
|
278
|
-
|
279
|
-
when
|
280
|
-
HexaPDF::Content::TransformationMatrix.new(-1, 0, 0, -1)
|
281
|
-
when 270
|
282
|
-
HexaPDF::Content::TransformationMatrix.new(0, 1, -1, 0)
|
278
|
+
when 90 then Content::TransformationMatrix.new(0, -1, 1, 0, -box.bottom, box.right)
|
279
|
+
when 180 then Content::TransformationMatrix.new(-1, 0, 0, -1, box.right, box.top)
|
280
|
+
when 270 then Content::TransformationMatrix.new(0, 1, -1, 0, box.top, -box.left)
|
283
281
|
end
|
284
282
|
|
285
|
-
|
286
|
-
next unless key?(box_name)
|
287
|
-
box = self[box_name]
|
283
|
+
rotate_box = lambda do |box|
|
288
284
|
llx, lly, urx, ury = \
|
289
285
|
case cw_angle
|
290
|
-
when 90
|
291
|
-
|
292
|
-
when
|
293
|
-
[box.right, box.top, box.left, box.bottom]
|
294
|
-
when 270
|
295
|
-
[box.left, box.top, box.right, box.bottom]
|
286
|
+
when 90 then [box.right, box.bottom, box.left, box.top]
|
287
|
+
when 180 then [box.right, box.top, box.left, box.bottom]
|
288
|
+
when 270 then [box.left, box.top, box.right, box.bottom]
|
296
289
|
end
|
297
|
-
|
290
|
+
box.value.replace(matrix.evaluate(llx, lly).concat(matrix.evaluate(urx, ury)))
|
291
|
+
end
|
292
|
+
|
293
|
+
[:MediaBox, :CropBox, :BleedBox, :TrimBox, :ArtBox].each do |box_name|
|
294
|
+
next unless key?(box_name)
|
295
|
+
rotate_box.call(self[box_name])
|
296
|
+
end
|
297
|
+
|
298
|
+
each_annotation do |annot|
|
299
|
+
rotate_box.call(annot[:Rect])
|
300
|
+
if (quad_points = annot[:QuadPoints])
|
301
|
+
quad_points = quad_points.value if quad_points.respond_to?(:value)
|
302
|
+
result = []
|
303
|
+
quad_points.each_slice(2) {|x, y| result.concat(matrix.evaluate(x, y)) }
|
304
|
+
quad_points.replace(result)
|
305
|
+
end
|
306
|
+
if (appearance = annot.appearance)
|
307
|
+
appearance[:Matrix] = matrix.dup.premultiply(*appearance[:Matrix].value).to_a
|
308
|
+
end
|
309
|
+
if annot[:Subtype] == :Widget
|
310
|
+
app_ch = annot[:MK] ||= document.wrap({}, type: :XXAppearanceCharacteristics)
|
311
|
+
app_ch[:R] = (app_ch[:R] + 360 - cw_angle) % 360
|
312
|
+
end
|
298
313
|
end
|
299
314
|
|
300
315
|
before_contents = document.add({}, stream: " q #{matrix.to_a.join(' ')} cm ")
|
@@ -519,15 +534,15 @@ module HexaPDF
|
|
519
534
|
# If an annotation is a form field widget, only the widget will be deleted but not the form
|
520
535
|
# field itself.
|
521
536
|
def flatten_annotations(annotations = self[:Annots])
|
522
|
-
|
537
|
+
not_flattened = (annotations || []).to_ary
|
538
|
+
return not_flattened unless key?(:Annots)
|
523
539
|
|
524
|
-
not_flattened = annotations.to_ary
|
525
540
|
annotations = not_flattened & self[:Annots] if annotations != self[:Annots]
|
526
541
|
return not_flattened if annotations.empty?
|
527
542
|
|
528
543
|
canvas = self.canvas(type: :overlay)
|
529
|
-
canvas.save_graphics_state
|
530
544
|
if (pos = canvas.graphics_state.ctm.evaluate(0, 0)) != [0, 0]
|
545
|
+
canvas.save_graphics_state
|
531
546
|
canvas.translate(-pos[0], -pos[1])
|
532
547
|
end
|
533
548
|
|
@@ -546,36 +561,31 @@ module HexaPDF
|
|
546
561
|
|
547
562
|
rect = annotation[:Rect]
|
548
563
|
box = appearance.box
|
549
|
-
matrix = appearance[:Matrix]
|
550
|
-
|
551
|
-
# Adjust position based on matrix
|
552
|
-
pos = [rect.left - matrix[4], rect.bottom - matrix[5]]
|
553
|
-
|
554
|
-
# In case of a rotation we need to counter the default translation in #xobject by adding
|
555
|
-
# box.left and box.bottom, and then translate the origin for the rotation
|
556
|
-
angle = (-Math.atan2(matrix[2], matrix[0]) * 180 / Math::PI).to_i
|
557
|
-
case angle
|
558
|
-
when 0
|
559
|
-
# Nothing to do, no rotation
|
560
|
-
when 90
|
561
|
-
pos[0] += box.top + box.left
|
562
|
-
pos[1] += -box.left + box.bottom
|
563
|
-
when -90
|
564
|
-
pos[0] += -box.bottom + box.left
|
565
|
-
pos[1] += box.right + box.bottom
|
566
|
-
when 180, -180
|
567
|
-
pos[0] += box.right + box.left
|
568
|
-
pos[1] += box.top + box.bottom
|
569
|
-
else
|
570
|
-
not_flattened << annotation
|
571
|
-
next
|
572
|
-
end
|
573
564
|
|
574
|
-
|
575
|
-
|
565
|
+
# PDF1.7 12.5.5 algorithm
|
566
|
+
# Step a) Calculate smallest rectangle containing transformed bounding box
|
567
|
+
matrix = HexaPDF::Content::TransformationMatrix.new(*appearance[:Matrix].value)
|
568
|
+
llx, lly = matrix.evaluate(box.left, box.bottom)
|
569
|
+
ulx, uly = matrix.evaluate(box.left, box.top)
|
570
|
+
lrx, lry = matrix.evaluate(box.right, box.bottom)
|
571
|
+
left, right = [llx, ulx, lrx, lrx + (ulx - llx)].minmax
|
572
|
+
bottom, top = [lly, uly, lry, lry + (uly - lly)].minmax
|
573
|
+
|
574
|
+
# Step b) Fit calculated rectangle to annotation rectangle by translating/scaling
|
575
|
+
a = HexaPDF::Content::TransformationMatrix.new
|
576
|
+
a.translate(rect.left - left, rect.bottom - bottom)
|
577
|
+
a.scale(rect.width.fdiv(right - left), rect.height.fdiv(top - bottom))
|
578
|
+
|
579
|
+
# Step c) Premultiply form matrix - done implicitly when drawing the XObject
|
580
|
+
|
581
|
+
canvas.transform(*a) do
|
582
|
+
# Use [box.left, box.bottom] to counter default translation in #xobject since that
|
583
|
+
# is already taken care of in matrix a
|
584
|
+
canvas.xobject(appearance, at: [box.left, box.bottom])
|
585
|
+
end
|
576
586
|
to_delete << annotation
|
577
587
|
end
|
578
|
-
canvas.restore_graphics_state
|
588
|
+
canvas.restore_graphics_state unless pos == [0, 0]
|
579
589
|
|
580
590
|
to_delete.each do |annotation|
|
581
591
|
if annotation[:Subtype] == :Widget
|
data/lib/hexapdf/version.rb
CHANGED
data/lib/hexapdf/writer.rb
CHANGED
@@ -114,7 +114,7 @@ module HexaPDF
|
|
114
114
|
@document.catalog[:Version] = @document.version.to_sym
|
115
115
|
end
|
116
116
|
@document.revisions.each do |rev|
|
117
|
-
rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
|
117
|
+
rev.each_modified_object(all: true) {|obj| revision.send(:add_without_check, obj) }
|
118
118
|
end
|
119
119
|
|
120
120
|
write_revision(revision, parser.startxref_offset)
|
@@ -135,8 +135,7 @@ module HexaPDF
|
|
135
135
|
|
136
136
|
revision = @document.revisions.add
|
137
137
|
@document.revisions.all[0..-2].each do |rev|
|
138
|
-
rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
|
139
|
-
rev.reset_objects
|
138
|
+
rev.each_modified_object(delete: true) {|obj| revision.send(:add_without_check, obj) }
|
140
139
|
end
|
141
140
|
@document.revisions.merge(-2..-1)
|
142
141
|
end
|
@@ -934,6 +934,11 @@ describe HexaPDF::Content::Canvas do
|
|
934
934
|
[:restore_graphics_state]])
|
935
935
|
end
|
936
936
|
|
937
|
+
it "correctly serializes the form when no transformation is needed" do
|
938
|
+
@canvas.image(@form, at: [100, 50])
|
939
|
+
assert_operators(@page.contents, [[:paint_xobject, [:XO1]]])
|
940
|
+
end
|
941
|
+
|
937
942
|
it "doesn't do anything if the form's width or height is zero" do
|
938
943
|
@form[:BBox] = [100, 50, 100, 200]
|
939
944
|
@canvas.xobject(@form, at: [0, 0])
|
@@ -166,7 +166,7 @@ describe HexaPDF::Document::Pages do
|
|
166
166
|
it "works for a single page label entry" do
|
167
167
|
@doc.catalog[:PageLabels] = {Nums: [0, {S: :r}]}
|
168
168
|
result = @doc.pages.each_labelling_range.to_a
|
169
|
-
assert_equal([[0, 10, {S: :r}]], result.map {|s, c, l| [s, c, l.value]})
|
169
|
+
assert_equal([[0, 10, {S: :r}]], result.map {|s, c, l| [s, c, l.value] })
|
170
170
|
assert_equal(:lowercase_roman, result[0].last.numbering_style)
|
171
171
|
end
|
172
172
|
|
@@ -174,7 +174,7 @@ describe HexaPDF::Document::Pages do
|
|
174
174
|
@doc.catalog[:PageLabels] = {Nums: [0, {S: :r}, 2, {S: :d}, 7, {S: :A}]}
|
175
175
|
result = @doc.pages.each_labelling_range.to_a
|
176
176
|
assert_equal([[0, 2, {S: :r}], [2, 5, {S: :d}], [7, 3, {S: :A}]],
|
177
|
-
result.map {|s, c, l| [s, c, l.value]})
|
177
|
+
result.map {|s, c, l| [s, c, l.value] })
|
178
178
|
end
|
179
179
|
|
180
180
|
it "returns a zero or negative count for the last range if there aren't enough pages" do
|
@@ -122,7 +122,7 @@ describe HexaPDF::Encryption::AES do
|
|
122
122
|
[4, 20, 40].each do |length|
|
123
123
|
assert_raises(HexaPDF::EncryptionError) do
|
124
124
|
collector(@algorithm_class.decryption_fiber('some' * 4,
|
125
|
-
|
125
|
+
Fiber.new { 'a' * length }))
|
126
126
|
end
|
127
127
|
end
|
128
128
|
end
|
@@ -131,7 +131,8 @@ describe HexaPDF::Layout::Box do
|
|
131
131
|
assert_equal([nil, box], box.split(150, 150, nil))
|
132
132
|
end
|
133
133
|
|
134
|
-
it "can't be split if it doesn't (completely) fit as the default implementation
|
134
|
+
it "can't be split if it doesn't (completely) fit as the default implementation " \
|
135
|
+
"knows nothing about the content" do
|
135
136
|
@box.style.position = :flow # make sure we would generally be splitable
|
136
137
|
@box.fit(90, 100, nil)
|
137
138
|
assert_equal([nil, @box], @box.split(150, 150, nil))
|
@@ -11,7 +11,7 @@ describe HexaPDF::Layout::ColumnBox do
|
|
11
11
|
@text_boxes = 5.times.map do
|
12
12
|
HexaPDF::Layout::TextBox.new(items: [inline_box] * 15, style: {position: :default})
|
13
13
|
end
|
14
|
-
draw_block = lambda do |canvas,
|
14
|
+
draw_block = lambda do |canvas, _box|
|
15
15
|
canvas.move_to(0, 0).end_path
|
16
16
|
end
|
17
17
|
@fixed_size_boxes = 15.times.map { HexaPDF::Layout::Box.new(width: 20, height: 10, &draw_block) }
|
@@ -222,7 +222,7 @@ describe HexaPDF::Layout::ListBox do
|
|
222
222
|
end
|
223
223
|
|
224
224
|
it "allows drawing custom markers" do
|
225
|
-
marker = lambda do |
|
225
|
+
marker = lambda do |_doc, _list_box, _index|
|
226
226
|
HexaPDF::Layout::Box.create(width: 10, height: 10) {}
|
227
227
|
end
|
228
228
|
box = create_box(children: @fixed_size_boxes[0, 1], item_type: marker)
|
@@ -163,14 +163,8 @@ describe HexaPDF::Document do
|
|
163
163
|
refute_equal(0, obj.oid)
|
164
164
|
end
|
165
165
|
|
166
|
-
it "
|
167
|
-
|
168
|
-
end
|
169
|
-
|
170
|
-
it "fails if the given object is associated with no or the destination document" do
|
171
|
-
assert_raises(ArgumentError) { @doc.import(HexaPDF::Object.new(5)) }
|
172
|
-
obj = @doc.add(5)
|
173
|
-
assert_raises(ArgumentError) { @doc.import(obj) }
|
166
|
+
it "works if the given object is not a PDF object" do
|
167
|
+
assert_equal(5, @doc.import(5))
|
174
168
|
end
|
175
169
|
end
|
176
170
|
|
@@ -31,12 +31,12 @@ describe HexaPDF::Importer do
|
|
31
31
|
@source.pages.add
|
32
32
|
@source.pages.root[:Rotate] = 90
|
33
33
|
@dest = HexaPDF::Document.new
|
34
|
-
@importer = HexaPDF::Importer.for(
|
34
|
+
@importer = HexaPDF::Importer.for(@dest)
|
35
35
|
end
|
36
36
|
|
37
37
|
describe "::for" do
|
38
38
|
it "caches the importer" do
|
39
|
-
assert_same(@importer, HexaPDF::Importer.for(
|
39
|
+
assert_same(@importer, HexaPDF::Importer.for(@dest))
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
@@ -61,8 +61,14 @@ describe HexaPDF::Importer do
|
|
61
61
|
end
|
62
62
|
|
63
63
|
it "can import a direct object" do
|
64
|
-
|
65
|
-
|
64
|
+
assert_nil(@importer.import(nil))
|
65
|
+
assert_equal(5, @importer.import(5))
|
66
|
+
assert(@dest.object?(@importer.import({key: @obj})[:key]))
|
67
|
+
end
|
68
|
+
|
69
|
+
it "determines the source document dynamically" do
|
70
|
+
obj = @importer.import(@obj.value)
|
71
|
+
assert_equal("test", obj[:ref].value)
|
66
72
|
end
|
67
73
|
|
68
74
|
it "copies the data of the imported objects" do
|
@@ -120,10 +126,11 @@ describe HexaPDF::Importer do
|
|
120
126
|
assert_equal(90, page[:Rotate])
|
121
127
|
end
|
122
128
|
|
123
|
-
it "
|
129
|
+
it "works for importing objects from different documents" do
|
124
130
|
other_doc = HexaPDF::Document.new
|
125
131
|
other_obj = other_doc.add("test")
|
126
|
-
|
132
|
+
imported = @importer.import(other_obj)
|
133
|
+
assert_equal("test", imported.value)
|
127
134
|
end
|
128
135
|
end
|
129
136
|
end
|
data/test/hexapdf/test_parser.rb
CHANGED
@@ -54,6 +54,23 @@ describe HexaPDF::Parser do
|
|
54
54
|
@parser = HexaPDF::Parser.new(@parse_io, @document)
|
55
55
|
end
|
56
56
|
|
57
|
+
describe "linearized?" do
|
58
|
+
it "can determine whether a document is linearized" do
|
59
|
+
create_parser("%PDF-1.7\n%abcdefgh\n1 0 obj\n<</Linearized 1/H [2 4]/O 1/E 1/N 1/T 1>>\nendobj")
|
60
|
+
assert(@parser.linearized?)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "returns false if the first object is not a linearization dictionary" do
|
64
|
+
create_parser("%PDF-1.7\n%abcdefgh\n1 0 obj\n<</Length 2 0 R>>\nstream\nhallo\nendstream\nendobj")
|
65
|
+
refute(@parser.linearized?)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "returns false if there is a parse error" do
|
69
|
+
create_parser("%PDF-1.7\n%abcdefgh\n1 a obj thing")
|
70
|
+
refute(@parser.linearized?)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
57
74
|
describe "parse_indirect_object" do
|
58
75
|
it "reads indirect objects sequentially" do
|
59
76
|
object, oid, gen, stream = @parser.parse_indirect_object
|
@@ -199,6 +199,14 @@ describe HexaPDF::Revision do
|
|
199
199
|
deleted = @rev.object(6)
|
200
200
|
@rev.delete(6)
|
201
201
|
assert_equal([obj, @obj, deleted], @rev.each_modified_object.to_a)
|
202
|
+
assert_same(obj, @rev.object(3))
|
203
|
+
end
|
204
|
+
|
205
|
+
it "optionally deletes the modified objects from the revision" do
|
206
|
+
obj = @rev.object(3)
|
207
|
+
obj.value = :other
|
208
|
+
assert_equal([obj], @rev.each_modified_object(delete: true).to_a)
|
209
|
+
refute_same(obj, @rev.object(3))
|
202
210
|
end
|
203
211
|
|
204
212
|
it "ignores object and xref streams that were deleted" do
|
@@ -207,6 +215,13 @@ describe HexaPDF::Revision do
|
|
207
215
|
assert_equal([], @rev.each_modified_object.to_a)
|
208
216
|
end
|
209
217
|
|
218
|
+
it "handles object and xref streams that were added appropriately depending on the 'all' arg" do
|
219
|
+
xref = @rev.add(HexaPDF::Dictionary.new({Type: :XRef}, oid: 8))
|
220
|
+
objstm = @rev.add(HexaPDF::Dictionary.new({Type: :ObjStm}, oid: 9))
|
221
|
+
assert_equal([], @rev.each_modified_object.to_a)
|
222
|
+
assert_equal([xref, objstm], @rev.each_modified_object(all: true).to_a)
|
223
|
+
end
|
224
|
+
|
210
225
|
it "doesn't return non-modified objects" do
|
211
226
|
@rev.object(2)
|
212
227
|
assert_equal([], @rev.each_modified_object.to_a)
|
@@ -230,18 +245,4 @@ describe HexaPDF::Revision do
|
|
230
245
|
assert_equal([], @rev.each_modified_object.to_a)
|
231
246
|
end
|
232
247
|
end
|
233
|
-
|
234
|
-
describe "reset_objects" do
|
235
|
-
it "deletes loaded objects" do
|
236
|
-
@rev.object(2)
|
237
|
-
@rev.reset_objects
|
238
|
-
assert(@rev.instance_variable_get(:@objects).oids.empty?)
|
239
|
-
end
|
240
|
-
|
241
|
-
it "deletes added objects" do
|
242
|
-
@rev.add(@obj)
|
243
|
-
@rev.reset_objects
|
244
|
-
assert(@rev.instance_variable_get(:@objects).oids.empty?)
|
245
|
-
end
|
246
|
-
end
|
247
248
|
end
|
@@ -356,4 +356,47 @@ describe HexaPDF::Revisions do
|
|
356
356
|
HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
|
357
357
|
end
|
358
358
|
end
|
359
|
+
|
360
|
+
it "merges the two revisions of a linearized PDF into one" do
|
361
|
+
io = StringIO.new(<<~EOF)
|
362
|
+
%PDF-1.2
|
363
|
+
5 0 obj
|
364
|
+
<</Linearized 1>>
|
365
|
+
endobj
|
366
|
+
xref
|
367
|
+
5 1
|
368
|
+
0000000009 00000 n
|
369
|
+
trailer
|
370
|
+
<</ID[(a)(b)]/Info 1 0 R/Root 2 0 R/Size 6/Prev 394>>
|
371
|
+
%
|
372
|
+
1 0 obj
|
373
|
+
<</ModDate(D:20221205233910+01'00')/Producer(HexaPDF version 0.27.0)>>
|
374
|
+
endobj
|
375
|
+
2 0 obj
|
376
|
+
<</Type/Catalog/Pages 3 0 R>>
|
377
|
+
endobj
|
378
|
+
3 0 obj
|
379
|
+
<</Type/Pages/Kids[4 0 R]/Count 1>>
|
380
|
+
endobj
|
381
|
+
4 0 obj
|
382
|
+
<</Type/Page/MediaBox[0 0 595 842]/Parent 3 0 R/Resources<<>>>>
|
383
|
+
endobj
|
384
|
+
xref
|
385
|
+
0 5
|
386
|
+
0000000000 65535 f
|
387
|
+
0000000133 00000 n
|
388
|
+
0000000219 00000 n
|
389
|
+
0000000264 00000 n
|
390
|
+
0000000315 00000 n
|
391
|
+
trailer
|
392
|
+
<</ID[(a)(b)]/Info 1 0 R/Root 2 0 R/Size 5>>
|
393
|
+
startxref
|
394
|
+
41
|
395
|
+
%%EOF
|
396
|
+
EOF
|
397
|
+
doc = HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
|
398
|
+
assert(doc.revisions.parser.linearized?)
|
399
|
+
assert_equal(1, doc.revisions.count)
|
400
|
+
assert_same(5, doc.revisions.current.xref_section.max_oid)
|
401
|
+
end
|
359
402
|
end
|
data/test/hexapdf/test_stream.rb
CHANGED
@@ -142,7 +142,7 @@ describe HexaPDF::Stream do
|
|
142
142
|
def encoded_data(str, encoders = [])
|
143
143
|
map = @document.config['filter.map']
|
144
144
|
tmp = feeder(str)
|
145
|
-
encoders.each {|e| tmp =
|
145
|
+
encoders.each {|e| tmp = Object.const_get(map[e]).encoder(tmp) }
|
146
146
|
collector(tmp)
|
147
147
|
end
|
148
148
|
|
@@ -14,15 +14,14 @@ describe HexaPDF::Tokenizer do
|
|
14
14
|
|
15
15
|
it "handles object references" do
|
16
16
|
#HexaPDF::Reference.new(1, 0), HexaPDF::Reference.new(1, 2), 2, -1, 'R', 0, 0, 'R', -1, 0, 'R',
|
17
|
-
create_tokenizer("1 0 R +2 +15 R 2 -1 R 0 0 R -1 0 R")
|
17
|
+
create_tokenizer("1 0 R +2 +15 R 2 -1 R 0 0 R 0 10 R -1 0 R")
|
18
18
|
assert_equal(HexaPDF::Reference.new(1, 0), @tokenizer.next_token)
|
19
19
|
assert_equal(HexaPDF::Reference.new(2, 15), @tokenizer.next_token)
|
20
20
|
assert_equal(2, @tokenizer.next_token)
|
21
21
|
assert_equal(-1, @tokenizer.next_token)
|
22
22
|
assert_equal('R', @tokenizer.next_token)
|
23
|
-
|
24
|
-
|
25
|
-
assert_equal('R', @tokenizer.next_token)
|
23
|
+
assert_nil(@tokenizer.next_token)
|
24
|
+
assert_nil(@tokenizer.next_token)
|
26
25
|
assert_equal(-1, @tokenizer.next_token)
|
27
26
|
assert_equal(0, @tokenizer.next_token)
|
28
27
|
assert_equal('R', @tokenizer.next_token)
|
data/test/hexapdf/test_writer.rb
CHANGED
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
|
|
40
40
|
219
|
41
41
|
%%EOF
|
42
42
|
3 0 obj
|
43
|
-
<</Producer(HexaPDF version 0.
|
43
|
+
<</Producer(HexaPDF version 0.28.0)>>
|
44
44
|
endobj
|
45
45
|
xref
|
46
46
|
3 1
|
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
|
|
72
72
|
141
|
73
73
|
%%EOF
|
74
74
|
6 0 obj
|
75
|
-
<</Producer(HexaPDF version 0.
|
75
|
+
<</Producer(HexaPDF version 0.28.0)>>
|
76
76
|
endobj
|
77
77
|
2 0 obj
|
78
78
|
<</Length 10>>stream
|
@@ -214,7 +214,7 @@ describe HexaPDF::Writer do
|
|
214
214
|
<</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
|
215
215
|
endobj
|
216
216
|
5 0 obj
|
217
|
-
<</Producer(HexaPDF version 0.
|
217
|
+
<</Producer(HexaPDF version 0.28.0)>>
|
218
218
|
endobj
|
219
219
|
4 0 obj
|
220
220
|
<</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream
|