combine_pdf 0.0.6 → 0.0.7

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
  SHA1:
3
- metadata.gz: 91a403f449ba29c772a9e6d12e114e0271129ecb
4
- data.tar.gz: 9b94530e869fe1a5936706305c2407eeeb9f35a7
3
+ metadata.gz: 6e1bf0dc605e123de2a5e4e809c079c64da812a5
4
+ data.tar.gz: a3f744fbab605cb9abd85332566219ab9735e421
5
5
  SHA512:
6
- metadata.gz: ce4a31ce78d6ff246f51ced6567b73fb5b5905cfbcf43fa2d980996d16e66570e1777a350cec53527d5772eae07be8dfc768f7b68811505428e4788cb55e195f
7
- data.tar.gz: 9fc48d3e72643e48b179ceb01d755fc928585b5d911f53bc83fa96f41c87353c46a517966c9c094ff0e116dcb95010d7aced07cb3aafe1a12ea8d0c1b21da2bd
6
+ metadata.gz: 73845fbdabd424d02f5c1494df97a2e790d38c0c8575ee78214ab2279ba359bd41533ba9a493e755059904a18bd8ef39e78d574f3eae2ccd60c411733735f942
7
+ data.tar.gz: 7bd08ed3ea3131a1170f583e1a2c17eae17b406af1f4cec8d19b6c8cbbc59c36dd651d1e70a848b13be7d0767bad127686f9eae288037eba881517c492505c27
@@ -34,23 +34,32 @@ require "combine_pdf/font_metrics/metrics_dictionary.rb"
34
34
  # In the future, this library will also allow stamping and watermarking PDFs (it allows this now, only with some issues).
35
35
  #
36
36
  # PDF objects can be used to combine or to inject data.
37
- # == Combine / Merge
37
+ # == Combine/Merge PDF files or Pages
38
38
  # To combine PDF files (or data):
39
39
  # pdf = CombinePDF.new
40
40
  # pdf << CombinePDF.new("file1.pdf") # one way to combine, very fast.
41
- # CombinePDF.new("file2.pdf").pages.each {|page| pdf << page} # different way to combine, slower.
41
+ # pdf << CombinePDF.new("file2.pdf")
42
42
  # pdf.save "combined.pdf"
43
- #
44
- # Or, you can do it all in one line:
45
- # ( CombinePDF.new("file1.pdf") << CombinePDF.new("file2.pdf") << CombinePDF.new("file3.pdf") ).save "combined.pdf"
46
- # == Stamp / Watermark
47
- # <b>has issues with specific PDF files - please see the issues</b>: https://github.com/boazsegev/combine_pdf/issues/2
48
- # To combine PDF files (or data), first create the stamp from a PDF file:
49
- # stamp_pdf_file = CombinePDF.new "stamp_pdf_file.pdf"
50
- # stamp_page = stamp_pdf_file.pages[0]
51
- # After the stamp was created, inject to PDF pages:
52
- # pdf = CombinePDF.new "file1.pdf"
53
- # pdf.pages.each {|page| page << stamp_page}
43
+ # or even a one liner:
44
+ # (CombinePDF.new("file1.pdf") << CombinePDF.new("file2.pdf") << CombinePDF.new("file3.pdf")).save("combined.pdf")
45
+ # you can also add just odd or even pages:
46
+ # pdf = CombinePDF.new
47
+ # i = 0
48
+ # CombinePDF.new("file.pdf").pages.each do |page
49
+ # i += 1
50
+ # pdf << page if i.even?
51
+ # end
52
+ # pdf.save "even_pages.pdf"
53
+ # notice that adding all the pages one by one is slower then adding the whole file.
54
+ # == Add content to existing pages (Stamp / Watermark)
55
+ # To add content to existing PDF pages, first import the new content from an existing PDF file.
56
+ # after that, add the content to each of the pages in your existing PDF.
57
+ #
58
+ # in this example, we will add a company logo to each page:
59
+ # company_logo = CombinePDF.new("company_logo.pdf").pages[0]
60
+ # pdf = CombinePDF.new "content_file.pdf"
61
+ # pdf.pages.each {|page| page << company_logo} # notice the << operator is on a page and not a PDF object.
62
+ # pdf.save "content_with_logo.pdf"
54
63
  # Notice the << operator is on a page and not a PDF object. The << operator acts differently on PDF objects and on Pages.
55
64
  #
56
65
  # The << operator defaults to secure injection by renaming references to avoid conflics. For overlaying pages using compressed data that might not be editable (due to limited filter support), you can use:
@@ -59,6 +68,24 @@ require "combine_pdf/font_metrics/metrics_dictionary.rb"
59
68
  #
60
69
  # Notice that page objects are Hash class objects and the << operator was added to the Page instances without altering the class.
61
70
  #
71
+ # == Page Numbering
72
+ # adding page numbers to a PDF object or file is as simple as can be:
73
+ # pdf = CombinePDF.new "file_to_number.pdf"
74
+ # pdf.number_pages
75
+ # pdf.save "file_with_numbering.pdf"
76
+ #
77
+ # numbering can be done with many different options, with different formating, with or without a box object, and even with opacity values.
78
+ #
79
+ # == Loading PDF data
80
+ # Loading PDF data can be done from file system or directly from the memory.
81
+ #
82
+ # Loading data from a file is easy:
83
+ # pdf = CombinePDF.new("file.pdf")
84
+ # you can also parse PDF files from memory:
85
+ # pdf_data = IO.read 'file.pdf' # for this demo, load a file to memory
86
+ # pdf = CombinePDF.parse(pdf_data)
87
+ # Loading from the memory is especially effective for importing PDF data recieved through the internet or from a different authoring library such as Prawn.
88
+ #
62
89
  # == Decryption & Filters
63
90
  #
64
91
  # Some PDF files are encrypted and some are compressed (the use of filters)...
@@ -10,12 +10,11 @@
10
10
 
