hexapdf 0.18.0 → 0.19.3

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 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