pdf-core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,315 @@
1
+ module PDF
2
+ module Core
3
+ # The Outline class organizes the outline tree items for the document.
4
+ # Note that the prev and parent instance variables are adjusted while navigating
5
+ # through the nested blocks. These variables along with the presence or absense
6
+ # of blocks are the primary means by which the relations for the various
7
+ # OutlineItems and the OutlineRoot are set. Unfortunately, the best way to
8
+ # understand how this works is to follow the method calls through a real example.
9
+ #
10
+ # Some ideas for the organization of this class were gleaned from name_tree. In
11
+ # particular the way in which the OutlineItems are finally rendered into document
12
+ # objects in PdfObject through a hash.
13
+ #
14
+ class Outline
15
+
16
+ extend Forwardable
17
+ def_delegator :@document, :page_number
18
+
19
+ attr_accessor :parent
20
+ attr_accessor :prev
21
+ attr_accessor :document
22
+ attr_accessor :items
23
+
24
+ def initialize(document)
25
+ @document = document
26
+ @parent = root
27
+ @prev = nil
28
+ @items = {}
29
+ end
30
+
31
+ # Defines/Updates an outline for the document.
32
+ # The outline is an optional nested index that appears on the side of a PDF
33
+ # document usually with direct links to pages. The outline DSL is defined by nested
34
+ # blocks involving two methods: section and page; see the documentation on those methods
35
+ # for their arguments and options. Note that one can also use outline#update
36
+ # to add more sections to the end of the outline tree using the same syntax and scope.
37
+ #
38
+ # The syntax is best illustrated with an example:
39
+ #
40
+ # Prawn::Document.generate(outlined_document.pdf) do
41
+ # text "Page 1. This is the first Chapter. "
42
+ # start_new_page
43
+ # text "Page 2. More in the first Chapter. "
44
+ # start_new_page
45
+ # outline.define do
46
+ # section 'Chapter 1', :destination => 1, :closed => true do
47
+ # page :destination => 1, :title => 'Page 1'
48
+ # page :destination => 2, :title => 'Page 2'
49
+ # end
50
+ # end
51
+ # start_new_page do
52
+ # outline.update do
53
+ # section 'Chapter 2', :destination => 2, do
54
+ # page :destination => 3, :title => 'Page 3'
55
+ # end
56
+ # end
57
+ # end
58
+ #
59
+ def define(&block)
60
+ instance_eval(&block) if block
61
+ end
62
+
63
+ alias :update :define
64
+
65
+ # Inserts an outline section to the outline tree (see outline#define).
66
+ # Although you will probably choose to exclusively use outline#define so
67
+ # that your outline tree is contained and easy to manage, this method
68
+ # gives you the option to insert sections to the outline tree at any point
69
+ # during document generation. This method allows you to add a child subsection
70
+ # to any other item at any level in the outline tree.
71
+ # Currently the only way to locate the place of entry is with the title for the
72
+ # item. If your title names are not unique consider using define_outline.
73
+ # The method takes the following arguments:
74
+ # title: a string that must match an outline title to add the subsection to
75
+ # position: either :first or :last(the default) where the subsection will be placed relative
76
+ # to other child elements. If you need to position your subsection in between
77
+ # other elements then consider using #insert_section_after
78
+ # block: uses the same DSL syntax as outline#define, for example:
79
+ #
80
+ # Consider using this method inside of outline.update if you want to have the outline object
81
+ # to be scoped as self (see #insert_section_after example).
82
+ #
83
+ # go_to_page 2
84
+ # start_new_page
85
+ # text "Inserted Page"
86
+ # outline.add_subsection_to :title => 'Page 2', :first do
87
+ # outline.page :destination => page_number, :title => "Inserted Page"
88
+ # end
89
+ #
90
+ def add_subsection_to(title, position = :last, &block)
91
+ @parent = items[title]
92
+ raise Prawn::Errors::UnknownOutlineTitle,
93
+ "\n No outline item with title: '#{title}' exists in the outline tree" unless @parent
94
+ @prev = position == :first ? nil : @parent.data.last
95
+ nxt = position == :first ? @parent.data.first : nil
96
+ insert_section(nxt, &block)
97
+ end
98
+
99
+ # Inserts an outline section to the outline tree (see outline#define).
100
+ # Although you will probably choose to exclusively use outline#define so
101
+ # that your outline tree is contained and easy to manage, this method
102
+ # gives you the option to insert sections to the outline tree at any point
103
+ # during document generation. Unlike outline.add_section, this method allows
104
+ # you to enter a section after any other item at any level in the outline tree.
105
+ # Currently the only way to locate the place of entry is with the title for the
106
+ # item. If your title names are not unique consider using define_outline.
107
+ # The method takes the following arguments:
108
+ # title: the title of other section or page to insert new section after
109
+ # block: uses the same DSL syntax as outline#define, for example:
110
+ #
111
+ # go_to_page 2
112
+ # start_new_page
113
+ # text "Inserted Page"
114
+ # update_outline do
115
+ # insert_section_after :title => 'Page 2' do
116
+ # page :destination => page_number, :title => "Inserted Page"
117
+ # end
118
+ # end
119
+ #
120
+ def insert_section_after(title, &block)
121
+ @prev = items[title]
122
+ raise Prawn::Errors::UnknownOutlineTitle,
123
+ "\n No outline item with title: '#{title}' exists in the outline tree" unless @prev
124
+ @parent = @prev.data.parent
125
+ nxt = @prev.data.next
126
+ insert_section(nxt, &block)
127
+ end
128
+
129
+ # See outline#define above for documentation on how this is used in that context
130
+ #
131
+ # Adds an outine section to the outline tree.
132
+ # Although you will probably choose to exclusively use outline#define so
133
+ # that your outline tree is contained and easy to manage, this method
134
+ # gives you the option to add sections to the outline tree at any point
135
+ # during document generation. When not being called from within another #section block
136
+ # the section will be added at the top level after the other root elements of the outline.
137
+ # For more flexible placement try using outline#insert_section_after and/or
138
+ # outline#add_subsection_to
139
+ # Takes the following arguments:
140
+ # title: the outline text that appears for the section.
141
+ # options: destination - optional integer defining the page number for a destination link
142
+ # to the top of the page (using a :FIT destination).
143
+ # - or an array with a custom destination (see the #dest_* methods of the
144
+ # PDF::Destination module)
145
+ # closed - whether the section should show its nested outline elements.
146
+ # - defaults to false.
147
+ # block: more nested subsections and/or page blocks
148
+ #
149
+ # example usage:
150
+ #
151
+ # outline.section 'Added Section', :destination => 3 do
152
+ # outline.page :destionation => 3, :title => 'Page 3'
153
+ # end
154
+ def section(title, options = {}, &block)
155
+ add_outline_item(title, options, &block)
156
+ end
157
+
158
+ # See Outline#define above for more documentation on how it is used in that context
159
+ #
160
+ # Adds a page to the outline.
161
+ # Although you will probably choose to exclusively use outline#define so
162
+ # that your outline tree is contained and easy to manage, this method also
163
+ # gives you the option to add pages to the root of outline tree at any point
164
+ # during document generation. Note that the page will be added at the
165
+ # top level after the other root outline elements. For more flexible placement try
166
+ # using outline#insert_section_after and/or outline#add_subsection_to.
167
+ #
168
+ # Takes the following arguments:
169
+ # options:
170
+ # title - REQUIRED. The outline text that appears for the page.
171
+ # destination - optional integer defining the page number for a destination link
172
+ # to the top of the page (using a :FIT destination).
173
+ # - or an array with a custom destination (see the #dest_* methods of the
174
+ # PDF::Destination module)
175
+ # closed - whether the section should show its nested outline elements.
176
+ # - defaults to false.
177
+ # example usage:
178
+ #
179
+ # outline.page :title => "Very Last Page"
180
+ # Note: this method is almost identical to section except that it does not accept a block
181
+ # thereby defining the outline item as a leaf on the outline tree structure.
182
+ def page(options = {})
183
+ if options[:title]
184
+ title = options[:title]
185
+ else
186
+ raise Prawn::Errors::RequiredOption,
187
+ "\nTitle is a required option for page"
188
+ end
189
+ add_outline_item(title, options)
190
+ end
191
+
192
+ private
193
+
194
+ # The Outline dictionary (12.3.3) for this document. It is
195
+ # lazily initialized, so that documents that do not have an outline
196
+ # do not incur the additional overhead.
197
+ def root
198
+ document.state.store.root.data[:Outlines] ||= document.ref!(OutlineRoot.new)
199
+ end
200
+
201
+ def add_outline_item(title, options, &block)
202
+ outline_item = create_outline_item(title, options)
203
+ set_relations(outline_item)
204
+ increase_count
205
+ set_variables_for_block(outline_item, block)
206
+ block.call if block
207
+ reset_parent(outline_item)
208
+ end
209
+
210
+ def create_outline_item(title, options)
211
+ outline_item = OutlineItem.new(title, parent, options)
212
+
213
+ case options[:destination]
214
+ when Integer
215
+ page_index = options[:destination] - 1
216
+ outline_item.dest = [document.state.pages[page_index].dictionary, :Fit]
217
+ when Array
218
+ outline_item.dest = options[:destination]
219
+ end
220
+
221
+ outline_item.prev = prev if @prev
222
+ items[title] = document.ref!(outline_item)
223
+ end
224
+
225
+ def set_relations(outline_item)
226
+ prev.data.next = outline_item if prev
227
+ parent.data.first = outline_item unless prev
228
+ parent.data.last = outline_item
229
+ end
230
+
231
+ def increase_count
232
+ counting_parent = parent
233
+ while counting_parent
234
+ counting_parent.data.count += 1
235
+ if counting_parent == root
236
+ counting_parent = nil
237
+ else
238
+ counting_parent = counting_parent.data.parent
239
+ end
240
+ end
241
+ end
242
+
243
+ def set_variables_for_block(outline_item, block)
244
+ self.prev = block ? nil : outline_item
245
+ self.parent = outline_item if block
246
+ end
247
+
248
+ def reset_parent(outline_item)
249
+ if parent == outline_item
250
+ self.prev = outline_item
251
+ self.parent = outline_item.data.parent
252
+ end
253
+ end
254
+
255
+ def insert_section(nxt, &block)
256
+ last = @parent.data.last
257
+ if block
258
+ block.call
259
+ end
260
+ adjust_relations(nxt, last)
261
+ reset_root_positioning
262
+ end
263
+
264
+ def adjust_relations(nxt, last)
265
+ if nxt
266
+ nxt.data.prev = @prev
267
+ @prev.data.next = nxt
268
+ @parent.data.last = last
269
+ end
270
+ end
271
+
272
+ def reset_root_positioning
273
+ @parent = root
274
+ @prev = root.data.last
275
+ end
276
+
277
+ end
278
+
279
+ class OutlineRoot #:nodoc:
280
+ attr_accessor :count, :first, :last
281
+
282
+ def initialize
283
+ @count = 0
284
+ end
285
+
286
+ def to_hash
287
+ {:Type => :Outlines, :Count => count, :First => first, :Last => last}
288
+ end
289
+ end
290
+
291
+ class OutlineItem #:nodoc:
292
+ attr_accessor :count, :first, :last, :next, :prev, :parent, :title, :dest, :closed
293
+
294
+ def initialize(title, parent, options)
295
+ @closed = options[:closed]
296
+ @title = title
297
+ @parent = parent
298
+ @count = 0
299
+ end
300
+
301
+ def to_hash
302
+ hash = { :Title => title,
303
+ :Parent => parent,
304
+ :Count => closed ? -count : count }
305
+ [{:First => first}, {:Last => last}, {:Next => defined?(@next) && @next},
306
+ {:Prev => prev}, {:Dest => dest}].each do |h|
307
+ unless h.values.first.nil?
308
+ hash.merge!(h)
309
+ end
310
+ end
311
+ hash
312
+ end
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,212 @@
1
+ # encoding: utf-8
2
+
3
+ # prawn/core/page.rb : Implements low-level representation of a PDF page
4
+ #
5
+ # Copyright February 2010, Gregory Brown. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ #
9
+
10
+ require_relative 'graphics_state'
11
+
12
+ module PDF
13
+ module Core
14
+ class Page #:nodoc:
15
+ attr_accessor :document, :margins, :stack
16
+ attr_writer :content, :dictionary
17
+
18
+ def initialize(document, options={})
19
+ @document = document
20
+ @margins = options[:margins] || { :left => 36,
21
+ :right => 36,
22
+ :top => 36,
23
+ :bottom => 36 }
24
+ @stack = GraphicStateStack.new(options[:graphic_state])
25
+ if options[:object_id]
26
+ init_from_object(options)
27
+ else
28
+ init_new_page(options)
29
+ end
30
+ end
31
+
32
+ def graphic_state
33
+ stack.current_state
34
+ end
35
+
36
+ def layout
37
+ return @layout if defined?(@layout) && @layout
38
+
39
+ mb = dictionary.data[:MediaBox]
40
+ if mb[3] > mb[2]
41
+ :portrait
42
+ else
43
+ :landscape
44
+ end
45
+ end
46
+
47
+ def size
48
+ defined?(@size) && @size || dimensions[2,2]
49
+ end
50
+
51
+ def in_stamp_stream?
52
+ !!@stamp_stream
53
+ end
54
+
55
+ def stamp_stream(dictionary)
56
+ @stamp_stream = ""
57
+ @stamp_dictionary = dictionary
58
+ graphic_stack_size = stack.stack.size
59
+
60
+ document.save_graphics_state
61
+ document.send(:freeze_stamp_graphics)
62
+ yield if block_given?
63
+
64
+ until graphic_stack_size == stack.stack.size
65
+ document.restore_graphics_state
66
+ end
67
+
68
+ @stamp_dictionary << @stamp_stream
69
+
70
+ @stamp_stream = nil
71
+ @stamp_dictionary = nil
72
+ end
73
+
74
+ def content
75
+ @stamp_stream || document.state.store[@content]
76
+ end
77
+
78
+ # As per the PDF spec, each page can have multiple content streams. This will
79
+ # add a fresh, empty content stream this the page, mainly for use in loading
80
+ # template files.
81
+ #
82
+ def new_content_stream
83
+ return if in_stamp_stream?
84
+
85
+ unless dictionary.data[:Contents].is_a?(Array)
86
+ dictionary.data[:Contents] = [content]
87
+ end
88
+ @content = document.ref({})
89
+ dictionary.data[:Contents] << document.state.store[@content]
90
+ document.open_graphics_state
91
+ end
92
+
93
+ def dictionary
94
+ defined?(@stamp_dictionary) && @stamp_dictionary || document.state.store[@dictionary]
95
+ end
96
+
97
+ def resources
98
+ if dictionary.data[:Resources]
99
+ document.deref(dictionary.data[:Resources])
100
+ else
101
+ dictionary.data[:Resources] = {}
102
+ end
103
+ end
104
+
105
+ def fonts
106
+ if resources[:Font]
107
+ document.deref(resources[:Font])
108
+ else
109
+ resources[:Font] = {}
110
+ end
111
+ end
112
+
113
+ def xobjects
114
+ if resources[:XObject]
115
+ document.deref(resources[:XObject])
116
+ else
117
+ resources[:XObject] = {}
118
+ end
119
+ end
120
+
121
+ def ext_gstates
122
+ if resources[:ExtGState]
123
+ document.deref(resources[:ExtGState])
124
+ else
125
+ resources[:ExtGState] = {}
126
+ end
127
+ end
128
+
129
+ def finalize
130
+ if dictionary.data[:Contents].is_a?(Array)
131
+ dictionary.data[:Contents].each do |stream|
132
+ stream.stream.compress! if document.compression_enabled?
133
+ end
134
+ else
135
+ content.stream.compress! if document.compression_enabled?
136
+ end
137
+ end
138
+
139
+ def imported_page?
140
+ @imported_page
141
+ end
142
+
143
+ def dimensions
144
+ return inherited_dictionary_value(:MediaBox) if imported_page?
145
+
146
+ coords = PDF::Core::PageGeometry::SIZES[size] || size
147
+ [0,0] + case(layout)
148
+ when :portrait
149
+ coords
150
+ when :landscape
151
+ coords.reverse
152
+ else
153
+ raise PDF::Core::Errors::InvalidPageLayout,
154
+ "Layout must be either :portrait or :landscape"
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ def init_from_object(options)
161
+ @dictionary = options[:object_id].to_i
162
+ dictionary.data[:Parent] = document.state.store.pages if options[:page_template]
163
+
164
+ unless dictionary.data[:Contents].is_a?(Array) # content only on leafs
165
+ @content = dictionary.data[:Contents].identifier
166
+ end
167
+
168
+ @stamp_stream = nil
169
+ @stamp_dictionary = nil
170
+ @imported_page = true
171
+ end
172
+
173
+ def init_new_page(options)
174
+ @size = options[:size] || "LETTER"
175
+ @layout = options[:layout] || :portrait
176
+
177
+ @stamp_stream = nil
178
+ @stamp_dictionary = nil
179
+ @imported_page = false
180
+
181
+ @content = document.ref({})
182
+ content << "q" << "\n"
183
+ @dictionary = document.ref(:Type => :Page,
184
+ :Parent => document.state.store.pages,
185
+ :MediaBox => dimensions,
186
+ :Contents => content)
187
+
188
+ resources[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI]
189
+ end
190
+
191
+ # some entries in the Page dict can be inherited from parent Pages dicts.
192
+ #
193
+ # Starting with the current page dict, this method will walk up the
194
+ # inheritance chain return the first value that is found for key
195
+ #
196
+ # inherited_dictionary_value(:MediaBox)
197
+ # => [ 0, 0, 595, 842 ]
198
+ #
199
+ def inherited_dictionary_value(key, local_dict = nil)
200
+ local_dict ||= dictionary.data
201
+
202
+ if local_dict.has_key?(key)
203
+ local_dict[key]
204
+ elsif local_dict.has_key?(:Parent)
205
+ inherited_dictionary_value(key, local_dict[:Parent].data)
206
+ else
207
+ nil
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end