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.
- checksums.yaml +7 -0
- data/COPYING +2 -0
- data/GPLv2 +339 -0
- data/GPLv3 +674 -0
- data/LICENSE +65 -0
- data/LICENSE-prawn-svg +21 -0
- data/README.md +107 -0
- data/data/fonts/Courier-Bold.afm +342 -0
- data/data/fonts/Courier-BoldOblique.afm +342 -0
- data/data/fonts/Courier-Oblique.afm +342 -0
- data/data/fonts/Courier.afm +342 -0
- data/data/fonts/Helvetica-Bold.afm +2827 -0
- data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/data/fonts/Helvetica.afm +3051 -0
- data/data/fonts/MustRead.html +19 -0
- data/data/fonts/Symbol.afm +213 -0
- data/data/fonts/Times-Bold.afm +2588 -0
- data/data/fonts/Times-BoldItalic.afm +2384 -0
- data/data/fonts/Times-Italic.afm +2667 -0
- data/data/fonts/Times-Roman.afm +2419 -0
- data/data/fonts/ZapfDingbats.afm +225 -0
- data/lib/pdf/core.rb +4 -0
- data/lib/prawn/measurement_extensions.rb +4 -0
- data/lib/prawn/svg/attributes/clip_path.rb +46 -0
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/opacity.rb +30 -0
- data/lib/prawn/svg/attributes/space.rb +10 -0
- data/lib/prawn/svg/attributes/stroke.rb +50 -0
- data/lib/prawn/svg/attributes/transform.rb +10 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +57 -0
- data/lib/prawn/svg/color.rb +240 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/font_family_parser.rb +39 -0
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/css/selector_parser.rb +178 -0
- data/lib/prawn/svg/css/stylesheets.rb +233 -0
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +170 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +312 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
- data/lib/prawn/svg/elements/circle.rb +25 -0
- data/lib/prawn/svg/elements/clip_path.rb +26 -0
- data/lib/prawn/svg/elements/container.rb +10 -0
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/ellipse.rb +22 -0
- data/lib/prawn/svg/elements/gradient.rb +184 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +177 -0
- data/lib/prawn/svg/elements/line.rb +27 -0
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +294 -0
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +25 -0
- data/lib/prawn/svg/elements/polyline.rb +23 -0
- data/lib/prawn/svg/elements/rect.rb +64 -0
- data/lib/prawn/svg/elements/root.rb +22 -0
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +99 -0
- data/lib/prawn/svg/elements/text_component.rb +346 -0
- data/lib/prawn/svg/elements/text_node.rb +308 -0
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +142 -0
- data/lib/prawn/svg/elements/viewport.rb +29 -0
- data/lib/prawn/svg/elements.rb +46 -0
- data/lib/prawn/svg/extension.rb +25 -0
- data/lib/prawn/svg/font.rb +20 -0
- data/lib/prawn/svg/font_metrics.rb +144 -0
- data/lib/prawn/svg/font_registry.rb +315 -0
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +332 -0
- data/lib/prawn/svg/gradients.rb +53 -0
- data/lib/prawn/svg/interface.rb +72 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/loaders/data.rb +17 -0
- data/lib/prawn/svg/loaders/file.rb +111 -0
- data/lib/prawn/svg/loaders/web.rb +24 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/pathable.rb +153 -0
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/pdfmatrix.rb +37 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +329 -0
- data/lib/prawn/svg/renderer.rb +249 -0
- data/lib/prawn/svg/state.rb +24 -0
- data/lib/prawn/svg/transform_parser.rb +78 -0
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +95 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +5 -0
- data/lib/prawn/table.rb +4 -0
- data/lib/prawn-svg.rb +53 -0
- data/lib/prawn.rb +10 -0
- data/lib/prawnx/annotations.rb +21 -0
- data/lib/prawnx/document/bounding_box.rb +193 -0
- data/lib/prawnx/document/column_box.rb +91 -0
- data/lib/prawnx/document/internals.rb +144 -0
- data/lib/prawnx/document/span.rb +25 -0
- data/lib/prawnx/document.rb +253 -0
- data/lib/prawnx/encoding.rb +29 -0
- data/lib/prawnx/errors.rb +37 -0
- data/lib/prawnx/font.rb +214 -0
- data/lib/prawnx/fonts/afm.rb +171 -0
- data/lib/prawnx/fonts/otf.rb +50 -0
- data/lib/prawnx/fonts/ttf.rb +410 -0
- data/lib/prawnx/graphic_state.rb +41 -0
- data/lib/prawnx/graphics/color.rb +148 -0
- data/lib/prawnx/graphics/patterns.rb +100 -0
- data/lib/prawnx/graphics/style.rb +171 -0
- data/lib/prawnx/graphics.rb +208 -0
- data/lib/prawnx/grid.rb +216 -0
- data/lib/prawnx/images/image.rb +244 -0
- data/lib/prawnx/images/png_decoder.rb +65 -0
- data/lib/prawnx/images.rb +115 -0
- data/lib/prawnx/measurement_extensions.rb +40 -0
- data/lib/prawnx/measurements.rb +62 -0
- data/lib/prawnx/outline.rb +146 -0
- data/lib/prawnx/page.rb +125 -0
- data/lib/prawnx/page_geometry.rb +27 -0
- data/lib/prawnx/pdf/outline_node.rb +53 -0
- data/lib/prawnx/pdf/reference.rb +31 -0
- data/lib/prawnx/pdf/writer.rb +105 -0
- data/lib/prawnx/pdf.rb +66 -0
- data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
- data/lib/prawnx/pdf_a.rb +130 -0
- data/lib/prawnx/pdf_core.rb +25 -0
- data/lib/prawnx/repeater.rb +79 -0
- data/lib/prawnx/security.rb +108 -0
- data/lib/prawnx/stamp.rb +74 -0
- data/lib/prawnx/table/cell/kinds.rb +246 -0
- data/lib/prawnx/table/cell.rb +331 -0
- data/lib/prawnx/table/cells.rb +208 -0
- data/lib/prawnx/table.rb +296 -0
- data/lib/prawnx/text/box.rb +16 -0
- data/lib/prawnx/text/formatted/arranger.rb +217 -0
- data/lib/prawnx/text/formatted/box.rb +312 -0
- data/lib/prawnx/text/formatted/fragment.rb +205 -0
- data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
- data/lib/prawnx/text/formatted/parser.rb +149 -0
- data/lib/prawnx/text/formatted/wrap.rb +103 -0
- data/lib/prawnx/text.rb +281 -0
- data/lib/prawnx/transformation_stack.rb +43 -0
- data/lib/prawnx/true_type/cff_subset.rb +157 -0
- data/lib/prawnx/true_type/file.rb +371 -0
- data/lib/prawnx/true_type/open_type_layout.rb +189 -0
- data/lib/prawnx/true_type/simple_subsets.rb +94 -0
- data/lib/prawnx/true_type/subset.rb +187 -0
- data/lib/prawnx/version.rb +7 -0
- data/lib/prawnx/view.rb +28 -0
- data/lib/prawnx.rb +102 -0
- metadata +251 -0
data/lib/prawnx/table.rb
ADDED
|
@@ -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
|