pdf-core 0.8.1 → 0.10.0
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 +5 -5
- checksums.yaml.gz.sig +0 -0
- data/lib/pdf/core/annotations.rb +51 -13
- data/lib/pdf/core/byte_string.rb +3 -2
- data/lib/pdf/core/destinations.rb +64 -24
- data/lib/pdf/core/document_state.rb +100 -17
- data/lib/pdf/core/filter_list.rb +44 -5
- data/lib/pdf/core/filters.rb +26 -7
- data/lib/pdf/core/graphics_state.rb +74 -30
- data/lib/pdf/core/literal_string.rb +9 -10
- data/lib/pdf/core/name_tree.rb +76 -16
- data/lib/pdf/core/object_store.rb +69 -19
- data/lib/pdf/core/outline_item.rb +53 -5
- data/lib/pdf/core/outline_root.rb +18 -2
- data/lib/pdf/core/page.rb +158 -32
- data/lib/pdf/core/page_geometry.rb +4 -58
- data/lib/pdf/core/pdf_object.rb +62 -37
- data/lib/pdf/core/reference.rb +58 -15
- data/lib/pdf/core/renderer.rb +116 -47
- data/lib/pdf/core/stream.rb +43 -7
- data/lib/pdf/core/text.rb +255 -106
- data/lib/pdf/core/utils.rb +10 -1
- data/lib/pdf/core.rb +26 -16
- data/pdf-core.gemspec +31 -27
- data.tar.gz.sig +0 -0
- metadata +36 -101
- metadata.gz.sig +2 -1
- data/Gemfile +0 -5
- data/Rakefile +0 -17
data/lib/pdf/core/renderer.rb
CHANGED
@@ -4,7 +4,9 @@ require 'stringio'
|
|
4
4
|
|
5
5
|
module PDF
|
6
6
|
module Core
|
7
|
+
# Document renderer serializes document into its binary representation.
|
7
8
|
class Renderer
|
9
|
+
# @param state [PDF::Core::DocumentState]
|
8
10
|
def initialize(state)
|
9
11
|
@state = state
|
10
12
|
@state.populate_pages_from_store(self)
|
@@ -14,92 +16,109 @@ module PDF
|
|
14
16
|
@page_number = 0
|
15
17
|
end
|
16
18
|
|
19
|
+
# Document state
|
20
|
+
# @return [PDF::Core::DocumentState]
|
17
21
|
attr_reader :state
|
18
22
|
|
19
|
-
# Creates a new Reference and adds it to the Document's object list.
|
20
|
-
# +data+ argument is anything that Prawn.pdf_object() can convert.
|
21
|
-
#
|
22
|
-
# Returns the identifier which points to the reference in the ObjectStore
|
23
|
+
# Creates a new Reference and adds it to the Document's object list.
|
23
24
|
#
|
25
|
+
# @param data [any] anything that {PDF::Core.pdf_object} can convert.
|
26
|
+
# @return [Integer] the identifier of the reference
|
24
27
|
def ref(data)
|
25
28
|
ref!(data).identifier
|
26
29
|
end
|
27
30
|
|
28
|
-
# Like ref, but returns the actual reference instead of its identifier.
|
31
|
+
# Like {ref}, but returns the actual reference instead of its identifier.
|
29
32
|
#
|
30
33
|
# While you can use this to build up nested references within the object
|
31
34
|
# tree, it is recommended to persist only identifiers, and then provide
|
32
|
-
# helper methods to look up the actual references in the ObjectStore
|
33
|
-
#
|
34
|
-
#
|
35
|
+
# helper methods to look up the actual references in the {ObjectStore} if
|
36
|
+
# needed. If you take this approach, `Document::Snapshot` will probably
|
37
|
+
# work with your extension.
|
35
38
|
#
|
39
|
+
# @param data [any] anything that {PDF::Core.pdf_object} can convert.
|
40
|
+
# @return [PDF::Core::Reference]
|
36
41
|
def ref!(data)
|
37
42
|
state.store.ref(data)
|
38
43
|
end
|
39
44
|
|
40
45
|
# At any stage in the object tree an object can be replaced with an
|
41
46
|
# indirect reference. To get access to the object safely, regardless
|
42
|
-
# of if it's hidden behind a
|
47
|
+
# of if it's hidden behind a {Reference}, wrap it in `deref()`.
|
43
48
|
#
|
49
|
+
# @param obj [PDF::Core::Reference, any]
|
50
|
+
# @return [any]
|
44
51
|
def deref(obj)
|
45
52
|
obj.is_a?(PDF::Core::Reference) ? obj.data : obj
|
46
53
|
end
|
47
54
|
|
48
55
|
# Appends a raw string to the current page content.
|
49
56
|
#
|
50
|
-
#
|
51
|
-
#
|
57
|
+
# @example Raw line drawing example
|
58
|
+
# x1, y1, x2, y2 = 100, 500, 300, 550
|
52
59
|
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
60
|
+
# pdf.add_content("#{PDF::Core.real_params([x1, y1])} m") # move
|
61
|
+
# pdf.add_content("#{PDF::Core.real_params([ x2, y2 ])} l") # draw path
|
62
|
+
# pdf.add_content('S') # stroke
|
56
63
|
#
|
64
|
+
# @param str [String]
|
65
|
+
# @return [void]
|
57
66
|
def add_content(str)
|
58
67
|
save_graphics_state if graphic_state.nil?
|
59
68
|
state.page.content << str << "\n"
|
60
69
|
end
|
61
70
|
|
62
|
-
# The Name dictionary
|
63
|
-
#
|
64
|
-
#
|
71
|
+
# The Name dictionary for this document. It is lazily initialized, so that
|
72
|
+
# documents that do not need a name dictionary do not incur the additional
|
73
|
+
# overhead.
|
65
74
|
#
|
75
|
+
# @return [PDF::Core::Reference<Hash>]
|
76
|
+
# @see # PDF 1.7 spec, section 3.6.3 Name Dictionary
|
66
77
|
def names
|
67
78
|
state.store.root.data[:Names] ||= ref!(Type: :Names)
|
68
79
|
end
|
69
80
|
|
70
81
|
# Returns true if the Names dictionary is in use for this document.
|
71
82
|
#
|
83
|
+
# @return [Boolean]
|
72
84
|
def names?
|
73
|
-
state.store.root.data
|
85
|
+
state.store.root.data.key?(:Names)
|
74
86
|
end
|
75
87
|
|
76
88
|
# Defines a block to be called just before the document is rendered.
|
77
89
|
#
|
90
|
+
# @yieldparam document_state [PDF::Core::DocumentState]
|
91
|
+
# @return [void]
|
78
92
|
def before_render(&block)
|
79
93
|
state.before_render_callbacks << block
|
80
94
|
end
|
81
95
|
|
82
96
|
# Defines a block to be called just before a new page is started.
|
83
97
|
#
|
98
|
+
# @yieldparam document_state [PDF::Core::DocumentState]
|
99
|
+
# @return [void]
|
84
100
|
def on_page_create(&block)
|
85
|
-
state.on_page_create_callback =
|
86
|
-
if block_given?
|
87
|
-
block
|
88
|
-
end
|
101
|
+
state.on_page_create_callback = block
|
89
102
|
end
|
90
103
|
|
104
|
+
# Create a new page and set it current.
|
105
|
+
#
|
106
|
+
# @param options [Hash]
|
107
|
+
# @option options :size [String, Array<Numeric>]
|
108
|
+
# @option options :layout [:portrait, :landscape]
|
109
|
+
# @return [void]
|
91
110
|
def start_new_page(options = {})
|
92
111
|
last_page = state.page
|
93
112
|
if last_page
|
94
|
-
last_page_size
|
95
|
-
last_page_layout
|
113
|
+
last_page_size = last_page.size
|
114
|
+
last_page_layout = last_page.layout
|
96
115
|
last_page_margins = last_page.margins
|
97
116
|
end
|
98
117
|
|
99
118
|
page_options = {
|
100
119
|
size: options[:size] || last_page_size,
|
101
120
|
layout: options[:layout] || last_page_layout,
|
102
|
-
margins: last_page_margins
|
121
|
+
margins: last_page_margins,
|
103
122
|
}
|
104
123
|
if last_page
|
105
124
|
if last_page.graphic_state
|
@@ -122,6 +141,9 @@ module PDF
|
|
122
141
|
state.on_page_create_action(self)
|
123
142
|
end
|
124
143
|
|
144
|
+
# Number of pages in the document.
|
145
|
+
#
|
146
|
+
# @return [Integer]
|
125
147
|
def page_count
|
126
148
|
state.page_count
|
127
149
|
end
|
@@ -129,16 +151,21 @@ module PDF
|
|
129
151
|
# Re-opens the page with the given (1-based) page number so that you can
|
130
152
|
# draw on it.
|
131
153
|
#
|
132
|
-
#
|
133
|
-
|
154
|
+
# @param page_number [Integer]
|
155
|
+
# @return [void]
|
156
|
+
# @see # Prawn::Document#number_pages for a sample usage of this capability.
|
134
157
|
def go_to_page(page_number)
|
135
158
|
@page_number = page_number
|
136
159
|
state.page = state.pages[page_number - 1]
|
137
160
|
end
|
138
161
|
|
162
|
+
# Finalize all pages
|
163
|
+
#
|
164
|
+
# @api private
|
165
|
+
# @return [void]
|
139
166
|
def finalize_all_page_contents
|
140
167
|
(1..page_count).each do |i|
|
141
|
-
go_to_page
|
168
|
+
go_to_page(i)
|
142
169
|
while graphic_stack.present?
|
143
170
|
restore_graphics_state
|
144
171
|
end
|
@@ -146,10 +173,13 @@ module PDF
|
|
146
173
|
end
|
147
174
|
end
|
148
175
|
|
149
|
-
#
|
176
|
+
# Raise the PDF version of the file we're going to generate.
|
150
177
|
# A private method, designed for internal use when the user adds a feature
|
151
178
|
# to their document that requires a particular version.
|
152
179
|
#
|
180
|
+
# @param min [Float]
|
181
|
+
# @return [void]
|
182
|
+
# @api private
|
153
183
|
def min_version(min)
|
154
184
|
state.version = min if min > state.version
|
155
185
|
end
|
@@ -157,35 +187,41 @@ module PDF
|
|
157
187
|
# Renders the PDF document to string.
|
158
188
|
# Pass an open file descriptor to render to file.
|
159
189
|
#
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
190
|
+
# @param output [#<<]
|
191
|
+
# @return [String]
|
192
|
+
def render(output = nil)
|
193
|
+
buffer = StringIO.new.binmode
|
194
|
+
|
164
195
|
finalize_all_page_contents
|
165
196
|
|
166
|
-
render_header(
|
167
|
-
render_body(
|
168
|
-
render_xref(
|
169
|
-
render_trailer(
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
return str
|
174
|
-
else
|
175
|
-
return nil
|
197
|
+
render_header(buffer)
|
198
|
+
render_body(buffer)
|
199
|
+
render_xref(buffer)
|
200
|
+
render_trailer(buffer)
|
201
|
+
|
202
|
+
if output.respond_to?(:<<)
|
203
|
+
output << buffer.string
|
176
204
|
end
|
205
|
+
|
206
|
+
buffer.string
|
177
207
|
end
|
178
208
|
|
179
209
|
# Renders the PDF document to file.
|
180
210
|
#
|
211
|
+
# @example
|
181
212
|
# pdf.render_file 'foo.pdf'
|
182
213
|
#
|
214
|
+
# @param filename [String, #to_path, Integer]
|
215
|
+
# @return [void]
|
183
216
|
def render_file(filename)
|
184
217
|
File.open(filename, 'wb') { |f| render(f) }
|
185
218
|
end
|
186
219
|
|
187
220
|
# Write out the PDF Header, as per spec 3.4.1
|
188
221
|
#
|
222
|
+
# @api private
|
223
|
+
# @param output [#<<]
|
224
|
+
# @return [void]
|
189
225
|
def render_header(output)
|
190
226
|
state.before_render_actions(self)
|
191
227
|
|
@@ -198,30 +234,39 @@ module PDF
|
|
198
234
|
|
199
235
|
# Write out the PDF Body, as per spec 3.4.2
|
200
236
|
#
|
237
|
+
# @api private
|
238
|
+
# @param output [(#<<, #size)]
|
239
|
+
# @return [void]
|
201
240
|
def render_body(output)
|
202
241
|
state.render_body(output)
|
203
242
|
end
|
204
243
|
|
205
244
|
# Write out the PDF Cross Reference Table, as per spec 3.4.3
|
206
245
|
#
|
246
|
+
# @api private
|
247
|
+
# @param output [(#<<, #size)]
|
248
|
+
# @return [void]
|
207
249
|
def render_xref(output)
|
208
250
|
@xref_offset = output.size
|
209
251
|
output << "xref\n"
|
210
252
|
output << "0 #{state.store.size + 1}\n"
|
211
253
|
output << "0000000000 65535 f \n"
|
212
254
|
state.store.each do |ref|
|
213
|
-
output.printf('
|
255
|
+
output.printf('%<offset>010d', offset: ref.offset)
|
214
256
|
output << " 00000 n \n"
|
215
257
|
end
|
216
258
|
end
|
217
259
|
|
218
260
|
# Write out the PDF Trailer, as per spec 3.4.4
|
219
261
|
#
|
262
|
+
# @api private
|
263
|
+
# @param output [#<<]
|
264
|
+
# @return [void]
|
220
265
|
def render_trailer(output)
|
221
266
|
trailer_hash = {
|
222
267
|
Size: state.store.size + 1,
|
223
268
|
Root: state.store.root,
|
224
|
-
Info: state.store.info
|
269
|
+
Info: state.store.info,
|
225
270
|
}
|
226
271
|
trailer_hash.merge!(state.trailer) if state.trailer
|
227
272
|
|
@@ -232,14 +277,29 @@ module PDF
|
|
232
277
|
output << '%%EOF' << "\n"
|
233
278
|
end
|
234
279
|
|
280
|
+
# Open (save) current graphic state in the content stream.
|
281
|
+
#
|
282
|
+
# @return [void]
|
235
283
|
def open_graphics_state
|
236
|
-
add_content
|
284
|
+
add_content('q')
|
237
285
|
end
|
238
286
|
|
287
|
+
# Close current graphic state (restore previous) in the content stream.
|
288
|
+
#
|
289
|
+
# @return [void]
|
239
290
|
def close_graphics_state
|
240
|
-
add_content
|
291
|
+
add_content('Q')
|
241
292
|
end
|
242
293
|
|
294
|
+
# Save surrent graphic state both in the graphic state stack and in the
|
295
|
+
# page content stream.
|
296
|
+
#
|
297
|
+
# If a block is given graphic state is automatically restored after the
|
298
|
+
# block execution.
|
299
|
+
#
|
300
|
+
# @param graphic_state [PDF::Core::GraphicState]
|
301
|
+
# @yield
|
302
|
+
# @return [void]
|
243
303
|
def save_graphics_state(graphic_state = nil)
|
244
304
|
graphic_stack.save_graphic_state(graphic_state)
|
245
305
|
open_graphics_state
|
@@ -252,12 +312,15 @@ module PDF
|
|
252
312
|
# Returns true if content streams will be compressed before rendering,
|
253
313
|
# false otherwise
|
254
314
|
#
|
315
|
+
# @return [Boolean]
|
255
316
|
def compression_enabled?
|
256
317
|
state.compress
|
257
318
|
end
|
258
319
|
|
259
320
|
# Pops the last saved graphics state off the graphics state stack and
|
260
321
|
# restores the state to those values
|
322
|
+
#
|
323
|
+
# @return [void]
|
261
324
|
def restore_graphics_state
|
262
325
|
if graphic_stack.empty?
|
263
326
|
raise PDF::Core::Errors::EmptyGraphicStateStack,
|
@@ -267,10 +330,16 @@ module PDF
|
|
267
330
|
graphic_stack.restore_graphic_state
|
268
331
|
end
|
269
332
|
|
333
|
+
# Graphic state stack of the current document.
|
334
|
+
#
|
335
|
+
# @return [PDF::Core::GraphicStateStack]
|
270
336
|
def graphic_stack
|
271
337
|
state.page.stack
|
272
338
|
end
|
273
339
|
|
340
|
+
# Current graphic state
|
341
|
+
#
|
342
|
+
# @return [PDF::Core::GraphicState]
|
274
343
|
def graphic_state
|
275
344
|
save_graphics_state unless graphic_stack.current_state
|
276
345
|
graphic_stack.current_state
|
data/lib/pdf/core/stream.rb
CHANGED
@@ -8,36 +8,56 @@
|
|
8
8
|
|
9
9
|
module PDF
|
10
10
|
module Core
|
11
|
+
# PDF Stream object
|
11
12
|
class Stream
|
13
|
+
# Stream filters
|
14
|
+
# @return [PDF::Core::FilterList]
|
12
15
|
attr_reader :filters
|
13
16
|
|
17
|
+
# @param io [String] must be mutable
|
14
18
|
def initialize(io = nil)
|
15
19
|
@filtered_stream = ''
|
16
20
|
@stream = io
|
17
21
|
@filters = FilterList.new
|
18
22
|
end
|
19
23
|
|
24
|
+
# Append data to stream.
|
25
|
+
#
|
26
|
+
# @param io [String]
|
27
|
+
# @return [self]
|
20
28
|
def <<(io)
|
21
29
|
(@stream ||= +'') << io
|
22
30
|
@filtered_stream = nil
|
23
31
|
self
|
24
32
|
end
|
25
33
|
|
34
|
+
# Set up stream to be compressed when serialized.
|
35
|
+
#
|
36
|
+
# @return [void]
|
26
37
|
def compress!
|
27
|
-
unless @filters.names.include?
|
38
|
+
unless @filters.names.include?(:FlateDecode)
|
28
39
|
@filtered_stream = nil
|
29
40
|
@filters << :FlateDecode
|
30
41
|
end
|
31
42
|
end
|
32
43
|
|
44
|
+
# Is this stream compressed?
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
33
47
|
def compressed?
|
34
|
-
@filters.names.include?
|
48
|
+
@filters.names.include?(:FlateDecode)
|
35
49
|
end
|
36
50
|
|
51
|
+
# Is there any data in this stream?
|
52
|
+
#
|
53
|
+
# @return [Boolean]
|
37
54
|
def empty?
|
38
55
|
@stream.nil?
|
39
56
|
end
|
40
57
|
|
58
|
+
# Stream data with filters applied.
|
59
|
+
#
|
60
|
+
# @return [Stream]
|
41
61
|
def filtered_stream
|
42
62
|
if @stream
|
43
63
|
if @filtered_stream.nil?
|
@@ -46,20 +66,25 @@ module PDF
|
|
46
66
|
@filters.each do |(filter_name, params)|
|
47
67
|
filter = PDF::Core::Filters.const_get(filter_name)
|
48
68
|
if filter
|
49
|
-
@filtered_stream = filter.encode
|
69
|
+
@filtered_stream = filter.encode(@filtered_stream, params)
|
50
70
|
end
|
51
71
|
end
|
52
72
|
end
|
53
73
|
|
54
74
|
@filtered_stream
|
55
|
-
# XXX Fillter stream
|
56
75
|
end
|
57
76
|
end
|
58
77
|
|
78
|
+
# Size of data in the stream
|
79
|
+
#
|
80
|
+
# @return [Integer]
|
59
81
|
def length
|
60
82
|
@stream.length
|
61
83
|
end
|
62
84
|
|
85
|
+
# Serialized stream data
|
86
|
+
#
|
87
|
+
# @return [String]
|
63
88
|
def object
|
64
89
|
if filtered_stream
|
65
90
|
"stream\n#{filtered_stream}\nendstream\n"
|
@@ -68,13 +93,16 @@ module PDF
|
|
68
93
|
end
|
69
94
|
end
|
70
95
|
|
96
|
+
# Stream dictionary
|
97
|
+
#
|
98
|
+
# @return [Hash]
|
71
99
|
def data
|
72
100
|
if @stream
|
73
101
|
filter_names = @filters.names
|
74
102
|
filter_params = @filters.decode_params
|
75
103
|
|
76
104
|
d = {
|
77
|
-
Length: filtered_stream.length
|
105
|
+
Length: filtered_stream.length,
|
78
106
|
}
|
79
107
|
if filter_names.any?
|
80
108
|
d[:Filter] = filter_names
|
@@ -89,9 +117,17 @@ module PDF
|
|
89
117
|
end
|
90
118
|
end
|
91
119
|
|
120
|
+
# String representation of the stream for debugging purposes.
|
121
|
+
#
|
122
|
+
# @return [String]
|
92
123
|
def inspect
|
93
|
-
|
94
|
-
|
124
|
+
format(
|
125
|
+
'#<%<class>s:0x%<object_id>014x @stream=%<stream>s, @filters=%<filters>s>',
|
126
|
+
class: self.class.name,
|
127
|
+
object_id: object_id,
|
128
|
+
stream: @stream.inspect,
|
129
|
+
filters: @filters.inspect,
|
130
|
+
)
|
95
131
|
end
|
96
132
|
end
|
97
133
|
end
|