11
11
  module CombinePDF
12
12
 
13
- #@private
14
13
  #:nodoc: all
15
- #
16
- # <b>This doesn't work yet!</b>
14
+
15
+ # <b>not fully tested!</b>
17
16
  #
18
- # and also, even when it will work, UNICODE SUPPORT IS MISSING!
17
+ # NO UNICODE SUPPORT!
19
18
  #
20
19
  # in the future I wish to make a simple PDF page writer, that has only one functions - the text box.
21
20
  # Once the simple writer is ready (creates a text box in a self contained Page element),
@@ -24,9 +23,9 @@ module CombinePDF
24
23
  #
25
24
  # The PDFWriter class is a subclass of Hash and represents a PDF Page object.
26
25
  #
27
- # Writing on this Page is done using the text_box function.
26
+ # Writing on this Page is done using the textbox function.
28
27
  #
29
- # Setting the page dimentions can be either at the new or using the media_box method.
28
+ # Setting the page dimentions can be either at the new or using the mediabox method.
30
29
  #
31
30
  # the rest of the methods are for internal use.
32
31
  #
@@ -36,32 +35,32 @@ module CombinePDF
36
35
  # We can either insert the PDFWriter as a new page:
37
36
  # pdf = CombinePDF.new
38
37
  # new_page = PDFWriter.new
39
- # new_page.text_box "some text"
38
+ # new_page.textbox "some text"
40
39
  # pdf << new_page
41
40
  # pdf.save "file_with_new_page.pdf"
42
41
  # Or we can insert the PDFWriter as an overlay (stamp / watermark) over existing pages:
43
42
  # pdf = CombinePDF.new
44
43
  # new_page = PDFWriter.new "some_file.pdf"
45
- # new_page.text_box "some text"
44
+ # new_page.textbox "some text"
46
45
  # pdf.pages.each {|page| page << new_page }
47
46
  # pdf.save "stamped_file.pdf"
48
47
  class PDFWriter < Hash
49
48
 
50
- def initialize(media_box = [0.0, 0.0, 612.0, 792.0])
49
+ def initialize(mediabox = [0.0, 0.0, 612.0, 792.0])
51
50
  # indirect_reference_id, :indirect_generation_number
52
51
  self[:Type] = :Page
53
52
  self[:indirect_reference_id] = 0
54
53
  self[:Resources] = {}
55
54
  self[:Contents] = { is_reference_only: true , referenced_object: {indirect_reference_id: 0, raw_stream_content: ""} }
56
- self[:MediaBox] = media_box
55
+ self[:MediaBox] = mediabox
57
56
  end
58
57
  # accessor (getter) for the :MediaBox element of the page
59
- def media_box
58
+ def mediabox
60
59
  self[:MediaBox]
61
60
  end
62
61
  # accessor (setter) for the :MediaBox element of the page
63
62
  # dimentions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
64
- def media_box=(dimentions = [0.0, 0.0, 612.0, 792.0])
63
+ def mediabox=(dimentions = [0.0, 0.0, 612.0, 792.0])
65
64
  self[:MediaBox] = dimentions
66
65
  end
67
66
 
@@ -76,78 +75,191 @@ module CombinePDF
76
75
  # y:: the BUTTOM position of the box.
77
76
  # length:: the length of the box.
78
77
  # height:: the height of the box.
79
- # font_name:: a Symbol representing one of the 14 standard fonts. defaults to ":Helvetica" @see add_font
78
+ # text_align:: symbol for horizontal text alignment, can be ":center" (default), ":right", ":left"
79
+ # text_valign:: symbol for vertical text alignment, can be ":center" (default), ":top", ":buttom"
80
+ # font_name:: a Symbol representing one of the 14 standard fonts. defaults to ":Helvetica". the options are:
81
+ # - :"Times-Roman"
82
+ # - :"Times-Bold"
83
+ # - :"Times-Italic"
84
+ # - :"Times-BoldItalic"
85
+ # - :Helvetica
86
+ # - :"Helvetica-Bold"
87
+ # - :"Helvetica-BoldOblique"
88
+ # - :"Helvetica- Oblique"
89
+ # - :Courier
90
+ # - :"Courier-Bold"
91
+ # - :"Courier-Oblique"
92
+ # - :"Courier-BoldOblique"
93
+ # - :Symbol
94
+ # - :ZapfDingbats
80
95
  # font_size:: a Fixnum for the font size, or :fit_text to fit the text in the box. defaults to ":fit_text"
81
- # text_color:: [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]").
82
- def text_box(text, properties = {})
96
+ # max_font_size:: if font_size is set to :fit_text, this will be the maximum font size. defaults to nil (no maximum)
97
+ # font_color:: text color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to black.
98
+ # stroke_color:: text stroke color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defounlts to nil (no stroke).
99
+ # stroke_width:: text stroke width in PDF units. defaults to 0 (none).
100
+ # box_color:: box fill color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to nil (none).
101
+ # border_color:: box border color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to nil (none).
102
+ # border_width:: border width in PDF units. defaults to nil (none).
103
+ # box_radius:: border radius in PDF units. defaults to 0 (no corner rounding).
104
+ # opacity:: textbox opacity, a float between 0 (transparent) and 1 (opaque)
105
+ # <b>now on testing mode, defaults are different! box defaults to gray with border and rounding.</b>
106
+ def textbox(text, properties = {})
83
107
  options = {
84
- text_alignment: :center,
85
- text_color: [0,0,0],
86
- text_stroke_color: nil,
87
- text_stroke_width: 0,
88
- font_name: :Helvetica,
89
- font_size: :fit_text,
90
- border_color: [0.5,0.5,0.5],
91
- border_width: 2,
92
- border_radius: 0,
93
- background_color: [0.7,0.7,0.7],
94
- opacity: 1,
95
108
  x: 0,
96
109
  y: 0,
97
110
  length: -1,
98
111
  height: -1,
112
+ text_align: :center,
113
+ text_valign: :center,
114
+ font_name: :Helvetica,
115
+ font_size: :fit_text,
116
+ max_font_size: nil,
117
+ font_color: [0,0,0],
118
+ stroke_color: nil,
119
+ stroke_width: 0,
120
+ box_color: nil,
121
+ border_color: nil,
122
+ border_width: 0,
123
+ box_radius: 0,
124
+ opacity: 1
99
125
  }
