File: page.rb

package info (click to toggle)
ruby-pdf-core 0.10.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 408 kB
  • sloc: ruby: 2,270; makefile: 4
file content (351 lines) | stat: -rw-r--r-- 10,098 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# frozen_string_literal: true

require_relative 'graphics_state'

module PDF
  module Core
    # Low-level representation of a PDF page
    #
    # @api private
    class Page
      # Page art box indents relative to page edges.
      #
      # @return [Hash<[:left, :right, :top, :bottom], Numeric>
      attr_accessor :art_indents

      # Page bleed box indents.
      #
      # @return [Hash<[:left, :right, :top, :bottom], Numeric>
      attr_accessor :bleeds

      # Page crop box indents.
      #
      # @return [Hash<[:left, :right, :top, :bottom], Numeric>
      attr_accessor :crops

      # Page trim box indents.
      #
      # @return [Hash<[:left, :right, :top, :bottom], Numeric>
      attr_accessor :trims

      # Page margins.
      #
      # @return [Hash<[:left, :right, :top, :bottom], Numeric>
      attr_accessor :margins

      # Owning document.
      #
      # @return [Prawn::Document]
      attr_accessor :document

      # Graphic state stack.
      #
      # @return [GraphicStateStack]
      attr_accessor :stack

      # Page content stream reference.
      #
      # @return [PDF::Core::Reference<Hash>]
      attr_writer :content

      # Page dictionary reference.
      #
      # @return [PDF::Core::Reference<Hash>]
      attr_writer :dictionary

      # A convenince constant of no indents.
      ZERO_INDENTS = {
        left: 0,
        bottom: 0,
        right: 0,
        top: 0,
      }.freeze

      # @param document [Prawn::Document]
      # @param options [Hash]
      # @option options :margins [Hash{:left, :right, :top, :bottom => Number}, nil]
      #   ({ left: 0, right: 0, top: 0, bottom: 0 }) Page margins
      # @option options :crop [Hash{:left, :right, :top, :bottom => Number}, nil] (ZERO_INDENTS)
      #   Page crop box
      # @option options :bleed [Hash{:left, :right, :top, :bottom => Number},  nil] (ZERO_INDENTS)
      #   Page bleed box
      # @option options :trims [Hash{:left, :right, :top, :bottom => Number}, nil] (ZERO_INDENTS)
      #   Page trim box
      # @option options :art_indents [Hash{:left, :right, :top, :bottom => Number}, Numeric>, nil] (ZERO_INDENTS)
      #   Page art box indents.
      # @option options :graphic_state [PDF::Core::GraphicState, nil] (nil)
      #   Initial graphic state
      # @option options :size [String, Array<Numeric>, nil] ('LETTER')
      #   Page size. A string identifies a named page size defined in
      #   {PageGeometry}. An array must be a two element array specifying width
      #   and height in points.
      # @option options :layout [:portrait, :landscape, nil] (:portrait)
      #   Page orientation.
      def initialize(document, options = {})
        @document = document
        @margins = options[:margins] || {
          left: 36,
          right: 36,
          top: 36,
          bottom: 36,
        }
        @crops = options[:crops] || ZERO_INDENTS
        @bleeds = options[:bleeds] || ZERO_INDENTS
        @trims = options[:trims] || ZERO_INDENTS
        @art_indents = options[:art_indents] || ZERO_INDENTS
        @stack = GraphicStateStack.new(options[:graphic_state])
        @size = options[:size] || 'LETTER'
        @layout = options[:layout] || :portrait

        @stamp_stream = nil
        @stamp_dictionary = nil

        @content = document.ref({})
        content << 'q' << "\n"
        @dictionary = document.ref(
          Type: :Page,
          Parent: document.state.store.pages,
          MediaBox: dimensions,
          CropBox: crop_box,
          BleedBox: bleed_box,
          TrimBox: trim_box,
          ArtBox: art_box,
          Contents: content,
        )

        resources[:ProcSet] = %i[PDF Text ImageB ImageC ImageI]
      end

      # Current graphic state.
      #
      # @return [PDF::Core::GraphicState]
      def graphic_state
        stack.current_state
      end

      # Page layout.
      #
      # @return [:portrait] if page is talled than wider
      # @return [:landscape] otherwise
      def layout
        return @layout if defined?(@layout) && @layout

        mb = dictionary.data[:MediaBox]
        if mb[3] > mb[2]
          :portrait
        else
          :landscape
        end
      end

      # Page size.
      #
      # @return [Array<Numeric>] a two-element array containing width and height
      #   of the page.
      def size
        (defined?(@size) && @size) || dimensions[2, 2]
      end

      # Are we drawing to a stamp right now?
      #
      # @return [Boolean]
      def in_stamp_stream?
        !@stamp_stream.nil?
      end

      # Draw to stamp.
      #
      # @param dictionary [PDF::Core::Reference<Hash>] stamp dictionary
      # @yield outputs to the stamp
      # @return [void]
      def stamp_stream(dictionary)
        @stamp_dictionary = dictionary
        @stamp_stream = @stamp_dictionary.stream
        graphic_stack_size = stack.stack.size

        document.save_graphics_state
        document.__send__(:freeze_stamp_graphics)
        yield if block_given?

        until graphic_stack_size == stack.stack.size
          document.restore_graphics_state
        end

        @stamp_stream = nil
        @stamp_dictionary = nil
      end

      # Current content stream. Can be either the page content stream or a stamp
      # content stream.
      #
      # @return [PDF::Core::Reference<Hash>]
      def content
        @stamp_stream || document.state.store[@content]
      end

      # Current content dictionary. Can be either the page dictionary or a stamp
      # dictionary.
      #
      # @return [PDF::Core::Reference<Hash>]
      def dictionary
        (defined?(@stamp_dictionary) && @stamp_dictionary) ||
          document.state.store[@dictionary]
      end

      # Page resources dictionary.
      #
      # @return [Hash]
      def resources
        if dictionary.data[:Resources]
          document.deref(dictionary.data[:Resources])
        else
          dictionary.data[:Resources] = {}
        end
      end

      # Fonts dictionary.
      #
      # @return [Hash]
      def fonts
        if resources[:Font]
          document.deref(resources[:Font])
        else
          resources[:Font] = {}
        end
      end

      # External objects dictionary.
      #
      # @return [Hash]
      def xobjects
        if resources[:XObject]
          document.deref(resources[:XObject])
        else
          resources[:XObject] = {}
        end
      end

      # Graphic state parameter dictionary.
      #
      # @return [Hash]
      def ext_gstates
        if resources[:ExtGState]
          document.deref(resources[:ExtGState])
        else
          resources[:ExtGState] = {}
        end
      end

      # Finalize page.
      #
      # @return [void]
      def finalize
        if dictionary.data[:Contents].is_a?(Array)
          dictionary.data[:Contents].each do |stream|
            stream.stream.compress! if document.compression_enabled?
          end
        elsif document.compression_enabled?
          content.stream.compress!
        end
      end

      # Page dimensions.
      #
      # @return [Array<Numeric>]
      def dimensions
        coords = PDF::Core::PageGeometry::SIZES[size] || size
        coords =
          case layout
          when :portrait
            coords
          when :landscape
            coords.reverse
          else
            raise PDF::Core::Errors::InvalidPageLayout,
              'Layout must be either :portrait or :landscape'
          end
        [0, 0].concat(coords)
      end

      # A rectangle, expressed in default user space units, defining the extent
      # of the page's meaningful content (including potential white space) as
      # intended by the page's creator.
      #
      # @return [Array<Numeric>]
      def art_box
        left, bottom, right, top = dimensions
        [
          left + art_indents[:left],
          bottom + art_indents[:bottom],
          right - art_indents[:right],
          top - art_indents[:top],
        ]
      end

      # Page bleed box. A rectangle, expressed in default user space units,
      # defining the region to which the contents of the page should be clipped
      # when output in a production environment.
      #
      # @return [Array<Numeric>]
      def bleed_box
        left, bottom, right, top = dimensions
        [
          left + bleeds[:left],
          bottom + bleeds[:bottom],
          right - bleeds[:right],
          top - bleeds[:top],
        ]
      end

      # A rectangle, expressed in default user space units, defining the visible
      # region of default user space. When the page is displayed or printed, its
      # contents are to be clipped (cropped) to this rectangle and then imposed
      # on the output medium in some implementation-defined manner.
      #
      # @return [Array<Numeric>]
      def crop_box
        left, bottom, right, top = dimensions
        [
          left + crops[:left],
          bottom + crops[:bottom],
          right - crops[:right],
          top - crops[:top],
        ]
      end

      # A rectangle, expressed in default user space units, defining the
      # intended dimensions of the finished page after trimming.
      #
      # @return [Array<Numeric>]
      def trim_box
        left, bottom, right, top = dimensions
        [
          left + trims[:left],
          bottom + trims[:bottom],
          right - trims[:right],
          top - trims[:top],
        ]
      end

      private

      # some entries in the Page dict can be inherited from parent Pages dicts.
      #
      # Starting with the current page dict, this method will walk up the
      # inheritance chain return the first value that is found for key
      #
      #     inherited_dictionary_value(:MediaBox)
      #     => [ 0, 0, 595, 842 ]
      #
      def inherited_dictionary_value(key, local_dict = nil)
        local_dict ||= dictionary.data

        if local_dict.key?(key)
          local_dict[key]
        elsif local_dict.key?(:Parent)
          inherited_dictionary_value(key, local_dict[:Parent].data)
        end
      end
    end
  end
end