combine_pdf 1.0.23 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +32 -0
- data/CHANGELOG.md +41 -1
- data/README.md +10 -10
- data/combine_pdf.gemspec +1 -1
- data/lib/combine_pdf/api.rb +1 -0
- data/lib/combine_pdf/basic_writer.rb +2 -1
- data/lib/combine_pdf/decrypt.rb +2 -1
- data/lib/combine_pdf/exceptions.rb +2 -0
- data/lib/combine_pdf/filter.rb +1 -0
- data/lib/combine_pdf/fonts.rb +1 -0
- data/lib/combine_pdf/page_methods.rb +5 -4
- data/lib/combine_pdf/parser.rb +27 -24
- data/lib/combine_pdf/pdf_protected.rb +21 -19
- data/lib/combine_pdf/pdf_public.rb +12 -7
- data/lib/combine_pdf/renderer.rb +8 -7
- data/lib/combine_pdf/version.rb +3 -1
- data/lib/combine_pdf.rb +14 -13
- data/test/automated +26 -25
- data/test/combine_pdf/renderer_test.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c479622755f0a124f3da336eda5869d8591068d30bd408d03c5c6ce2689d12d
|
4
|
+
data.tar.gz: 4591c79c64670d11c9693f7edaf61ad2361e6132ba15dc2d5507bf95207f329f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c31f00b8ff30ed3fa2927cfc34953b48d152d98771b5a73c4534ea04d0c5f67321ffaac1dae2d052ad9f9458256d2c5d344f62e608f6d31bc923631922606760
|
7
|
+
data.tar.gz: 155834559ec9edd5eb4ad6f8935c12bad910918d1316f9a95963e878bf43fab3b035b2da2e5aae185188b86a5ce6261a7f0730933cb6acfe5ad23d48bd98082e
|
@@ -0,0 +1,32 @@
|
|
1
|
+
name: Main
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
tests:
|
7
|
+
name: Tests
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
strategy:
|
10
|
+
fail-fast: false
|
11
|
+
matrix:
|
12
|
+
ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
|
13
|
+
rubyopt: [""]
|
14
|
+
include:
|
15
|
+
- ruby: "3.3"
|
16
|
+
rubyopt: "--enable-frozen-string-literal --debug-frozen-string-literal"
|
17
|
+
|
18
|
+
steps:
|
19
|
+
- name: Checkout code
|
20
|
+
uses: actions/checkout@v3
|
21
|
+
|
22
|
+
- name: Setup Ruby
|
23
|
+
uses: ruby/setup-ruby@v1
|
24
|
+
with:
|
25
|
+
ruby-version: ${{ matrix.ruby }}
|
26
|
+
bundler-cache: true
|
27
|
+
|
28
|
+
- name: Generate lockfile
|
29
|
+
run: bundle lock
|
30
|
+
|
31
|
+
- name: Run tests
|
32
|
+
run: bundle exec rake test RUBYOPT="${{ matrix.rubyopt }}"
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,46 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
#### Change log v.1.0.
|
3
|
+
#### Change log v.1.0.31 (2025-04-08)
|
4
|
+
|
5
|
+
**Fix**: RangeError: index out of range errors occurred with some malformed PDFs, when the number of bytes in a PDF `stream` didn't match the number of bytes expected according to the `Length` property. Credit to @Laykou and others for opening multiple issues (i.e., #205), as well as @julitrows, @mtwzim, and @Kaiito630, for pushing on this.
|
6
|
+
|
7
|
+
**Fix**: frozen string literal lingering issues. Credit to @pauline-koch, @qdegraeve, @isaporto, @ncreuschling, @francescob, @@anthonykaufman and @ma-matsui for their input on this issue. Credit to @anthonykaufman for offering one possible solution and @Markus-Munk-Shipmondo for pushing on this. Credit to @mfazekas for opening PR #215 and for @RBIII, @juliolinarez, and @osvaldoalvaradodev for supporting it.
|
8
|
+
|
9
|
+
**Fix**: possible permission issues. Credit to @davidwessman, @visini, @sander-deryckere, and @LindseySaari for exploring this.
|
10
|
+
|
11
|
+
**Fix**: calling CombinePDF.parse with a frozen string literal. Credit to @lovro-bikic for offering one possible solution.
|
12
|
+
|
13
|
+
**Fix**: Ruby 3.4 warning. Credit to @chaadow for offering one possible solution.
|
14
|
+
|
15
|
+
#### Change log v.1.0.29 (2024-12-07)
|
16
|
+
|
17
|
+
**Fix**: frozen string literal support fix. Credit to @francescob (Francesco) for PR #245.
|
18
|
+
|
19
|
+
#### Change log v.1.0.28 (2024-11-12)
|
20
|
+
|
21
|
+
**Fix**: use `require` to load code (instead of `load`). Credit to @casperisfine (Jean byroot Boussier) for PR #216.
|
22
|
+
|
23
|
+
#### Change log v.1.0.27 (2024-11-10)
|
24
|
+
|
25
|
+
**Performance**: fix performance issues with `object_id` usage in Ruby 3+. Credit to @amomchilov (Alexander Momchilov) for PR #241.
|
26
|
+
|
27
|
+
**Performance**: use frozen string literals. Credit to @casperisfine (Jean byroot Boussier) for PR #239.
|
28
|
+
|
29
|
+
#### Change log v.1.0.26 (2023-12-22)
|
30
|
+
|
31
|
+
**Performance**: possible performance bump. Credit to @denislavski (Denislav Naydenov) for opening PR #235.
|
32
|
+
|
33
|
+
#### Change log v.1.0.25 (2023-12-19)
|
34
|
+
|
35
|
+
**Fix**: possible improve memory usage. Credit to @denislavski (Denislav Naydenov) for opening PR #233 and suggesting this change.
|
36
|
+
|
37
|
+
#### Change log v.1.0.24 (2023-10-19)
|
38
|
+
|
39
|
+
**Fix**: possible `nil` in loop. Credit to @jkowens for PR #231 and adding a quick fix using a simple guard.
|
40
|
+
|
41
|
+
**Fix**: preserve file creation date metadata where relevant.
|
42
|
+
|
43
|
+
#### Change log v.1.0.23 (2023-04-04)
|
4
44
|
|
5
45
|
**Feature**: merged PR #177 for the `raise_on_encrypted: true` option support. Credit to @leviwilson and @kimyu92 for the PR.
|
6
46
|
|
data/README.md
CHANGED
@@ -7,23 +7,23 @@
|
|
7
7
|
|
8
8
|
CombinePDF is a nifty model, written in pure Ruby, to parse PDF files and combine (merge) them with other PDF files, watermark them or stamp them (all using the PDF file format and pure Ruby code).
|
9
9
|
|
10
|
-
##
|
10
|
+
## Unmaintained - Help Wanted(!)
|
11
11
|
|
12
|
-
|
12
|
+
I decided to stop maintaining this gem and hope someone could take over the PR reviews and maintenance of this gem (or simply open a successful fork).
|
13
13
|
|
14
|
-
|
15
|
-
gem install combine_pdf
|
16
|
-
```
|
14
|
+
I wrote this gem because I needed to solve an issue with bates-numbering existing PDF documents.
|
17
15
|
|
18
|
-
|
16
|
+
However, since 2014 I have been maintaining the gem for free and for no reason at all, except that I enjoyed sharing it with the community.
|
19
17
|
|
20
|
-
I
|
18
|
+
I love this gem, but I cannot keep maintaining it as I have my own projects to focus own and I need both the time and (more importantly) the mindspace.
|
21
19
|
|
22
|
-
|
20
|
+
## Install
|
23
21
|
|
24
|
-
|
22
|
+
Install with ruby gems:
|
25
23
|
|
26
|
-
|
24
|
+
```ruby
|
25
|
+
gem install combine_pdf
|
26
|
+
```
|
27
27
|
|
28
28
|
## Known Limitations
|
29
29
|
|
data/combine_pdf.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.name = "combine_pdf"
|
8
8
|
spec.version = CombinePDF::VERSION
|
9
9
|
spec.authors = ["Boaz Segev"]
|
10
|
-
spec.email = ["
|
10
|
+
spec.email = ["bo@bowild.com"]
|
11
11
|
spec.summary = %q{Combine, stamp and watermark PDF files in pure Ruby.}
|
12
12
|
spec.description = %q{A nifty gem, in pure Ruby, to parse PDF files and combine (merge) them with other PDF files, number the pages, watermark them or stamp them, create tables, add basic text objects etc` (all using the PDF file format).}
|
13
13
|
spec.homepage = "https://github.com/boazsegev/combine_pdf"
|
data/lib/combine_pdf/api.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
########################################################
|
3
4
|
## Thoughts from reading the ISO 32000-1:2008
|
4
5
|
## this file is part of the CombinePDF library and the code
|
@@ -35,7 +36,7 @@ module CombinePDF
|
|
35
36
|
# mediabox:: the PDF page size in PDF points. defaults to [0, 0, 612.0, 792.0] (US Letter)
|
36
37
|
def initialize(mediabox = [0, 0, 612.0, 792.0])
|
37
38
|
# indirect_reference_id, :indirect_generation_number
|
38
|
-
@contents =
|
39
|
+
@contents = String.new
|
39
40
|
@base_font_name = 'Writer' + SecureRandom.hex(7) + 'PDF'
|
40
41
|
self[:Type] = :Page
|
41
42
|
self[:indirect_reference_id] = 0
|
data/lib/combine_pdf/decrypt.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
########################################################
|
3
4
|
## Thoughts from reading the ISO 32000-1:2008
|
4
5
|
## this file is part of the CombinePDF library and the code
|
@@ -137,7 +138,7 @@ module CombinePDF
|
|
137
138
|
object_key = @key.dup
|
138
139
|
object_key << [encrypted_id].pack('i')[0..2]
|
139
140
|
object_key << [encrypted_generation].pack('i')[0..1]
|
140
|
-
object_key << 'sAlT'.
|
141
|
+
object_key << 'sAlT'.b
|
141
142
|
key_length = object_key.length < 16 ? object_key.length : 16
|
142
143
|
|
143
144
|
begin
|
data/lib/combine_pdf/filter.rb
CHANGED
data/lib/combine_pdf/fonts.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
########################################################
|
3
4
|
## Thoughts from reading the ISO 32000-1:2008
|
4
5
|
## this file is part of the CombinePDF library and the code
|
@@ -214,7 +215,7 @@ module CombinePDF
|
|
214
215
|
options[:text_padding] = 0 if options[:text_padding].to_f >= 1
|
215
216
|
|
216
217
|
# create box stream
|
217
|
-
box_stream = ''
|
218
|
+
box_stream = +''
|
218
219
|
# set graphic state for box
|
219
220
|
if options[:box_color] || (options[:border_width].to_i > 0 && options[:border_color])
|
220
221
|
# compute x and y position for text
|
@@ -290,7 +291,7 @@ module CombinePDF
|
|
290
291
|
# reset x,y by text alignment - x,y are calculated from the bottom left
|
291
292
|
# each unit (1) is 1/72 Inch
|
292
293
|
# create text stream
|
293
|
-
text_stream = ''
|
294
|
+
text_stream = +''
|
294
295
|
if !text.to_s.empty? && options[:font_size] != 0 && (options[:font_color] || options[:stroke_color])
|
295
296
|
# compute x and y position for text
|
296
297
|
x = options[:x] + (options[:width] * options[:text_padding])
|
@@ -679,7 +680,7 @@ module CombinePDF
|
|
679
680
|
insert_content 'Q'
|
680
681
|
|
681
682
|
# Prep content
|
682
|
-
@contents = ''
|
683
|
+
@contents = +''
|
683
684
|
insert_content @contents
|
684
685
|
@contents
|
685
686
|
end
|
@@ -788,7 +789,7 @@ module CombinePDF
|
|
788
789
|
# add to array
|
789
790
|
if out.last.nil? || out.last[0] != fonts[i]
|
790
791
|
out.last[1] << '>' unless out.last.nil?
|
791
|
-
out << [fonts[i], '<', 0, 0]
|
792
|
+
out << [fonts[i], (+'<'), 0, 0]
|
792
793
|
end
|
793
794
|
out.last[1] << (fonts_array[i].cmap.nil? ? (c.unpack('H*')[0]) : fonts_array[i].cmap[c])
|
794
795
|
if fonts_array[i].metrics[c]
|
data/lib/combine_pdf/parser.rb
CHANGED
@@ -41,7 +41,7 @@ module CombinePDF
|
|
41
41
|
# string:: the data to be parsed, as a String object.
|
42
42
|
def initialize(string, options = {})
|
43
43
|
raise TypeError, "couldn't parse data, expecting type String" unless string.is_a? String
|
44
|
-
@string_to_parse = string.force_encoding(Encoding::ASCII_8BIT)
|
44
|
+
@string_to_parse = (string.frozen? ? string.dup : string).force_encoding(Encoding::ASCII_8BIT)
|
45
45
|
@literal_strings = [].dup
|
46
46
|
@hex_strings = [].dup
|
47
47
|
@streams = [].dup
|
@@ -262,7 +262,7 @@ module CombinePDF
|
|
262
262
|
##########################################
|
263
263
|
elsif @scanner.scan(/\(/)
|
264
264
|
# warn "Found a literal string"
|
265
|
-
str = ''.
|
265
|
+
str = ''.b
|
266
266
|
count = 1
|
267
267
|
while count > 0 && @scanner.rest?
|
268
268
|
scn = @scanner.scan_until(/[\(\)]/)
|
@@ -323,8 +323,8 @@ module CombinePDF
|
|
323
323
|
str << 12
|
324
324
|
when 48..57 # octal notation for byte?
|
325
325
|
rep -= 48
|
326
|
-
rep = (rep << 3) + (str_bytes.shift-48) if str_bytes[0]
|
327
|
-
rep = (rep << 3) + (str_bytes.shift-48) if str_bytes[0]
|
326
|
+
rep = (rep << 3) + (str_bytes.shift-48) if str_bytes[0]&.between?(48, 57)
|
327
|
+
rep = (rep << 3) + (str_bytes.shift-48) if str_bytes[0]&.between?(48, 57) && (((rep << 3) + (str_bytes[0] - 48)) <= 255)
|
328
328
|
str << rep
|
329
329
|
when 10 # new line, ignore
|
330
330
|
str_bytes.shift if str_bytes[0] == 13
|
@@ -358,28 +358,30 @@ module CombinePDF
|
|
358
358
|
##########################################
|
359
359
|
## parse a Stream
|
360
360
|
##########################################
|
361
|
-
elsif @scanner.scan(/stream[ \t]
|
362
|
-
@scanner.pos += 1 if @scanner.peek(1) == "\n".freeze && @scanner.matched[-1] != "\n".freeze
|
361
|
+
elsif @scanner.scan(/stream[ \t]*\r?\n?/)
|
363
362
|
# advance by the publshed stream length (if any)
|
364
363
|
old_pos = @scanner.pos
|
365
|
-
if(out.last.is_a?(Hash) && out.last[:Length].is_a?(Integer) && out.last[:Length]
|
366
|
-
@scanner.pos += out.last[:Length]
|
364
|
+
if(out.last.is_a?(Hash) && out.last[:Length].is_a?(Integer) && out.last[:Length])
|
365
|
+
@scanner.pos += out.last[:Length]
|
366
|
+
unless(@scanner.skip(/\r?\n?endstream/))
|
367
|
+
@scanner.pos = old_pos
|
368
|
+
# raise error if the stream doesn't end.
|
369
|
+
unless @scanner.skip_until(/endstream/)
|
370
|
+
raise ParsingError, "Parsing Error: PDF file error - a stream object wasn't properly closed using 'endstream'!"
|
371
|
+
end
|
372
|
+
end
|
373
|
+
else
|
374
|
+
# raise error if the stream doesn't end.
|
375
|
+
unless @scanner.skip_until(/endstream/)
|
376
|
+
raise ParsingError, "Parsing Error: PDF file error - a stream object wasn't properly closed using 'endstream'!"
|
377
|
+
end
|
367
378
|
end
|
368
379
|
|
369
|
-
# the following was dicarded because some PDF files didn't have an EOL marker as required
|
370
|
-
# str = @scanner.scan_until(/(\r\n|\r|\n)endstream/)
|
371
|
-
# instead, a non-strict RegExp is used:
|
372
|
-
|
373
|
-
|
374
|
-
# raise error if the stream doesn't end.
|
375
|
-
unless @scanner.skip_until(/endstream/)
|
376
|
-
raise ParsingError, "Parsing Error: PDF file error - a stream object wasn't properly closed using 'endstream'!"
|
377
|
-
end
|
378
380
|
length = @scanner.pos - (old_pos + 9)
|
379
381
|
length = 0 if(length < 0)
|
380
382
|
length -= 1 if(@scanner.string[old_pos + length - 1] == "\n")
|
381
383
|
length -= 1 if(@scanner.string[old_pos + length - 1] == "\r")
|
382
|
-
str = (length > 0) ? @scanner.string.slice(old_pos, length) : ''
|
384
|
+
str = (length > 0) ? @scanner.string.slice(old_pos, length) : +''
|
383
385
|
|
384
386
|
# warn "CombinePDF parser: detected Stream #{str.length} bytes long #{str[0..3]}...#{str[-4..-1]}"
|
385
387
|
|
@@ -632,17 +634,17 @@ module CombinePDF
|
|
632
634
|
#
|
633
635
|
def serialize_objects_and_references
|
634
636
|
obj_dir = {}
|
635
|
-
objid_cache = {}
|
637
|
+
objid_cache = {}.compare_by_identity
|
636
638
|
# create a dictionary for referenced objects (no value resolution at this point)
|
637
639
|
# at the same time, delete duplicates and old versions when objects have multiple versions
|
638
640
|
@parsed.uniq!
|
639
641
|
@parsed.length.times do |i|
|
640
642
|
o = @parsed[i]
|
641
|
-
objid_cache[o
|
643
|
+
objid_cache[o] = i
|
642
644
|
tmp_key = [o[:indirect_reference_id], o[:indirect_generation_number]]
|
643
645
|
if tmp_found = obj_dir[tmp_key]
|
644
646
|
tmp_found.clear
|
645
|
-
@parsed[objid_cache[tmp_found
|
647
|
+
@parsed[objid_cache[tmp_found]] = nil
|
646
648
|
end
|
647
649
|
obj_dir[tmp_key] = o
|
648
650
|
end
|
@@ -724,6 +726,7 @@ module CombinePDF
|
|
724
726
|
|
725
727
|
# All Strings are one String
|
726
728
|
def unify_string(str)
|
729
|
+
str = str.dup if(str.frozen?)
|
727
730
|
str.force_encoding(Encoding::ASCII_8BIT)
|
728
731
|
@strings_dictionary[str] ||= str
|
729
732
|
end
|
@@ -765,9 +768,9 @@ module CombinePDF
|
|
765
768
|
# end
|
766
769
|
|
767
770
|
# # run block of code on evey PDF object (PDF objects are class Hash)
|
768
|
-
# def each_object(object, limit_references = true, already_visited = {}, &block)
|
771
|
+
# def each_object(object, limit_references = true, already_visited = {}.compare_by_identity, &block)
|
769
772
|
# unless limit_references
|
770
|
-
# already_visited[object
|
773
|
+
# already_visited[object] = true
|
771
774
|
# end
|
772
775
|
# case
|
773
776
|
# when object.is_a?(Array)
|
@@ -776,7 +779,7 @@ module CombinePDF
|
|
776
779
|
# yield(object)
|
777
780
|
# unless limit_references && object[:is_reference_only]
|
778
781
|
# object.each do |k,v|
|
779
|
-
# each_object(v, limit_references, already_visited, &block) unless already_visited[v
|
782
|
+
# each_object(v, limit_references, already_visited, &block) unless already_visited[v]
|
780
783
|
# end
|
781
784
|
# end
|
782
785
|
# end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
########################################################
|
3
4
|
## Thoughts from reading the ISO 32000-1:2008
|
4
5
|
## this file is part of the CombinePDF library and the code
|
@@ -21,19 +22,19 @@ module CombinePDF
|
|
21
22
|
# this is used for internal operations, such as injectng data using the << operator.
|
22
23
|
def add_referenced()
|
23
24
|
# an existing object map
|
24
|
-
resolved = {}.
|
25
|
-
existing = {}
|
26
|
-
should_resolve = []
|
25
|
+
resolved = {}.compare_by_identity
|
26
|
+
existing = {}
|
27
|
+
should_resolve = []
|
27
28
|
#set all existing objects as resolved and register their children for future resolution
|
28
|
-
@objects.each { |obj| existing[obj] = obj ; resolved[obj
|
29
|
+
@objects.each { |obj| existing[obj] = obj ; resolved[obj] = obj; should_resolve << obj.values}
|
29
30
|
# loop until should_resolve is empty
|
30
31
|
while should_resolve.any?
|
31
32
|
obj = should_resolve.pop
|
32
|
-
next if resolved[obj
|
33
|
+
next if resolved[obj] # the object exists
|
33
34
|
if obj.is_a?(Hash)
|
34
35
|
referenced = obj[:referenced_object]
|
35
36
|
if referenced && referenced.any?
|
36
|
-
tmp = resolved[referenced
|
37
|
+
tmp = resolved[referenced]
|
37
38
|
if !tmp && referenced[:raw_stream_content]
|
38
39
|
tmp = existing[referenced[:raw_stream_content]]
|
39
40
|
# Avoid endless recursion by limiting it to a number of layers (default == 2)
|
@@ -42,18 +43,18 @@ module CombinePDF
|
|
42
43
|
if tmp
|
43
44
|
obj[:referenced_object] = tmp
|
44
45
|
else
|
45
|
-
resolved[obj
|
46
|
+
resolved[obj] = referenced
|
46
47
|
# existing[referenced] = referenced
|
47
48
|
existing[referenced[:raw_stream_content]] = referenced
|
48
49
|
should_resolve << referenced
|
49
50
|
@objects << referenced
|
50
51
|
end
|
51
52
|
else
|
52
|
-
resolved[obj
|
53
|
-
obj.keys.each { |k| should_resolve << obj[k] unless !obj[k].is_a?(Enumerable) || resolved[obj[k]
|
53
|
+
resolved[obj] = obj
|
54
|
+
obj.keys.each { |k| should_resolve << obj[k] unless !obj[k].is_a?(Enumerable) || resolved[obj[k]] }
|
54
55
|
end
|
55
56
|
elsif obj.is_a?(Array)
|
56
|
-
resolved[obj
|
57
|
+
resolved[obj] = obj
|
57
58
|
should_resolve.concat obj
|
58
59
|
end
|
59
60
|
end
|
@@ -78,14 +79,14 @@ module CombinePDF
|
|
78
79
|
page_list.concat(with_pages) unless with_pages.empty?
|
79
80
|
|
80
81
|
# duplicate any non-unique pages - This is a special case to resolve Adobe Acrobat Reader issues (see issues #19 and #81)
|
81
|
-
uniqueness = {}.
|
82
|
-
page_list.each { |page| page = page[:referenced_object] || page; page = page.dup if uniqueness[page
|
82
|
+
uniqueness = {}.compare_by_identity
|
83
|
+
page_list.each { |page| page = page[:referenced_object] || page; page = page.dup if uniqueness[page]; uniqueness[page] = page }
|
83
84
|
page_list.clear
|
84
85
|
page_list = uniqueness.values
|
85
86
|
uniqueness.clear
|
86
87
|
|
87
88
|
# build new Pages object
|
88
|
-
page_object_kids = []
|
89
|
+
page_object_kids = []
|
89
90
|
pages_object = { Type: :Pages, Count: page_list.length, Kids: page_object_kids }
|
90
91
|
pages_object_reference = { referenced_object: pages_object, is_reference_only: true }
|
91
92
|
page_list.each { |pg| pg[:Parent] = pages_object_reference; page_object_kids << ({ referenced_object: pg, is_reference_only: true }) }
|
@@ -186,17 +187,18 @@ module CombinePDF
|
|
186
187
|
POSSIBLE_NAME_TREES = [:Dests, :AP, :Pages, :IDS, :Templates, :URLS, :JavaScript, :EmbeddedFiles, :AlternatePresentations, :Renditions].to_set.freeze
|
187
188
|
|
188
189
|
def rebuild_names(name_tree = nil, base = 'CombinePDF_0000000')
|
190
|
+
base = +base
|
189
191
|
if name_tree
|
190
192
|
return nil unless name_tree.is_a?(Hash)
|
191
193
|
name_tree = name_tree[:referenced_object] || name_tree
|
192
194
|
dic = []
|
193
195
|
# map a names tree and return a valid name tree. Do not recourse.
|
194
196
|
should_resolve = [name_tree[:Kids], name_tree[:Names]]
|
195
|
-
resolved =
|
197
|
+
resolved = Set.new.compare_by_identity
|
196
198
|
while should_resolve.any?
|
197
199
|
pos = should_resolve.pop
|
198
200
|
if pos.is_a? Array
|
199
|
-
next if resolved.include?(pos
|
201
|
+
next if resolved.include?(pos)
|
200
202
|
if pos[0].is_a? String
|
201
203
|
(pos.length / 2).times do |i|
|
202
204
|
dic << (pos[i * 2].clear << base.next!)
|
@@ -209,16 +211,16 @@ module CombinePDF
|
|
209
211
|
end
|
210
212
|
elsif pos.is_a? Hash
|
211
213
|
pos = pos[:referenced_object] || pos
|
212
|
-
next if resolved.include?(pos
|
214
|
+
next if resolved.include?(pos)
|
213
215
|
should_resolve << pos[:Kids] if pos[:Kids]
|
214
216
|
should_resolve << pos[:Names] if pos[:Names]
|
215
217
|
end
|
216
|
-
resolved << pos
|
218
|
+
resolved << pos
|
217
219
|
end
|
218
220
|
return { referenced_object: { Names: dic }, is_reference_only: true }
|
219
221
|
end
|
220
222
|
@names ||= @names[:referenced_object]
|
221
|
-
new_names = { Type: :Names }
|
223
|
+
new_names = { Type: :Names }
|
222
224
|
POSSIBLE_NAME_TREES.each do |ntree|
|
223
225
|
if @names[ntree]
|
224
226
|
new_names[ntree] = rebuild_names(@names[ntree], base)
|
@@ -373,7 +375,7 @@ module CombinePDF
|
|
373
375
|
private
|
374
376
|
|
375
377
|
def equal_layers obj1, obj2, layer = CombinePDF.eq_depth_limit
|
376
|
-
return true if obj1.
|
378
|
+
return true if obj1.equal?(obj2)
|
377
379
|
if obj1.is_a? Hash
|
378
380
|
return false unless obj2.is_a? Hash
|
379
381
|
return false unless obj1.length == obj2.length
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
-
|
2
|
+
## frozen_string_literal: true
|
3
|
+
#######################################################
|
3
4
|
## Thoughts from reading the ISO 32000-1:2008
|
4
5
|
## this file is part of the CombinePDF library and the code
|
5
6
|
## is subject to the same license.
|
@@ -93,7 +94,7 @@ module CombinePDF
|
|
93
94
|
@version = 0
|
94
95
|
@viewer_preferences = {}
|
95
96
|
@info = {}
|
96
|
-
parser ||= PDFParser.new('')
|
97
|
+
parser ||= PDFParser.new(+'')
|
97
98
|
raise TypeError, "initialization error, expecting CombinePDF::PDFParser or nil, but got #{parser.class.name}" unless parser.is_a? PDFParser
|
98
99
|
@objects = parser.parse
|
99
100
|
|
@@ -175,8 +176,12 @@ module CombinePDF
|
|
175
176
|
def to_pdf(options = {})
|
176
177
|
# reset version if not specified
|
177
178
|
@version = 1.5 if @version.to_f == 0.0
|
179
|
+
|
178
180
|
# set info for merged file
|
179
|
-
@info[:
|
181
|
+
unless(@info[:CreationDate].is_a?(String))
|
182
|
+
@info[:CreationDate] = Time.now unless @info[:CreationDate].is_a?(Time)
|
183
|
+
@info[:CreationDate] = @info[:CreationDate].getgm.strftime("D:%Y%m%d%H%M%S%:::z'00")
|
184
|
+
end
|
180
185
|
@info[:Subject] = options[:subject] if options[:subject]
|
181
186
|
@info[:Producer] = options[:producer] if options[:producer]
|
182
187
|
# rebuild_catalog
|
@@ -202,9 +207,9 @@ module CombinePDF
|
|
202
207
|
xref_location = loc
|
203
208
|
# xref_location = 0
|
204
209
|
# out.each { |line| xref_location += line.bytesize + 1}
|
205
|
-
out << "xref\n0 #{indirect_object_count}\n0000000000 65535 f
|
206
|
-
xref.each { |offset| out << (
|
207
|
-
out <<
|
210
|
+
out << "xref\n0 #{indirect_object_count}\n0000000000 65535 f "
|
211
|
+
xref.each { |offset| out << ("%010d 00000 n ".freeze % offset) }
|
212
|
+
out << 'trailer'.freeze
|
208
213
|
out << "<<\n/Root #{false || "#{catalog[:indirect_reference_id]} #{catalog[:indirect_generation_number]} R"}"
|
209
214
|
out << "/Size #{indirect_object_count}"
|
210
215
|
out << "/Info #{@info[:indirect_reference_id]} #{@info[:indirect_generation_number]} R"
|
@@ -212,7 +217,7 @@ module CombinePDF
|
|
212
217
|
# when finished, remove the numbering system and keep only pointers
|
213
218
|
remove_old_ids
|
214
219
|
# output the pdf stream
|
215
|
-
out.join("\n".
|
220
|
+
out.join("\n".b).force_encoding(Encoding::ASCII_8BIT)
|
216
221
|
end
|
217
222
|
|
218
223
|
# this method returns all the pages cataloged in the catalog.
|
data/lib/combine_pdf/renderer.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module CombinePDF
|
2
3
|
################################################################
|
3
4
|
## These are common functions, used within the different classes
|
@@ -51,7 +52,7 @@ module CombinePDF
|
|
51
52
|
if object.length == 0 || obj_bytes.min <= 31 || obj_bytes.max >= 127 # || (obj_bytes[0] != 68 object.match(/[^D\:\d\+\-Z\']/))
|
52
53
|
# A hexadecimal string shall be written as a sequence of hexadecimal digits (0-9 and either A-F or a-f)
|
53
54
|
# encoded as ASCII characters and enclosed within angle brackets (using LESS-THAN SIGN (3Ch) and GREATER- THAN SIGN (3Eh)).
|
54
|
-
"<#{object.unpack('H*')[0]}>"
|
55
|
+
"<#{object.unpack('H*')[0]}>"
|
55
56
|
else
|
56
57
|
# a good fit for a Literal String or the string is a date (MUST be literal)
|
57
58
|
('(' + ([].tap { |out| obj_bytes.each { |byte| out.concat(STRING_REPLACEMENT_ARRAY[byte]) } } ).pack('C*') + ')').force_encoding(Encoding::ASCII_8BIT)
|
@@ -100,7 +101,7 @@ module CombinePDF
|
|
100
101
|
end
|
101
102
|
object[:indirect_reference_id] ||= 0
|
102
103
|
object[:indirect_generation_number] ||= 0
|
103
|
-
return "#{object[:indirect_reference_id]} #{object[:indirect_generation_number]} R"
|
104
|
+
return "#{object[:indirect_reference_id]} #{object[:indirect_generation_number]} R"
|
104
105
|
end
|
105
106
|
|
106
107
|
# if the object is indirect...
|
@@ -108,7 +109,7 @@ module CombinePDF
|
|
108
109
|
if object[:indirect_reference_id]
|
109
110
|
object[:indirect_reference_id] ||= 0
|
110
111
|
object[:indirect_generation_number] ||= 0
|
111
|
-
out << "#{object[:indirect_reference_id]} #{object[:indirect_generation_number]} obj\n"
|
112
|
+
out << "#{object[:indirect_reference_id]} #{object[:indirect_generation_number]} obj\n"
|
112
113
|
if object[:indirect_without_dictionary]
|
113
114
|
out << object_to_pdf(object[:indirect_without_dictionary])
|
114
115
|
out << "\nendobj\n"
|
@@ -123,13 +124,13 @@ module CombinePDF
|
|
123
124
|
# if the object is not a simple object, it is a dictionary
|
124
125
|
# A dictionary shall be written as a sequence of key-value pairs enclosed in double angle brackets (<<...>>)
|
125
126
|
# (using LESS-THAN SIGNs (3Ch) and GREATER-THAN SIGNs (3Eh)).
|
126
|
-
out << "<<\n".
|
127
|
+
out << "<<\n".b
|
127
128
|
object.each do |key, value|
|
128
|
-
out << "#{object_to_pdf key} #{object_to_pdf value}\n"
|
129
|
+
out << "#{object_to_pdf key} #{object_to_pdf value}\n" unless PDF::PRIVATE_HASH_KEYS.include? key
|
129
130
|
end
|
130
131
|
object.delete :Length
|
131
|
-
out << '>>'.
|
132
|
-
out << "\nstream\n#{object[:raw_stream_content]}\nendstream"
|
132
|
+
out << '>>'.b
|
133
|
+
out << "\nstream\n#{object[:raw_stream_content]}\nendstream" if object[:raw_stream_content]
|
133
134
|
out << "\nendobj\n" if object[:indirect_reference_id]
|
134
135
|
out.join.force_encoding(Encoding::ASCII_8BIT)
|
135
136
|
end
|
data/lib/combine_pdf/version.rb
CHANGED
data/lib/combine_pdf.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'zlib'
|
4
5
|
require 'securerandom'
|
@@ -10,21 +11,21 @@ require 'digest'
|
|
10
11
|
# require the RC4 Gem
|
11
12
|
require 'rc4'
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
14
|
+
require 'combine_pdf/api'
|
15
|
+
require 'combine_pdf/renderer'
|
16
|
+
require 'combine_pdf/page_methods'
|
17
|
+
require 'combine_pdf/basic_writer'
|
18
|
+
require 'combine_pdf/decrypt'
|
19
|
+
require 'combine_pdf/fonts'
|
20
|
+
require 'combine_pdf/filter'
|
21
|
+
require 'combine_pdf/parser'
|
22
|
+
require 'combine_pdf/pdf_public'
|
23
|
+
require 'combine_pdf/pdf_protected'
|
24
|
+
require 'combine_pdf/exceptions'
|
24
25
|
|
25
|
-
#
|
26
|
+
# require 'combine_pdf/operations'
|
26
27
|
|
27
|
-
|
28
|
+
require 'combine_pdf/version'
|
28
29
|
|
29
30
|
# This is a pure ruby library to combine/merge, stmap/overlay and number PDF files - as well as to create tables (ment for indexing combined files).
|
30
31
|
#
|
data/test/automated
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
$VERBOSE = true
|
4
4
|
|
5
5
|
require 'benchmark'
|
6
|
-
|
6
|
+
Dir.chdir File.expand_path(File.join('..', '..', 'lib'), __FILE__)
|
7
|
+
$LOAD_PATH.unshift Dir.pwd
|
7
8
|
require 'combine_pdf'
|
8
9
|
# require 'bundler/setup'
|
9
10
|
|
@@ -15,51 +16,51 @@ require 'combine_pdf'
|
|
15
16
|
# Pry.start
|
16
17
|
|
17
18
|
pdf = CombinePDF.load "../../test\ pdfs/filled_form.pdf"
|
18
|
-
pdf.save '01_check_radio_buttuns.pdf'
|
19
|
+
pdf.save '../tmp/01_check_radio_buttuns.pdf'
|
19
20
|
pdf = CombinePDF.load "../../test\ pdfs/filled_form.pdf"
|
20
21
|
pdf << CombinePDF.load("../../test\ pdfs/empty_form.pdf")
|
21
22
|
pdf << CombinePDF.load("../../test\ pdfs/filled_form.pdf")
|
22
|
-
pdf.save '02_check_form_unification_middle_is_empty.pdf'
|
23
|
+
pdf.save '../tmp/02_check_form_unification_middle_is_empty.pdf'
|
23
24
|
|
24
25
|
pdf = CombinePDF.load "../../test\ pdfs/check_form_data__objstreams_w_versions.pdf"
|
25
|
-
pdf.save '02_01_check_form_data_ordering_issue.pdf'
|
26
|
+
pdf.save '../tmp/02_01_check_form_data_ordering_issue.pdf'
|
26
27
|
|
27
28
|
|
28
29
|
pdf = CombinePDF.load '../../test pdfs/share-font-background.pdf'
|
29
30
|
pdf2 = CombinePDF.load '../../test pdfs/share-font-foreground.pdf'
|
30
31
|
i = 0
|
31
32
|
pdf.pages.each { |pg| pg << pdf2.pages[i] }
|
32
|
-
pdf.save '03_check_font_conflict.pdf'
|
33
|
+
pdf.save '../tmp/03_check_font_conflict.pdf'
|
33
34
|
|
34
35
|
pdf = CombinePDF.load '../../test pdfs/nil_1.pdf'
|
35
36
|
pdf2 = CombinePDF.load '../../test pdfs/nil_2.pdf'
|
36
37
|
pdf << pdf2
|
37
|
-
pdf.save '03_01_nil_value_conflict.pdf'
|
38
|
+
pdf.save '../tmp/03_01_nil_value_conflict.pdf'
|
38
39
|
|
39
40
|
pdf = CombinePDF.load '../../test pdfs/space_after_streram_keyword.pdf'
|
40
|
-
pdf.save '03_02_extra_space_after_stream_keyword.pdf'
|
41
|
+
pdf.save '../tmp/03_02_extra_space_after_stream_keyword.pdf'
|
41
42
|
|
42
43
|
pdf = CombinePDF.load '../../test pdfs/nested_difference.pdf'
|
43
|
-
pdf.save '03_03_nested_difference.pdf'
|
44
|
+
pdf.save '../tmp/03_03_nested_difference.pdf'
|
44
45
|
|
45
46
|
pdf = CombinePDF.load '../../test pdfs/names_go_haywire_0.pdf'
|
46
47
|
pdf << CombinePDF.load('../../test pdfs/names_go_haywire_1.pdf')
|
47
|
-
pdf.save '04_check_view_and_names_reference.pdf'
|
48
|
+
pdf.save '../tmp/04_check_view_and_names_reference.pdf'
|
48
49
|
|
49
50
|
pdf = CombinePDF.load('../../test pdfs/outlines/self_merge_err.pdf')
|
50
|
-
pdf.save '05_x1_scribus_test.pdf'
|
51
|
+
pdf.save '../tmp/05_x1_scribus_test.pdf'
|
51
52
|
pdf = CombinePDF.load('../../test pdfs/outlines/self_merge_err.pdf')
|
52
53
|
pdf << CombinePDF.load('../../test pdfs/outlines/self_merge_err.pdf')
|
53
|
-
pdf.save '05_x2_scribus_test.pdf'
|
54
|
+
pdf.save '../tmp/05_x2_scribus_test.pdf'
|
54
55
|
pdf = CombinePDF.load "../../test pdfs/outlines/named_dest.pdf";nil
|
55
|
-
pdf.save '05_check_named_dest_links.pdf' # this will take a while
|
56
|
+
pdf.save '../tmp/05_check_named_dest_links.pdf' # this will take a while
|
56
57
|
pdf = CombinePDF.load "../../test pdfs/outlines/named_dest.pdf";nil
|
57
58
|
pdf << CombinePDF.load('../../test pdfs/outlines/named_dest.pdf'); nil
|
58
|
-
pdf.save '05_1_timeless_check_named_dest_links.pdf' # never ends... :-(
|
59
|
+
pdf.save '../tmp/05_1_timeless_check_named_dest_links.pdf' # never ends... :-(
|
59
60
|
|
60
61
|
pdf = CombinePDF.load '../../test pdfs/outline_small.pdf'
|
61
62
|
pdf << CombinePDF.load('../../test pdfs/outline_small.pdf')
|
62
|
-
pdf.save '06_check_links_to_second_copy.pdf'
|
63
|
+
pdf.save '../tmp/06_check_links_to_second_copy.pdf'
|
63
64
|
|
64
65
|
lists = %w(../../test\ pdfs/outlines/self_merge_err.pdf ../../test\ pdfs/outlines/big_toc.pdf ../../test\ pdfs/outlines/bigger_toc.pdf ../../test\ pdfs/outlines/named_dest_no_toc.pdf ../../test\ pdfs/outlines/named_dest_no_toc2.pdf ../../test\ pdfs/outlines/named_dest.pdf ../../test\ pdfs/outlines/named_dest2.pdf)
|
65
66
|
|
@@ -76,7 +77,7 @@ lists.each do |n|
|
|
76
77
|
end
|
77
78
|
pdf = CombinePDF.new
|
78
79
|
lists.each { |n| pdf << CombinePDF.load(n) }
|
79
|
-
pdf.save('07_named destinations.pdf')
|
80
|
+
pdf.save('../tmp/07_named destinations.pdf')
|
80
81
|
|
81
82
|
pdf = CombinePDF.new
|
82
83
|
lists.each { |n| pdf << CombinePDF.load(n) }
|
@@ -90,15 +91,15 @@ pdf.number_pages(start_at: 1,
|
|
90
91
|
number_location: [:top, :bottom],
|
91
92
|
opacity: 0.75)
|
92
93
|
|
93
|
-
pdf.save('07_named destinations_numbered.pdf')
|
94
|
+
pdf.save('../tmp/07_named destinations_numbered.pdf')
|
94
95
|
|
95
|
-
CombinePDF.load("../../test\ pdfs/Scribus-unknown_err.pdf").save '08_1-unknown-err-empty-str.pdf'
|
96
|
-
CombinePDF.load("../../test\ pdfs/Scribus-unknown_err2.pdf").save '08_2-unknown-err-empty-str.pdf'
|
97
|
-
CombinePDF.load("../../test\ pdfs/Scribus-unknown_err3.pdf").save '08_3-unknown-err-empty-str.pdf'
|
98
|
-
CombinePDF.load("../../test\ pdfs/xref_in_middle.pdf").save '08_4-xref-in-middle.pdf'
|
99
|
-
CombinePDF.load("../../test\ pdfs/xref_split.pdf").save '08_5-xref-fragmented.pdf'
|
96
|
+
CombinePDF.load("../../test\ pdfs/Scribus-unknown_err.pdf").save '../tmp/08_1-unknown-err-empty-str.pdf'
|
97
|
+
CombinePDF.load("../../test\ pdfs/Scribus-unknown_err2.pdf").save '../tmp/08_2-unknown-err-empty-str.pdf'
|
98
|
+
CombinePDF.load("../../test\ pdfs/Scribus-unknown_err3.pdf").save '../tmp/08_3-unknown-err-empty-str.pdf'
|
99
|
+
CombinePDF.load("../../test\ pdfs/xref_in_middle.pdf").save '../tmp/08_4-xref-in-middle.pdf'
|
100
|
+
CombinePDF.load("../../test\ pdfs/xref_split.pdf").save '../tmp/08_5-xref-fragmented.pdf'
|
100
101
|
|
101
|
-
CombinePDF.load("../../test\ pdfs/nil_object.pdf").save('09_nil_in_parsed_array.pdf')
|
102
|
+
CombinePDF.load("../../test\ pdfs/nil_object.pdf").save('../tmp/09_nil_in_parsed_array.pdf')
|
102
103
|
|
103
104
|
encrypted = [ "../../test\ pdfs/pdf-reader/encrypted_version4_revision4_128bit_aes_user_pass_apples_enc_metadata.pdf",
|
104
105
|
"../../test\ pdfs/AESv2\ encrypted.pdf",
|
@@ -109,7 +110,7 @@ encrypted = [ "../../test\ pdfs/pdf-reader/encrypted_version4_revision4_128bit_a
|
|
109
110
|
encrypted.length.times do |i|
|
110
111
|
fname = File.basename encrypted[i]
|
111
112
|
begin
|
112
|
-
CombinePDF.load(encrypted[i]).save "10_#{i}_#{fname}"
|
113
|
+
CombinePDF.load(encrypted[i]).save "../tmp/10_#{i}_#{fname}"
|
113
114
|
rescue => e
|
114
115
|
puts e.class.name, e.message
|
115
116
|
if(i == 0)
|
@@ -125,10 +126,10 @@ IO.binwrite '11_prawn.pdf', (Prawn::Document.new { text 'Hello World!' }).render
|
|
125
126
|
page = CombinePDF.parse((Prawn::Document.new { text 'Hello World!' }).render)
|
126
127
|
pdf = CombinePDF.new
|
127
128
|
pdf << page
|
128
|
-
pdf.save '11_parsed_from_prawn.pdf'
|
129
|
+
pdf.save '../tmp/11_parsed_from_prawn.pdf'
|
129
130
|
pdf = CombinePDF.new
|
130
131
|
pdf << page << page
|
131
|
-
pdf.save('11_AcrobatReader_is_unique_page.pdf')
|
132
|
+
pdf.save('../tmp/11_AcrobatReader_is_unique_page.pdf')
|
132
133
|
|
133
134
|
puts GC.stat.inspect
|
134
135
|
# unify = [
|
@@ -12,7 +12,7 @@ class CombinePDFRendererTest < Minitest::Test
|
|
12
12
|
|
13
13
|
def test_numeric_array_to_pdf
|
14
14
|
input = [1.234567, 0.000054, 5, -0.000099]
|
15
|
-
expected = "[1.234567 0.000054 5 -0.000099]".
|
15
|
+
expected = "[1.234567 0.000054 5 -0.000099]".b
|
16
16
|
actual = TestRenderer.new.test_object(input)
|
17
17
|
|
18
18
|
assert_equal(expected, actual)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: combine_pdf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.31
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Boaz Segev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-04-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-rc4
|
@@ -84,11 +84,12 @@ description: A nifty gem, in pure Ruby, to parse PDF files and combine (merge) t
|
|
84
84
|
with other PDF files, number the pages, watermark them or stamp them, create tables,
|
85
85
|
add basic text objects etc` (all using the PDF file format).
|
86
86
|
email:
|
87
|
-
-
|
87
|
+
- bo@bowild.com
|
88
88
|
executables: []
|
89
89
|
extensions: []
|
90
90
|
extra_rdoc_files: []
|
91
91
|
files:
|
92
|
+
- ".github/workflows/main.yml"
|
92
93
|
- ".gitignore"
|
93
94
|
- ".travis.yml"
|
94
95
|
- CHANGELOG.md
|
@@ -136,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
137
|
- !ruby/object:Gem::Version
|
137
138
|
version: '0'
|
138
139
|
requirements: []
|
139
|
-
rubygems_version: 3.
|
140
|
+
rubygems_version: 3.5.22
|
140
141
|
signing_key:
|
141
142
|
specification_version: 4
|
142
143
|
summary: Combine, stamp and watermark PDF files in pure Ruby.
|