100
126
  options.update properties
101
127
  # reset the length and height to meaningful values, if negative
102
- options[:length] = media_box[2] - options[:x] if options[:length] < 0
103
- options[:height] = media_box[3] - options[:y] if options[:height] < 0
128
+ options[:length] = mediabox[2] - options[:x] if options[:length] < 0
129
+ options[:height] = mediabox[3] - options[:y] if options[:height] < 0
104
130
  # fit text in box, if requested
131
+ font_size = options[:font_size]
105
132
  if options[:font_size] == :fit_text
106
- options[:font_size] = self.fit_text text, options[:font_name], options[:length], options[:height]
133
+ font_size = self.fit_text text, options[:font_name], options[:length], options[:height]
134
+ font_size = options[:max_font_size] if options[:max_font_size] && font_size > options[:max_font_size]
107
135
  end
108
136
 
109
137
 
110
138
  # create box stream
139
+ box_stream = ""
140
+ # set graphic state for box
141
+ if options[:box_color] || (options[:border_width].to_i > 0 && options[:border_color])
142
+ # compute x and y position for text
143
+ x = options[:x]
144
+ y = options[:y]
145
+
146
+ # set graphic state for the box
147
+ box_stream << "q\nq\nq\n"
148
+ box_graphic_state = { ca: options[:opacity], CA: options[:opacity], LW: options[:border_width], LC: 0, LJ: 0, LD: 0 }
149
+ if options[:box_radius] != 0 # if the text box has rounded corners
150
+ box_graphic_state[:LC], box_graphic_state[:LJ] = 2, 1
151
+ end
152
+ box_graphic_state = graphic_state box_graphic_state # adds the graphic state to Resources and gets the reference
153
+ box_stream << "#{PDFOperations._object_to_pdf box_graphic_state} gs\n"
154
+ box_stream << "DeviceRGB CS\nDeviceRGB cs\n"
155
+ if options[:box_color]
156
+ box_stream << "#{options[:box_color].join(' ')} scn\n"
157
+ end
158
+ if options[:border_width].to_i > 0 && options[:border_color]
159
+ box_stream << "#{options[:border_color].join(' ')} SCN\n"
160
+ end
161
+ # create the path
162
+ radius = options[:box_radius]
163
+ half_radius = radius.to_f / 2
164
+ ## set starting point
165
+ box_stream << "#{options[:x] + radius} #{options[:y]} m\n"
166
+ ## buttom and right corner - first line and first corner
167
+ box_stream << "#{options[:x] + options[:length] - radius} #{options[:y]} l\n" #buttom
168
+ if options[:box_radius] != 0 # make first corner, if not straight.
169
+ box_stream << "#{options[:x] + options[:length] - half_radius} #{options[:y]} "
170
+ box_stream << "#{options[:x] + options[:length]} #{options[:y] + half_radius} "
171
+ box_stream << "#{options[:x] + options[:length]} #{options[:y] + radius} c\n"
172
+ end
173
+ ## right and top-right corner
174
+ box_stream << "#{options[:x] + options[:length]} #{options[:y] + options[:height] - radius} l\n"
175
+ if options[:box_radius] != 0
176
+ box_stream << "#{options[:x] + options[:length]} #{options[:y] + options[:height] - half_radius} "
177
+ box_stream << "#{options[:x] + options[:length] - half_radius} #{options[:y] + options[:height]} "
178
+ box_stream << "#{options[:x] + options[:length] - radius} #{options[:y] + options[:height]} c\n"
179
+ end
180
+ ## top and top-left corner
181
+ box_stream << "#{options[:x] + radius} #{options[:y] + options[:height]} l\n"
182
+ if options[:box_radius] != 0
183
+ box_stream << "#{options[:x] + half_radius} #{options[:y] + options[:height]} "
184
+ box_stream << "#{options[:x]} #{options[:y] + options[:height] - half_radius} "
185
+ box_stream << "#{options[:x]} #{options[:y] + options[:height] - radius} c\n"
186
+ end
187
+ ## left and buttom-left corner
188
+ box_stream << "#{options[:x]} #{options[:y] + radius} l\n"
189
+ if options[:box_radius] != 0
190
+ box_stream << "#{options[:x]} #{options[:y] + half_radius} "
191
+ box_stream << "#{options[:x] + half_radius} #{options[:y]} "
192
+ box_stream << "#{options[:x] + radius} #{options[:y]} c\n"
193
+ end
194
+ # fill / stroke path
195
+ box_stream << "h\n"
196
+ if options[:box_color] && options[:border_width].to_i > 0 && options[:border_color]
197
+ box_stream << "B\n"
198
+ elsif options[:box_color] # fill if fill color is set
199
+ box_stream << "f\n"
200
+ elsif options[:border_width].to_i > 0 && options[:border_color] # stroke if border is set
201
+ box_stream << "S\n"
202
+ end
203
+
204
+ # exit graphic state for the box
205
+ box_stream << "Q\nQ\nQ\n"
206
+ end
207
+ contents << box_stream
111
208
 
112
209
  # reset x,y by text alignment - x,y are calculated from the buttom left
113
210
  # each unit (1) is 1/72 Inch
114
- x = options[:x]
115
- y = options[:y]
116
211
  # create text stream
117
212
  text_stream = ""
118
- text_stream << "BT\n" # the Begine Text marker
119
- text_stream << PDFOperations._format_name_to_pdf(font options[:font_name]) # Set font name
120
- text_stream << " #{options[:font_size].to_f} Tf\n" # set font size and add font operator
121
- text_stream << "#{options[:text_color][0]} #{options[:text_color][0]} #{options[:text_color][0]} rg\n" # sets the color state
122
- text_stream << "#{x} #{y} Td\n" # set location for text object
123
- text_stream << PDFOperations._format_string_to_pdf(text) # insert the string in PDF format
124
- text_stream << " Tj\n ET\n" # the Text object operator and the End Text marker
125
-
126
- final_stream = ""
127
- # set graphic state for box
128
- final_stream << "q\nq\nq\n"
129
- box_graphic_state = graphic_state ca: options[:opacity], CA: options[:opacity], LW: options[:border_width], LC: 2, LJ:1, LD: 0
130
- final_stream << "#{PDFOperations._object_to_pdf box_graphic_state} gs\n"
131
- final_stream << "DeviceRGB CS\nDeviceRGB cs\n"
132
-
133
- # set graphic state for text
134
- final_stream << "q\nq\nq\n"
135
- text_graphic_state = graphic_state({ca: options[:opacity], CA: options[:opacity], LW: options[:text_stroke_width], LC: 2, LJ: 1, LD: 0})
136
- final_stream << "#{PDFOperations._object_to_pdf text_graphic_state} gs\n"
137
- final_stream << "DeviceRGB CS\nDeviceRGB cs\n"
138
- final_stream << "#{options[:text_color][0]} #{options[:text_color][1]} #{options[:text_color][2]} scn\n"
139
- if options[:text_stroke_width].to_i > 0 && options[:text_stroke_color]
140
- final_stream << "#{options[:text_stroke_color][0]} #{options[:text_stroke_color][1]} #{options[:text_stroke_color][2]} SCN\n"
141
- final_stream << "2 Tr\n"
142
- else
143
- final_stream << "0 Tr\n"
213
+ if text.to_s != "" && font_size != 0 && (options[:font_color] || options[:stroke_color])
214
+ # compute x and y position for text
215
+ x = options[:x]
216
+ y = options[:y]
217
+
218
+ text_size = dimentions_of text, options[:font_name], font_size
219
+ if options[:text_align] == :center
220
+ x = (options[:length] - text_size[0])/2 + x
221
+ elsif options[:text_align] == :right
222
+ x = (options[:length] - text_size[0]) + x
223
+ end
224
+ if options[:text_valign] == :center
225
+ y = (options[:height] - text_size[1])/2 + y
226
+ elsif options[:text_valign] == :top
227
+ y = (options[:height] - text_size[1]) + y
228
+ end
229
+ # set graphic state for text
230
+ text_stream << "q\nq\nq\n"
231
+ text_graphic_state = graphic_state({ca: options[:opacity], CA: options[:opacity], LW: options[:stroke_width].to_f, LC: 2, LJ: 1, LD: 0})
232
+ text_stream << "#{PDFOperations._object_to_pdf text_graphic_state} gs\n"
233
+ text_stream << "DeviceRGB CS\nDeviceRGB cs\n"
234
+ # set text render mode
235
+ if options[:font_color]
236
+ text_stream << "#{options[:font_color].join(' ')} scn\n"
237
+ end
238
+ if options[:stroke_width].to_i > 0 && options[:stroke_color]
239
+ text_stream << "#{options[:stroke_color].join(' ')} SCN\n"
240
+ if options[:font_color]
241
+ text_stream << "2 Tr\n"
242
+ else
243
+ final_stream << "1 Tr\n"
244
+ end
245
+ elsif options[:font_color]
246
+ text_stream << "0 Tr\n"
247
+ else
248
+ text_stream << "3 Tr\n"
249
+ end
250
+ # format text object
251
+ text_stream << "BT\n" # the Begine Text marker
252
+ text_stream << PDFOperations._format_name_to_pdf(font options[:font_name]) # Set font name
253
+ text_stream << " #{font_size} Tf\n" # set font size and add font operator
254
+ text_stream << "#{options[:font_color].join(' ')} rg\n" # sets the color state
255
+ text_stream << "#{x} #{y} Td\n" # set location for text object
256
+ text_stream << PDFOperations._format_string_to_pdf(text) # insert the string in PDF format
257
+ text_stream << " Tj\n ET\n" # the Text object operator and the End Text marker
258
+ # exit graphic state for text
259
+ text_stream << "Q\nQ\nQ\n"
144
260
  end
261
+ contents << text_stream
145
262
 
146
- # clear graphic states
147
- final_stream << "Q\nQ\nQ\n"
148
- final_stream << "Q\nQ\nQ\n"
149
-
150
- contents << final_stream
151
263
  self
152
264
  end
153
265
 
@@ -236,75 +348,4 @@ end
236
348
 
237
349
 
238
350
 
