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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d95ce1575c017f44b2c0f96e7e5a927b8c4f8c3adf6aa0f3a7dc983c5dfa77a8
4
- data.tar.gz: e49a23655e5ce4f4ded50c5ac0c90d7892c41bd526dbdfe72ad85cca4891098b
3
+ metadata.gz: 85c063a63af9729acc10a54ef53fba69d73b75f1c06ff0e6de246df920e17dd9
4
+ data.tar.gz: dcea10d0ccfe66282c92e6bb41c1d57927d525e1618753d598a910546c8a8e82
5
5
  SHA512:
6
- metadata.gz: 37e3b09a059bb7875c797f50f60642b080f20a081fa2540f74837ac51a8cb9c1aa5b93e6502dd9f242d178f68ea654c0619b2e55d4cb934e36badca4a5057d1c
7
- data.tar.gz: cd9fc830b8b4f5387478d7e6c32260672a9332bb2faf3aa7afcb4d71bf7c39481616f7f33d731229a6099e44dac84bbecd651c3618593f6c7ba872b9e958a3cb
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.make_direct] for making objects and all parts of them direct
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
- * [HexaPDF::Dictionary#perform_validation] to correctly handle objects that
73
+ * HexaPDF::Dictionary#perform_validation to correctly handle objects that
30
74
  should not be indirect objects
31
75
 
32
76
 
@@ -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::GraphicsState#line_cap_style.
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. For use with Content::GraphicsState#line_join_style.
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 the meet at an angle.
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
- # a triangle.
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::GraphicsState#line_dash_pattern.
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::GraphicsState#rendering_intent.
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::GraphicsState#text_rendering_mode.
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
- attr_accessor :font
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 value T_c multiplied by #scaled_horizontal_scaling.
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 value T_w multiplied by #scaled_horizontal_scaling.
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 value T_fs / 1000 multiplied by #scaled_horizontal_scaling.
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
- @scaled_font_size = size / 1000.0 * @scaled_horizontal_scaling
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
- @scaled_font_size = @font_size / 1000.0 * @scaled_horizontal_scaling
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\d)'?|\z)?)?\z/n
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
- raise(HexaPDF::EncryptionError,
332
- "Document ID for needed for decryption")
331
+ document.trailer[:ID] = ['', '']
333
332
  end
334
333
  @trailer_id_hash = trailer_id_hash
335
334
 
@@ -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 / 1000.0 * scaled_horizontal_scaling
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.
@@ -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
- unless eof_index
354
+ if !eof_index
347
355
  eof_not_found = true
348
- next
349
- end
350
- unless eof_index >= 2 && lines[eof_index - 2].strip == "startxref"
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
- next
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 = lines[eof_index - 1].to_i
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
@@ -98,6 +98,11 @@ module HexaPDF
98
98
  embedded?
99
99
  end
100
100
 
101
+ # Returns the glyph scaling factor for transforming from glyph space to text space.
102
+ def glyph_scaling_factor
103
+ 0.001
104
+ end
105
+
101
106
  private
102
107
 
103
108
  # Parses and caches the ToUnicode CMap.
@@ -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
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.18.0'
40
+ VERSION = '0.19.3'
41
41
 
42
42
  end
@@ -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
- raise HexaPDF::Error, "Cannot use object streams when there is no xref stream"
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: [:Test, 10]}}
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(:Test, gs.font)
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
- @processor.resources[:Font] = {F1: :test}
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 ID in the document's trailer is missing although it is needed" do
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: 2})
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(/Document ID/i, exp.message)
237
+ assert_match(/Invalid password/i, exp.message)
237
238
  end
238
239
 
239
- it "fails if the supplied password is invalid" do
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: 2, R: 6, U: 'a' * 48, O: 'a' * 48,
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"]],
@@ -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
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.18.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.18.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
- it "writes a document in incremental mode" do
107
- doc = HexaPDF::Document.new(io: @std_input_io)
108
- doc.pages.add
109
- output_io = StringIO.new
110
- HexaPDF::Writer.write(doc, output_io, incremental: true)
111
- assert_equal(output_io.string[0, @std_input_io.string.length], @std_input_io.string)
112
- doc = HexaPDF::Document.new(io: output_io)
113
- assert_equal(4, doc.revisions.size)
114
- assert_equal(2, doc.revisions.current.each.to_a.size)
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 "raises an error if no xref stream is in a revision but object streams are" do
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
- assert_raises(HexaPDF::Error) { HexaPDF::Writer.new(document, StringIO.new).write }
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
@@ -64,4 +64,8 @@ describe HexaPDF::Type::Font do
64
64
  assert_equal(5, @font.font_file)
65
65
  end
66
66
  end
67
+
68
+ it "returns the glyph scaling factor" do
69
+ assert_equal(0.001, @font.glyph_scaling_factor)
70
+ end
67
71
  end
@@ -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, 0, 100, 100], FontMatrix: [1, 0, 0, 1, 0, 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.18.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-04 00:00:00.000000000 Z
11
+ date: 2021-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse