File: arranger.rb

package info (click to toggle)
ruby-prawn 2.3.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 4,380 kB
  • sloc: ruby: 15,820; sh: 43; makefile: 20
file content (316 lines) | stat: -rw-r--r-- 9,341 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
# frozen_string_literal: true

# core/text/formatted/arranger.rb : Implements a data structure for 2-stage
#                                   processing of lines of formatted text
#
# Copyright February 2010, Daniel Nelson. All Rights Reserved.
#
# This is free software. Please see the LICENSE and COPYING files for details.

module Prawn
  module Text
    module Formatted #:nodoc:
      # @private

      class Arranger #:nodoc:
        attr_reader :max_line_height
        attr_reader :max_descender
        attr_reader :max_ascender
        attr_reader :finalized
        attr_accessor :consumed

        # The following present only for testing purposes
        attr_reader :unconsumed
        attr_reader :fragments
        attr_reader :current_format_state

        def initialize(document, options = {})
          @document = document
          @fragments = []
          @unconsumed = []
          @kerning = options[:kerning]
        end

        def space_count
          unless finalized
            raise 'Lines must be finalized before calling #space_count'
          end

          @fragments.inject(0) do |sum, fragment|
            sum + fragment.space_count
          end
        end

        def line_width
          unless finalized
            raise 'Lines must be finalized before calling #line_width'
          end

          @fragments.inject(0) do |sum, fragment|
            sum + fragment.width
          end
        end

        def line
          unless finalized
            raise 'Lines must be finalized before calling #line'
          end

          @fragments.collect do |fragment|
            fragment.text.dup.encode(::Encoding::UTF_8)
          rescue ::Encoding::InvalidByteSequenceError,
                 ::Encoding::UndefinedConversionError
            fragment.text.dup.force_encoding(::Encoding::UTF_8)
          end.join
        end

        def finalize_line
          @finalized = true

          omit_trailing_whitespace_from_line_width
          @fragments = []
          @consumed.each do |hash|
            text = hash[:text]
            format_state = hash.dup
            format_state.delete(:text)
            fragment = Prawn::Text::Formatted::Fragment.new(
              text,
              format_state,
              @document
            )
            @fragments << fragment
            self.fragment_measurements = fragment
            self.line_measurement_maximums = fragment
          end
        end

        def format_array=(array)
          initialize_line
          @unconsumed = []
          array.each do |hash|
            hash[:text].scan(/[^\n]+|\n/) do |line|
              @unconsumed << hash.merge(text: line)
            end
          end
        end

        def initialize_line
          @finalized = false
          @max_line_height = 0
          @max_descender = 0
          @max_ascender = 0

          @consumed = []
          @fragments = []
        end

        def finished?
          @unconsumed.empty?
        end

        def next_string
          if finalized
            raise 'Lines must not be finalized when calling #next_string'
          end

          next_unconsumed_hash = @unconsumed.shift

          if next_unconsumed_hash
            @consumed << next_unconsumed_hash.dup
            @current_format_state = next_unconsumed_hash.dup
            @current_format_state.delete(:text)

            next_unconsumed_hash[:text]
          end
        end

        def preview_next_string
          next_unconsumed_hash = @unconsumed.first

          if next_unconsumed_hash
            next_unconsumed_hash[:text]
          end
        end

        def apply_color_and_font_settings(fragment, &block)
          if fragment.color
            original_fill_color = @document.fill_color
            original_stroke_color = @document.stroke_color
            @document.fill_color(*fragment.color)
            @document.stroke_color(*fragment.color)
            apply_font_settings(fragment, &block)
            @document.stroke_color = original_stroke_color
            @document.fill_color = original_fill_color
          else
            apply_font_settings(fragment, &block)
          end
        end

        def apply_font_settings(fragment = nil, &block)
          if fragment.nil?
            font = current_format_state[:font]
            size = current_format_state[:size]
            character_spacing = current_format_state[:character_spacing] ||
              @document.character_spacing
            styles = current_format_state[:styles]
          else
            font = fragment.font
            size = fragment.size
            character_spacing = fragment.character_spacing
            styles = fragment.styles
          end
          font_style = font_style(styles)

          @document.character_spacing(character_spacing) do
            if font || font_style != :normal
              raise 'Bad font family' unless @document.font.family

              @document.font(
                font || @document.font.family, style: font_style
              ) do
                apply_font_size(size, styles, &block)
              end
            else
              apply_font_size(size, styles, &block)
            end
          end
        end

        def update_last_string(printed, unprinted, normalized_soft_hyphen = nil)
          return if printed.nil?

          if printed.empty?
            @consumed.pop
          else
            @consumed.last[:text] = printed
            if normalized_soft_hyphen
              @consumed.last[:normalized_soft_hyphen] = normalized_soft_hyphen
            end
          end

          unless unprinted.empty?
            @unconsumed.unshift(@current_format_state.merge(text: unprinted))
          end

          load_previous_format_state if printed.empty?
        end

        def retrieve_fragment
          unless finalized
            raise 'Lines must be finalized before fragments can be retrieved'
          end

          @fragments.shift
        end

        def repack_unretrieved
          new_unconsumed = []
          # rubocop: disable Lint/AssignmentInCondition
          while fragment = retrieve_fragment
            # rubocop: enable Lint/AssignmentInCondition
            fragment.include_trailing_white_space!
            new_unconsumed << fragment.format_state.merge(text: fragment.text)
          end
          @unconsumed = new_unconsumed.concat(@unconsumed)
        end

        def font_style(styles)
          if styles.nil?
            :normal
          elsif styles.include?(:bold) && styles.include?(:italic)
            :bold_italic
          elsif styles.include?(:bold)
            :bold
          elsif styles.include?(:italic)
            :italic
          else
            :normal
          end
        end

        private

        def load_previous_format_state
          if @consumed.empty?
            @current_format_state = {}
          else
            hash = @consumed.last
            @current_format_state = hash.dup
            @current_format_state.delete(:text)
          end
        end

        def apply_font_size(size, styles)
          if subscript?(styles) || superscript?(styles)
            relative_size = 0.583
            size = if size.nil?
                     @document.font_size * relative_size
                   else
                     size * relative_size
                   end
          end
          if size.nil?
            yield
          else
            @document.font_size(size) { yield }
          end
        end

        def subscript?(styles)
          if styles.nil? then false
          else styles.include?(:subscript)
          end
        end

        def superscript?(styles)
          if styles.nil? then false
          else styles.include?(:superscript)
          end
        end

        def omit_trailing_whitespace_from_line_width
          @consumed.reverse_each do |hash|
            if hash[:text] == "\n"
              break
            elsif hash[:text].strip.empty? && @consumed.length > 1
              # this entire fragment is trailing white space
              hash[:exclude_trailing_white_space] = true
            else
              # this fragment contains the first non-white space we have
              # encountered since the end of the line
              hash[:exclude_trailing_white_space] = true
              break
            end
          end
        end

        def fragment_measurements=(fragment)
          apply_font_settings(fragment) do
            fragment.width = @document.width_of(
              fragment.text,
              kerning: @kerning
            )
            fragment.line_height = @document.font.height
            fragment.descender = @document.font.descender
            fragment.ascender = @document.font.ascender
          end
        end

        def line_measurement_maximums=(fragment)
          @max_line_height = [
            defined?(@max_line_height) && @max_line_height,
            fragment.line_height
          ].compact.max
          @max_descender = [
            defined?(@max_descender) && @max_descender,
            fragment.descender
          ].compact.max
          @max_ascender = [
            defined?(@max_ascender) && @max_ascender,
            fragment.ascender
          ].compact.max
        end
      end
    end
  end
end