pdf-core 0.4.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.
@@ -1,8 +1,56 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PDF
2
4
  module Core
3
- class OutlineItem #:nodoc:
4
- attr_accessor :count, :first, :last, :next, :prev, :parent, :title, :dest, :closed
5
+ # Outline item.
6
+ #
7
+ # @api private
8
+ # @see # PDF 1.7 spec, section 8.2.2 Document Outline
9
+ class OutlineItem
10
+ # The total number of its open descendants at all lower levels of the
11
+ # outline hierarchy.
12
+ # @return [Integer]
13
+ attr_accessor :count
14
+
15
+ # The first of this item’s immediate children in the outline hierarchy.
16
+ # @return [Reference<PDF::Core::OutlineItem>]
17
+ attr_accessor :first
18
+
19
+ # The last of this item’s immediate children in the outline hierarchy.
20
+ # @return [Reference<PDF::Core::OutlineItem>]
21
+ attr_accessor :last
22
+
23
+ # The next item at this outline level.
24
+ # @return [Reference<PDF::Core::OutlineItem>]
25
+ attr_accessor :next
26
+
27
+ # The previous item at this outline level.
28
+ # @return [Reference<PDF::Core::OutlineItem>]
29
+ attr_accessor :prev
30
+
31
+ # The parent of this item in the outline hierarchy.
32
+ # @return [Reference<[PDF::Core::OutlineItem, PDF::Core::OutlineRoot]>]
33
+ attr_accessor :parent
34
+
35
+ # The text to be displayed on the screen for this item.
36
+ # @return [String]
37
+ attr_accessor :title
38
+
39
+ # The destination to be displayed when this item is activated.
40
+ # @return [String]
41
+ # @return [Symbol]
42
+ # @return [Array]
43
+ # @see Destinations
44
+ attr_accessor :dest
45
+
46
+ # Is this item open or closed.
47
+ # @return [Boolean]
48
+ attr_accessor :closed
5
49
 
50
+ # @param title [String]
51
+ # @param parent [PDF::Core::OutlineRoot, PDF::Core::OutlineItem]
52
+ # @param options [Hash]
53
+ # @option options :closed [Boolean]
6
54
  def initialize(title, parent, options)
7
55
  @closed = options[:closed]
8
56
  @title = title
@@ -10,12 +58,19 @@ module PDF
10
58
  @count = 0
11
59
  end
12
60
 
61
+ # A hash representation of this outline item.
62
+ #
63
+ # @return [Hash]
13
64
  def to_hash
14
- hash = { :Title => title,
15
- :Parent => parent,
16
- :Count => closed ? -count : count }
17
- [{:First => first}, {:Last => last}, {:Next => defined?(@next) && @next},
18
- {:Prev => prev}, {:Dest => dest}].each do |h|
65
+ hash = {
66
+ Title: title,
67
+ Parent: parent,
68
+ Count: closed ? -count : count,
69
+ }
70
+ [
71
+ { First: first }, { Last: last }, { Next: defined?(@next) && @next },
72
+ { Prev: prev }, { Dest: dest },
73
+ ].each do |h|
19
74
  unless h.values.first.nil?
20
75
  hash.merge!(h)
21
76
  end
@@ -1,14 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PDF
2
4
  module Core
3
- class OutlineRoot #:nodoc:
4
- attr_accessor :count, :first, :last
5
+ # Document Outline root.
6
+ #
7
+ # @api private
8
+ # @see # PDF 1.7 spec, section 8.2.2 Document Outline
9
+ class OutlineRoot
10
+ # The total number of open items at all levels of the outline.
11
+ # @return [Integer]
12
+ attr_accessor :count
13
+
14
+ # The first top-level item in the outline.
15
+ # @return [Reference]
16
+ attr_accessor :first
17
+
18
+ # The last top-level item in the outline.
19
+ # @return [Reference]
20
+ attr_accessor :last
5
21
 
6
22
  def initialize
7
23
  @count = 0
8
24
  end
9
25
 
26
+ # Hash representation of the outline root
27
+ # @return [Hash]
10
28
  def to_hash
11
- {:Type => :Outlines, :Count => count, :First => first, :Last => last}
29
+ { Type: :Outlines, Count: count, First: first, Last: last }
12
30
  end
13
31
  end
14
32
  end
data/lib/pdf/core/page.rb CHANGED
@@ -1,38 +1,132 @@
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
- #
1
+ # frozen_string_literal: true
9
2
 
10
3
  require_relative 'graphics_state'
11
4
 
12
5
  module PDF
13
6
  module Core
