pdf-core 0.9.0 → 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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/pdf/core/annotations.rb +51 -13
- data/lib/pdf/core/byte_string.rb +3 -1
- data/lib/pdf/core/destinations.rb +64 -24
- data/lib/pdf/core/document_state.rb +97 -14
- data/lib/pdf/core/filter_list.rb +30 -1
- data/lib/pdf/core/filters.rb +26 -7
- data/lib/pdf/core/graphics_state.rb +68 -23
- data/lib/pdf/core/literal_string.rb +9 -9
- data/lib/pdf/core/name_tree.rb +74 -13
- data/lib/pdf/core/object_store.rb +69 -19
- data/lib/pdf/core/outline_item.rb +53 -4
- data/lib/pdf/core/outline_root.rb +18 -2
- data/lib/pdf/core/page.rb +148 -23
- data/lib/pdf/core/page_geometry.rb +4 -58
- data/lib/pdf/core/pdf_object.rb +57 -36
- data/lib/pdf/core/reference.rb +50 -14
- data/lib/pdf/core/renderer.rb +115 -44
- data/lib/pdf/core/stream.rb +38 -8
- data/lib/pdf/core/text.rb +242 -102
- data/lib/pdf/core/utils.rb +8 -0
- data/lib/pdf/core.rb +26 -16
- data/pdf-core.gemspec +27 -23
- data.tar.gz.sig +0 -0
- metadata +44 -107
- metadata.gz.sig +2 -2
- data/Gemfile +0 -5
- data/Rakefile +0 -29
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,33 +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
|
-
str
|
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
|
174
204
|
end
|
205
|
+
|
206
|
+
buffer.string
|
175
207
|
end
|
176
208
|
|
177
209
|
# Renders the PDF document to file.
|
178
210
|
#
|
211
|
+
# @example
|
179
212
|
# pdf.render_file 'foo.pdf'
|
180
213
|
#
|
214
|
+
# @param filename [String, #to_path, Integer]
|
215
|
+
# @return [void]
|
181
216
|
def render_file(filename)
|
182
217
|
File.open(filename, 'wb') { |f| render(f) }
|
183
218
|
end
|
184
219
|
|
185
220
|
# Write out the PDF Header, as per spec 3.4.1
|
186
221
|
#
|
222
|
+
# @api private
|
223
|
+
# @param output [#<<]
|
224
|
+
# @return [void]
|
187
225
|
def render_header(output)
|
188
226
|
state.before_render_actions(self)
|
189
227
|
|
@@ -196,12 +234,18 @@ module PDF
|
|
196
234
|
|
197
235
|
# Write out the PDF Body, as per spec 3.4.2
|
198
236
|
#
|
237
|
+
# @api private
|
238
|
+
# @param output [(#<<, #size)]
|
239
|
+
# @return [void]
|
199
240
|
def render_body(output)
|
200
241
|
state.render_body(output)
|
201
242
|
end
|
202
243
|
|
203
244
|
# Write out the PDF Cross Reference Table, as per spec 3.4.3
|
204
245
|
#
|
246
|
+
# @api private
|
247
|
+
# @param output [(#<<, #size)]
|
248
|
+
# @return [void]
|
205
249
|
def render_xref(output)
|
206
250
|
@xref_offset = output.size
|
207
251
|
output << "xref\n"
|
@@ -215,11 +259,14 @@ module PDF
|
|
215
259
|
|
216
260
|
# Write out the PDF Trailer, as per spec 3.4.4
|
217
261
|
#
|
262
|
+
# @api private
|
263
|
+
# @param output [#<<]
|
264
|
+
# @return [void]
|
218
265
|
def render_trailer(output)
|
219
266
|
trailer_hash = {
|
220
267
|
Size: state.store.size + 1,
|
221
268
|
Root: state.store.root,
|
222
|
-
Info: state.store.info
|
269
|
+
Info: state.store.info,
|
223
270
|
}
|
224
271
|
trailer_hash.merge!(state.trailer) if state.trailer
|
225
272
|
|
@@ -230,14 +277,29 @@ module PDF
|
|
230
277
|
output << '%%EOF' << "\n"
|
231
278
|
end
|
232
279
|
|
280
|
+
# Open (save) current graphic state in the content stream.
|
281
|
+
#
|
282
|
+
# @return [void]
|
233
283
|
def open_graphics_state
|
234
|
-
add_content
|
284
|
+
add_content('q')
|
235
285
|
end
|
236
286
|
|
287
|
+
# Close current graphic state (restore previous) in the content stream.
|
288
|
+
#
|
289
|
+
# @return [void]
|
237
290
|
def close_graphics_state
|
238
|
-
add_content
|
291
|
+
add_content('Q')
|
239
292
|
end
|
240
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]
|
241
303
|
def save_graphics_state(graphic_state = nil)
|
242
304
|
graphic_stack.save_graphic_state(graphic_state)
|
243
305
|
open_graphics_state
|
@@ -250,12 +312,15 @@ module PDF
|
|
250
312
|
# Returns true if content streams will be compressed before rendering,
|
251
313
|
# false otherwise
|
252
314
|
#
|
315
|
+
# @return [Boolean]
|
253
316
|
def compression_enabled?
|
254
317
|
state.compress
|
255
318
|
end
|
256
319
|
|
257
320
|
# Pops the last saved graphics state off the graphics state stack and
|
258
321
|
# restores the state to those values
|
322
|
+
#
|
323
|
+
# @return [void]
|
259
324
|
def restore_graphics_state
|
260
325
|
if graphic_stack.empty?
|
261
326
|
raise PDF::Core::Errors::EmptyGraphicStateStack,
|
@@ -265,10 +330,16 @@ module PDF
|
|
265
330
|
graphic_stack.restore_graphic_state
|
266
331
|
end
|
267
332
|
|
333
|
+
# Graphic state stack of the current document.
|
334
|
+
#
|
335
|
+
# @return [PDF::Core::GraphicStateStack]
|
268
336
|
def graphic_stack
|
269
337
|
state.page.stack
|
270
338
|
end
|
271
339
|
|
340
|
+
# Current graphic state
|
341
|
+
#
|
342
|
+
# @return [PDF::Core::GraphicState]
|
272
343
|
def graphic_state
|
273
344
|
save_graphics_state unless graphic_stack.current_state
|
274
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,14 +117,16 @@ 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
124
|
format(
|
94
|
-
'#<%<class>s:0x%<object_id>014x '
|
95
|
-
'@stream=%<stream>s, @filters=%<filters>s>',
|
125
|
+
'#<%<class>s:0x%<object_id>014x @stream=%<stream>s, @filters=%<filters>s>',
|
96
126
|
class: self.class.name,
|
97
127
|
object_id: object_id,
|
98
128
|
stream: @stream.inspect,
|
99
|
-
filters: @filters.inspect
|
129
|
+
filters: @filters.inspect,
|
100
130
|
)
|
101
131
|
end
|
102
132
|
end
|