239
- # # text_box output example
240
- # q
241
- # q
242
- # /GraphiStateName gs
243
- # /DeviceRGB cs
244
- # 0.867 0.867 0.867 scn
245
- # 293.328 747.000 m
246
- # 318.672 747.000 l
247
- # 323.090 747.000 326.672 743.418 326.672 739.000 c
248
- # 326.672 735.800 l
249
- # 326.672 731.382 323.090 727.800 318.672 727.800 c
250
- # 293.328 727.800 l
251
- # 288.910 727.800 285.328 731.382 285.328 735.800 c
252
- # 285.328 739.000 l
253
- # 285.328 743.418 288.910 747.000 293.328 747.000 c
254
- # h
255
- # 293.328 64.200 m
256
- # 318.672 64.200 l
257
- # 323.090 64.200 326.672 60.618 326.672 56.200 c
258
- # 326.672 53.000 l
259
- # 326.672 48.582 323.090 45.000 318.672 45.000 c
260
- # 293.328 45.000 l
261
- # 288.910 45.000 285.328 48.582 285.328 53.000 c
262
- # 285.328 56.200 l
263
- # 285.328 60.618 288.910 64.200 293.328 64.200 c
264
- # h
265
- # f
266
- # 0.000 0.000 0.000 scn
267
- # /DeviceRGB CS
268
- # 1.000 1.000 1.000 SCN
269
-
270
- # 2 Tr
271
- # 0.000 0.000 0.000 scn
272
- # 0.000 0.000 0.000 SCN
273
- # 1.000 1.000 1.000 SCN
274
- # 0.000 0.000 0.000 scn
275
- # 0.000 0.000 0.000 scn
276
- # 0.000 0.000 0.000 SCN
277
-
278
- # BT
279
- # 291.776 733.3119999999999 Td
280
- # /FontName 16 Tf
281
- # [<2d2032202d>] TJ
282
- # ET
283
-
284
- # 1.000 1.000 1.000 SCN
285
- # 0.000 0.000 0.000 scn
286
- # 0.000 0.000 0.000 scn
287
- # 0.000 0.000 0.000 SCN
288
- # 1.000 1.000 1.000 SCN
289
- # 0.000 0.000 0.000 scn
290
- # 0.000 0.000 0.000 scn
291
- # 0.000 0.000 0.000 SCN
292
-
293
- # BT
294
- # 291.776 50.512 Td
295
- # /FontName 16 Tf
296
- # [<2d2032202d>] TJ
297
- # ET
298
-
299
- # 1.000 1.000 1.000 SCN
300
- # 0.000 0.000 0.000 scn
301
-
302
- # 0 Tr
303
- # Q
304
- # Q
305
-
306
-
307
-
308
-
309
-
310
351
 
@@ -8,15 +8,20 @@
8
8
 
9
9
 
10
10
  module CombinePDF
11
- #@private
12
11
  #:nodoc: all
12
+
13
+ # @private
14
+ # This is an internal class. you don't need it.
13
15
  class PDFDecrypt
14
16
 
15
- def initialize objects=[], root_doctionary = {}
17
+ # make a new Decrypt object. requires:
18
+ # objects:: an array containing the encrypted objects.
19
+ # root_dictionary:: the root PDF dictionary, containing the Encrypt dictionary.
20
+ def initialize objects=[], root_dictionary = {}
16
21
  @objects = objects
17
- @encryption_dictionary = root_doctionary[:Encrypt]
22
+ @encryption_dictionary = root_dictionary[:Encrypt]
18
23
  raise "Cannot decrypt an encrypted file without an encryption dictionary!" unless @encryption_dictionary
