combine_pdf 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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: []