combine_pdf 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/combine_pdf.rb +27 -375
- data/lib/combine_pdf/combine_pdf_basic_writer.rb +249 -61
- data/lib/combine_pdf/combine_pdf_filter.rb +8 -1
- data/lib/combine_pdf/combine_pdf_operations.rb +381 -0
- data/lib/combine_pdf/combine_pdf_pdf.rb +31 -4
- data/lib/combine_pdf/font_metrics/courier-bold_metrics.rb +2211 -0
- data/lib/combine_pdf/font_metrics/courier-boldoblique_metrics.rb +2211 -0
- data/lib/combine_pdf/font_metrics/courier-oblique_metrics.rb +2211 -0
- data/lib/combine_pdf/font_metrics/courier_metrics.rb +2211 -0
- data/lib/combine_pdf/font_metrics/helvetica-bold_metrics.rb +2211 -0
- data/lib/combine_pdf/font_metrics/helvetica-boldoblique_metrics.rb +2211 -0
- data/lib/combine_pdf/font_metrics/helvetica-oblique_metrics.rb +2211 -0
- data/lib/combine_pdf/font_metrics/helvetica_metrics.rb +2211 -0
- data/lib/combine_pdf/font_metrics/metrics_dictionary.rb +69 -0
- data/lib/combine_pdf/font_metrics/symbol_metrics.rb +1336 -0
- data/lib/combine_pdf/font_metrics/times-bold_metrics.rb +2212 -0
- data/lib/combine_pdf/font_metrics/times-bolditalic_metrics.rb +2212 -0
- data/lib/combine_pdf/font_metrics/times-italic_metrics.rb +2212 -0
- data/lib/combine_pdf/font_metrics/times-roman_metrics.rb +2212 -0
- data/lib/combine_pdf/font_metrics/zapfdingbats_metrics.rb +1421 -0
- metadata +17 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 169d63fc09f86f09a633e199582c65212fbcd158
|
4
|
+
data.tar.gz: 6f5d94234603827a33100c77e4b66ac41492c60c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ffb49820460e29f7d6d857fe6f66af7a9b0f97edae28a2ab1caa814eb5e84d521f91b76e091b2b33fe36f716684253a30fc51cc777e8696c5ee09ed21ebae0ae
|
7
|
+
data.tar.gz: fb6e175436280fd64499684151f29c7cf9254ca21be6a0052e42f80c6d29ede70ed982afbf7aef07fd31cbcf88c460f9eb1a84bea3f363a74ac78d3bb5bc44e2
|
data/lib/combine_pdf.rb
CHANGED
@@ -1,10 +1,33 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
require 'zlib'
|
3
|
+
require 'securerandom'
|
3
4
|
require 'strscan'
|
4
|
-
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
5
|
+
|
6
|
+
require "combine_pdf/combine_pdf_operations.rb"
|
7
|
+
require "combine_pdf/combine_pdf_basic_writer.rb"
|
8
|
+
require "combine_pdf/combine_pdf_decrypt.rb"
|
9
|
+
require "combine_pdf/combine_pdf_filter.rb"
|
10
|
+
require "combine_pdf/combine_pdf_parser.rb"
|
11
|
+
require "combine_pdf/combine_pdf_pdf.rb"
|
12
|
+
|
13
|
+
require "combine_pdf/font_metrics/courier-bold_metrics.rb"
|
14
|
+
require "combine_pdf/font_metrics/courier-boldoblique_metrics.rb"
|
15
|
+
require "combine_pdf/font_metrics/courier-oblique_metrics.rb"
|
16
|
+
require "combine_pdf/font_metrics/courier_metrics.rb"
|
17
|
+
require "combine_pdf/font_metrics/helvetica-bold_metrics.rb"
|
18
|
+
require "combine_pdf/font_metrics/helvetica-boldoblique_metrics.rb"
|
19
|
+
require "combine_pdf/font_metrics/helvetica-oblique_metrics.rb"
|
20
|
+
require "combine_pdf/font_metrics/helvetica_metrics.rb"
|
21
|
+
require "combine_pdf/font_metrics/symbol_metrics.rb"
|
22
|
+
require "combine_pdf/font_metrics/times-bold_metrics.rb"
|
23
|
+
require "combine_pdf/font_metrics/times-bolditalic_metrics.rb"
|
24
|
+
require "combine_pdf/font_metrics/times-italic_metrics.rb"
|
25
|
+
require "combine_pdf/font_metrics/times-roman_metrics.rb"
|
26
|
+
require "combine_pdf/font_metrics/zapfdingbats_metrics.rb"
|
27
|
+
|
28
|
+
require "combine_pdf/font_metrics/metrics_dictionary.rb"
|
29
|
+
|
30
|
+
|
8
31
|
|
9
32
|
|
10
33
|
# This is a pure ruby library to merge PDF files.
|
@@ -74,378 +97,7 @@ module CombinePDF
|
|
74
97
|
end
|
75
98
|
end
|
76
99
|
|
77
|
-
module CombinePDF
|
78
|
-
|
79
|
-
#:nodoc: all
|
80
|
-
################################################################
|
81
|
-
## These are common functions, used within the different classes
|
82
|
-
## These functions aren't open to the public.
|
83
|
-
################################################################
|
84
|
-
#@private
|
85
|
-
PRIVATE_HASH_KEYS = [:indirect_reference_id, :indirect_generation_number, :raw_stream_content, :is_reference_only, :referenced_object, :indirect_without_dictionary]
|
86
|
-
#@private
|
87
|
-
LITERAL_STRING_REPLACEMENT_HASH = {
|
88
|
-
110 => 10, # "\\n".bytes = [92, 110] "\n".ord = 10
|
89
|
-
114 => 13, #r
|
90
|
-
116 => 9, #t
|
91
|
-
98 => 8, #b
|
92
|
-
102 => 255, #f
|
93
|
-
40 => 40, #(
|
94
|
-
41 => 41, #)
|
95
|
-
92 => 92 #\
|
96
|
-
}
|
97
|
-
#@private
|
98
|
-
#:nodoc: all
|
99
|
-
module PDFOperations
|
100
|
-
module_function
|
101
|
-
def inject_to_page page = {Type: :Page, MediaBox: [0,0,612.0,792.0], Resources: {}, Contents: []}, stream = nil, top = true
|
102
|
-
# make sure both the page reciving the new data and the injected page are of the correct data type.
|
103
|
-
return false unless page.is_a?(Hash) && stream.is_a?(Hash)
|
104
|
-
|
105
|
-
# following the reference chain and assigning a pointer to the correct Resouces object.
|
106
|
-
# (assignments of Strings, Arrays and Hashes are pointers in Ruby, unless the .dup method is called)
|
107
|
-
original_resources = page[:Resources]
|
108
|
-
if original_resources[:is_reference_only]
|
109
|
-
original_resources = original_resources[:referenced_object]
|
110
|
-
raise "Couldn't tap into resources dictionary, as it is a reference and isn't linked." unless original_resources
|
111
|
-
end
|
112
|
-
original_contents = page[:Contents]
|
113
|
-
original_contents = [original_contents] unless original_contents.is_a? Array
|
114
|
-
|
115
|
-
stream_resources = stream[:Resources]
|
116
|
-
if stream_resources[:is_reference_only]
|
117
|
-
stream_resources = stream_resources[:referenced_object]
|
118
|
-
raise "Couldn't tap into resources dictionary, as it is a reference and isn't linked." unless stream_resources
|
119
|
-
end
|
120
|
-
stream_contents = stream[:Contents]
|
121
|
-
stream_contents = [stream_contents] unless stream_contents.is_a? Array
|
122
|
-
|
123
|
-
# collect keys as objects - this is to make sure that
|
124
|
-
# we are working on the actual resource data, rather then references
|
125
|
-
flatten_resources_dictionaries stream_resources
|
126
|
-
flatten_resources_dictionaries original_resources
|
127
|
-
|
128
|
-
# injecting each of the values in the injected Page
|
129
|
-
stream_resources.each do |key, new_val|
|
130
|
-
unless PRIVATE_HASH_KEYS.include? key # keep CombinePDF structual data intact.
|
131
|
-
if original_resources[key].nil?
|
132
|
-
original_resources[key] = new_val
|
133
|
-
elsif original_resources[key].is_a?(Hash) && new_val.is_a?(Hash)
|
134
|
-
new_val.update original_resources[key] # make sure the old values are respected
|
135
|
-
original_resources[key].update new_val # transfer old and new values to the injected page
|
136
|
-
end #Do nothing if array - ot is the PROC array, which is an issue
|
137
|
-
end
|
138
|
-
end
|
139
|
-
original_resources[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI] # this was recommended by the ISO. 32000-1:2008
|
140
|
-
|
141
|
-
if top # if this is a stamp (overlay)
|
142
|
-
page[:Contents] = original_contents
|
143
|
-
page[:Contents].push *stream_contents
|
144
|
-
else #if this was a watermark (underlay? would be lost if the page was scanned, as white might not be transparent)
|
145
|
-
page[:Contents] = stream_contents
|
146
|
-
page[:Contents].push *original_contents
|
147
|
-
end
|
148
|
-
|
149
|
-
page
|
150
|
-
end
|
151
|
-
# copy_and_secure_for_injection(page)
|
152
|
-
# - page is a page in the pages array, i.e.
|
153
|
-
# pdf.pages[0]
|
154
|
-
# takes a page object and:
|
155
|
-
#
|
156
|
-
# makes a deep copy of the page (Ruby defaults to pointers, so this will copy the memory).
|
157
|
-
#
|
158
|
-
# then it will rewrite the content stream with renamed resources, so as to avoid name conflicts.
|
159
|
-
def copy_and_secure_for_injection(page)
|
160
|
-
# copy page
|
161
|
-
new_page = create_deep_copy page
|
162
|
-
|
163
|
-
# initiate dictionary from old names to new names
|
164
|
-
names_dictionary = {}
|
165
|
-
|
166
|
-
# itirate through all keys that are name objects and give them new names (add to dic)
|
167
|
-
# this should be done for every dictionary in :Resources
|
168
|
-
# this is a few steps stage:
|
169
|
-
|
170
|
-
# 1. get resources object
|
171
|
-
resources = new_page[:Resources]
|
172
|
-
if resources[:is_reference_only]
|
173
|
-
resources = resources[:referenced_object]
|
174
|
-
raise "Couldn't tap into resources dictionary, as it is a reference and isn't linked." unless resources
|
175
|
-
end
|
176
|
-
|
177
|
-
# 2. establich direct access to dictionaries and remove reference values
|
178
|
-
flatten_resources_dictionaries resources
|
179
|
-
|
180
|
-
# 3. travel every dictionary to pick up names (keys), change them and add them to the dictionary
|
181
|
-
resources.each do |k,v|
|
182
|
-
if v.is_a?(Hash)
|
183
|
-
new_dictionary = {}
|
184
|
-
v.each do |old_key, value|
|
185
|
-
new_key = ("CombinePDF" + SecureRandom.urlsafe_base64(9)).to_sym
|
186
|
-
names_dictionary[old_key] = new_key
|
187
|
-
new_dictionary[new_key] = value
|
188
|
-
end
|
189
|
-
resources[k] = new_dictionary
|
190
|
-
end
|
191
|
-
end
|
192
100
|
|
193
|
-
# now that we have replaced the names in the resources dictionaries,
|
194
|
-
# it is time to replace the names inside the stream
|
195
|
-
# we will need to make sure we have access to the stream injected
|
196
|
-
# we will user PDFFilter.inflate_object
|
197
|
-
(new_page[:Contents].is_a?(Array) ? new_page[:Contents] : [new_page[:Contents] ]).each do |c|
|
198
|
-
stream = c[:referenced_object]
|
199
|
-
PDFFilter.inflate_object stream
|
200
|
-
names_dictionary.each do |old_key, new_key|
|
201
|
-
stream[:raw_stream_content].gsub! _object_to_pdf(old_key), _object_to_pdf(new_key) ##### PRAY(!) that the parsed datawill be correctly reproduced!
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
new_page
|
206
|
-
end
|
207
|
-
def flatten_resources_dictionaries(resources)
|
208
|
-
resources.each do |k,v|
|
209
|
-
if v.is_a?(Hash) && v[:is_reference_only]
|
210
|
-
if v[:referenced_object]
|
211
|
-
resources[k] = resources[k][:referenced_object].dup
|
212
|
-
resources[k].delete(:indirect_reference_id)
|
213
|
-
resources[k].delete(:indirect_generation_number)
|
214
|
-
elsif v[:indirect_without_dictionary]
|
215
|
-
resources[k] = resources[k][:indirect_without_dictionary]
|
216
|
-
end
|
217
|
-
end
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
|
222
|
-
# Ruby normally assigns pointes.
|
223
|
-
# noramlly:
|
224
|
-
# a = [1,2,3] # => [1,2,3]
|
225
|
-
# b = a # => [1,2,3]
|
226
|
-
# a << 4 # => [1,2,3,4]
|
227
|
-
# b # => [1,2,3,4]
|
228
|
-
# This method makes sure that the memory is copied instead of a pointer assigned.
|
229
|
-
# this works using recursion, so that arrays and hashes within arrays and hashes are also copied and not pointed to.
|
230
|
-
# One needs to be careful of infinit loops using this function.
|
231
|
-
def create_deep_copy object
|
232
|
-
if object.is_a?(Array)
|
233
|
-
return object.map { |e| create_deep_copy e }
|
234
|
-
elsif object.is_a?(Hash)
|
235
|
-
return {}.tap {|out| object.each {|k,v| out[create_deep_copy(k)] = create_deep_copy(v) unless k == :Parent} }
|
236
|
-
elsif object.is_a?(String)
|
237
|
-
return object.dup
|
238
|
-
else
|
239
|
-
return object # objects that aren't Strings, Arrays or Hashes (such as Symbols and Fixnums) aren't pointers in Ruby and are always copied.
|
240
|
-
end
|
241
|
-
end
|
242
|
-
def get_refernced_object(objects_array = [], reference_hash = {})
|
243
|
-
objects_array.each do |stored_object|
|
244
|
-
return stored_object if ( stored_object.is_a?(Hash) &&
|
245
|
-
reference_hash[:indirect_reference_id] == stored_object[:indirect_reference_id] &&
|
246
|
-
reference_hash[:indirect_generation_number] == stored_object[:indirect_generation_number] )
|
247
|
-
end
|
248
|
-
warn "didn't find reference #{reference_hash}"
|
249
|
-
nil
|
250
|
-
end
|
251
|
-
def change_references_to_actual_values(objects_array = [], hash_with_references = {})
|
252
|
-
hash_with_references.each do |k,v|
|
253
|
-
if v.is_a?(Hash) && v[:is_reference_only]
|
254
|
-
hash_with_references[k] = PDFOperations.get_refernced_object( objects_array, v)
|
255
|
-
hash_with_references[k] = hash_with_references[k][:indirect_without_dictionary] if hash_with_references[k].is_a?(Hash) && hash_with_references[k][:indirect_without_dictionary]
|
256
|
-
warn "Couldn't connect all values from references - didn't find reference #{hash_with_references}!!!" if hash_with_references[k] == nil
|
257
|
-
hash_with_references[k] = v unless hash_with_references[k]
|
258
|
-
end
|
259
|
-
end
|
260
|
-
hash_with_references
|
261
|
-
end
|
262
|
-
def change_connected_references_to_actual_values(hash_with_references = {})
|
263
|
-
if hash_with_references.is_a?(Hash)
|
264
|
-
hash_with_references.each do |k,v|
|
265
|
-
if v.is_a?(Hash) && v[:is_reference_only]
|
266
|
-
if v[:indirect_without_dictionary]
|
267
|
-
hash_with_references[k] = v[:indirect_without_dictionary]
|
268
|
-
elsif v[:referenced_object]
|
269
|
-
hash_with_references[k] = v[:referenced_object]
|
270
|
-
else
|
271
|
-
raise "Cannot change references to values, as they are disconnected!"
|
272
|
-
end
|
273
|
-
end
|
274
|
-
end
|
275
|
-
hash_with_references.each {|k, v| change_connected_references_to_actual_values(v) if v.is_a?(Hash) || v.is_a?(Array)}
|
276
|
-
elsif hash_with_references.is_a?(Array)
|
277
|
-
hash_with_references.each {|item| change_connected_references_to_actual_values(item) if item.is_a?(Hash) || item.is_a?(Array)}
|
278
|
-
end
|
279
|
-
hash_with_references
|
280
|
-
end
|
281
|
-
def connect_references_and_actual_values(objects_array = [], hash_with_references = {})
|
282
|
-
ret = true
|
283
|
-
hash_with_references.each do |k,v|
|
284
|
-
if v.is_a?(Hash) && v[:is_reference_only]
|
285
|
-
ref_obj = PDFOperations.get_refernced_object( objects_array, v)
|
286
|
-
hash_with_references[k] = ref_obj[:indirect_without_dictionary] if ref_obj.is_a?(Hash) && ref_obj[:indirect_without_dictionary]
|
287
|
-
ret = false
|
288
|
-
end
|
289
|
-
end
|
290
|
-
ret
|
291
|
-
end
|
292
|
-
|
293
|
-
|
294
|
-
def _each_object(object, limit_references = true, first_call = true, &block)
|
295
|
-
# #####################
|
296
|
-
# ## v.1.2 needs optimazation
|
297
|
-
# case
|
298
|
-
# when object.is_a?(Array)
|
299
|
-
# object.each {|obj| _each_object(obj, limit_references, &block)}
|
300
|
-
# when object.is_a?(Hash)
|
301
|
-
# yield(object)
|
302
|
-
# object.each do |k,v|
|
303
|
-
# unless (limit_references && k == :referenced_object)
|
304
|
-
# unless k == :Parent
|
305
|
-
# _each_object(v, limit_references, &block)
|
306
|
-
# end
|
307
|
-
# end
|
308
|
-
# end
|
309
|
-
# end
|
310
|
-
#####################
|
311
|
-
## v.2.1 needs optimazation
|
312
|
-
## version 2.1 is slightly faster then v.1.2
|
313
|
-
@already_visited = [] if first_call
|
314
|
-
unless limit_references
|
315
|
-
@already_visited << object.object_id
|
316
|
-
end
|
317
|
-
case
|
318
|
-
when object.is_a?(Array)
|
319
|
-
object.each {|obj| _each_object(obj, limit_references, false, &block)}
|
320
|
-
when object.is_a?(Hash)
|
321
|
-
yield(object)
|
322
|
-
unless limit_references && object[:is_reference_only]
|
323
|
-
object.each do |k,v|
|
324
|
-
_each_object(v, limit_references, false, &block) unless @already_visited.include? v.object_id
|
325
|
-
end
|
326
|
-
end
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
# Formats an object into PDF format. This is used my the PDF object to format the PDF file and it is used in the secure injection which is still being developed.
|
333
|
-
def _object_to_pdf object
|
334
|
-
case
|
335
|
-
when object.nil?
|
336
|
-
return "null"
|
337
|
-
when object.is_a?(String)
|
338
|
-
return _format_string_to_pdf object
|
339
|
-
when object.is_a?(Symbol)
|
340
|
-
return _format_name_to_pdf object
|
341
|
-
when object.is_a?(Array)
|
342
|
-
return _format_array_to_pdf object
|
343
|
-
when object.is_a?(Fixnum), object.is_a?(Float), object.is_a?(TrueClass), object.is_a?(FalseClass)
|
344
|
-
return object.to_s + " "
|
345
|
-
when object.is_a?(Hash)
|
346
|
-
return _format_hash_to_pdf object
|
347
|
-
else
|
348
|
-
return ''
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
def _format_string_to_pdf(object)
|
353
|
-
if @string_output == :literal #if format is set to Literal
|
354
|
-
#### can be better...
|
355
|
-
replacement_hash = {
|
356
|
-
"\x0A" => "\\n",
|
357
|
-
"\x0D" => "\\r",
|
358
|
-
"\x09" => "\\t",
|
359
|
-
"\x08" => "\\b",
|
360
|
-
"\xFF" => "\\f",
|
361
|
-
"\x28" => "\\(",
|
362
|
-
"\x29" => "\\)",
|
363
|
-
"\x5C" => "\\\\"
|
364
|
-
}
|
365
|
-
32.times {|i| replacement_hash[i.chr] ||= "\\#{i}"}
|
366
|
-
(256-128).times {|i| replacement_hash[(i + 127).chr] ||= "\\#{i+127}"}
|
367
|
-
("(" + ([].tap {|out| object.bytes.each {|byte| replacement_hash[ byte.chr ] ? (replacement_hash[ byte.chr ].bytes.each {|b| out << b}) : out << byte } }).pack('C*') + ")").force_encoding(Encoding::ASCII_8BIT)
|
368
|
-
else
|
369
|
-
# A hexadecimal string shall be written as a sequence of hexadecimal digits (0–9 and either A–F or a–f)
|
370
|
-
# encoded as ASCII characters and enclosed within angle brackets (using LESS-THAN SIGN (3Ch) and GREATER- THAN SIGN (3Eh)).
|
371
|
-
("<" + object.unpack('H*')[0] + ">").force_encoding(Encoding::ASCII_8BIT)
|
372
|
-
end
|
373
|
-
end
|
374
|
-
def _format_name_to_pdf(object)
|
375
|
-
# a name object is an atomic symbol uniquely defined by a sequence of ANY characters (8-bit values) except null (character code 0).
|
376
|
-
# print name as a simple string. all characters between ~ and ! (except #) can be raw
|
377
|
-
# the rest will have a number sign and their HEX equivalant
|
378
|
-
# from the standard:
|
379
|
-
# When writing a name in a PDF file, a SOLIDUS (2Fh) (/) shall be used to introduce a name. The SOLIDUS is not part of the name but is a prefix indicating that what follows is a sequence of characters representing the name in the PDF file and shall follow these rules:
|
380
|
-
# a) A NUMBER SIGN (23h) (#) in a name shall be written by using its 2-digit hexadecimal code (23), preceded by the NUMBER SIGN.
|
381
|
-
# b) Any character in a name that is a regular character (other than NUMBER SIGN) shall be written as itself or by using its 2-digit hexadecimal code, preceded by the NUMBER SIGN.
|
382
|
-
# c) Any character that is not a regular character shall be written using its 2-digit hexadecimal code, preceded by the NUMBER SIGN only.
|
383
|
-
# [0x00, 0x09, 0x0a, 0x0c, 0x0d, 0x20, 0x28, 0x29, 0x3c, 0x3e, 0x5b, 0x5d, 0x7b, 0x7d, 0x2f, 0x25]
|
384
|
-
out = object.to_s.bytes.map do |b|
|
385
|
-
case b
|
386
|
-
when 0..15
|
387
|
-
'#0' + b.to_s(16)
|
388
|
-
when 15..32, 35, 37, 40, 41, 47, 60, 62, 91, 93, 123, 125, 127..256
|
389
|
-
'#' + b.to_s(16)
|
390
|
-
else
|
391
|
-
b.chr
|
392
|
-
end
|
393
|
-
end
|
394
|
-
"/" + out.join()
|
395
|
-
end
|
396
|
-
def _format_array_to_pdf(object)
|
397
|
-
# An array shall be written as a sequence of objects enclosed in SQUARE BRACKETS (using LEFT SQUARE BRACKET (5Bh) and RIGHT SQUARE BRACKET (5Dh)).
|
398
|
-
# EXAMPLE [549 3.14 false (Ralph) /SomeName]
|
399
|
-
("[" + (object.collect {|item| _object_to_pdf(item)}).join(' ') + "]").force_encoding(Encoding::ASCII_8BIT)
|
400
|
-
|
401
|
-
end
|
402
|
-
|
403
|
-
def _format_hash_to_pdf(object)
|
404
|
-
# if the object is only a reference:
|
405
|
-
# special conditions apply, and there is only the setting of the reference (if needed) and output
|
406
|
-
if object[:is_reference_only]
|
407
|
-
#
|
408
|
-
if object[:referenced_object] && object[:referenced_object].is_a?(Hash)
|
409
|
-
object[:indirect_reference_id] = object[:referenced_object][:indirect_reference_id]
|
410
|
-
object[:indirect_generation_number] = object[:referenced_object][:indirect_generation_number]
|
411
|
-
end
|
412
|
-
object[:indirect_reference_id] ||= 0
|
413
|
-
object[:indirect_generation_number] ||= 0
|
414
|
-
return "#{object[:indirect_reference_id].to_s} #{object[:indirect_generation_number].to_s} R".force_encoding(Encoding::ASCII_8BIT)
|
415
|
-
end
|
416
|
-
|
417
|
-
# if the object is indirect...
|
418
|
-
out = []
|
419
|
-
if object[:indirect_reference_id]
|
420
|
-
object[:indirect_reference_id] ||= 0
|
421
|
-
object[:indirect_generation_number] ||= 0
|
422
|
-
out << "#{object[:indirect_reference_id].to_s} #{object[:indirect_generation_number].to_s} obj\n".force_encoding(Encoding::ASCII_8BIT)
|
423
|
-
if object[:indirect_without_dictionary]
|
424
|
-
out << _object_to_pdf(object[:indirect_without_dictionary])
|
425
|
-
out << "\nendobj\n"
|
426
|
-
return out.join().force_encoding(Encoding::ASCII_8BIT)
|
427
|
-
end
|
428
|
-
end
|
429
|
-
# correct stream length, if the object is a stream.
|
430
|
-
object[:Length] = object[:raw_stream_content].bytesize if object[:raw_stream_content]
|
431
|
-
|
432
|
-
# if the object is not a simple object, it is a dictionary
|
433
|
-
# A dictionary shall be written as a sequence of key-value pairs enclosed in double angle brackets (<<...>>)
|
434
|
-
# (using LESS-THAN SIGNs (3Ch) and GREATER-THAN SIGNs (3Eh)).
|
435
|
-
out << "<<\n".force_encoding(Encoding::ASCII_8BIT)
|
436
|
-
object.each do |key, value|
|
437
|
-
out << "#{_object_to_pdf key} #{_object_to_pdf value}\n".force_encoding(Encoding::ASCII_8BIT) unless PRIVATE_HASH_KEYS.include? key
|
438
|
-
end
|
439
|
-
out << ">>".force_encoding(Encoding::ASCII_8BIT)
|
440
|
-
out << "\nstream\n#{object[:raw_stream_content]}\nendstream".force_encoding(Encoding::ASCII_8BIT) if object[:raw_stream_content]
|
441
|
-
out << "\nendobj\n" if object[:indirect_reference_id]
|
442
|
-
out.join().force_encoding(Encoding::ASCII_8BIT)
|
443
|
-
end
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
end
|
448
|
-
end
|
449
101
|
|
450
102
|
#########################################################
|
451
103
|
# this file is part of the CombinePDF library and the code
|
@@ -7,58 +7,181 @@
|
|
7
7
|
|
8
8
|
|
9
9
|
|
10
|
+
|
10
11
|
module CombinePDF
|
11
12
|
|
12
13
|
#@private
|
13
14
|
#:nodoc: all
|
14
|
-
#
|
15
|
+
#
|
16
|
+
# <b>This doesn't work yet!</b>
|
17
|
+
#
|
18
|
+
# and alsom, for even when it will work, UNICODE SUPPORT IS MISSING!
|
19
|
+
#
|
15
20
|
# in the future I wish to make a simple PDF page writer, that has only one functions - the text box.
|
16
21
|
# Once the simple writer is ready (creates a text box in a self contained Page element),
|
17
22
|
# I could add it to the << operators and add it as either a self contained page or as an overlay.
|
18
23
|
# if all goes well, maybe I will also create an add_image function.
|
19
|
-
|
24
|
+
#
|
25
|
+
# The PDFWriter class is a subclass of Hash and represents a PDF Page object.
|
26
|
+
#
|
27
|
+
# Writing on this Page is done using the text_box function.
|
28
|
+
#
|
29
|
+
# the rest of the functions are for internal use.
|
30
|
+
#
|
31
|
+
# Once the Page is completed (the last text box was added),
|
32
|
+
# we can insert the page to a CombinePDF object.
|
33
|
+
#
|
34
|
+
# We can either insert the PDFWriter as a new page:
|
35
|
+
# pdf = CombinePDF.new
|
36
|
+
# new_page = PDFWriter.new
|
37
|
+
# new_page.text_box "some text"
|
38
|
+
# pdf << new_page
|
39
|
+
# pdf.save "file_with_new_page.pdf"
|
40
|
+
# Or we can insert the PDFWriter as an overlay (stamp / watermark) over existing pages:
|
41
|
+
# pdf = CombinePDF.new
|
42
|
+
# new_page = PDFWriter.new "some_file.pdf"
|
43
|
+
# new_page.text_box "some text"
|
44
|
+
# pdf.pages.each {|page| page << new_page }
|
45
|
+
# pdf.save "stamped_file.pdf"
|
46
|
+
class PDFWriter < Hash
|
20
47
|
|
21
48
|
def initialize(media_box = [0.0, 0.0, 612.0, 792.0])
|
22
|
-
|
23
|
-
|
49
|
+
# indirect_reference_id, :indirect_generation_number
|
50
|
+
self[:Type] = :Page
|
51
|
+
self[:indirect_reference_id] = 0
|
52
|
+
self[:Resources] = {}
|
53
|
+
self[:Contents] = { is_reference_only: true , referenced_object: {indirect_reference_id: 0, raw_stream_content: ""} }
|
54
|
+
self[:MediaBox] = media_box
|
55
|
+
end
|
56
|
+
|
57
|
+
# accessor (getter) for the :Resources element of the page
|
58
|
+
def resources
|
59
|
+
self[:Resources]
|
60
|
+
end
|
61
|
+
# accessor (getter) for the stream in the :Contents element of the page
|
62
|
+
# after getting the string object, you can operate on it but not replace it (use << or other String methods).
|
63
|
+
def contents
|
64
|
+
self[:Contents][:referenced_object][:raw_stream_content]
|
65
|
+
end
|
66
|
+
# accessor (getter) for the :MediaBox element of the page
|
67
|
+
def media_box
|
68
|
+
self[:MediaBox]
|
69
|
+
end
|
70
|
+
# accessor (setter) for the :MediaBox element of the page
|
71
|
+
# dimentions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
|
72
|
+
def media_box=(dimentions = [0.0, 0.0, 612.0, 792.0])
|
73
|
+
self[:MediaBox] = dimentions
|
74
|
+
end
|
75
|
+
# creates a font object and adds the font to the resources dictionary
|
76
|
+
# returns the name of the font for the content stream.
|
77
|
+
# font_name:: a Symbol of one of the 14 Type 1 fonts, known as the standard 14 fonts:
|
78
|
+
# - :"Times-Roman"
|
79
|
+
# - :"Times-Bold"
|
80
|
+
# - :"Times-Italic"
|
81
|
+
# - :"Times-BoldItalic"
|
82
|
+
# - :Helvetica
|
83
|
+
# - :"Helvetica-Bold"
|
84
|
+
# - :"Helvetica-BoldOblique"
|
85
|
+
# - :"Helvetica- Oblique"
|
86
|
+
# - :Courier
|
87
|
+
# - :"Courier-Bold"
|
88
|
+
# - :"Courier-Oblique"
|
89
|
+
# - :"Courier-BoldOblique"
|
90
|
+
# - :Symbol
|
91
|
+
# - :ZapfDingbats
|
92
|
+
def font(font_name = :Helvetica)
|
93
|
+
# refuse any other fonts that arn't basic standard fonts
|
94
|
+
allow_fonts = [ :"Times-Roman",
|
95
|
+
:"Times-Bold",
|
96
|
+
:"Times-Italic",
|
97
|
+
:"Times-BoldItalic",
|
98
|
+
:Helvetica,
|
99
|
+
:"Helvetica-Bold",
|
100
|
+
:"Helvetica-BoldOblique",
|
101
|
+
:"Helvetica-Oblique",
|
102
|
+
:Courier,
|
103
|
+
:"Courier-Bold",
|
104
|
+
:"Courier-Oblique",
|
105
|
+
:"Courier-BoldOblique",
|
106
|
+
:Symbol,
|
107
|
+
:ZapfDingbats ]
|
108
|
+
raise "add_font(font_name) accepts only one of the 14 standards fonts - wrong font_name!" unless allow_fonts.include? font_name
|
109
|
+
# if the font exists, return it's name
|
110
|
+
resources[:Font] ||= {}
|
111
|
+
resources[:Font].each do |k,v|
|
112
|
+
if v.is_a?(Hash) && v[:Type] == :Font && v[:BaseFont] == font_name
|
113
|
+
return k
|
114
|
+
end
|
115
|
+
end
|
116
|
+
# create font object
|
117
|
+
font_object = { Type: :Font, Subtype: :Type1, BaseFont: font_name}
|
118
|
+
# set a secure name for the font
|
119
|
+
name = (SecureRandom.urlsafe_base64(9)).to_sym
|
120
|
+
# add object to reasource
|
121
|
+
resources[:Font][name] = font_object
|
122
|
+
#return name
|
123
|
+
name
|
124
|
+
end
|
125
|
+
def graphic_state(graphic_state_dictionary = {})
|
126
|
+
# if the graphic state exists, return it's name
|
127
|
+
resources[:ExtGState] ||= {}
|
128
|
+
resources[:ExtGState].each do |k,v|
|
129
|
+
if v.is_a?(Hash) && v == graphic_state_dictionary
|
130
|
+
return k
|
131
|
+
end
|
132
|
+
end
|
133
|
+
# set graphic state type
|
134
|
+
graphic_state_dictionary[:Type] = :ExtGState
|
135
|
+
# set a secure name for the graphic state
|
136
|
+
name = (SecureRandom.urlsafe_base64(9)).to_sym
|
137
|
+
# add object to reasource
|
138
|
+
resources[:ExtGState][name] = graphic_state_dictionary
|
139
|
+
#return name
|
140
|
+
name
|
24
141
|
end
|
25
142
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
143
|
+
# <b>INCOMPLETE</b>
|
144
|
+
#
|
145
|
+
# This function, when completed, will add a simple text box to the Page represented by the PDFWriter class.
|
146
|
+
# This function takes two values:
|
147
|
+
# text:: the text to potin the box.
|
148
|
+
# properties:: a Hash of box properties.
|
149
|
+
# the symbols and values in the properties Hash could be any or all of the following:
|
150
|
+
# x:: the left position of the box.
|
151
|
+
# y:: the BUTTOM position of the box.
|
152
|
+
# length:: the length of the box.
|
153
|
+
# height:: the height of the box.
|
154
|
+
# font_name:: a Symbol representing one of the 14 standard fonts. defaults to ":Helvetica" @see add_font
|
155
|
+
# font_size:: a Fixnum for the font size, or :fit_text to fit the text in the box. defaults to ":fit_text"
|
156
|
+
# 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]").
|
157
|
+
def text_box(text, properties = {})
|
35
158
|
options = {
|
36
159
|
text_alignment: :center,
|
37
|
-
text_color: [
|
38
|
-
|
160
|
+
text_color: [0,0,0],
|
161
|
+
text_stroke_color: nil,
|
162
|
+
text_stroke_width: 0,
|
39
163
|
font_name: :Helvetica,
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
border_radius: nil,
|
46
|
-
background_color: nil,
|
164
|
+
font_size: :fit_text,
|
165
|
+
border_color: [0.5,0.5,0.5],
|
166
|
+
border_width: 2,
|
167
|
+
border_radius: 0,
|
168
|
+
background_color: [0.7,0.7,0.7],
|
47
169
|
opacity: 1,
|
48
170
|
x: 0,
|
49
171
|
y: 0,
|
50
172
|
length: -1,
|
51
173
|
height: -1,
|
52
174
|
}
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
175
|
+
options.update properties
|
176
|
+
# reset the length and height to meaningful values, if negative
|
177
|
+
options[:length] = media_box[2] - options[:x] if options[:length] < 0
|
178
|
+
options[:height] = media_box[3] - options[:y] if options[:height] < 0
|
179
|
+
# fit text in box, if requested
|
180
|
+
if options[:font_size] == :fit_text
|
181
|
+
options[:font_size] = self.fit_text text, options[:font_name], options[:length], options[:height]
|
57
182
|
end
|
58
183
|
|
59
|
-
|
60
|
-
font_name = ("MyFont" + rand(99) ).to_sym
|
61
|
-
resources_object = {Resources: {Font: { font_name => font_object } } }
|
184
|
+
|
62
185
|
# create box stream
|
63
186
|
|
64
187
|
# reset x,y by text alignment - x,y are calculated from the buttom left
|
@@ -68,47 +191,39 @@ module CombinePDF
|
|
68
191
|
# create text stream
|
69
192
|
text_stream = ""
|
70
193
|
text_stream << "BT\n" # the Begine Text marker
|
71
|
-
text_stream << PDFOperations._format_name_to_pdf(font_name) # Set font name
|
194
|
+
text_stream << PDFOperations._format_name_to_pdf(font options[:font_name]) # Set font name
|
72
195
|
text_stream << " #{options[:font_size].to_f} Tf\n" # set font size and add font operator
|
73
196
|
text_stream << "#{options[:text_color][0]} #{options[:text_color][0]} #{options[:text_color][0]} rg\n" # sets the color state
|
74
|
-
text_stream << " #{options[:opacity].to_f} ca\n" # set opacity (alpha) for graphic state.
|
75
197
|
text_stream << "#{x} #{y} Td\n" # set location for text object
|
76
198
|
text_stream << PDFOperations._format_string_to_pdf(text) # insert the string in PDF format
|
77
199
|
text_stream << " Tj\n ET\n" # the Text object operator and the End Text marker
|
78
|
-
end
|
79
200
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
pages.each {|p| add_content_to_pages p, location}
|
87
|
-
elsif pages.is_a?(Hash)
|
88
|
-
#####
|
89
|
-
##add content stream to page
|
90
|
-
end
|
91
|
-
end
|
92
|
-
########################################################
|
93
|
-
## make_into_page()
|
94
|
-
## takes no arguments and returns the contents stream within a page (to be added as an indipendent page to the PDF object)
|
95
|
-
def make_into_page
|
96
|
-
{Type: :Page, }
|
97
|
-
end
|
201
|
+
final_stream = ""
|
202
|
+
# set graphic state for box
|
203
|
+
final_stream << "q\nq\nq\n"
|
204
|
+
box_graphic_state = graphic_state ca: options[:opacity], CA: options[:opacity], LW: options[:border_width], LC: 2, LJ:1, LD: 0
|
205
|
+
final_stream << "#{PDFOperations._object_to_pdf box_graphic_state} gs\n"
|
206
|
+
final_stream << "DeviceRGB CS\nDeviceRGB cs\n"
|
98
207
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
pdf.save file_name
|
208
|
+
# set graphic state for text
|
209
|
+
final_stream << "q\nq\nq\n"
|
210
|
+
text_graphic_state = graphic_state({ca: options[:opacity], CA: options[:opacity], LW: options[:text_stroke_width], LC: 2, LJ: 1, LD: 0})
|
211
|
+
final_stream << "#{PDFOperations._object_to_pdf text_graphic_state} gs\n"
|
212
|
+
final_stream << "DeviceRGB CS\nDeviceRGB cs\n"
|
213
|
+
final_stream << "#{options[:text_color][0]} #{options[:text_color][1]} #{options[:text_color][2]} scn\n"
|
214
|
+
if options[:text_stroke_width].to_i > 0 && options[:text_stroke_color]
|
215
|
+
final_stream << "#{options[:text_stroke_color][0]} #{options[:text_stroke_color][1]} #{options[:text_stroke_color][2]} SCN\n"
|
216
|
+
final_stream << "2 Tr\n"
|
109
217
|
else
|
110
|
-
|
218
|
+
final_stream << "0 Tr\n"
|
111
219
|
end
|
220
|
+
|
221
|
+
# clear graphic states
|
222
|
+
final_stream << "Q\nQ\nQ\n"
|
223
|
+
final_stream << "Q\nQ\nQ\n"
|
224
|
+
|
225
|
+
contents << final_stream
|
226
|
+
self
|
112
227
|
end
|
113
228
|
|
114
229
|
end
|
@@ -117,3 +232,76 @@ end
|
|
117
232
|
|
118
233
|
|
119
234
|
|
235
|
+
|
236
|
+
# # text_box output example
|
237
|
+
# q
|
238
|
+
# q
|
239
|
+
# /GraphiStateName gs
|
240
|
+
# /DeviceRGB cs
|
241
|
+
# 0.867 0.867 0.867 scn
|
242
|
+
# 293.328 747.000 m
|
243
|
+
# 318.672 747.000 l
|
244
|
+
# 323.090 747.000 326.672 743.418 326.672 739.000 c
|
245
|
+
# 326.672 735.800 l
|
246
|
+
# 326.672 731.382 323.090 727.800 318.672 727.800 c
|
247
|
+
# 293.328 727.800 l
|
248
|
+
# 288.910 727.800 285.328 731.382 285.328 735.800 c
|
249
|
+
# 285.328 739.000 l
|
250
|
+
# 285.328 743.418 288.910 747.000 293.328 747.000 c
|
251
|
+
# h
|
252
|
+
# 293.328 64.200 m
|
253
|
+
# 318.672 64.200 l
|
254
|
+
# 323.090 64.200 326.672 60.618 326.672 56.200 c
|
255
|
+
# 326.672 53.000 l
|
256
|
+
# 326.672 48.582 323.090 45.000 318.672 45.000 c
|
257
|
+
# 293.328 45.000 l
|
258
|
+
# 288.910 45.000 285.328 48.582 285.328 53.000 c
|
259
|
+
# 285.328 56.200 l
|
260
|
+
# 285.328 60.618 288.910 64.200 293.328 64.200 c
|
261
|
+
# h
|
262
|
+
# f
|
263
|
+
# 0.000 0.000 0.000 scn
|
264
|
+
# /DeviceRGB CS
|
265
|
+
# 1.000 1.000 1.000 SCN
|
266
|
+
|
267
|
+
# 2 Tr
|
268
|
+
# 0.000 0.000 0.000 scn
|
269
|
+
# 0.000 0.000 0.000 SCN
|
270
|
+
# 1.000 1.000 1.000 SCN
|
271
|
+
# 0.000 0.000 0.000 scn
|
272
|
+
# 0.000 0.000 0.000 scn
|
273
|
+
# 0.000 0.000 0.000 SCN
|
274
|
+
|
275
|
+
# BT
|
276
|
+
# 291.776 733.3119999999999 Td
|
277
|
+
# /FontName 16 Tf
|
278
|
+
# [<2d2032202d>] TJ
|
279
|
+
# ET
|
280
|
+
|
281
|
+
# 1.000 1.000 1.000 SCN
|
282
|
+
# 0.000 0.000 0.000 scn
|
283
|
+
# 0.000 0.000 0.000 scn
|
284
|
+
# 0.000 0.000 0.000 SCN
|
285
|
+
# 1.000 1.000 1.000 SCN
|
286
|
+
# 0.000 0.000 0.000 scn
|
287
|
+
# 0.000 0.000 0.000 scn
|
288
|
+
# 0.000 0.000 0.000 SCN
|
289
|
+
|
290
|
+
# BT
|
291
|
+
# 291.776 50.512 Td
|
292
|
+
# /FontName 16 Tf
|
293
|
+
# [<2d2032202d>] TJ
|
294
|
+
# ET
|
295
|
+
|
296
|
+
# 1.000 1.000 1.000 SCN
|
297
|
+
# 0.000 0.000 0.000 scn
|
298
|
+
|
299
|
+
# 0 Tr
|
300
|
+
# Q
|
301
|
+
# Q
|
302
|
+
|
303
|
+
|
304
|
+
|
305
|
+
|
306
|
+
|
307
|
+
|