19
- @root_doctionary = root_doctionary
24
+ @root_dictionary = root_dictionary
20
25
  @padding_key = [ 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41,
21
26
  0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
22
27
  0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
@@ -25,6 +30,25 @@ module CombinePDF
25
30
  @encryption_iv = nil
26
31
  PDFOperations.change_references_to_actual_values @objects, @encryption_dictionary
27
32
  end
33
+
34
+ # call this to start the decryption.
35
+ def decrypt
36
+ raise_encrypted_error @encryption_dictionary unless @encryption_dictionary[:Filter] == :Standard
37
+ @key = set_general_key
38
+ case @encryption_dictionary[:V]
39
+ when 1,2
40
+ warn "trying to decrypt with RC4."
41
+ # raise_encrypted_error
42
+ _perform_decrypt_proc_ @objects, self.method(:decrypt_RC4)
43
+ else
44
+ raise_encrypted_error
45
+ end
46
+ #rebuild stream lengths?
47
+ @objects
48
+ end
49
+
50
+ protected
51
+
28
52
  def set_general_key(password = "")
29
53
  # 1) make sure the initial key is 32 byte long (if no password, uses padding).
30
54
  key = (password.bytes[0..32] + @padding_key)[0..31].pack('C*').force_encoding(Encoding::ASCII_8BIT)
@@ -35,7 +59,7 @@ module CombinePDF
35
59
  key << [@encryption_dictionary[:P]].pack('i')
36
60
  # 4) Pass the first element of the file’s file identifier array
37
61
  # (the value of the ID entry in the document’s trailer dictionary
38
- key << @root_doctionary[:ID][0]
62
+ key << @root_dictionary[:ID][0]
39
63
  # # 4(a) (Security handlers of revision 4 or greater)
40
64
  # # if document metadata is not being encrypted, add 4 bytes with the value 0xFFFFFFFF.
41
65
  if @encryption_dictionary[:R] >= 4
@@ -68,20 +92,6 @@ module CombinePDF
68
92
  end
69
93
  @key
70
94
  end
71
- def decrypt
72
- raise_encrypted_error @encryption_dictionary unless @encryption_dictionary[:Filter] == :Standard
73
- @key = set_general_key
74
- case @encryption_dictionary[:V]
75
- when 1,2
76
- warn "trying to decrypt with RC4."
77
- # raise_encrypted_error
78
- _perform_decrypt_proc_ @objects, self.method(:decrypt_RC4)
79
- else
80
- raise_encrypted_error
81
- end
82
- #rebuild stream lengths?
83
- @objects
84
- end
85
95
  def decrypt_none(encrypted, encrypted_id, encrypted_generation, encrypted_filter)
86
96
  "encrypted"
87
97
  end
@@ -8,17 +8,27 @@
8
8
 
9
9
 
10
10
  module CombinePDF
11
-
12
11
  #@private
13
12
  #:nodoc: all
13
+
14
+ # This is an internal class. you don't need it.
14
15
  module PDFFilter
15
16
  module_function
16
17
 
17
- def deflate_object object = nil
18
+ # deflate / compress an object.
19
+ #
20
+ # <b>isn't supported yet!</b>
21
+ #
22
+ # object:: object to compress.
23
+ # filter:: filter to use.
24
+ def deflate_object object = nil, filter = :none
18
25
  false
19
26
  end
20
27
 
21
- def inflate_object object = nil, filter = :none
28
+ # inflate / decompress an object
29
+ #
30
+ # object:: object to decompress.
31
+ def inflate_object object = nil
22
32
  filter_array = object[:Filter]
23
33
  if filter_array.is_a?(Hash) && filter_array[:is_reference_only]
24
34
  filter_array = filter_array[:referenced_object]
@@ -71,6 +81,9 @@ module CombinePDF
71
81
  object.delete(:Filter)
72
82
  true
73
83
  end
84
+
85
+ protected
86
+
74
87
  def raise_unsupported_error (object = {})
75
88
  raise "Filter #{object} unsupported. couldn't deflate object"
76
89
  end
@@ -5,21 +5,22 @@ module CombinePDF
5
5
  ## These are common functions, used within the different classes
6
6
  ## These functions aren't open to the public.
7
7
  ################################################################
8
+
8
9
  #@private
10
+ # lists the Hash keys used for PDF objects
11
+ #
12
+ # the CombinePDF library doesn't use special classes for its objects (PDFPage class, PDFStream class or anything like that).
13
+ #
14
+ # there is only one PDF class which represents the whole of the PDF file.
15
+ #
16
+ # this Hash lists the private Hash keys that the CombinePDF library uses to
17
+ # differentiate between complex PDF objects.
9
18
  PRIVATE_HASH_KEYS = [:indirect_reference_id, :indirect_generation_number, :raw_stream_content, :is_reference_only, :referenced_object, :indirect_without_dictionary]
10
19
  #@private
11
- LITERAL_STRING_REPLACEMENT_HASH = {
12
- 110 => 10, # "\\n".bytes = [92, 110] "\n".ord = 10
13
- 114 => 13, #r
14
- 116 => 9, #t
15
- 98 => 8, #b
16
- 102 => 255, #f
17
- 40 => 40, #(
18
- 41 => 41, #)
19
- 92 => 92 #\
20
- }
21
- #@private
22
- #:nodoc: all
20
+ #:nodoc: all
21
+
22
+
23
+ # This is an internal class. you don't need it.
23
24
  module PDFOperations
24
25
  module_function
25
26
  def inject_to_page page = {Type: :Page, MediaBox: [0,0,612.0,792.0], Resources: {}, Contents: []}, stream = nil, top = true
@@ -11,15 +11,16 @@
11
11
  module CombinePDF
12
12
 
13
13
 
14
- #######################################################
15
14
  #@private
16
15
  #:nodoc: all
16
+
17
17
  # This is the Parser class.
18
18
  #
19
19
  # It takes PDF data and parses it.
20
20
  #
21
21
  # The information is then used to initialize a PDF object.
22
- #######################################################
22
+ #
23
+ # This is an internal class. you don't need it.
23
24
  class PDFParser
24
25
 
25
26
  # the array containing all the parsed data (PDF Objects)
@@ -30,6 +31,12 @@ module CombinePDF
30
31
  #
31
32
  # they are mainly to used to know if the file is (was) encrypted and to get more details.
32
33
  attr_reader :info_object, :root_object
34
+
35
+ # when creating a parser, it is important to set the data (String) we wish to parse.
36
+ #
37
+ # <b>the data is required and it is not possible to set the data at a later stage</b>
38
+ #
39
+ # string:: the data to be parsed, as a String object.
33
40
  def initialize (string)
34
41
  raise TypeError, "couldn't parse and data, expecting type String" unless string.is_a? String
35
42
  @string_to_parse = string.force_encoding(Encoding::ASCII_8BIT)
@@ -43,7 +50,7 @@ module CombinePDF
43
50
  @scanner = nil
44
51
  end
45
52
 
46
- # parse the data in the parser (set in the initialize / new method)
53
+ # parse the data in the new parser (the data already set through the initialize / new method)
47
54
  def parse
48
55
  return @parsed unless @parsed.empty?
49
56
  @scanner = StringScanner.new @string_to_parse
@@ -114,8 +121,9 @@ module CombinePDF
114
121
  @parsed
115
122
  end
116
123
 
117
- protected
118
-
124
+ # the actual recoursive parsing is done here.
125
+ #
126
+ # this is an internal function, but it was left exposed for posible future features.
119
127
  def _parse_
120
128
  out = []
121
129
  str = ''
@@ -11,26 +11,63 @@
11
11
 
12
12
 
13
13
  module CombinePDF
14
- #######################################################
14
+
15
15
  # PDF class is the PDF object that can save itself to
16
16
  # a file and that can be used as a container for a full
17
- # PDF file data, including version etc'.
17
+ # PDF file data, including version, information etc'.
18
18
  #
19
19
  # PDF objects can be used to combine or to inject data.
20
- # == Combine
20
+ # == Combine/Merge PDF files or Pages
21
21
  # To combine PDF files (or data):
22
22
  # pdf = CombinePDF.new
23
- # pdf << CombinePDF.new "file1.pdf" # one way to combine, very fast.
24
- # CombinePDF.new("file2.pdf").pages.each {|page| pdf << page} # different way to combine, slower.
23
+ # pdf << CombinePDF.new("file1.pdf") # one way to combine, very fast.
24
+ # pdf << CombinePDF.new("file2.pdf")
25
25
  # pdf.save "combined.pdf"
26
- # == Stamp / Watermark
27
- # To combine PDF files (or data), first create the stamp from a PDF file:
28
- # stamp_pdf_file = CombinePDF.new "stamp_pdf_file.pdf"
29
- # stamp_page = stamp_pdf_file.pages[0]
30
- # After the stamp was created, inject to PDF pages:
31
- # pdf = CombinePDF.new "file1.pdf"
32
- # pdf.pages.each {|page| page << stamp_page} # notice the << operator is on a page and not a PDF object.
33
- #######################################################
26
+ # or even a one liner:
27
+ # (CombinePDF.new("file1.pdf") << CombinePDF.new("file2.pdf") << CombinePDF.new("file3.pdf")).save("combined.pdf")
28
+ # you can also add just odd or even pages:
29
+ # pdf = CombinePDF.new
30
+ # i = 0
31
+ # CombinePDF.new("file.pdf").pages.each do |page
32
+ # i += 1
33
+ # pdf << page if i.even?
34
+ # end
35
+ # pdf.save "even_pages.pdf"
36
+ # notice that adding all the pages one by one is slower then adding the whole file.
37
+ # == Add content to existing pages (Stamp / Watermark)
38
+ # To add content to existing PDF pages, first import the new content from an existing PDF file.
39
+ # after that, add the content to each of the pages in your existing PDF.
40
+ #
41
+ # in this example, we will add a company logo to each page:
42
+ # company_logo = CombinePDF.new("company_logo.pdf").pages[0]
43
+ # pdf = CombinePDF.new "content_file.pdf"
44
+ # pdf.pages.each {|page| page << company_logo} # notice the << operator is on a page and not a PDF object.
45
+ # pdf.save "content_with_logo.pdf"
46
+ # Notice the << operator is on a page and not a PDF object. The << operator acts differently on PDF objects and on Pages.
47
+ #
48
+ # The << operator defaults to secure injection by renaming references to avoid conflics. For overlaying pages using compressed data that might not be editable (due to limited filter support), you can use:
49
+ # pdf.pages(nil, false).each {|page| page << stamp_page}
50
+ #
51
+ #
52
+ # Notice that page objects are Hash class objects and the << operator was added to the Page instances without altering the class.
53
+ #
54
+ # == Page Numbering
55
+ # adding page numbers to a PDF object or file is as simple as can be:
56
+ # pdf = CombinePDF.new "file_to_number.pdf"
57
+ # pdf.number_pages
58
+ # pdf.save "file_with_numbering.pdf"
59
+ #
60
+ # numbering can be done with many different options, with different formating, with or without a box object, and even with opacity values.
61
+ #
62
+ # == Loading PDF data
63
+ # Loading PDF data can be done from file system or directly from the memory.
64
+ #
65
+ # Loading data from a file is easy:
66
+ # pdf = CombinePDF.new("file.pdf")
67
+ # you can also parse PDF files from memory:
68
+ # pdf_data = IO.read 'file.pdf' # for this demo, load a file to memory
69
+ # pdf = CombinePDF.parse(pdf_data)
70
+ # Loading from the memory is especially effective for importing PDF data recieved through the internet or from a different authoring library such as Prawn.
34
71
  class PDF
35
72
  # the objects attribute is an Array containing all the PDF sub-objects for te class.
36
73
  attr_reader :objects
@@ -144,7 +181,8 @@ module CombinePDF
144
181
  # the content added is compressed using unsupported filters or options.
145
182
  #
146
183
  # the default is for the << operator to attempt a secure copy, by attempting to rename the content references and avoiding conflicts.
147
- # because of not all PDF files are created equal (some might have formating errors or differences), it is imposiible to learn if the attempt wa successful.
184
+ # because not all PDF files are created equal (some might have formating errors or variations),
185
+ # it is imposiible to learn if the attempt was successful.
148
186
  #
149
187
  # (page objects are Hash class objects. the << operator is added to the specific instances without changing the class)
150
188
  #
@@ -240,6 +278,83 @@ module CombinePDF
240
278
  return self #return self object for injection chaining (pdf << page << page << page)
241
279
  end
242
280
 
281
+ # and page numbers to the PDF
282
+ # options:: a Hash of options setting the behavior and format of the page numbers:
283
+ # - :number_format a string representing the format for page number. defaults to ' - %d - '.
284
+ # - :number_location an Array containing the location for the page numbers, can be :top, :buttom, :top_left, :top_right, :bottom_left, :bottom_right. defaults to [:top, :buttom].
285
+ # - :start_at a Fixnum that sets the number for first page number. defaults to 1.
286
+ # - :margin_from_height a number (PDF points) for the top and buttom margins. defaults to 45.
287
+ # - :margin_from_side a number (PDF points) for the left and right margins. defaults to 15.
288
+ # also take all the options for PDFWriter.textbox.
289
+ # defaults to font_name: :Helvetica, font_size: 12 and no box (:border_width => 0, :box_color => nil).
290
+ def number_pages(options = {})
291
+ opt = {
292
+ number_format: ' - %d - ',
293
+ number_location: [:top, :bottom],
294
+ start_at: 1,
295
+ font_size: 12,
296
+ font_name: :Helvetica,
297
+ margin_from_height: 45,
298
+ margin_from_side: 15
299
+ }
300
+ opt.update options
301
+ page_number = opt[:start_at]
302
+ pages.each do |page|
303
+ # create a "stamp" PDF page with the same size as the target page
304
+ mediabox = page[:MediaBox]
305
+ stamp = PDFWriter.new mediabox
306
+ # set the visible dimentions to the CropBox, if it exists.
307
+ cropbox = page[:CropBox]
308
+ mediabox = cropbox if cropbox
309
+ # set stamp text
310
+ text = opt[:number_format] % page_number
311
+ # compute locations for text boxes
312
+ text_dimantions = stamp.dimentions_of( text, opt[:font_name], opt[:font_size] )
313
+ box_width = text_dimantions[0] * 1.2
314
+ box_height = text_dimantions[1] * 2
315
+ opt[:length] ||= box_width
316
+ opt[:height] ||= box_height
317
+ from_height = 45
318
+ from_side = 15
319
+ page_width = mediabox[2]
320
+ page_height = mediabox[3]
321
+ center_position = (page_width - box_width)/2
322
+ left_position = from_side
323
+ right_position = page_width - from_side - box_width
324
+ top_position = page_height - from_height
325
+ buttom_position = from_height + box_height
326
+ x = center_position
327
+ y = top_position
328
+ if opt[:number_location].include? :top
329
+ stamp.textbox text, {x: x, y: y }.merge(opt)
330
+ end
331
+ y = buttom_position #bottom position
332
+ if opt[:number_location].include? :bottom
333
+ stamp.textbox text, {x: x, y: y }.merge(opt)
334
+ end
335
+ y = top_position #top position
336
+ x = left_position # left posotion
337
+ if opt[:number_location].include? :top_left
338
+ stamp.textbox text, {x: x, y: y }.merge(opt)
339
+ end
340
+ y = buttom_position #bottom position
341
+ if opt[:number_location].include? :bottom_left
342
+ stamp.textbox text, {x: x, y: y }.merge(opt)
343
+ end
344
+ x = right_position # right posotion
345
+ y = top_position #top position
346
+ if opt[:number_location].include? :top_right
347
+ stamp.textbox text, {x: x, y: y }.merge(opt)
348
+ end
349
+ y = buttom_position #bottom position
350
+ if opt[:number_location].include? :bottom_right
351
+ stamp.textbox text, {x: x, y: y }.merge(opt)
352
+ end
353
+ page << stamp
354
+ page_number += 1
355
+ end
356
+ end
357
+
243
358
  # get the title for the pdf
244
359
  # The title is stored in the information dictionary and isn't required
245
360
  def title
@@ -264,7 +379,11 @@ module CombinePDF
264
379
  @info[:Author] = new_author
265
380
  end
266
381
  end
267
- class PDF #:nodoc: all
382
+
383
+ #:nodoc: all
384
+
385
+
386
+ class PDF
268
387
  # @private
269
388
  # Some PDF objects contain references to other PDF objects.
270
389
  #
@@ -296,7 +415,9 @@ module CombinePDF
296
415
  def each_object(&block)
297
416
  PDFOperations._each_object(@objects, &block)
298
417
  end
418
+
299
419
  protected
420
+
300
421
  # @private
301
422
  # this function returns all the Page objects - regardless of order and even if not cataloged
302
423
  # could be used for finding "lost" pages... but actually rather useless.
@@ -1,26 +1,5 @@
1
1
  module CombinePDF
2
2
  class PDFWriter < Hash
3
- protected
4
- METRICS_DICTIONARY = {
5
- :"Times-Roman" => TIMES_ROMAN_METRICS,
6
- :"Times-Bold" => TIMES_BOLD_METRICS,
7
- :"Times-Italic" => TIMES_ITALIC_METRICS,
8
- :"Times-BoldItalic" => TIMES_BOLDITALIC_METRICS,
9
- :Helvetica => HELVETICA_METRICS,
10
- :"Helvetica-Bold" => HELVETICA_BOLD_METRICS,
11
- :"Helvetica-BoldOblique"=> HELVETICA_BOLDOBLIQUE_METRICS,
12
- :"Helvetica-Oblique" => HELVETICA_OBLIQUE_METRICS,
13
- :Courier => COURIER_METRICS,
14
- :"Courier-Bold" => COURIER_BOLD_METRICS,
15
- :"Courier-Oblique" => COURIER_OBLIQUE_METRICS,
16
- :"Courier-BoldOblique" => COURIER_BOLDOBLIQUE_METRICS,
17
- :Symbol => SYMBOL_METRICS,
18
- :ZapfDingbats => ZAPFDINGBATS_METRICS
19
- }
20
- def self.get_metrics(font_name)
21
- METRICS_DICTIONARY[font_name]
22
- end
23
-
24
3
  # This function calculates the dimentions of a string in a PDF.
25
4
  #
26
5
  # UNICODE SUPPORT IS MISSING!
@@ -46,6 +25,29 @@ module CombinePDF
46
25
  end
47
26
  [width.to_f/1000*size, height.to_f/1000*size]
48
27
  end
28
+
29
+ protected
30
+
31
+ METRICS_DICTIONARY = {
32
+ :"Times-Roman" => TIMES_ROMAN_METRICS,
33
+ :"Times-Bold" => TIMES_BOLD_METRICS,
34
+ :"Times-Italic" => TIMES_ITALIC_METRICS,
35
+ :"Times-BoldItalic" => TIMES_BOLDITALIC_METRICS,
36
+ :Helvetica => HELVETICA_METRICS,
37
+ :"Helvetica-Bold" => HELVETICA_BOLD_METRICS,
38
+ :"Helvetica-BoldOblique"=> HELVETICA_BOLDOBLIQUE_METRICS,
39
+ :"Helvetica-Oblique" => HELVETICA_OBLIQUE_METRICS,
40
+ :Courier => COURIER_METRICS,
41
+ :"Courier-Bold" => COURIER_BOLD_METRICS,
42
+ :"Courier-Oblique" => COURIER_OBLIQUE_METRICS,
43
+ :"Courier-BoldOblique" => COURIER_BOLDOBLIQUE_METRICS,
44
+ :Symbol => SYMBOL_METRICS,
45
+ :ZapfDingbats => ZAPFDINGBATS_METRICS
46
+ }
47
+ def self.get_metrics(font_name)
48
+ METRICS_DICTIONARY[font_name]
49
+ end
50
+
49
51
  # this method returns the size for which the text fits the requested metrices
50
52
  # the size is type Float and is rather exact
51
53
  # if the text cannot fit such a small place, returns zero (0).
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: combine_pdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Boaz Segev
@@ -26,7 +26,8 @@ dependencies:
26
26
  - !ruby/object:Gem::Version
27
27
  version: 0.1.5
28
28
  description: A nifty gem, in pure Ruby, to parse PDF files and combine (merge) them
29
- with other PDF files, watermark them or stamp them (all using the PDF file format).
29
+ with other PDF files, number the pages, watermark them or stamp them (all using
30
+ the PDF file format).
30
31
  email: bsegev@gmail.com
31
32
  executables: []
32
33
  extensions: []