hexapdf 0.18.0 → 0.19.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -2
- data/lib/hexapdf/cli/command.rb +7 -1
- data/lib/hexapdf/content/canvas.rb +2 -2
- data/lib/hexapdf/content/graphics_state.rb +167 -25
- data/lib/hexapdf/dictionary_fields.rb +1 -1
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -2
- data/lib/hexapdf/layout/style.rb +2 -1
- data/lib/hexapdf/parser.rb +21 -9
- data/lib/hexapdf/task/optimize.rb +46 -3
- data/lib/hexapdf/type/font.rb +5 -0
- data/lib/hexapdf/type/font_type3.rb +20 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +8 -2
- data/test/hexapdf/content/test_graphics_state.rb +9 -1
- data/test/hexapdf/content/test_operator.rb +8 -3
- data/test/hexapdf/encryption/test_standard_security_handler.rb +8 -6
- data/test/hexapdf/layout/test_style.rb +11 -0
- data/test/hexapdf/task/test_optimize.rb +26 -0
- data/test/hexapdf/test_dictionary_fields.rb +1 -0
- data/test/hexapdf/test_parser.rb +14 -0
- data/test/hexapdf/test_writer.rb +42 -13
- data/test/hexapdf/type/test_font.rb +4 -0
- data/test/hexapdf/type/test_font_type3.rb +16 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85c063a63af9729acc10a54ef53fba69d73b75f1c06ff0e6de246df920e17dd9
|
4
|
+
data.tar.gz: dcea10d0ccfe66282c92e6bb41c1d57927d525e1618753d598a910546c8a8e82
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f1d468375c4ce336e55a09d897de9a8c95babcfa1b4441cc04a9dc712435c64906016647baa030f5695f9434d18214c30bca746f245b53a69321139f63256b9
|
7
|
+
data.tar.gz: 1f1895ca7ad46bae1bf33790d4c8daa7f72adfcc00cc26e1611b7ece64d7a5a76e7f1e06be4022bb58f5dbd84154e9846cb4feb94f727c9abe99f5b7c8b87fcf
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,47 @@
|
|
1
|
+
## 0.19.3 - 2021-12-14
|
2
|
+
|
3
|
+
### Fixed
|
4
|
+
|
5
|
+
* Handling of invalid files where the "startxref" keyword and its value are on
|
6
|
+
the same line
|
7
|
+
|
8
|
+
|
9
|
+
## 0.19.2 - 2021-12-14
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
|
13
|
+
* Set the trailer's ID field to an array of two empty strings when decrypting in
|
14
|
+
case it is missing
|
15
|
+
* Incremental writing when one of the existing revisions contains a
|
16
|
+
cross-reference stream
|
17
|
+
|
18
|
+
|
19
|
+
## 0.19.1 - 2021-12-12
|
20
|
+
|
21
|
+
### Added
|
22
|
+
|
23
|
+
* [HexaPDF::Type::FontType3#bounding_box] to fix content stream processing error
|
24
|
+
|
25
|
+
### Fixed
|
26
|
+
|
27
|
+
* Calculation of scaled font size for [HexaPDF::Content::GraphicsState] and
|
28
|
+
[HexaPDF::Layout::Style] when Type3 fonts are used
|
29
|
+
|
30
|
+
|
31
|
+
## 0.19.0 - 2021-11-24
|
32
|
+
|
33
|
+
### Added
|
34
|
+
|
35
|
+
* Page resource pruning to the optimization task
|
36
|
+
* An option for page resources pruning to the optimization options of the
|
37
|
+
`hexapdf` command
|
38
|
+
|
39
|
+
### Fixed
|
40
|
+
|
41
|
+
* Handling of invalid date strings with a minute time zone offset greater than
|
42
|
+
59
|
43
|
+
|
44
|
+
|
1
45
|
## 0.18.0 - 2021-11-04
|
2
46
|
|
3
47
|
### Added
|
@@ -6,7 +50,7 @@
|
|
6
50
|
device colors in parts other than the canvas
|
7
51
|
* [HexaPDF::Type::AcroForm::VariableTextField::create_appearance_string] for
|
8
52
|
centralized creation of appearance strings
|
9
|
-
* [HexaPDF::Object
|
53
|
+
* [HexaPDF::Object::make_direct] for making objects and all parts of them direct
|
10
54
|
instead of indirect
|
11
55
|
|
12
56
|
### Changed
|
@@ -26,7 +70,7 @@
|
|
26
70
|
dictionary are indirect objects
|
27
71
|
* [HexaPDF::Content::GraphicObject::EndpointArc] to correctly determine the
|
28
72
|
start and end points
|
29
|
-
*
|
73
|
+
* HexaPDF::Dictionary#perform_validation to correctly handle objects that
|
30
74
|
should not be indirect objects
|
31
75
|
|
32
76
|
|
data/lib/hexapdf/cli/command.rb
CHANGED
@@ -66,6 +66,7 @@ module HexaPDF
|
|
66
66
|
@out_options.xref_streams = :preserve
|
67
67
|
@out_options.streams = :preserve
|
68
68
|
@out_options.optimize_fonts = false
|
69
|
+
@out_options.prune_page_resources = false
|
69
70
|
|
70
71
|
@out_options.encryption = :preserve
|
71
72
|
@out_options.enc_user_pwd = @out_options.enc_owner_pwd = nil
|
@@ -169,6 +170,10 @@ module HexaPDF
|
|
169
170
|
"time; default: #{@out_options.compress_pages})") do |c|
|
170
171
|
@out_options.compress_pages = c
|
171
172
|
end
|
173
|
+
options.on("--[no-]prune-page-resources", "Prunes unused objects from the page resources " \
|
174
|
+
"(may take a long time; default: #{@out_options.prune_page_resources})") do |c|
|
175
|
+
@out_options.prune_page_resources = c
|
176
|
+
end
|
172
177
|
options.on("--[no-]optimize-fonts", "Optimize embedded font files; " \
|
173
178
|
"default: #{@out_options.optimize_fonts})") do |o|
|
174
179
|
@out_options.optimize_fonts = o
|
@@ -236,7 +241,8 @@ module HexaPDF
|
|
236
241
|
doc.task(:optimize, compact: @out_options.compact,
|
237
242
|
object_streams: @out_options.object_streams,
|
238
243
|
xref_streams: @out_options.xref_streams,
|
239
|
-
compress_pages: @out_options.compress_pages
|
244
|
+
compress_pages: @out_options.compress_pages,
|
245
|
+
prune_page_resources: @out_options.prune_page_resources)
|
240
246
|
if @out_options.streams != :preserve || @out_options.optimize_fonts
|
241
247
|
doc.each(only_current: false) do |obj|
|
242
248
|
optimize_stream(obj)
|
@@ -589,7 +589,7 @@ module HexaPDF
|
|
589
589
|
#
|
590
590
|
# The line cap style specifies how the ends of stroked open paths should look like.
|
591
591
|
#
|
592
|
-
# The +style+ parameter can be one of:
|
592
|
+
# The +style+ parameter can be one of (also see LineCapStyle):
|
593
593
|
#
|
594
594
|
# :butt or 0::
|
595
595
|
# Stroke is squared off at the endpoint of a path.
|
@@ -641,7 +641,7 @@ module HexaPDF
|
|
641
641
|
#
|
642
642
|
# The line join style specifies the shape that is used at the corners of stroked paths.
|
643
643
|
#
|
644
|
-
# The +style+ parameter can be one of:
|
644
|
+
# The +style+ parameter can be one of (also see LineJoinStyle):
|
645
645
|
#
|
646
646
|
# :miter or 0::
|
647
647
|
# The outer lines of the two segments continue until the meet at an angle.
|
@@ -73,7 +73,7 @@ module HexaPDF
|
|
73
73
|
end
|
74
74
|
|
75
75
|
# Defines all available line cap styles as constants. Each line cap style is an instance of
|
76
|
-
# NamedValue. For use with Content::
|
76
|
+
# NamedValue, see ::normalize. For use with e.g. Content::Canvas#line_cap_style.
|
77
77
|
#
|
78
78
|
# See: PDF1.7 s8.4.3.3
|
79
79
|
module LineCapStyle
|
@@ -95,18 +95,39 @@ module HexaPDF
|
|
95
95
|
end
|
96
96
|
|
97
97
|
# Stroke is squared off at the endpoint of a path.
|
98
|
+
#
|
99
|
+
# Specify as 0 or :butt.
|
100
|
+
#
|
101
|
+
# #>pdf-small-hide
|
102
|
+
# canvas.line_cap_style(:butt)
|
103
|
+
# canvas.line_width(10).line(50, 20, 50, 80).stroke
|
104
|
+
# canvas.stroke_color("white").line_width(1).line(50, 20, 50, 80).stroke
|
98
105
|
BUTT_CAP = NamedValue.new(:butt, 0)
|
99
106
|
|
100
107
|
# A semicircular arc is drawn at the endpoint of a path.
|
108
|
+
#
|
109
|
+
# Specify as 1 or :round.
|
110
|
+
#
|
111
|
+
# #>pdf-small-hide
|
112
|
+
# canvas.line_cap_style(:round)
|
113
|
+
# canvas.line_width(10).line(50, 20, 50, 80).stroke
|
114
|
+
# canvas.stroke_color("white").line_width(1).line(50, 20, 50, 80).stroke
|
101
115
|
ROUND_CAP = NamedValue.new(:round, 1)
|
102
116
|
|
103
117
|
# The stroke continues half the line width beyond the endpoint of a path.
|
118
|
+
#
|
119
|
+
# Specify as 2 or :projecting_square.
|
120
|
+
#
|
121
|
+
# #>pdf-small-hide
|
122
|
+
# canvas.line_cap_style(:projecting_square)
|
123
|
+
# canvas.line_width(10).line(50, 20, 50, 80).stroke
|
124
|
+
# canvas.stroke_color("white").line_width(1).line(50, 20, 50, 80).stroke
|
104
125
|
PROJECTING_SQUARE_CAP = NamedValue.new(:projecting_square, 2)
|
105
126
|
|
106
127
|
end
|
107
128
|
|
108
129
|
# Defines all available line join styles as constants. Each line join style is an instance of
|
109
|
-
# NamedValue
|
130
|
+
# NamedValue, see ::normalize For use with e.g. Content::Canvas#line_join_style.
|
110
131
|
#
|
111
132
|
# See: PDF1.7 s8.4.3.4
|
112
133
|
module LineJoinStyle
|
@@ -127,20 +148,47 @@ module HexaPDF
|
|
127
148
|
end
|
128
149
|
end
|
129
150
|
|
130
|
-
# The outer lines of the two segments continue until
|
151
|
+
# The outer lines of the two segments continue until they meet at an angle.
|
152
|
+
#
|
153
|
+
# Specify as 0 or :miter.
|
154
|
+
#
|
155
|
+
# #>pdf-small-hide
|
156
|
+
# canvas.line_join_style(:miter)
|
157
|
+
# canvas.line_width(10).
|
158
|
+
# polyline(20, 20, 50, 80, 80, 20).stroke
|
159
|
+
# canvas.stroke_color("white").line_width(1).line_join_style(:bevel).
|
160
|
+
# polyline(20, 20, 50, 80, 80, 20).stroke
|
131
161
|
MITER_JOIN = NamedValue.new(:miter, 0)
|
132
162
|
|
133
163
|
# An arc of a circle is drawn around the point where the segments meet.
|
164
|
+
#
|
165
|
+
# Specify as 1 or :round.
|
166
|
+
#
|
167
|
+
# #>pdf-small-hide
|
168
|
+
# canvas.line_join_style(:round)
|
169
|
+
# canvas.line_width(10).
|
170
|
+
# polyline(20, 20, 50, 80, 80, 20).stroke
|
171
|
+
# canvas.stroke_color("white").line_width(1).line_join_style(:bevel).
|
172
|
+
# polyline(20, 20, 50, 80, 80, 20).stroke
|
134
173
|
ROUND_JOIN = NamedValue.new(:round, 1)
|
135
174
|
|
136
|
-
# The two segments are finished with butt caps and the space between the ends is filled with
|
137
|
-
#
|
175
|
+
# The two segments are finished with butt caps and the space between the ends is filled with a
|
176
|
+
# triangle.
|
177
|
+
#
|
178
|
+
# Specify as 2 or :bevel.
|
179
|
+
#
|
180
|
+
# #>pdf-small-hide
|
181
|
+
# canvas.line_join_style(:bevel)
|
182
|
+
# canvas.line_width(10).
|
183
|
+
# polyline(20, 20, 50, 80, 80, 20).stroke
|
184
|
+
# canvas.stroke_color("white").line_width(1).line_join_style(:bevel).
|
185
|
+
# polyline(20, 20, 50, 80, 80, 20).stroke
|
138
186
|
BEVEL_JOIN = NamedValue.new(:bevel, 2)
|
139
187
|
|
140
188
|
end
|
141
189
|
|
142
|
-
# The line dash pattern defines how a line should be dashed. For use with
|
143
|
-
# Content::
|
190
|
+
# The line dash pattern defines how a line should be dashed. For use with e.g.
|
191
|
+
# Content::Canvas#line_dash_pattern.
|
144
192
|
#
|
145
193
|
# A dash pattern consists of two parts: the dash array and the dash phase. The dash array
|
146
194
|
# defines the length of alternating dashes and gaps (important: starting with dashes). And the
|
@@ -159,6 +207,12 @@ module HexaPDF
|
|
159
207
|
# See: PDF1.7 s8.4.3.6
|
160
208
|
class LineDashPattern
|
161
209
|
|
210
|
+
# :call-seq:
|
211
|
+
# LineDashPattern.normalize(line_dash_pattern) -> line_dash_pattern
|
212
|
+
# LineDashPattern.normalize(array, phase = 0) -> LineDashPattern.new(array, phase)
|
213
|
+
# LineDashPattern.normalize(number, phase = 0) -> LineDashPattern.new([number], phase)
|
214
|
+
# LineDashPattern.normalize(0) -> LineDashPattern.new
|
215
|
+
#
|
162
216
|
# Returns the arguments normalized to a valid LineDashPattern instance.
|
163
217
|
#
|
164
218
|
# If +array+ is 0, the default line dash pattern representing a solid line will be used. If it
|
@@ -206,8 +260,8 @@ module HexaPDF
|
|
206
260
|
|
207
261
|
end
|
208
262
|
|
209
|
-
# Defines all available rendering intents as constants. For use with
|
210
|
-
# Content::
|
263
|
+
# Defines all available rendering intents as constants. For use with e.g.
|
264
|
+
# Content::Canvas#rendering_intent.
|
211
265
|
#
|
212
266
|
# See: PDF1.7 s8.6.5.8
|
213
267
|
module RenderingIntent
|
@@ -241,7 +295,7 @@ module HexaPDF
|
|
241
295
|
end
|
242
296
|
|
243
297
|
# Defines all available text rendering modes as constants. Each text rendering mode is an
|
244
|
-
# instance of NamedValue. For use with Content::
|
298
|
+
# instance of NamedValue. For use with e.g. Content::Canvas#text_rendering_mode.
|
245
299
|
#
|
246
300
|
# See: PDF1.7 s9.3.6
|
247
301
|
module TextRenderingMode
|
@@ -272,28 +326,97 @@ module HexaPDF
|
|
272
326
|
end
|
273
327
|
end
|
274
328
|
|
275
|
-
# Fill text
|
329
|
+
# Fill text.
|
330
|
+
#
|
331
|
+
# Specify as 0 or :fill.
|
332
|
+
#
|
333
|
+
# #>pdf-small-hide
|
334
|
+
# canvas.font("Helvetica", size: 13)
|
335
|
+
# canvas.stroke_color("green").line_width(0.5)
|
336
|
+
# canvas.text_rendering_mode(:fill)
|
337
|
+
# canvas.text("#{canvas.text_rendering_mode.name}", at: [10, 50])
|
276
338
|
FILL = NamedValue.new(:fill, 0)
|
277
339
|
|
278
|
-
# Stroke text
|
340
|
+
# Stroke text.
|
341
|
+
#
|
342
|
+
# Specify as 1 or :stroke.
|
343
|
+
#
|
344
|
+
# #>pdf-small-hide
|
345
|
+
# canvas.font("Helvetica", size: 13)
|
346
|
+
# canvas.stroke_color("green").line_width(0.5)
|
347
|
+
# canvas.text_rendering_mode(:stroke)
|
348
|
+
# canvas.text("#{canvas.text_rendering_mode.name}", at: [10, 50])
|
279
349
|
STROKE = NamedValue.new(:stroke, 1)
|
280
350
|
|
281
|
-
# Fill, then stroke text
|
351
|
+
# Fill, then stroke text.
|
352
|
+
#
|
353
|
+
# Specify as 2 or :fill_stroke.
|
354
|
+
#
|
355
|
+
# #>pdf-small-hide
|
356
|
+
# canvas.font("Helvetica", size: 13)
|
357
|
+
# canvas.stroke_color("green").line_width(0.5)
|
358
|
+
# canvas.text_rendering_mode(:fill_stroke)
|
359
|
+
# canvas.text("#{canvas.text_rendering_mode.name}", at: [10, 50])
|
282
360
|
FILL_STROKE = NamedValue.new(:fill_stroke, 2)
|
283
361
|
|
284
|
-
# Neither fill nor stroke text (invisible)
|
362
|
+
# Neither fill nor stroke text (invisible).
|
363
|
+
#
|
364
|
+
# Specify as 3 or :invisible.
|
365
|
+
#
|
366
|
+
# #>pdf-small-hide
|
367
|
+
# canvas.font("Helvetica", size: 13)
|
368
|
+
# canvas.stroke_color("green").line_width(0.5)
|
369
|
+
# canvas.text_rendering_mode(:invisible)
|
370
|
+
# canvas.text("#{canvas.text_rendering_mode.name}", at: [10, 50])
|
371
|
+
# canvas.stroke_color("red").line_width(20).line(30, 20, 30, 80).stroke
|
285
372
|
INVISIBLE = NamedValue.new(:invisible, 3)
|
286
373
|
|
287
|
-
# Fill text and add to path for clipping
|
374
|
+
# Fill text and add to path for clipping.
|
375
|
+
#
|
376
|
+
# Specify as 4 or :fill_clip.
|
377
|
+
#
|
378
|
+
# #>pdf-small-hide
|
379
|
+
# canvas.font("Helvetica", size: 13)
|
380
|
+
# canvas.stroke_color("green").line_width(0.5)
|
381
|
+
# canvas.text_rendering_mode(:fill_clip)
|
382
|
+
# canvas.text("#{canvas.text_rendering_mode.name}", at: [10, 50])
|
383
|
+
# canvas.stroke_color("red").line_width(20).line(30, 20, 30, 80).stroke
|
288
384
|
FILL_CLIP = NamedValue.new(:fill_clip, 4)
|
289
385
|
|
290
|
-
# Stroke text and add to path for clipping
|
386
|
+
# Stroke text and add to path for clipping.
|
387
|
+
#
|
388
|
+
# Specify as 5 or :stroke_clip.
|
389
|
+
#
|
390
|
+
# #>pdf-small-hide
|
391
|
+
# canvas.font("Helvetica", size: 13)
|
392
|
+
# canvas.stroke_color("green").line_width(0.5)
|
393
|
+
# canvas.text_rendering_mode(:stroke_clip)
|
394
|
+
# canvas.text("#{canvas.text_rendering_mode.name}", at: [10, 50])
|
395
|
+
# canvas.stroke_color("red").line_width(20).line(30, 20, 30, 80).stroke
|
291
396
|
STROKE_CLIP = NamedValue.new(:stroke_clip, 5)
|
292
397
|
|
293
|
-
# Fill, then stroke text and add to path for clipping
|
398
|
+
# Fill, then stroke text and add to path for clipping.
|
399
|
+
#
|
400
|
+
# Specify as 6 or :fill_stroke_clip.
|
401
|
+
#
|
402
|
+
# #>pdf-small-hide
|
403
|
+
# canvas.font("Helvetica", size: 13)
|
404
|
+
# canvas.stroke_color("green").line_width(0.5)
|
405
|
+
# canvas.text_rendering_mode(:fill_stroke_clip)
|
406
|
+
# canvas.text("#{canvas.text_rendering_mode.name}", at: [10, 50])
|
407
|
+
# canvas.stroke_color("red").line_width(20).line(30, 20, 30, 80).stroke
|
294
408
|
FILL_STROKE_CLIP = NamedValue.new(:fill_stroke_clip, 6)
|
295
409
|
|
296
|
-
# Add text to path for clipping
|
410
|
+
# Add text to path for clipping.
|
411
|
+
#
|
412
|
+
# Specify as 7 or :clip.
|
413
|
+
#
|
414
|
+
# #>pdf-small-hide
|
415
|
+
# canvas.font("Helvetica", size: 13)
|
416
|
+
# canvas.stroke_color("green").line_width(0.5)
|
417
|
+
# canvas.text_rendering_mode(:clip)
|
418
|
+
# canvas.text("#{canvas.text_rendering_mode.name}", at: [10, 50])
|
419
|
+
# canvas.stroke_color("red").line_width(20).line(30, 20, 30, 80).stroke
|
297
420
|
CLIP = NamedValue.new(:clip, 7)
|
298
421
|
|
299
422
|
end
|
@@ -389,7 +512,7 @@ module HexaPDF
|
|
389
512
|
attr_accessor :leading
|
390
513
|
|
391
514
|
# The font for the text.
|
392
|
-
|
515
|
+
attr_reader :font
|
393
516
|
|
394
517
|
# The font size.
|
395
518
|
attr_reader :font_size
|
@@ -415,23 +538,25 @@ module HexaPDF
|
|
415
538
|
|
416
539
|
# The scaled character spacing used in glyph displacement calculations.
|
417
540
|
#
|
418
|
-
# This returns the
|
541
|
+
# This returns the character spacing multiplied by #scaled_horizontal_scaling.
|
419
542
|
#
|
420
543
|
# See PDF1.7 s9.4.4
|
421
544
|
attr_reader :scaled_character_spacing
|
422
545
|
|
423
546
|
# The scaled word spacing used in glyph displacement calculations.
|
424
547
|
#
|
425
|
-
# This returns the
|
548
|
+
# This returns the word spacing multiplied by #scaled_horizontal_scaling.
|
426
549
|
#
|
427
550
|
# See PDF1.7 s9.4.4
|
428
551
|
attr_reader :scaled_word_spacing
|
429
552
|
|
430
553
|
# The scaled font size used in glyph displacement calculations.
|
431
554
|
#
|
432
|
-
# This returns the
|
555
|
+
# This returns the font size multiplied by the scaling factor from glyph space to text space
|
556
|
+
# (0.001 for all fonts except Type3 fonts or the scaling specified in /FontMatrix for Type3
|
557
|
+
# fonts) and multiplied by #scaled_horizontal_scaling.
|
433
558
|
#
|
434
|
-
# See PDF1.7 s9.4.4
|
559
|
+
# See PDF1.7 s9.4.4, HexaPDF::Type::FontType3
|
435
560
|
attr_reader :scaled_font_size
|
436
561
|
|
437
562
|
# The scaled horizontal scaling used in glyph displacement calculations.
|
@@ -542,6 +667,15 @@ module HexaPDF
|
|
542
667
|
self.fill_color = color_space.default_color
|
543
668
|
end
|
544
669
|
|
670
|
+
##
|
671
|
+
# :attr_writer: font
|
672
|
+
#
|
673
|
+
# Sets the font and updates the glyph space to text space scaling.
|
674
|
+
def font=(font)
|
675
|
+
@font = font
|
676
|
+
update_scaled_font_size
|
677
|
+
end
|
678
|
+
|
545
679
|
##
|
546
680
|
# :attr_writer: character_spacing
|
547
681
|
#
|
@@ -566,7 +700,7 @@ module HexaPDF
|
|
566
700
|
# Sets the font size and updates the scaled font size.
|
567
701
|
def font_size=(size)
|
568
702
|
@font_size = size
|
569
|
-
|
703
|
+
update_scaled_font_size
|
570
704
|
end
|
571
705
|
|
572
706
|
##
|
@@ -579,7 +713,15 @@ module HexaPDF
|
|
579
713
|
@scaled_horizontal_scaling = scaling / 100.0
|
580
714
|
@scaled_character_spacing = @character_spacing * @scaled_horizontal_scaling
|
581
715
|
@scaled_word_spacing = @word_spacing * @scaled_horizontal_scaling
|
582
|
-
|
716
|
+
update_scaled_font_size
|
717
|
+
end
|
718
|
+
|
719
|
+
private
|
720
|
+
|
721
|
+
# Updates the cached value for the scaled font size.
|
722
|
+
def update_scaled_font_size
|
723
|
+
@scaled_font_size = @font_size * (@font&.glyph_scaling_factor || 0.001) *
|
724
|
+
@scaled_horizontal_scaling
|
583
725
|
end
|
584
726
|
|
585
727
|
end
|
@@ -293,7 +293,7 @@ module HexaPDF
|
|
293
293
|
end
|
294
294
|
|
295
295
|
# :nodoc:
|
296
|
-
DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d\d)(?:'|'(\d
|
296
|
+
DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d\d)(?:'|'([0-5]\d)'?|\z)?)?\z/n
|
297
297
|
|
298
298
|
# Checks if the given object is a string and converts into a Time object if possible.
|
299
299
|
# Otherwise returns +nil+.
|
@@ -328,8 +328,7 @@ module HexaPDF
|
|
328
328
|
raise(HexaPDF::UnsupportedEncryptionError,
|
329
329
|
"Invalid /R value for standard security handler")
|
330
330
|
elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray)
|
331
|
-
|
332
|
-
"Document ID for needed for decryption")
|
331
|
+
document.trailer[:ID] = ['', '']
|
333
332
|
end
|
334
333
|
@trailer_id_hash = trailer_id_hash
|
335
334
|
|
data/lib/hexapdf/layout/style.rb
CHANGED
@@ -1069,7 +1069,8 @@ module HexaPDF
|
|
1069
1069
|
|
1070
1070
|
# The font size scaled appropriately.
|
1071
1071
|
def scaled_font_size
|
1072
|
-
@scaled_font_size ||= calculated_font_size
|
1072
|
+
@scaled_font_size ||= calculated_font_size * font.pdf_object.glyph_scaling_factor *
|
1073
|
+
scaled_horizontal_scaling
|
1073
1074
|
end
|
1074
1075
|
|
1075
1076
|
# The character spacing scaled appropriately.
|
data/lib/hexapdf/parser.rb
CHANGED
@@ -62,9 +62,15 @@ module HexaPDF
|
|
62
62
|
@object_stream_data = {}
|
63
63
|
@reconstructed_revision = nil
|
64
64
|
@in_reconstruct_revision = false
|
65
|
+
@contains_xref_streams = false
|
65
66
|
retrieve_pdf_header_offset_and_version
|
66
67
|
end
|
67
68
|
|
69
|
+
# Returns +true+ if the PDF file contains cross-reference streams.
|
70
|
+
def contains_xref_streams?
|
71
|
+
@contains_xref_streams
|
72
|
+
end
|
73
|
+
|
68
74
|
# Loads the indirect (potentially compressed) object specified by the given cross-reference
|
69
75
|
# entry.
|
70
76
|
#
|
@@ -230,6 +236,7 @@ module HexaPDF
|
|
230
236
|
maybe_raise("Cross-reference stream doesn't contain entry for itself", pos: pos)
|
231
237
|
xref_section.add_in_use_entry(obj.oid, obj.gen, pos)
|
232
238
|
end
|
239
|
+
@contains_xref_streams = true
|
233
240
|
end
|
234
241
|
xref_section.delete(0)
|
235
242
|
[xref_section, trailer]
|
@@ -335,7 +342,8 @@ module HexaPDF
|
|
335
342
|
step_size = 1024
|
336
343
|
pos = @io.pos
|
337
344
|
eof_not_found = pos == 0
|
338
|
-
startxref_missing = false
|
345
|
+
startxref_missing = startxref_mangled = false
|
346
|
+
startxref_offset = nil
|
339
347
|
|
340
348
|
while pos != 0
|
341
349
|
@io.pos = [pos - step_size, 0].max
|
@@ -343,27 +351,31 @@ module HexaPDF
|
|
343
351
|
lines = @io.read(step_size + 40).split(/[\r\n]+/)
|
344
352
|
|
345
353
|
eof_index = lines.rindex {|l| l.strip == '%%EOF' }
|
346
|
-
|
354
|
+
if !eof_index
|
347
355
|
eof_not_found = true
|
348
|
-
|
349
|
-
|
350
|
-
|
356
|
+
elsif lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
|
357
|
+
startxref_offset = $1.to_i
|
358
|
+
startxref_mangled = true
|
359
|
+
break # we found it even if it the syntax is not entirely correct
|
360
|
+
elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
|
351
361
|
startxref_missing = true
|
352
|
-
|
362
|
+
else
|
363
|
+
startxref_offset = lines[eof_index - 1].to_i
|
364
|
+
break # we found it
|
353
365
|
end
|
354
|
-
|
355
|
-
break # we found the startxref offset
|
356
366
|
end
|
357
367
|
|
358
368
|
if eof_not_found
|
359
369
|
maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
|
360
370
|
force: !eof_index)
|
371
|
+
elsif startxref_mangled
|
372
|
+
maybe_raise("PDF file trailer keyword startxref on same line as value", pos: pos)
|
361
373
|
elsif startxref_missing
|
362
374
|
maybe_raise("PDF file trailer is missing startxref keyword", pos: pos,
|
363
375
|
force: eof_index < 2 || lines[eof_index - 2].strip != "startxref")
|
364
376
|
end
|
365
377
|
|
366
|
-
@startxref_offset =
|
378
|
+
@startxref_offset = startxref_offset
|
367
379
|
end
|
368
380
|
|
369
381
|
# Returns the reconstructed revision.
|
@@ -72,8 +72,19 @@ module HexaPDF
|
|
72
72
|
# Compresses the content streams of all pages if set to +true+. Note that this can take a
|
73
73
|
# *very* long time because each content stream has to be unfiltered, parsed, serialized
|
74
74
|
# and then filtered again.
|
75
|
+
#
|
76
|
+
# prune_page_resources::
|
77
|
+
# Removes all unused XObjects from the resources dictionaries of all pages. It is
|
78
|
+
# recommended to also set the +compact+ argument because otherwise the unused XObjects won't
|
79
|
+
# be deleted from the document.
|
80
|
+
#
|
81
|
+
# This is sometimes necessary after importing pages from other PDF files that use a single
|
82
|
+
# resources dictionary for all pages.
|
75
83
|
def self.call(doc, compact: false, object_streams: :preserve, xref_streams: :preserve,
|
76
|
-
compress_pages: false)
|
84
|
+
compress_pages: false, prune_page_resources: false)
|
85
|
+
used_refs = compress_pages(doc) if compress_pages
|
86
|
+
prune_page_resources(doc, used_refs) if prune_page_resources
|
87
|
+
|
77
88
|
if compact
|
78
89
|
compact(doc, object_streams, xref_streams)
|
79
90
|
elsif object_streams != :preserve
|
@@ -83,8 +94,6 @@ module HexaPDF
|
|
83
94
|
else
|
84
95
|
doc.each(only_current: false, &method(:delete_fields_with_defaults))
|
85
96
|
end
|
86
|
-
|
87
|
-
compress_pages(doc) if compress_pages
|
88
97
|
end
|
89
98
|
|
90
99
|
# Compacts the document by merging all revisions into one, deleting null and unused entries
|
@@ -214,12 +223,41 @@ module HexaPDF
|
|
214
223
|
|
215
224
|
# Compresses the contents of all pages by parsing and then serializing again. The HexaPDF
|
216
225
|
# serializer is already optimized for small output size so nothing else needs to be done.
|
226
|
+
#
|
227
|
+
# Returns a hash of the form key=>true where the keys are the used XObjects (for use with
|
228
|
+
# #prune_page_resources).
|
217
229
|
def self.compress_pages(doc)
|
230
|
+
used_refs = {}
|
218
231
|
doc.pages.each do |page|
|
219
232
|
processor = SerializationProcessor.new
|
220
233
|
HexaPDF::Content::Parser.parse(page.contents, processor)
|
221
234
|
page.contents = processor.result
|
222
235
|
page[:Contents].set_filter(:FlateDecode)
|
236
|
+
xobjects = page.resources[:XObject]
|
237
|
+
processor.used_references.each {|ref| used_refs[xobjects[ref]] = true }
|
238
|
+
end
|
239
|
+
used_refs
|
240
|
+
end
|
241
|
+
|
242
|
+
# Deletes all XObject entries from the resources dictionaries of all pages whose names do not
|
243
|
+
# match the keys in +used_refs+.
|
244
|
+
def self.prune_page_resources(doc, used_refs)
|
245
|
+
unless used_refs
|
246
|
+
used_refs = {}
|
247
|
+
doc.pages.each do |page|
|
248
|
+
xobjects = page.resources[:XObject]
|
249
|
+
HexaPDF::Content::Parser.parse(page.contents) do |op, operands|
|
250
|
+
used_refs[xobjects[operands[0]]] = true if op == :Do
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
doc.pages.each do |page|
|
256
|
+
xobjects = page.resources[:XObject]
|
257
|
+
xobjects.each do |key, obj|
|
258
|
+
next if used_refs[obj]
|
259
|
+
xobjects.delete(key)
|
260
|
+
end
|
223
261
|
end
|
224
262
|
end
|
225
263
|
|
@@ -228,14 +266,19 @@ module HexaPDF
|
|
228
266
|
|
229
267
|
attr_reader :result #:nodoc:
|
230
268
|
|
269
|
+
# Contains all found references
|
270
|
+
attr_reader :used_references
|
271
|
+
|
231
272
|
def initialize #:nodoc:
|
232
273
|
@result = ''.b
|
233
274
|
@serializer = HexaPDF::Serializer.new
|
275
|
+
@used_references = []
|
234
276
|
end
|
235
277
|
|
236
278
|
def process(op, operands) #:nodoc:
|
237
279
|
@result << HexaPDF::Content::Operator::DEFAULT_OPERATORS[op].
|
238
280
|
serialize(@serializer, *operands)
|
281
|
+
@used_references << operands[0] if op == :Do
|
239
282
|
end
|
240
283
|
|
241
284
|
end
|
data/lib/hexapdf/type/font.rb
CHANGED
@@ -41,6 +41,10 @@ module HexaPDF
|
|
41
41
|
|
42
42
|
# Represents a Type 3 font.
|
43
43
|
#
|
44
|
+
# Note: We assume the /FontMatrix is only used for scaling, i.e. of the form [x 0 0 +/-x 0 0].
|
45
|
+
# If it is of a different form, things won't work correctly. This will be handled once such a
|
46
|
+
# case is found.
|
47
|
+
#
|
44
48
|
# See: PDF1.7 s9.6.5
|
45
49
|
class FontType3 < FontSimple
|
46
50
|
|
@@ -51,6 +55,22 @@ module HexaPDF
|
|
51
55
|
define_field :CharProcs, type: Dictionary, required: true
|
52
56
|
define_field :Resources, type: Dictionary, version: '1.2'
|
53
57
|
|
58
|
+
# Returns the bounding box of the font.
|
59
|
+
def bounding_box
|
60
|
+
matrix = self[:FontMatrix]
|
61
|
+
bbox = self[:FontBBox].value
|
62
|
+
if matrix[3] < 0 # Some writers invert the y-axis
|
63
|
+
bbox = bbox.dup
|
64
|
+
bbox[1], bbox[3] = -bbox[3], -bbox[1]
|
65
|
+
end
|
66
|
+
bbox
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the glyph scaling factor for transforming from glyph space to text space.
|
70
|
+
def glyph_scaling_factor
|
71
|
+
self[:FontMatrix][0]
|
72
|
+
end
|
73
|
+
|
54
74
|
private
|
55
75
|
|
56
76
|
def perform_validation
|
data/lib/hexapdf/version.rb
CHANGED
data/lib/hexapdf/writer.rb
CHANGED
@@ -66,6 +66,8 @@ module HexaPDF
|
|
66
66
|
@serializer = Serializer.new
|
67
67
|
@serializer.encrypter = @document.encrypted? ? @document.security_handler : nil
|
68
68
|
@rev_size = 0
|
69
|
+
|
70
|
+
@use_xref_streams = false
|
69
71
|
end
|
70
72
|
|
71
73
|
# Writes the document to the IO object.
|
@@ -87,6 +89,7 @@ module HexaPDF
|
|
87
89
|
IO.copy_stream(@document.revisions.parser.io, @io)
|
88
90
|
|
89
91
|
@rev_size = @document.revisions.current.next_free_oid
|
92
|
+
@use_xref_streams = @document.revisions.parser.contains_xref_streams?
|
90
93
|
|
91
94
|
revision = Revision.new(@document.revisions.current.trailer)
|
92
95
|
@document.revisions.each do |rev|
|
@@ -170,10 +173,13 @@ module HexaPDF
|
|
170
173
|
end
|
171
174
|
end
|
172
175
|
|
173
|
-
if !object_streams.empty? && xref_stream.nil?
|
174
|
-
|
176
|
+
if (!object_streams.empty? || @use_xref_streams) && xref_stream.nil?
|
177
|
+
xref_stream = @document.wrap({Type: :XRef}, oid: rev.next_free_oid)
|
178
|
+
rev.add(xref_stream)
|
175
179
|
end
|
176
180
|
|
181
|
+
@use_xref_streams = true if xref_stream
|
182
|
+
|
177
183
|
[xref_stream, object_streams]
|
178
184
|
end
|
179
185
|
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'test_helper'
|
4
4
|
require 'hexapdf/content/graphics_state'
|
5
|
+
require 'ostruct'
|
5
6
|
|
6
7
|
# Dummy class used as wrapper so that constant lookup works correctly
|
7
8
|
class GraphicsStateWrapper < Minitest::Spec
|
@@ -146,6 +147,13 @@ class GraphicsStateWrapper < Minitest::Spec
|
|
146
147
|
it "fails when restoring the graphics state if the stack is empty" do
|
147
148
|
assert_raises(HexaPDF::Error) { @gs.restore }
|
148
149
|
end
|
149
|
-
end
|
150
150
|
|
151
|
+
it "uses the correct glyph to text space scaling" do
|
152
|
+
font = OpenStruct.new
|
153
|
+
font.glyph_scaling_factor = 0.002
|
154
|
+
@gs.font = font
|
155
|
+
@gs.font_size = 10
|
156
|
+
assert_equal(0.02, @gs.scaled_font_size)
|
157
|
+
end
|
158
|
+
end
|
151
159
|
end
|
@@ -4,6 +4,7 @@ require 'test_helper'
|
|
4
4
|
require 'hexapdf/content/operator'
|
5
5
|
require 'hexapdf/content/processor'
|
6
6
|
require 'hexapdf/serializer'
|
7
|
+
require 'ostruct'
|
7
8
|
|
8
9
|
describe HexaPDF::Content::Operator::BaseOperator do
|
9
10
|
before do
|
@@ -190,9 +191,11 @@ end
|
|
190
191
|
|
191
192
|
describe_operator :SetGraphicsStateParameters, :gs do
|
192
193
|
it "applies parameters from an ExtGState dictionary" do
|
194
|
+
font = OpenStruct.new
|
195
|
+
font.glyph_scaling_factor = 0.01
|
193
196
|
@processor.resources[:ExtGState] = {Name: {LW: 10, LC: 2, LJ: 2, ML: 2, D: [[3, 5], 2],
|
194
197
|
RI: 2, SA: true, BM: :Multiply, CA: 0.5, ca: 0.5,
|
195
|
-
AIS: true, TK: false, Font: [
|
198
|
+
AIS: true, TK: false, Font: [font, 10]}}
|
196
199
|
@processor.resources.define_singleton_method(:document) do
|
197
200
|
Object.new.tap {|obj| obj.define_singleton_method(:deref) {|o| o } }
|
198
201
|
end
|
@@ -210,7 +213,7 @@ describe_operator :SetGraphicsStateParameters, :gs do
|
|
210
213
|
assert_equal(0.5, gs.stroke_alpha)
|
211
214
|
assert_equal(0.5, gs.fill_alpha)
|
212
215
|
assert(gs.alpha_source)
|
213
|
-
assert_equal(
|
216
|
+
assert_equal(font, gs.font)
|
214
217
|
assert_equal(10, gs.font_size)
|
215
218
|
refute(gs.text_knockout)
|
216
219
|
end
|
@@ -448,7 +451,9 @@ describe_operator :SetFontAndSize, :Tf do
|
|
448
451
|
self[:Font] && self[:Font][name]
|
449
452
|
end
|
450
453
|
|
451
|
-
|
454
|
+
font = OpenStruct.new
|
455
|
+
font.glyph_scaling_factor = 0.01
|
456
|
+
@processor.resources[:Font] = {F1: font}
|
452
457
|
invoke(:F1, 10)
|
453
458
|
assert_equal(@processor.resources.font(:F1), @processor.graphics_state.font)
|
454
459
|
assert_equal(10, @processor.graphics_state.font_size)
|
@@ -229,19 +229,21 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
|
|
229
229
|
assert_match(/Invalid \/R/i, exp.message)
|
230
230
|
end
|
231
231
|
|
232
|
-
it "fails if the
|
232
|
+
it "fails if the supplied password is invalid" do
|
233
233
|
exp = assert_raises(HexaPDF::EncryptionError) do
|
234
|
-
@handler.set_up_decryption({Filter: :Standard, V: 2, R:
|
234
|
+
@handler.set_up_decryption({Filter: :Standard, V: 2, R: 6, U: 'a' * 48, O: 'a' * 48,
|
235
|
+
UE: 'a' * 32, OE: 'a' * 32})
|
235
236
|
end
|
236
|
-
assert_match(/
|
237
|
+
assert_match(/Invalid password/i, exp.message)
|
237
238
|
end
|
238
239
|
|
239
|
-
it "
|
240
|
+
it "assigns empty strings to the trailer's ID field if it is missing" do
|
241
|
+
refute(@document.trailer.key?(:ID))
|
240
242
|
exp = assert_raises(HexaPDF::EncryptionError) do
|
241
|
-
@handler.set_up_decryption({Filter: :Standard, V:
|
242
|
-
UE: 'a' * 32, OE: 'a' * 32})
|
243
|
+
@handler.set_up_decryption({Filter: :Standard, V: 1, R: 2, U: 'a' * 48, O: 'a' * 48, P: 15})
|
243
244
|
end
|
244
245
|
assert_match(/Invalid password/i, exp.message)
|
246
|
+
assert_equal(['', ''], @document.trailer[:ID].value)
|
245
247
|
end
|
246
248
|
|
247
249
|
describe "/Perms field checking" do
|
@@ -597,6 +597,11 @@ end
|
|
597
597
|
describe HexaPDF::Layout::Style do
|
598
598
|
before do
|
599
599
|
@style = HexaPDF::Layout::Style.new
|
600
|
+
@style.font = Object.new.tap do |obj|
|
601
|
+
obj.define_singleton_method(:pdf_object) do
|
602
|
+
Object.new.tap {|pdf| pdf.define_singleton_method(:glyph_scaling_factor) { 0.001 } }
|
603
|
+
end
|
604
|
+
end
|
600
605
|
end
|
601
606
|
|
602
607
|
it "can assign values on initialization" do
|
@@ -644,6 +649,7 @@ describe HexaPDF::Layout::Style do
|
|
644
649
|
end
|
645
650
|
|
646
651
|
it "has several simple and dynamically generated properties with default values" do
|
652
|
+
@style = HexaPDF::Layout::Style.new
|
647
653
|
assert_raises(HexaPDF::Error) { @style.font }
|
648
654
|
assert_equal(10, @style.font_size)
|
649
655
|
assert_equal(0, @style.character_spacing)
|
@@ -725,6 +731,11 @@ describe HexaPDF::Layout::Style do
|
|
725
731
|
font = Object.new
|
726
732
|
font.define_singleton_method(:scaling_factor) { 1 }
|
727
733
|
font.define_singleton_method(:wrapped_font) { wrapped_font }
|
734
|
+
font.define_singleton_method(:pdf_object) do
|
735
|
+
obj = Object.new
|
736
|
+
obj.define_singleton_method(:glyph_scaling_factor) { 0.001 }
|
737
|
+
obj
|
738
|
+
end
|
728
739
|
@style.font = font
|
729
740
|
end
|
730
741
|
|
@@ -159,4 +159,30 @@ describe HexaPDF::Task::Optimize do
|
|
159
159
|
assert_equal("10 10 m\nq\nQ\nBI\n/Name 5 ID\ndataEI\n", page.contents)
|
160
160
|
end
|
161
161
|
end
|
162
|
+
|
163
|
+
describe "prune_page_resources" do
|
164
|
+
it "removes all unused XObject references" do
|
165
|
+
[false, true].each do |compress_pages|
|
166
|
+
page1 = @doc.pages.add
|
167
|
+
page1.resources[:XObject] = {}
|
168
|
+
page1.resources[:XObject][:test] = @doc.add({})
|
169
|
+
page1.resources[:XObject][:used_on_page2] = @doc.add({})
|
170
|
+
page1.resources[:XObject][:unused] = @doc.add({})
|
171
|
+
page1.contents = "/test Do"
|
172
|
+
page2 = @doc.pages.add
|
173
|
+
page2.resources[:XObject] = {}
|
174
|
+
page2.resources[:XObject][:used_on2] = page1.resources[:XObject][:used_on_page2]
|
175
|
+
page2.resources[:XObject][:also_unused] = page1.resources[:XObject][:unused]
|
176
|
+
page2.contents = "/used_on2 Do"
|
177
|
+
|
178
|
+
@doc.task(:optimize, prune_page_resources: true, compress_pages: compress_pages)
|
179
|
+
|
180
|
+
assert(page1.resources[:XObject].key?(:test))
|
181
|
+
assert(page1.resources[:XObject].key?(:used_on_page2))
|
182
|
+
refute(page1.resources[:XObject].key?(:unused))
|
183
|
+
assert(page2.resources[:XObject].key?(:used_on2))
|
184
|
+
refute(page2.resources[:XObject].key?(:also_unused))
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
162
188
|
end
|
@@ -173,6 +173,7 @@ describe HexaPDF::DictionaryFields do
|
|
173
173
|
|
174
174
|
it "allows conversion to a Time object from a binary string" do
|
175
175
|
refute(@field.convert('test'.b, self))
|
176
|
+
refute(@field.convert('D:01211016165909+00\'64'.b, self))
|
176
177
|
|
177
178
|
[
|
178
179
|
["D:1998", [1998, 01, 01, 00, 00, 00, "-00:00"]],
|
data/test/hexapdf/test_parser.rb
CHANGED
@@ -338,6 +338,11 @@ describe HexaPDF::Parser do
|
|
338
338
|
assert_equal(5, @parser.startxref_offset)
|
339
339
|
end
|
340
340
|
|
341
|
+
it "handles the case of startxref and its value being on the same line" do
|
342
|
+
create_parser("startxref 5\n%%EOF")
|
343
|
+
assert_equal(5, @parser.startxref_offset)
|
344
|
+
end
|
345
|
+
|
341
346
|
it "fails even in big files when nothing is found" do
|
342
347
|
create_parser("\nhallo" * 5000)
|
343
348
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
|
@@ -366,6 +371,13 @@ describe HexaPDF::Parser do
|
|
366
371
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
|
367
372
|
assert_match(/end-of-file marker not found/, exp.message)
|
368
373
|
end
|
374
|
+
|
375
|
+
it "fails on strict parsing if the startxref is on the same line as its value" do
|
376
|
+
@document.config['parser.on_correctable_error'] = proc { true }
|
377
|
+
create_parser("startxref 5\n%%EOF")
|
378
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
|
379
|
+
assert_match(/startxref on same line/, exp.message)
|
380
|
+
end
|
369
381
|
end
|
370
382
|
|
371
383
|
describe "file_header_version" do
|
@@ -531,12 +543,14 @@ describe HexaPDF::Parser do
|
|
531
543
|
xref_section, trailer = @parser.load_revision(@parser.startxref_offset)
|
532
544
|
assert_equal({Test: 'now'}, trailer)
|
533
545
|
assert(xref_section[1].in_use?)
|
546
|
+
refute(@parser.contains_xref_streams?)
|
534
547
|
end
|
535
548
|
|
536
549
|
it "works for a cross-reference stream" do
|
537
550
|
xref_section, trailer = @parser.load_revision(212)
|
538
551
|
assert_equal({Size: 2}, trailer)
|
539
552
|
assert(xref_section[1].in_use?)
|
553
|
+
assert(@parser.contains_xref_streams?)
|
540
554
|
end
|
541
555
|
|
542
556
|
it "fails if another object is found instead of a cross-reference stream" do
|
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.19.3)>>
|
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.19.3)>>
|
76
76
|
endobj
|
77
77
|
2 0 obj
|
78
78
|
<</Length 10>>stream
|
@@ -103,21 +103,50 @@ describe HexaPDF::Writer do
|
|
103
103
|
assert_document_conversion(@compressed_input_io)
|
104
104
|
end
|
105
105
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
106
|
+
describe "write_incremental" do
|
107
|
+
it "writes a document in incremental mode" do
|
108
|
+
doc = HexaPDF::Document.new(io: @std_input_io)
|
109
|
+
doc.pages.add
|
110
|
+
output_io = StringIO.new
|
111
|
+
HexaPDF::Writer.write(doc, output_io, incremental: true)
|
112
|
+
assert_equal(output_io.string[0, @std_input_io.string.length], @std_input_io.string)
|
113
|
+
doc = HexaPDF::Document.new(io: output_io)
|
114
|
+
assert_equal(4, doc.revisions.size)
|
115
|
+
assert_equal(2, doc.revisions.current.each.to_a.size)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "uses an xref stream if the document already contains at least one" do
|
119
|
+
doc = HexaPDF::Document.new(io: @compressed_input_io)
|
120
|
+
doc.pages.add
|
121
|
+
output_io = StringIO.new
|
122
|
+
HexaPDF::Writer.write(doc, output_io, incremental: true)
|
123
|
+
refute_match(/^trailer/, output_io.string)
|
124
|
+
end
|
115
125
|
end
|
116
126
|
|
117
|
-
it "
|
127
|
+
it "creates an xref stream if no xref stream is in a revision but object streams are" do
|
118
128
|
document = HexaPDF::Document.new
|
119
129
|
document.add({Type: :ObjStm})
|
120
|
-
|
130
|
+
HexaPDF::Writer.new(document, StringIO.new).write
|
131
|
+
assert(:XRef, document.object(2).type)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "creates an xref stream if a previous revision had one" do
|
135
|
+
document = HexaPDF::Document.new
|
136
|
+
document.pages.add
|
137
|
+
document.revisions.add
|
138
|
+
document.pages.add
|
139
|
+
document.add({Type: :ObjStm})
|
140
|
+
document.revisions.add
|
141
|
+
document.pages.add
|
142
|
+
io = StringIO.new
|
143
|
+
HexaPDF::Writer.new(document, io).write
|
144
|
+
|
145
|
+
document = HexaPDF::Document.new(io: io)
|
146
|
+
assert_equal(3, document.revisions.count)
|
147
|
+
assert(document.revisions[0].none? {|obj| obj.type == :XRef })
|
148
|
+
assert(document.revisions[1].one? {|obj| obj.type == :XRef })
|
149
|
+
assert(document.revisions[2].one? {|obj| obj.type == :XRef })
|
121
150
|
end
|
122
151
|
|
123
152
|
it "raises an error if the class is misused and an xref section contains invalid entries" do
|
@@ -9,10 +9,25 @@ describe HexaPDF::Type::FontType3 do
|
|
9
9
|
@doc = HexaPDF::Document.new
|
10
10
|
@font = @doc.add({Type: :Font, Subtype: :Type3, Encoding: :WinAnsiEncoding,
|
11
11
|
FirstChar: 32, LastChar: 34, Widths: [600, 0, 700],
|
12
|
-
FontBBox: [0,
|
12
|
+
FontBBox: [0, 100, 100, 0], FontMatrix: [0.002, 0, 0, 0.002, 0, 0],
|
13
13
|
CharProcs: {}})
|
14
14
|
end
|
15
15
|
|
16
|
+
describe "bounding_box" do
|
17
|
+
it "returns the font's bounding box" do
|
18
|
+
assert_equal([0, 0, 100, 100], @font.bounding_box)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "inverts the y-values if necessary based on /FontMatrix" do
|
22
|
+
@font[:FontMatrix][3] *= -1
|
23
|
+
assert_equal([0, -100, 100, 0], @font.bounding_box)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns the glyph scaling factor" do
|
28
|
+
assert_equal(0.002, @font.glyph_scaling_factor)
|
29
|
+
end
|
30
|
+
|
16
31
|
describe "validation" do
|
17
32
|
it "works for valid objects" do
|
18
33
|
assert(@font.validate)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hexapdf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.19.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Leitner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cmdparse
|