14
- class Page #:nodoc:
15
- attr_accessor :document, :margins, :stack
16
- attr_writer :content, :dictionary
7
+ # Low-level representation of a PDF page
8
+ #
9
+ # @api private
10
+ class Page
11
+ # Page art box indents relative to page edges.
12
+ #
13
+ # @return [Hash<[:left, :right, :top, :bottom], Numeric>
14
+ attr_accessor :art_indents
15
+
16
+ # Page bleed box indents.
17
+ #
18
+ # @return [Hash<[:left, :right, :top, :bottom], Numeric>
19
+ attr_accessor :bleeds
20
+
21
+ # Page crop box indents.
22
+ #
23
+ # @return [Hash<[:left, :right, :top, :bottom], Numeric>
24
+ attr_accessor :crops
25
+
26
+ # Page trim box indents.
27
+ #
28
+ # @return [Hash<[:left, :right, :top, :bottom], Numeric>
29
+ attr_accessor :trims
30
+
31
+ # Page margins.
32
+ #
33
+ # @return [Hash<[:left, :right, :top, :bottom], Numeric>
34
+ attr_accessor :margins
35
+
36
+ # Owning document.
37
+ #
38
+ # @return [Prawn::Document]
39
+ attr_accessor :document
40
+
41
+ # Graphic state stack.
42
+ #
43
+ # @return [GraphicStateStack]
44
+ attr_accessor :stack
17
45
 
18
- def initialize(document, options={})
46
+ # Page content stream reference.
47
+ #
48
+ # @return [PDF::Core::Reference<Hash>]
49
+ attr_writer :content
50
+
51
+ # Page dictionary reference.
52
+ #
53
+ # @return [PDF::Core::Reference<Hash>]
54
+ attr_writer :dictionary
55
+
56
+ # A convenince constant of no indents.
57
+ ZERO_INDENTS = {
58
+ left: 0,
59
+ bottom: 0,
60
+ right: 0,
61
+ top: 0,
62
+ }.freeze
63
+
64
+ # @param document [Prawn::Document]
65
+ # @param options [Hash]
66
+ # @option options :margins [Hash{:left, :right, :top, :bottom => Number}, nil]
67
+ # ({ left: 0, right: 0, top: 0, bottom: 0 }) Page margins
68
+ # @option options :crop [Hash{:left, :right, :top, :bottom => Number}, nil] (ZERO_INDENTS)
69
+ # Page crop box
70
+ # @option options :bleed [Hash{:left, :right, :top, :bottom => Number}, nil] (ZERO_INDENTS)
71
+ # Page bleed box
72
+ # @option options :trims [Hash{:left, :right, :top, :bottom => Number}, nil] (ZERO_INDENTS)
73
+ # Page trim box
74
+ # @option options :art_indents [Hash{:left, :right, :top, :bottom => Number}, Numeric>, nil] (ZERO_INDENTS)
75
+ # Page art box indents.
76
+ # @option options :graphic_state [PDF::Core::GraphicState, nil] (nil)
77
+ # Initial graphic state
78
+ # @option options :size [String, Array<Numeric>, nil] ('LETTER')
79
+ # Page size. A string identifies a named page size defined in
80
+ # {PageGeometry}. An array must be a two element array specifying width
81
+ # and height in points.
82
+ # @option options :layout [:portrait, :landscape, nil] (:portrait)
83
+ # Page orientation.
84
+ def initialize(document, options = {})
19
85
  @document = document
20
- @margins = options[:margins] || { :left => 36,
21
- :right => 36,
22
- :top => 36,
23
- :bottom => 36 }
86
+ @margins = options[:margins] || {
87
+ left: 36,
88
+ right: 36,
89
+ top: 36,
90
+ bottom: 36,
91
+ }
92
+ @crops = options[:crops] || ZERO_INDENTS
93
+ @bleeds = options[:bleeds] || ZERO_INDENTS
94
+ @trims = options[:trims] || ZERO_INDENTS
95
+ @art_indents = options[:art_indents] || ZERO_INDENTS
24
96
  @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
97
+ @size = options[:size] || 'LETTER'
98
+ @layout = options[:layout] || :portrait
99
+
100
+ @stamp_stream = nil
101
+ @stamp_dictionary = nil
102
+
103
+ @content = document.ref({})
104
+ content << 'q' << "\n"
105
+ @dictionary = document.ref(
106
+ Type: :Page,
107
+ Parent: document.state.store.pages,
108
+ MediaBox: dimensions,
109
+ CropBox: crop_box,
110
+ BleedBox: bleed_box,
111
+ TrimBox: trim_box,
112
+ ArtBox: art_box,
113
+ Contents: content,
114
+ )
115
+
116
+ resources[:ProcSet] = %i[PDF Text ImageB ImageC ImageI]
30
117
  end
31
118
 
