prawnx 0.1.1

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.
Files changed (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. metadata +251 -0
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnX
4
+ module Errors
5
+ InvalidTableData = Class.new(StandardError)
6
+ EmptyTable = Class.new(StandardError)
7
+ end
8
+
9
+ # A table of cells laid out in rows and columns, sized to its content, drawn across pages with its header
10
+ # repeated: prawn-table's API.
11
+ class Table
12
+ module Interface
13
+ def table(data, options = {}, &)
14
+ table = Table.new(data, self, options, &)
15
+ table.draw
16
+ table
17
+ end
18
+
19
+ def make_table(data, options = {}, &)
20
+ Table.new(data, self, options, &)
21
+ end
22
+ end
23
+
24
+ attr_reader :row_length, :column_length, :cells
25
+ attr_writer :width, :position, :header, :row_colors
26
+
27
+ def initialize(data, document, options = {}, &block)
28
+ @pdf = document
29
+ @cells = make_cells(data)
30
+ @header = false
31
+ options.each { |key, value| send(:"#{key}=", value) }
32
+ block.arity < 1 ? instance_eval(&block) : block.call(self) if block
33
+ set_column_widths
34
+ set_row_heights
35
+ position_cells
36
+ end
37
+
38
+ def before_rendering_page(&block)
39
+ @before_rendering_page = block
40
+ end
41
+
42
+ def width
43
+ @width ||= [natural_width, @pdf.bounds.width].min
44
+ end
45
+
46
+ def column_widths=(widths)
47
+ case widths
48
+ when Array then widths.each_with_index { |width, index| column(index).width = width }
49
+ when Hash then widths.each { |index, width| column(index).width = width }
50
+ when Numeric then cells.width = widths
51
+ else raise ArgumentError, "cannot interpret column widths"
52
+ end
53
+ end
54
+
55
+ def height
56
+ cells.height
57
+ end
58
+
59
+ def cell_style=(style_hash)
60
+ cells.style(style_hash)
61
+ end
62
+
63
+ def style(stylable, style_hash = {}, &)
64
+ stylable.style(style_hash, &)
65
+ end
66
+
67
+ def rows(row_spec)
68
+ cells.rows(row_spec)
69
+ end
70
+ alias row rows
71
+
72
+ def columns(column_spec)
73
+ cells.columns(column_spec)
74
+ end
75
+ alias column columns
76
+
77
+ def draw
78
+ with_position do
79
+ reference_bounds = @pdf.reference_bounds
80
+ started_new_page_at_row = initial_row_on_initial_page
81
+ offset = @pdf.y
82
+ @header_row = header_rows if @header
83
+ cells_this_page = []
84
+ @cells.each do |cell|
85
+ if start_new_page?(cell, offset, reference_bounds)
86
+ cells_this_page, offset = ink_and_draw_cells_and_start_new_page(cells_this_page, cell)
87
+ started_new_page_at_row = cell.row
88
+ end
89
+ cell = set_background_color(cell, started_new_page_at_row)
90
+ cells_this_page << [cell, [cell.relative_x, cell.relative_y(offset)]]
91
+ end
92
+ ink_and_draw_cells(cells_this_page)
93
+ @pdf.move_cursor_to(@cells.last.relative_y(offset) - @cells.last.height)
94
+ end
95
+ end
96
+
97
+ def column_widths
98
+ @column_widths ||= begin
99
+ if width - cells.min_width < -FLOAT_PRECISION
100
+ raise Errors::CannotFit, "Table's width was set too small to contain its contents (min width #{cells.min_width}, requested #{width})"
101
+ end
102
+ if width - cells.max_width > FLOAT_PRECISION
103
+ raise Errors::CannotFit, "Table's width was set larger than its contents' maximum width (max width #{cells.max_width}, requested #{width})"
104
+ end
105
+
106
+ if width - natural_width < -FLOAT_PRECISION
107
+ ratio = (width - cells.min_width).to_f / (natural_width - cells.min_width)
108
+ (0...column_length).map do |index|
109
+ minimum = column(index).min_width
110
+ (ratio * (natural_column_widths[index] - minimum)) + minimum
111
+ end
112
+ elsif width - natural_width > FLOAT_PRECISION
113
+ ratio = (width - cells.width).to_f / (cells.max_width - cells.width)
114
+ (0...column_length).map do |index|
115
+ natural = natural_column_widths[index]
116
+ (ratio * (column(index).max_width - natural)) + natural
117
+ end
118
+ else
119
+ natural_column_widths
120
+ end
121
+ end
122
+ end
123
+
124
+ def row_heights
125
+ @natural_row_heights ||= begin
126
+ heights_by_row = Hash.new(0)
127
+ cells.each do |cell|
128
+ next if cell.is_a?(Cell::SpanDummy)
129
+
130
+ height_per_row = cell.height.to_f / cell.rowspan
131
+ cell.rowspan.times { heights_by_row[cell.row + it] = [heights_by_row[cell.row + it], height_per_row].max }
132
+ end
133
+ heights_by_row.sort_by { |row, _| row }.map { |_, height| height }
134
+ end
135
+ end
136
+
137
+ protected
138
+
139
+ def set_background_color(cell, started_new_page_at_row)
140
+ if @row_colors && (!@header || cell.row.positive?)
141
+ index = cell.row - [started_new_page_at_row, number_of_header_rows].max
142
+ cell.background_color ||= @row_colors[index % @row_colors.length]
143
+ end
144
+ cell
145
+ end
146
+
147
+ def number_of_header_rows
148
+ return @header if @header.is_a?(Integer)
149
+
150
+ @header ? 1 : 0
151
+ end
152
+
153
+ def start_new_page?(cell, offset, reference_bounds)
154
+ cell.column.zero? && cell.row.positive? && !row(cell.row).fits_on_current_page?(offset, reference_bounds)
155
+ end
156
+
157
+ def ink_and_draw_cells(cells_this_page, draw_cells = true)
158
+ ink_cells(cells_this_page)
159
+ Cell.draw_cells(cells_this_page) if draw_cells
160
+ end
161
+
162
+ def ink_and_draw_cells_and_start_new_page(cells_this_page, cell)
163
+ ink_and_draw_cells(cells_this_page, @header_row.nil? || cells_this_page.size > @header_row.size)
164
+ @pdf.bounds.move_past_bottom
165
+ offset = @pdf.y - cell.y
166
+ cells_next_page = []
167
+ offset -= add_header(cell.row, cells_next_page)
168
+ [cells_next_page, offset]
169
+ end
170
+
171
+ def ink_cells(cells_this_page)
172
+ @before_rendering_page&.call(Cells.new(cells_this_page.map(&:first)))
173
+ end
174
+
175
+ def initial_row_on_initial_page
176
+ return 0 if fits_on_page?(@pdf.bounds.height)
177
+ return -1 if fits_on_page?(row(0..number_of_header_rows).height, true)
178
+
179
+ @pdf.bounds.move_past_bottom
180
+ 0
181
+ end
182
+
183
+ def fits_on_page?(needed_height, use_reference_bounds = false)
184
+ bounds = use_reference_bounds ? @pdf.reference_bounds : @pdf.bounds
185
+ needed_height < @pdf.y - (bounds.absolute_bottom - FLOAT_PRECISION)
186
+ end
187
+
188
+ def header_rows
189
+ header_rows = Cells.new
190
+ number_of_header_rows.times do |row_index|
191
+ row(row_index).each { header_rows[it.row, it.column] = it.dup }
192
+ end
193
+ header_rows
194
+ end
195
+
196
+ def make_cells(data)
197
+ assert_proper_table_data(data)
198
+ cells = Cells.new
199
+ data.each_with_index do |row_cells, row_number|
200
+ column_number = 0
201
+ row_cells.each do |cell_data|
202
+ column_number += 1 until cells[row_number, column_number].nil?
203
+ cell = Cell.make(@pdf, cell_data)
204
+ cells[row_number, column_number] = cell
205
+ cell.rowspan.times do |row_offset|
206
+ cell.colspan.times do |column_offset|
207
+ next if row_offset.zero? && column_offset.zero?
208
+ if cells[row_number + row_offset, column_number + column_offset]
209
+ raise Errors::InvalidTableSpan, "Spans overlap at row #{row_number + row_offset}, column #{column_number + column_offset}."
210
+ end
211
+
212
+ dummy = Cell::SpanDummy.new(@pdf, cell)
213
+ cells[row_number + row_offset, column_number + column_offset] = dummy
214
+ cell.dummy_cells << dummy
215
+ end
216
+ end
217
+ column_number += cell.colspan
218
+ end
219
+ end
220
+ @row_length = cells.map { it.row + it.rowspan }.max
221
+ @column_length = cells.map { it.column + it.colspan }.max
222
+ cells
223
+ end
224
+
225
+ def add_header(row_number, cells_this_page)
226
+ x_offset = @pdf.bounds.left_side - @pdf.bounds.absolute_left
227
+ header_height = 0
228
+ if row_number.positive? && @header
229
+ y = @pdf.cursor
230
+ number_of_header_rows.times do |header_index|
231
+ header_height += add_one_header_row(cells_this_page, x_offset, y - header_height, row_number - 1, header_index)
232
+ end
233
+ end
234
+ header_height
235
+ end
236
+
237
+ def add_one_header_row(page_of_cells, x_offset, y, row, row_of_header = nil)
238
+ rows_to_operate_on = row_of_header ? @header_row.rows(row_of_header) : @header_row
239
+ rows_to_operate_on.each do |cell|
240
+ cell.row = row
241
+ cell.dummy_cells.each { cell.rowspan > 1 ? it.row += row : it.row = row }
242
+ page_of_cells << [cell, [cell.x + x_offset, y]]
243
+ end
244
+ rows_to_operate_on.height
245
+ end
246
+
247
+ def assert_proper_table_data(data)
248
+ if data.nil? || data.empty?
249
+ raise Errors::EmptyTable, "data must be a non-empty, non-nil, two dimensional array of cell-convertible objects"
250
+ end
251
+ raise Errors::InvalidTableData, "data must be a two dimensional array of cellable objects" unless data.all?(Array)
252
+ end
253
+
254
+ def natural_column_widths
255
+ @natural_column_widths ||= ColumnWidthCalculator.new(cells).natural_widths
256
+ end
257
+
258
+ def natural_width
259
+ @natural_width ||= natural_column_widths.inject(0, :+)
260
+ end
261
+
262
+ def set_column_widths
263
+ column_widths.each_with_index { |width, index| column(index).width = width }
264
+ end
265
+
266
+ def set_row_heights
267
+ row_heights.each_with_index { |height, index| row(index).height = height }
268
+ end
269
+
270
+ def position_cells
271
+ column_widths.each_with_object([0]) { |width, positions| positions << (positions.last + width) }[0..-2]
272
+ .each_with_index { |x, index| column(index).x = x }
273
+ row_heights.each_with_object([0]) { |height, positions| positions << (positions.last - height) }[0..-2]
274
+ .each_with_index { |y, index| row(index).y = y }
275
+ end
276
+
277
+ def with_position
278
+ x =
279
+ case @position || :left
280
+ when :left then return yield
281
+ when :center then (@pdf.bounds.width - width) / 2.0
282
+ when :right then @pdf.bounds.width - width
283
+ when Numeric then @position
284
+ else raise ArgumentError, "unknown position #{@position.inspect}"
285
+ end
286
+ dy = @pdf.bounds.absolute_top - @pdf.y
287
+ final_y = nil
288
+ @pdf.bounding_box([x, @pdf.bounds.top], width:) do
289
+ @pdf.move_down dy
290
+ yield
291
+ final_y = @pdf.y
292
+ end
293
+ @pdf.y = final_y
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnX
4
+ module Text
5
+ # A text box of plain text: what does not fit comes back as a string.
6
+ class Box < Formatted::Box
7
+ def initialize(string, options = {})
8
+ super([{text: string}], options)
9
+ end
10
+
11
+ def render(flags = {})
12
+ super.map { it[:text] }.join
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnX
4
+ module Text
5
+ module Formatted
6
+ # Hands the line wrapper one string at a time out of formatted text, takes back what did not fit, and
7
+ # measures the fragments of each finished line.
8
+ class Arranger
9
+ class NotFinalized < StandardError
10
+ DEFAULT_MESSAGE = "Lines must be finalized"
11
+
12
+ def initialize(message = DEFAULT_MESSAGE, method: nil)
13
+ super(method && message == DEFAULT_MESSAGE ? "Lines must be finalized before calling ##{method}" : message)
14
+ end
15
+ end
16
+
17
+ class BadFontFamily < StandardError
18
+ def initialize(message = "Bad font family")
19
+ super
20
+ end
21
+ end
22
+
23
+ # Relative size of subscript and superscript text.
24
+ SCRIPT_SIZE_RATIO = 0.583
25
+
26
+ attr_reader :max_line_height, :max_descender, :max_ascender, :finalized, :unconsumed, :fragments, :current_format_state
27
+ attr_accessor :consumed
28
+
29
+ def initialize(document, options = {})
30
+ @document = document
31
+ @fragments = []
32
+ @unconsumed = []
33
+ @kerning = options[:kerning]
34
+ end
35
+
36
+ def space_count
37
+ raise NotFinalized.new(method: "space_count") unless finalized
38
+
39
+ @fragments.sum(&:space_count)
40
+ end
41
+
42
+ def line_width
43
+ raise NotFinalized.new(method: "line_width") unless finalized
44
+
45
+ @fragments.inject(0) { |sum, fragment| sum + fragment.width }
46
+ end
47
+
48
+ def line
49
+ raise NotFinalized.new(method: "line") unless finalized
50
+
51
+ @fragments.map do |fragment|
52
+ fragment.text.encode(::Encoding::UTF_8)
53
+ rescue ::Encoding::InvalidByteSequenceError, ::Encoding::UndefinedConversionError
54
+ fragment.text.dup.force_encoding(::Encoding::UTF_8)
55
+ end.join
56
+ end
57
+
58
+ def finalize_line
59
+ @finalized = true
60
+ omit_trailing_whitespace_from_line_width
61
+ @fragments = []
62
+ @consumed.each do |hash|
63
+ fragment = Fragment.new(hash[:text], hash.except(:text), @document)
64
+ @fragments << fragment
65
+ measure(fragment)
66
+ @max_line_height = [@max_line_height, fragment.line_height].compact.max
67
+ @max_descender = [@max_descender, fragment.descender].compact.max
68
+ @max_ascender = [@max_ascender, fragment.ascender].compact.max
69
+ end
70
+ end
71
+
72
+ def format_array=(array)
73
+ initialize_line
74
+ @unconsumed = []
75
+ array.each do |hash|
76
+ hash[:text].scan(/[^\n]+|\n/) { @unconsumed << hash.merge(text: it) }
77
+ end
78
+ end
79
+
80
+ def initialize_line
81
+ @finalized = false
82
+ @max_line_height = 0
83
+ @max_descender = 0
84
+ @max_ascender = 0
85
+ @consumed = []
86
+ @fragments = []
87
+ end
88
+
89
+ def finished?
90
+ @unconsumed.empty?
91
+ end
92
+
93
+ def next_string
94
+ raise NotFinalized.new(method: "next_string") if finalized
95
+
96
+ next_unconsumed_hash = @unconsumed.shift
97
+ return unless next_unconsumed_hash
98
+
99
+ @consumed << next_unconsumed_hash.dup
100
+ @current_format_state = next_unconsumed_hash.except(:text)
101
+ next_unconsumed_hash[:text]
102
+ end
103
+
104
+ def preview_next_string
105
+ @unconsumed.first&.fetch(:text)
106
+ end
107
+
108
+ def apply_color_and_font_settings(fragment, &)
109
+ return apply_font_settings(fragment, &) unless fragment.color
110
+
111
+ original_fill_color = @document.fill_color
112
+ original_stroke_color = @document.stroke_color
113
+ @document.fill_color(*fragment.color)
114
+ @document.stroke_color(*fragment.color)
115
+ apply_font_settings(fragment, &)
116
+ @document.stroke_color = original_stroke_color
117
+ @document.fill_color = original_fill_color
118
+ end
119
+
120
+ def apply_font_settings(fragment = nil, &)
121
+ if fragment
122
+ font = fragment.font
123
+ size = fragment.size
124
+ character_spacing = fragment.character_spacing
125
+ styles = fragment.styles
126
+ else
127
+ font = current_format_state[:font]
128
+ size = current_format_state[:size]
129
+ character_spacing = current_format_state[:character_spacing] || @document.character_spacing
130
+ styles = current_format_state[:styles]
131
+ end
132
+ font_style = font_style(styles)
133
+
134
+ @document.character_spacing(character_spacing) do
135
+ if font || font_style != :normal
136
+ raise BadFontFamily unless @document.font.family
137
+
138
+ @document.font(font || @document.font.family, style: font_style) { apply_font_size(size, styles, &) }
139
+ else
140
+ apply_font_size(size, styles, &)
141
+ end
142
+ end
143
+ end
144
+
145
+ def update_last_string(printed, unprinted, normalized_soft_hyphen = nil)
146
+ return if printed.nil?
147
+
148
+ if printed.empty?
149
+ @consumed.pop
150
+ else
151
+ @consumed.last[:text] = printed
152
+ @consumed.last[:normalized_soft_hyphen] = normalized_soft_hyphen if normalized_soft_hyphen
153
+ end
154
+ @unconsumed.unshift(@current_format_state.merge(text: unprinted)) unless unprinted.empty?
155
+ load_previous_format_state if printed.empty?
156
+ end
157
+
158
+ def retrieve_fragment
159
+ raise NotFinalized, "Lines must be finalized before fragments can be retrieved" unless finalized
160
+
161
+ @fragments.shift
162
+ end
163
+
164
+ def repack_unretrieved
165
+ new_unconsumed = []
166
+ while (fragment = retrieve_fragment)
167
+ fragment.include_trailing_white_space!
168
+ new_unconsumed << fragment.format_state.merge(text: fragment.text)
169
+ end
170
+ @unconsumed = new_unconsumed.concat(@unconsumed)
171
+ end
172
+
173
+ def font_style(styles)
174
+ styles = Array(styles)
175
+ bold = styles.include?(:bold)
176
+ italic = styles.include?(:italic)
177
+ if bold && italic then :bold_italic
178
+ elsif bold then :bold
179
+ elsif italic then :italic
180
+ else :normal
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def load_previous_format_state
187
+ @current_format_state = @consumed.empty? ? {} : @consumed.last.except(:text)
188
+ end
189
+
190
+ def apply_font_size(size, styles, &)
191
+ if styles&.include?(:subscript) || styles&.include?(:superscript)
192
+ size = (size || @document.font_size) * SCRIPT_SIZE_RATIO
193
+ end
194
+ size.nil? ? yield : @document.font_size(size, &)
195
+ end
196
+
197
+ def omit_trailing_whitespace_from_line_width
198
+ @consumed.reverse_each do |hash|
199
+ break if hash[:text] == "\n"
200
+
201
+ hash[:exclude_trailing_white_space] = true
202
+ break unless hash[:text].strip.empty? && @consumed.length > 1
203
+ end
204
+ end
205
+
206
+ def measure(fragment)
207
+ apply_font_settings(fragment) do
208
+ fragment.width = @document.width_of(fragment.text, kerning: @kerning)
209
+ fragment.line_height = @document.font.height
210
+ fragment.descender = @document.font.descender
211
+ fragment.ascender = @document.font.ascender
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end