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
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module PrawnX
|
|
6
|
+
class Document
|
|
7
|
+
def cell(options = {})
|
|
8
|
+
cell = Table::Cell.make(self, options.delete(:content), options)
|
|
9
|
+
cell.draw
|
|
10
|
+
cell
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def make_cell(content, options = {})
|
|
14
|
+
Table::Cell.make(self, content, options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Runs the block, then puts back the listed attributes as they were.
|
|
18
|
+
def mask(*fields)
|
|
19
|
+
stored = fields.to_h { [it, public_send(it)] }
|
|
20
|
+
yield
|
|
21
|
+
stored.each { |field, value| public_send(:"#{field}=", value) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Table
|
|
26
|
+
class Cell
|
|
27
|
+
# Extra width given to a cell's content box, so text measured to fit exactly is not wrapped.
|
|
28
|
+
FPTolerance = 1
|
|
29
|
+
SIDE_INDEX = {top: 0, right: 1, bottom: 2, left: 3}.freeze
|
|
30
|
+
|
|
31
|
+
attr_reader :padding, :colspan, :rowspan, :dummy_cells, :border_widths, :border_colors, :border_lines
|
|
32
|
+
attr_writer :height
|
|
33
|
+
attr_accessor :borders, :content, :background_color
|
|
34
|
+
|
|
35
|
+
def self.make(pdf, content, options = {})
|
|
36
|
+
at = options.delete(:at) || [0, pdf.cursor]
|
|
37
|
+
content = content.to_s if content.nil? || content.is_a?(Numeric) || content.is_a?(Date)
|
|
38
|
+
if content.is_a?(Hash)
|
|
39
|
+
return Cell::Image.new(pdf, at, content) if content[:image]
|
|
40
|
+
|
|
41
|
+
options.update(content)
|
|
42
|
+
content = options[:content]
|
|
43
|
+
else
|
|
44
|
+
options[:content] = content
|
|
45
|
+
end
|
|
46
|
+
options[:content] = content = "" if content.nil?
|
|
47
|
+
|
|
48
|
+
case content
|
|
49
|
+
when Cell then content
|
|
50
|
+
when String then Cell::Text.new(pdf, at, options)
|
|
51
|
+
when Table then Cell::Subtable.new(pdf, at, options)
|
|
52
|
+
when Array then Cell::Subtable.new(pdf, at, options.merge(content: Table.new(options[:content], pdf, {})))
|
|
53
|
+
else raise Errors::UnrecognizedTableContent
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.draw_cells(cells)
|
|
58
|
+
cells.each do |cell, point|
|
|
59
|
+
cell.set_width_constraints
|
|
60
|
+
cell.draw_background(point)
|
|
61
|
+
end
|
|
62
|
+
cells.each do |cell, point|
|
|
63
|
+
cell.draw_borders(point)
|
|
64
|
+
cell.draw_bounded_content(point)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize(pdf, point, options = {})
|
|
69
|
+
@pdf = pdf
|
|
70
|
+
@point = point
|
|
71
|
+
@padding = [5, 5, 5, 5]
|
|
72
|
+
@borders = %i[top bottom left right]
|
|
73
|
+
@border_widths = [1] * 4
|
|
74
|
+
@border_colors = ["000000"] * 4
|
|
75
|
+
@border_lines = [:solid] * 4
|
|
76
|
+
@colspan = 1
|
|
77
|
+
@rowspan = 1
|
|
78
|
+
@dummy_cells = []
|
|
79
|
+
options.each { |key, value| send(:"#{key}=", value) }
|
|
80
|
+
@initializer_run = true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def min_width_ignoring_span
|
|
84
|
+
set_width_constraints
|
|
85
|
+
@min_width
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def min_width
|
|
89
|
+
return min_width_ignoring_span if @colspan == 1
|
|
90
|
+
|
|
91
|
+
min_widths = Hash.new(0)
|
|
92
|
+
dummy_cells.each { min_widths[it.column] = [min_widths[it.column], it.min_width].max }
|
|
93
|
+
min_widths[column] = [min_widths[column], min_width_ignoring_span].max
|
|
94
|
+
min_widths.values.inject(0, :+)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def avg_spanned_min_width
|
|
98
|
+
min_width.to_f / colspan
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def max_width_ignoring_span
|
|
102
|
+
set_width_constraints
|
|
103
|
+
@max_width
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def max_width
|
|
107
|
+
return max_width_ignoring_span if @colspan == 1
|
|
108
|
+
|
|
109
|
+
max_widths = Hash.new(0)
|
|
110
|
+
dummy_cells.each { max_widths[it.column] = [max_widths[it.column], it.max_width].min }
|
|
111
|
+
max_widths[column] = [max_widths[column], max_width_ignoring_span].min
|
|
112
|
+
max_widths.values.inject(0, :+)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def style(options = {}, &block)
|
|
116
|
+
options.each { |key, value| send(:"#{key}=", value) if respond_to?(:"#{key}=") }
|
|
117
|
+
block&.call(self)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def width_ignoring_span
|
|
121
|
+
@width || (content_width + padding_left + padding_right)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def width
|
|
125
|
+
return width_ignoring_span if @colspan == 1 && @rowspan == 1
|
|
126
|
+
|
|
127
|
+
column_widths = Hash.new(0)
|
|
128
|
+
dummy_cells.each { column_widths[it.column] = [column_widths[it.column], it.width].max }
|
|
129
|
+
column_widths[column] = [column_widths[column], width_ignoring_span].max
|
|
130
|
+
column_widths.values.inject(0, :+)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def width=(width)
|
|
134
|
+
@width = @min_width = @max_width = width
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def content_width
|
|
138
|
+
return @width - padding_left - padding_right if @width
|
|
139
|
+
|
|
140
|
+
natural_content_width
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def spanned_content_width
|
|
144
|
+
width - padding_left - padding_right
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def natural_content_width
|
|
148
|
+
raise NotImplementedError, "subclasses must implement natural_content_width"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def height_ignoring_span
|
|
152
|
+
@height || (content_height + padding_top + padding_bottom)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def height
|
|
156
|
+
return height_ignoring_span if @colspan == 1 && @rowspan == 1
|
|
157
|
+
|
|
158
|
+
row_heights = Hash.new(0)
|
|
159
|
+
dummy_cells.each { row_heights[it.row] = [row_heights[it.row], it.height].max }
|
|
160
|
+
row_heights[row] = [row_heights[row], height_ignoring_span].max
|
|
161
|
+
row_heights.values.inject(0, :+)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def content_height
|
|
165
|
+
return @height - padding_top - padding_bottom if @height
|
|
166
|
+
|
|
167
|
+
natural_content_height
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def spanned_content_height
|
|
171
|
+
height - padding_top - padding_bottom
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def natural_content_height
|
|
175
|
+
raise NotImplementedError, "subclasses must implement natural_content_height"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def colspan=(span)
|
|
179
|
+
raise Errors::InvalidTableSpan, "colspan must be provided in the table's structure, never in the initialization block. See Prawn's documentation for details." if @initializer_run
|
|
180
|
+
|
|
181
|
+
@colspan = span
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def rowspan=(span)
|
|
185
|
+
raise Errors::InvalidTableSpan, "rowspan must be provided in the table's structure, never in the initialization block. See Prawn's documentation for details." if @initializer_run
|
|
186
|
+
|
|
187
|
+
@rowspan = span
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def draw(point = [x, y])
|
|
191
|
+
Cell.draw_cells([[self, point]])
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def draw_bounded_content(point)
|
|
195
|
+
@pdf.float do
|
|
196
|
+
@pdf.bounding_box([point[0] + padding_left, point[1] - padding_top],
|
|
197
|
+
width: spanned_content_width + FPTolerance, height: spanned_content_height + FPTolerance) { draw_content }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def x
|
|
202
|
+
@point[0]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def x=(value)
|
|
206
|
+
@point[0] = value
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def relative_x
|
|
210
|
+
x + @pdf.bounds.left_side - @pdf.bounds.absolute_left
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def y
|
|
214
|
+
@point[1]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def y=(value)
|
|
218
|
+
@point[1] = value
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def relative_y(offset = 0)
|
|
222
|
+
y + offset - @pdf.bounds.absolute_bottom
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def padding=(padding)
|
|
226
|
+
@padding = four_sides(padding, nil => [0, 0, 0, 0], Numeric => [padding] * 4) do
|
|
227
|
+
raise ArgumentError, ":padding must be a number or an array [v,h] or [t,r,b,l]"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
%i[top right bottom left].each_with_index do |side, index|
|
|
232
|
+
define_method(:"padding_#{side}") { @padding[index] }
|
|
233
|
+
define_method(:"padding_#{side}=") { |value| @padding[index] = value }
|
|
234
|
+
define_method(:"border_#{side}_color") { @border_colors[index] }
|
|
235
|
+
define_method(:"border_#{side}_color=") { |value| @border_colors[index] = value }
|
|
236
|
+
define_method(:"border_#{side}_width") { @borders.include?(side) ? @border_widths[index] : 0 }
|
|
237
|
+
define_method(:"border_#{side}_width=") { |value| @border_widths[index] = value }
|
|
238
|
+
define_method(:"border_#{side}_line") { @borders.include?(side) ? @border_lines[index] : 0 }
|
|
239
|
+
define_method(:"border_#{side}_line=") { |value| @border_lines[index] = value }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def border_color=(color)
|
|
243
|
+
@border_colors = four_sides(color, nil => ["000000"] * 4, String => [color] * 4) do
|
|
244
|
+
raise ArgumentError, ":border_color must be a string or an array [v,h] or [t,r,b,l]"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
alias border_colors= border_color=
|
|
248
|
+
|
|
249
|
+
# Prawn-table resets a nil border width to four "000000": kept for compatibility.
|
|
250
|
+
def border_width=(width)
|
|
251
|
+
@border_widths = four_sides(width, nil => ["000000"] * 4, Numeric => [width] * 4) do
|
|
252
|
+
raise ArgumentError, ":border_width must be a string or an array [v,h] or [t,r,b,l]"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
alias border_widths= border_width=
|
|
256
|
+
|
|
257
|
+
def border_line=(line)
|
|
258
|
+
@border_lines =
|
|
259
|
+
if line.nil? then [:solid] * 4
|
|
260
|
+
elsif line.length == 1 then [line[0]] * 4
|
|
261
|
+
else four_sides(line) { raise ArgumentError, "border_line must be one of :solid, :dashed, :dotted or an array [v,h] or [t,r,b,l]" }
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
alias border_lines= border_line=
|
|
265
|
+
|
|
266
|
+
def set_width_constraints
|
|
267
|
+
@min_width ||= padding_left + padding_right
|
|
268
|
+
@max_width ||= @pdf.bounds.width
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def draw_background(point)
|
|
272
|
+
return unless background_color
|
|
273
|
+
|
|
274
|
+
@pdf.mask(:fill_color) do
|
|
275
|
+
@pdf.fill_color background_color
|
|
276
|
+
@pdf.fill_rectangle point, width, height
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def draw_borders(point)
|
|
281
|
+
x, y = point
|
|
282
|
+
@pdf.mask(:line_width, :stroke_color) do
|
|
283
|
+
@borders.each do |border|
|
|
284
|
+
index = SIDE_INDEX[border]
|
|
285
|
+
border_width = @border_widths[index]
|
|
286
|
+
next if border_width <= 0
|
|
287
|
+
|
|
288
|
+
from, to =
|
|
289
|
+
case border
|
|
290
|
+
when :top then [[x, y], [x + width, y]]
|
|
291
|
+
when :bottom then [[x, y - height], [x + width, y - height]]
|
|
292
|
+
when :left then [[x, y + (border_top_width / 2.0)], [x, y - height - (border_bottom_width / 2.0)]]
|
|
293
|
+
when :right then [[x + width, y + (border_top_width / 2.0)], [x + width, y - height - (border_bottom_width / 2.0)]]
|
|
294
|
+
end
|
|
295
|
+
case @border_lines[index]
|
|
296
|
+
when :dashed then @pdf.dash border_width * 4
|
|
297
|
+
when :dotted then @pdf.dash border_width, space: border_width * 2
|
|
298
|
+
when :solid then nil
|
|
299
|
+
else raise ArgumentError, "border_line must be :solid, :dotted or :dashed"
|
|
300
|
+
end
|
|
301
|
+
@pdf.line_width = border_width
|
|
302
|
+
@pdf.stroke_color = @border_colors[index]
|
|
303
|
+
@pdf.stroke_line(from, to)
|
|
304
|
+
@pdf.undash if @pdf.dashed?
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def draw_content
|
|
310
|
+
raise NotImplementedError, "subclasses must implement draw_content"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
private
|
|
314
|
+
|
|
315
|
+
# A value for each side, CSS style: one for all, [vertical, horizontal], [top, horizontal, bottom] or four.
|
|
316
|
+
def four_sides(value, special_cases = {})
|
|
317
|
+
special_cases.each { |kind, sides| return sides if kind.nil? ? value.nil? : kind === value }
|
|
318
|
+
case value.length
|
|
319
|
+
when 2 then [value[0], value[1], value[0], value[1]]
|
|
320
|
+
when 3 then [value[0], value[1], value[2], value[1]]
|
|
321
|
+
when 4 then [value[0], value[1], value[2], value[3]]
|
|
322
|
+
else yield
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
module InTable
|
|
327
|
+
attr_accessor :row, :column
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
class Table
|
|
5
|
+
# A selection of a table's cells, addressable by row and column, styled all at once.
|
|
6
|
+
class Cells < Array
|
|
7
|
+
def fits_on_current_page?(offset, reference_bounds)
|
|
8
|
+
return true if empty?
|
|
9
|
+
|
|
10
|
+
height_with_span < (self[0, 0].y + offset) - reference_bounds.absolute_bottom
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def rows(row_spec)
|
|
14
|
+
index_cells unless @indexed
|
|
15
|
+
row_spec = transform_spec(row_spec, @first_row, @row_count)
|
|
16
|
+
Cells.new(@rows[row_spec] ||= select { row_spec.respond_to?(:include?) ? row_spec.include?(it.row) : row_spec === it.row })
|
|
17
|
+
end
|
|
18
|
+
alias row rows
|
|
19
|
+
|
|
20
|
+
def row_count
|
|
21
|
+
index_cells unless @indexed
|
|
22
|
+
@row_count
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def columns(column_spec)
|
|
26
|
+
index_cells unless @indexed
|
|
27
|
+
column_spec = transform_spec(column_spec, @first_column, @column_count)
|
|
28
|
+
Cells.new(@columns[column_spec] ||= select { column_spec.respond_to?(:include?) ? column_spec.include?(it.column) : column_spec === it.column })
|
|
29
|
+
end
|
|
30
|
+
alias column columns
|
|
31
|
+
|
|
32
|
+
def column_count
|
|
33
|
+
index_cells unless @indexed
|
|
34
|
+
@column_count
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def filter(&)
|
|
38
|
+
Cells.new(select(&))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def [](row, column)
|
|
42
|
+
return nil if empty?
|
|
43
|
+
|
|
44
|
+
index_cells unless @indexed
|
|
45
|
+
row_cells = @rows[@first_row + row] || []
|
|
46
|
+
column_cells = @columns[@first_column + column] || []
|
|
47
|
+
if row_cells.length < column_cells.length
|
|
48
|
+
row_cells.find { it.column == @first_column + column }
|
|
49
|
+
else
|
|
50
|
+
column_cells.find { it.row == @first_row + row }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def []=(row, column, cell)
|
|
55
|
+
cell.extend(Cell::InTable)
|
|
56
|
+
cell.row = row
|
|
57
|
+
cell.column = column
|
|
58
|
+
if @indexed
|
|
59
|
+
(@rows[row] ||= []) << cell
|
|
60
|
+
(@columns[column] ||= []) << cell
|
|
61
|
+
@first_row = row if !@first_row || row < @first_row
|
|
62
|
+
@first_column = column if !@first_column || column < @first_column
|
|
63
|
+
@row_count = @rows.size
|
|
64
|
+
@column_count = @columns.size
|
|
65
|
+
end
|
|
66
|
+
self << cell
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def style(options = {}, &)
|
|
70
|
+
each { it.style(options, &) unless it.is_a?(Cell::SpanDummy) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def width
|
|
74
|
+
ColumnWidthCalculator.new(self).natural_widths.inject(0, :+)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def min_width
|
|
78
|
+
aggregate_cell_values(:column, :avg_spanned_min_width, :max)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def max_width
|
|
82
|
+
aggregate_cell_values(:column, :max_width_ignoring_span, :max)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def height
|
|
86
|
+
aggregate_cell_values(:row, :height_ignoring_span, :max)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def height_with_span
|
|
90
|
+
aggregate_cell_values(:row, :height, :max)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Setters reach every cell that has them: cells.background_color = "eeeeee".
|
|
94
|
+
def method_missing(name, *arguments, &)
|
|
95
|
+
return super unless name.end_with?("=")
|
|
96
|
+
|
|
97
|
+
each { it.public_send(name, *arguments, &) if it.respond_to?(name) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def respond_to_missing?(name, include_private = false)
|
|
101
|
+
name.end_with?("=") || super
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
protected
|
|
105
|
+
|
|
106
|
+
def index_cells
|
|
107
|
+
@rows = {}
|
|
108
|
+
@columns = {}
|
|
109
|
+
each do |cell|
|
|
110
|
+
(@rows[cell.row] ||= []) << cell
|
|
111
|
+
(@columns[cell.column] ||= []) << cell
|
|
112
|
+
end
|
|
113
|
+
@first_row = @rows.keys.min
|
|
114
|
+
@first_column = @columns.keys.min
|
|
115
|
+
@row_count = @rows.size
|
|
116
|
+
@column_count = @columns.size
|
|
117
|
+
@indexed = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def aggregate_cell_values(row_or_column, method, aggregate)
|
|
121
|
+
ColumnWidthCalculator.new(self).aggregate_cell_values(row_or_column, method, aggregate)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def transform_spec(spec, first, total)
|
|
125
|
+
case spec
|
|
126
|
+
when Range then transform_spec(spec.begin, first, total)..transform_spec(spec.end, first, total)
|
|
127
|
+
when Integer then spec.negative? ? first + total + spec : first + spec
|
|
128
|
+
when Enumerable then spec.map { first + it }
|
|
129
|
+
else raise "Don't understand spec #{spec.inspect}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Widths of columns, and sums of per-row or per-column cell values, accounting for cells spanning several.
|
|
135
|
+
class ColumnWidthCalculator
|
|
136
|
+
def initialize(cells)
|
|
137
|
+
@cells = cells
|
|
138
|
+
@widths_by_column = Hash.new(0)
|
|
139
|
+
@rows_with_a_span_dummy = Hash.new(false)
|
|
140
|
+
@cells.each { @rows_with_a_span_dummy[it.row] = true if it.is_a?(Cell::SpanDummy) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def has_a_span_dummy?(row)
|
|
144
|
+
@rows_with_a_span_dummy[row]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def fill_values_if_needed(values, cell, index, method)
|
|
148
|
+
missing_count = cell.colspan.times.count { values[index + it].nil? }
|
|
149
|
+
return values if missing_count.zero? || missing_count == cell.colspan || !has_a_span_dummy?(cell.row)
|
|
150
|
+
|
|
151
|
+
new_sum = cell.send(method) * cell.colspan
|
|
152
|
+
cell.colspan.times { new_sum -= values[index + it] unless values[index + it].nil? }
|
|
153
|
+
new_value = new_sum.to_f / missing_count
|
|
154
|
+
cell.colspan.times { values[index + it] = new_value if values[index + it].nil? }
|
|
155
|
+
values
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def natural_widths
|
|
159
|
+
@cells.each do |cell|
|
|
160
|
+
@widths_by_column[cell.column] = [@widths_by_column[cell.column], cell.width.to_f].max unless has_a_span_dummy?(cell.row)
|
|
161
|
+
end
|
|
162
|
+
@cells.each do |cell|
|
|
163
|
+
next unless has_a_span_dummy?(cell.row)
|
|
164
|
+
next if cell.is_a?(Cell::SpanDummy)
|
|
165
|
+
|
|
166
|
+
if cell.colspan == 1
|
|
167
|
+
@widths_by_column[cell.column] = [@widths_by_column[cell.column], cell.width.to_f].max
|
|
168
|
+
else
|
|
169
|
+
current_width_of_spanned_cells = @widths_by_column.to_a[cell.column..(cell.column + cell.colspan - 1)].map(&:last).inject(0, :+)
|
|
170
|
+
if (cell.width.to_f - current_width_of_spanned_cells) > FLOAT_PRECISION
|
|
171
|
+
cell.colspan.times { @widths_by_column[cell.column + it] = cell.width.to_f / cell.colspan }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
@widths_by_column.sort_by { |column, _| column }.map { |_, width| width }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def aggregate_cell_values(row_or_column, method, aggregate)
|
|
179
|
+
values = {}
|
|
180
|
+
@cells.each do |cell|
|
|
181
|
+
next unless cell.colspan == 1 && cell.class != Cell::SpanDummy
|
|
182
|
+
|
|
183
|
+
index = cell.send(row_or_column)
|
|
184
|
+
values[index] = [values[index], cell.send(method)].compact.send(aggregate)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
spanned_width_needs_fixing = true
|
|
188
|
+
@cells.each do |cell|
|
|
189
|
+
index = cell.send(row_or_column)
|
|
190
|
+
if cell.colspan > 1
|
|
191
|
+
values = fill_values_if_needed(values, cell, index, method) if row_or_column == :column
|
|
192
|
+
old_sum = cell.colspan.times.inject(0) { |sum, offset| values[index + offset] ? sum + values[index + offset] : sum }
|
|
193
|
+
new_sum = cell.send(method) * cell.colspan
|
|
194
|
+
spanned_width_needs_fixing = (new_sum - old_sum) > FLOAT_PRECISION
|
|
195
|
+
if spanned_width_needs_fixing
|
|
196
|
+
values[index] = [values[index], cell.send(method)].compact.send(aggregate)
|
|
197
|
+
entries_exist = cell.colspan.times.none? { values[index + it].nil? }
|
|
198
|
+
cell.colspan.times { values[index + it] = cell.send(method) if entries_exist }
|
|
199
|
+
end
|
|
200
|
+
elsif spanned_width_needs_fixing && cell.class == Cell::SpanDummy
|
|
201
|
+
values[index] = [values[index], cell.send(method)].compact.send(aggregate)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
values.values.inject(0, :+)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|