119
+ # Current graphic state.
120
+ #
121
+ # @return [PDF::Core::GraphicState]
32
122
  def graphic_state
33
123
  stack.current_state
34
124
  end
35
125
 
126
+ # Page layout.
127
+ #
128
+ # @return [:portrait] if page is talled than wider
129
+ # @return [:landscape] otherwise
36
130
  def layout
37
131
  return @layout if defined?(@layout) && @layout
38
132
 
@@ -44,41 +138,63 @@ module PDF
44
138
  end
45
139
  end
46
140
 
141
+ # Page size.
142
+ #
143
+ # @return [Array<Numeric>] a two-element array containing width and height
144
+ # of the page.
47
145
  def size
48
- defined?(@size) && @size || dimensions[2,2]
146
+ (defined?(@size) && @size) || dimensions[2, 2]
49
147
  end
50
148
 
149
+ # Are we drawing to a stamp right now?
150
+ #
151
+ # @return [Boolean]
51
152
  def in_stamp_stream?
52
- !!@stamp_stream
153
+ !@stamp_stream.nil?
53
154
  end
54
155
 
156
+ # Draw to stamp.
157
+ #
158
+ # @param dictionary [PDF::Core::Reference<Hash>] stamp dictionary
159
+ # @yield outputs to the stamp
160
+ # @return [void]
55
161
  def stamp_stream(dictionary)
56
- @stamp_stream = ""
57
162
  @stamp_dictionary = dictionary
163
+ @stamp_stream = @stamp_dictionary.stream
58
164
  graphic_stack_size = stack.stack.size
59
165
 
60
166
  document.save_graphics_state
61
- document.send(:freeze_stamp_graphics)
167
+ document.__send__(:freeze_stamp_graphics)
62
168
  yield if block_given?
63
169
 
64
170
  until graphic_stack_size == stack.stack.size
65
171
  document.restore_graphics_state
66
172
  end
67
173
 
68
- @stamp_dictionary << @stamp_stream
69
-
70
- @stamp_stream = nil
71
- @stamp_dictionary = nil
174
+ @stamp_stream = nil
175
+ @stamp_dictionary = nil
72
176
  end
73
177
 
178
+ # Current content stream. Can be either the page content stream or a stamp
179
+ # content stream.
180
+ #
181
+ # @return [PDF::Core::Reference<Hash>]
74
182
  def content
75
183
  @stamp_stream || document.state.store[@content]
76
184
  end
77
185
 
186
+ # Current content dictionary. Can be either the page dictionary or a stamp
187
+ # dictionary.
188
+ #
189
+ # @return [PDF::Core::Reference<Hash>]
78
190
  def dictionary
79
- defined?(@stamp_dictionary) && @stamp_dictionary || document.state.store[@dictionary]
191
+ (defined?(@stamp_dictionary) && @stamp_dictionary) ||
192
+ document.state.store[@dictionary]
80
193
  end
81
194
 
195
+ # Page resources dictionary.
196
+ #
197
+ # @return [Hash]
82
198
  def resources
83
199
  if dictionary.data[:Resources]
84
200
  document.deref(dictionary.data[:Resources])
@@ -87,6 +203,9 @@ module PDF
87
203
  end
88
204
  end
89
205
 
206
+ # Fonts dictionary.
207
+ #
208
+ # @return [Hash]
90
209
  def fonts
91
210
  if resources[:Font]
92
211
  document.deref(resources[:Font])
@@ -95,6 +214,9 @@ module PDF
95
214
  end
96
215
  end
97
216
 
217
+ # External objects dictionary.
218
+ #
219
+ # @return [Hash]
98
220
  def xobjects
99
221
  if resources[:XObject]
100
222
  document.deref(resources[:XObject])
@@ -103,6 +225,9 @@ module PDF
103
225
  end
104
226
  end
105
227
 
228
+ # Graphic state parameter dictionary.
229
+ #
230
+ # @return [Hash]
106
231
  def ext_gstates
107
232
  if resources[:ExtGState]
108
233
  document.deref(resources[:ExtGState])
@@ -111,67 +236,99 @@ module PDF
111
236
  end
112
237
  end
113
238
 
239
+ # Finalize page.
240
+ #
241
+ # @return [void]
114
242
  def finalize
115
243
  if dictionary.data[:Contents].is_a?(Array)
116
244
  dictionary.data[:Contents].each do |stream|
117
245
  stream.stream.compress! if document.compression_enabled?
118
246
  end
119
- else
120
- content.stream.compress! if document.compression_enabled?
247
+ elsif document.compression_enabled?
248
+ content.stream.compress!
121
249
  end
122
250
  end
123
251
 
124
- def imported_page?
125
- @imported_page
126
- end
127
-
252
+ # Page dimensions.
253
+ #
254
+ # @return [Array<Numeric>]
128
255
  def dimensions
129
- return inherited_dictionary_value(:MediaBox) if imported_page?
130
-
131
256
  coords = PDF::Core::PageGeometry::SIZES[size] || size
132
- [0,0] + case(layout)
133
- when :portrait
134
- coords
135
- when :landscape
136
- coords.reverse
137
- else
138
- raise PDF::Core::Errors::InvalidPageLayout,
139
- "Layout must be either :portrait or :landscape"
140
- end
257
+ coords =
258
+ case layout
259
+ when :portrait
260
+ coords
261
+ when :landscape
262
+ coords.reverse
263
+ else
264
+ raise PDF::Core::Errors::InvalidPageLayout,
265
+ 'Layout must be either :portrait or :landscape'
266
+ end
267
+ [0, 0].concat(coords)
141
268
  end
142
269
 
143
- private
144
-
145
- def init_from_object(options)
146
- @dictionary = options[:object_id].to_i
147
-
148
- unless dictionary.data[:Contents].is_a?(Array) # content only on leafs
149
- @content = dictionary.data[:Contents].identifier
150
- end
151
-
152
- @stamp_stream = nil
153
- @stamp_dictionary = nil
154
- @imported_page = true
270
+ # A rectangle, expressed in default user space units, defining the extent
271
+ # of the page's meaningful content (including potential white space) as
272
+ # intended by the page's creator.
273
+ #
274
+ # @return [Array<Numeric>]
275
+ def art_box
276
+ left, bottom, right, top = dimensions
277
+ [
278
+ left + art_indents[:left],
279
+ bottom + art_indents[:bottom],
280
+ right - art_indents[:right],
281
+ top - art_indents[:top],
282
+ ]
155
283
  end
156
284
 
157
- def init_new_page(options)
158
- @size = options[:size] || "LETTER"
159
- @layout = options[:layout] || :portrait
160
-
161
- @stamp_stream = nil
162
- @stamp_dictionary = nil
163
- @imported_page = false
285
+ # Page bleed box. A rectangle, expressed in default user space units,
286
+ # defining the region to which the contents of the page should be clipped
287
+ # when output in a production environment.
288
+ #
289
+ # @return [Array<Numeric>]
290
+ def bleed_box
291
+ left, bottom, right, top = dimensions
292
+ [
293
+ left + bleeds[:left],
294
+ bottom + bleeds[:bottom],
295
+ right - bleeds[:right],
296
+ top - bleeds[:top],
297
+ ]
298
+ end
164
299
 
165
- @content = document.ref({})
166
- content << "q" << "\n"
167
- @dictionary = document.ref(:Type => :Page,
168
- :Parent => document.state.store.pages,
169
- :MediaBox => dimensions,
170
- :Contents => content)
300
+ # A rectangle, expressed in default user space units, defining the visible
301
+ # region of default user space. When the page is displayed or printed, its
302
+ # contents are to be clipped (cropped) to this rectangle and then imposed
303
+ # on the output medium in some implementation-defined manner.
304
+ #
305
+ # @return [Array<Numeric>]
306
+ def crop_box
307
+ left, bottom, right, top = dimensions
308
+ [
309
+ left + crops[:left],
310
+ bottom + crops[:bottom],
311
+ right - crops[:right],
312
+ top - crops[:top],
313
+ ]
314
+ end
171
315
 
172
- resources[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI]
316
+ # A rectangle, expressed in default user space units, defining the
317
+ # intended dimensions of the finished page after trimming.
318
+ #
319
+ # @return [Array<Numeric>]
320
+ def trim_box
321
+ left, bottom, right, top = dimensions
322
+ [
323
+ left + trims[:left],
324
+ bottom + trims[:bottom],
325
+ right - trims[:right],
326
+ top - trims[:top],
327
+ ]
173
328
  end
174
329
 
330
+ private
331
+
175
332
  # some entries in the Page dict can be inherited from parent Pages dicts.
176
333
  #
177
334
  # Starting with the current page dict, this method will walk up the
@@ -183,12 +340,10 @@ module PDF
183
340
  def inherited_dictionary_value(key, local_dict = nil)
184
341
  local_dict ||= dictionary.data
185
342
 
186
- if local_dict.has_key?(key)
343
+ if local_dict.key?(key)
187
344
  local_dict[key]
188
- elsif local_dict.has_key?(:Parent)
345
+ elsif local_dict.key?(:Parent)
189
346
  inherited_dictionary_value(key, local_dict[:Parent].data)
190
- else
191
- nil
192
347
  end
193
348
  end
194
349
  end