gamba 2.0.0
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/CHANGELOG.md +80 -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 +148 -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/gamba/annotations.rb +21 -0
- data/lib/gamba/combine.rb +307 -0
- data/lib/gamba/document/bounding_box.rb +193 -0
- data/lib/gamba/document/column_box.rb +91 -0
- data/lib/gamba/document/internals.rb +264 -0
- data/lib/gamba/document/span.rb +25 -0
- data/lib/gamba/document.rb +268 -0
- data/lib/gamba/encoding.rb +13 -0
- data/lib/gamba/errors.rb +39 -0
- data/lib/gamba/font.rb +228 -0
- data/lib/gamba/fonts/afm.rb +171 -0
- data/lib/gamba/fonts/otf.rb +50 -0
- data/lib/gamba/fonts/ttf.rb +413 -0
- data/lib/gamba/graphic_state.rb +84 -0
- data/lib/gamba/graphics/color.rb +174 -0
- data/lib/gamba/graphics/patterns.rb +100 -0
- data/lib/gamba/graphics/style.rb +159 -0
- data/lib/gamba/graphics.rb +212 -0
- data/lib/gamba/grid.rb +216 -0
- data/lib/gamba/images/image.rb +271 -0
- data/lib/gamba/images/png_decoder.rb +65 -0
- data/lib/gamba/images.rb +115 -0
- data/lib/gamba/measurement_extensions.rb +40 -0
- data/lib/gamba/measurements.rb +62 -0
- data/lib/gamba/outline.rb +148 -0
- data/lib/gamba/page.rb +125 -0
- data/lib/gamba/page_geometry.rb +27 -0
- data/lib/gamba/pdf/decryption.rb +165 -0
- data/lib/gamba/pdf/outline_node.rb +53 -0
- data/lib/gamba/pdf/reader.rb +616 -0
- data/lib/gamba/pdf/reference.rb +31 -0
- data/lib/gamba/pdf/writer.rb +105 -0
- data/lib/gamba/pdf.rb +87 -0
- data/lib/gamba/pdf_a/srgb_profile.rb +61 -0
- data/lib/gamba/pdf_a.rb +132 -0
- data/lib/gamba/repeater.rb +79 -0
- data/lib/gamba/security.rb +111 -0
- data/lib/gamba/stamp.rb +76 -0
- data/lib/gamba/table/cell/kinds.rb +246 -0
- data/lib/gamba/table/cell.rb +331 -0
- data/lib/gamba/table/cells.rb +208 -0
- data/lib/gamba/table.rb +298 -0
- data/lib/gamba/text/box.rb +16 -0
- data/lib/gamba/text/formatted/arranger.rb +217 -0
- data/lib/gamba/text/formatted/box.rb +312 -0
- data/lib/gamba/text/formatted/fragment.rb +205 -0
- data/lib/gamba/text/formatted/line_wrap.rb +205 -0
- data/lib/gamba/text/formatted/parser.rb +150 -0
- data/lib/gamba/text/formatted/wrap.rb +103 -0
- data/lib/gamba/text.rb +310 -0
- data/lib/gamba/transformation_stack.rb +43 -0
- data/lib/gamba/true_type/cff_subset.rb +157 -0
- data/lib/gamba/true_type/file.rb +372 -0
- data/lib/gamba/true_type/open_type_layout.rb +189 -0
- data/lib/gamba/true_type/simple_subsets.rb +108 -0
- data/lib/gamba/true_type/subset.rb +206 -0
- data/lib/gamba/version.rb +10 -0
- data/lib/gamba/view.rb +28 -0
- data/lib/gamba.rb +124 -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 +180 -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 +169 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +315 -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 +19 -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 +28 -0
- data/lib/prawn/svg/elements/ellipse.rb +20 -0
- data/lib/prawn/svg/elements/gradient.rb +183 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +178 -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 +352 -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 +39 -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 +52 -0
- data/lib/prawn.rb +3 -0
- metadata +263 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
module Graphics
|
|
5
|
+
include BlendMode
|
|
6
|
+
include Color
|
|
7
|
+
include Dash
|
|
8
|
+
include CapStyle
|
|
9
|
+
include JoinStyle
|
|
10
|
+
include Transparency
|
|
11
|
+
include Transformation
|
|
12
|
+
include Patterns
|
|
13
|
+
|
|
14
|
+
# Distance of a cubic Bézier control point approximating a quarter circle of radius 1.
|
|
15
|
+
KAPPA = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0)
|
|
16
|
+
|
|
17
|
+
def move_to(*point)
|
|
18
|
+
x, y = map_to_absolute(point)
|
|
19
|
+
add_content("#{PDF::Core.real(x)} #{PDF::Core.real(y)} m")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def line_to(*point)
|
|
23
|
+
x, y = map_to_absolute(point)
|
|
24
|
+
add_content("#{PDF::Core.real(x)} #{PDF::Core.real(y)} l")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def curve_to(destination, options = {})
|
|
28
|
+
options[:bounds] || raise(Errors::InvalidGraphicsPath, "Bounding points for bezier curve must be specified as :bounds => [[x1,y1],[x2,y2]]")
|
|
29
|
+
|
|
30
|
+
add_content("#{PDF::Core.real_params((options[:bounds] << destination).flat_map { map_to_absolute(_1) })} c")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def rectangle(point, width, height)
|
|
34
|
+
x, y = map_to_absolute(point)
|
|
35
|
+
add_content("#{PDF::Core.real_params([x, y - height, width, height])} re")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def rounded_rectangle(point, width, height, radius)
|
|
39
|
+
x, y = point
|
|
40
|
+
rounded_polygon(radius, point, [x + width, y], [x + width, y - height], [x, y - height])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def line_width=(width)
|
|
44
|
+
graphic_state.line_width = width
|
|
45
|
+
write_line_width
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def line_width(width = nil)
|
|
49
|
+
return graphic_state.line_width unless width
|
|
50
|
+
|
|
51
|
+
self.line_width = width
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def line(*points)
|
|
55
|
+
x0, y0, x1, y1 = points.flatten
|
|
56
|
+
move_to(x0, y0)
|
|
57
|
+
line_to(x1, y1)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def horizontal_line(x1, x2, options = {})
|
|
61
|
+
y1 = options[:at] || (y - bounds.absolute_bottom)
|
|
62
|
+
line(x1, y1, x2, y1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def horizontal_rule
|
|
66
|
+
horizontal_line(bounds.left, bounds.right)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def vertical_line(y1, y2, params)
|
|
70
|
+
line(params[:at], y1, params[:at], y2)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def curve(origin, destination, options = {})
|
|
74
|
+
move_to(*origin)
|
|
75
|
+
curve_to(destination, options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def circle(center, radius)
|
|
79
|
+
ellipse(center, radius, radius)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def ellipse(point, radius1, radius2 = radius1)
|
|
83
|
+
x, y = point
|
|
84
|
+
l1 = radius1 * KAPPA
|
|
85
|
+
l2 = radius2 * KAPPA
|
|
86
|
+
move_to(x + radius1, y)
|
|
87
|
+
curve_to([x, y + radius2], bounds: [[x + radius1, y + l2], [x + l1, y + radius2]])
|
|
88
|
+
curve_to([x - radius1, y], bounds: [[x - l1, y + radius2], [x - radius1, y + l2]])
|
|
89
|
+
curve_to([x, y - radius2], bounds: [[x - radius1, y - l2], [x - l1, y - radius2]])
|
|
90
|
+
curve_to([x + radius1, y], bounds: [[x + l1, y - radius2], [x + radius1, y - l2]])
|
|
91
|
+
move_to(x, y)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def polygon(*points)
|
|
95
|
+
move_to(points[0])
|
|
96
|
+
(points.drop(1) << points[0]).each { line_to(*_1) }
|
|
97
|
+
add_content("h")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def rounded_polygon(radius, *points)
|
|
101
|
+
move_to(point_on_line(radius, points[1], points[0]))
|
|
102
|
+
sides = points.size
|
|
103
|
+
points << points[0] << points[1]
|
|
104
|
+
sides.times { |side| rounded_vertex(radius, points[side], points[side + 1], points[side + 2]) }
|
|
105
|
+
add_content("h")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def rounded_vertex(radius, *points)
|
|
109
|
+
radial_point1 = point_on_line(radius, points[0], points[1])
|
|
110
|
+
bezier_point1 = point_on_line(radius - (radius * KAPPA), points[0], points[1])
|
|
111
|
+
radial_point2 = point_on_line(radius, points[2], points[1])
|
|
112
|
+
bezier_point2 = point_on_line(radius - (radius * KAPPA), points[2], points[1])
|
|
113
|
+
line_to(radial_point1)
|
|
114
|
+
curve_to(radial_point2, bounds: [bezier_point1, bezier_point2])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def stroke
|
|
118
|
+
yield if block_given?
|
|
119
|
+
add_content("S")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def close_and_stroke
|
|
123
|
+
yield if block_given?
|
|
124
|
+
add_content("s")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def stroke_bounds
|
|
128
|
+
stroke_rectangle(bounds.top_left, bounds.width, bounds.height)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def stroke_axis(options = {})
|
|
132
|
+
origin = options[:at] || [0, 0]
|
|
133
|
+
options = {
|
|
134
|
+
at: [0, 0], height: bounds.height - origin[1], width: bounds.width - origin[0],
|
|
135
|
+
step_length: 100, negative_axes_length: 20, color: "000000"
|
|
136
|
+
}.merge(options)
|
|
137
|
+
Prawn.verify_options(%i[at width height step_length negative_axes_length color], options)
|
|
138
|
+
x, y = options[:at]
|
|
139
|
+
|
|
140
|
+
save_graphics_state do
|
|
141
|
+
fill_color(options[:color])
|
|
142
|
+
stroke_color(options[:color])
|
|
143
|
+
dash(1, space: 4)
|
|
144
|
+
stroke_horizontal_line(x - options[:negative_axes_length], x + options[:width], at: y)
|
|
145
|
+
stroke_vertical_line(y - options[:negative_axes_length], y + options[:height], at: x)
|
|
146
|
+
undash
|
|
147
|
+
fill_circle(options[:at], 1)
|
|
148
|
+
(options[:step_length]..options[:width]).step(options[:step_length]) do |point|
|
|
149
|
+
fill_circle([x + point, y], 1)
|
|
150
|
+
draw_text(point, at: [x + point - 5, y - 10], size: 7)
|
|
151
|
+
end
|
|
152
|
+
(options[:step_length]..options[:height]).step(options[:step_length]) do |point|
|
|
153
|
+
fill_circle([x, y + point], 1)
|
|
154
|
+
draw_text(point, at: [x - 17, y + point - 2], size: 7)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def fill(options = {})
|
|
160
|
+
yield if block_given?
|
|
161
|
+
add_content((options[:fill_rule] == :even_odd) ? "f*" : "f")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def fill_and_stroke(options = {})
|
|
165
|
+
yield if block_given?
|
|
166
|
+
add_content((options[:fill_rule] == :even_odd) ? "b*" : "b")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def close_path
|
|
170
|
+
add_content("h")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
SHAPES = %i[
|
|
174
|
+
line_to curve_to rectangle rounded_rectangle line horizontal_line horizontal_rule vertical_line curve circle_at circle
|
|
175
|
+
ellipse_at ellipse polygon rounded_polygon rounded_vertex
|
|
176
|
+
].freeze
|
|
177
|
+
|
|
178
|
+
%i[fill stroke fill_and_stroke].product(SHAPES).each do |operation, shape|
|
|
179
|
+
define_method(:"#{operation}_#{shape}") do |*arguments|
|
|
180
|
+
public_send(operation) { public_send(shape, *arguments) }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def write_line_width
|
|
187
|
+
add_content("#{graphic_state.line_width} w")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Called with x, y or [x, y] for every point drawn: those two shapes skip the copy flatten makes.
|
|
191
|
+
def map_to_absolute(*point)
|
|
192
|
+
point = point[0] if point.size == 1 && point[0].is_a?(Array)
|
|
193
|
+
x, y = (point.size == 2 && point[0].is_a?(Numeric) && point[1].is_a?(Numeric)) ? point : point.flatten
|
|
194
|
+
[@bounding_box.absolute_left + x, @bounding_box.absolute_bottom + y]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def map_to_absolute!(point)
|
|
198
|
+
point.replace(map_to_absolute(point))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def degree_to_rad(angle)
|
|
202
|
+
angle * Math::PI / 180
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def point_on_line(distance_from_end, *points)
|
|
206
|
+
x0, y0, x1, y1 = points.flatten
|
|
207
|
+
length = Math.sqrt(((x1 - x0)**2) + ((y1 - y0)**2))
|
|
208
|
+
ratio = (length - distance_from_end) / length
|
|
209
|
+
[x0 + (ratio * (x1 - x0)), y0 + (ratio * (y1 - y0))]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
data/lib/gamba/grid.rb
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
class Document
|
|
5
|
+
def define_grid(options = {})
|
|
6
|
+
@boxes = nil
|
|
7
|
+
@grid = Grid.new(self, options)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def grid(*arguments)
|
|
11
|
+
@boxes ||= {}
|
|
12
|
+
@boxes[arguments] ||=
|
|
13
|
+
if arguments.empty?
|
|
14
|
+
@grid
|
|
15
|
+
else
|
|
16
|
+
first, second = arguments
|
|
17
|
+
if first.is_a?(Array) && second.is_a?(Array) && first.length == 2 && second.length == 2
|
|
18
|
+
MultiBox.new(self, GridBox.new(self, *first), GridBox.new(self, *second))
|
|
19
|
+
else
|
|
20
|
+
GridBox.new(self, first, second)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Rows and columns of equal size dividing the current bounds, separated by gutters.
|
|
26
|
+
class Grid
|
|
27
|
+
attr_reader :pdf, :columns, :rows, :gutter, :row_gutter, :column_gutter
|
|
28
|
+
|
|
29
|
+
def initialize(pdf, options = {})
|
|
30
|
+
Prawn.verify_options(%i[columns rows gutter row_gutter column_gutter], options)
|
|
31
|
+
@pdf = pdf
|
|
32
|
+
@columns = options[:columns]
|
|
33
|
+
@rows = options[:rows]
|
|
34
|
+
if options.key?(:gutter)
|
|
35
|
+
@gutter = Float(options[:gutter])
|
|
36
|
+
@row_gutter = @gutter
|
|
37
|
+
@column_gutter = @gutter
|
|
38
|
+
else
|
|
39
|
+
@row_gutter = Float(options[:row_gutter])
|
|
40
|
+
@column_gutter = Float(options[:column_gutter])
|
|
41
|
+
@gutter = 0
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def column_width
|
|
46
|
+
@column_width ||= subdivide(pdf.bounds.width, columns, column_gutter)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def row_height
|
|
50
|
+
@row_height ||= subdivide(pdf.bounds.height, rows, row_gutter)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def show_all(color = "CCCCCC")
|
|
54
|
+
rows.times { |row| columns.times { |column| pdf.grid(row, column).show(color) } }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def subdivide(total, count, gutter)
|
|
60
|
+
(Float(total) - (gutter * Float(count - 1))) / Float(count)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# What a grid cell and a span of cells share: corners, a bounding box, and a way to show themselves.
|
|
65
|
+
module GridArea
|
|
66
|
+
def top_left
|
|
67
|
+
[left, top]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def top_right
|
|
71
|
+
[right, top]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def bottom_left
|
|
75
|
+
[left, bottom]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def bottom_right
|
|
79
|
+
[right, bottom]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def bounding_box(&)
|
|
83
|
+
pdf.bounding_box(top_left, width:, height:, &)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def show(grid_color = "CCCCCC")
|
|
87
|
+
bounding_box do
|
|
88
|
+
original_stroke_color = pdf.stroke_color
|
|
89
|
+
pdf.stroke_color = grid_color
|
|
90
|
+
pdf.text(name)
|
|
91
|
+
pdf.stroke_bounds
|
|
92
|
+
pdf.stroke_color = original_stroke_color
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class GridBox
|
|
98
|
+
include GridArea
|
|
99
|
+
|
|
100
|
+
attr_reader :pdf
|
|
101
|
+
|
|
102
|
+
def initialize(pdf, rows, columns)
|
|
103
|
+
@pdf = pdf
|
|
104
|
+
@rows = rows
|
|
105
|
+
@columns = columns
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def name
|
|
109
|
+
"#{@rows},#{@columns}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def total_height
|
|
113
|
+
Float(pdf.bounds.height)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def width
|
|
117
|
+
Float(grid.column_width)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def height
|
|
121
|
+
Float(grid.row_height)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def gutter
|
|
125
|
+
Float(grid.gutter)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def left
|
|
129
|
+
@left ||= (width + grid.column_gutter) * Float(@columns)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def right
|
|
133
|
+
@right ||= left + width
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def top
|
|
137
|
+
@top ||= total_height - ((height + grid.row_gutter) * Float(@rows))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def bottom
|
|
141
|
+
@bottom ||= top - height
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def grid
|
|
147
|
+
pdf.grid
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class MultiBox
|
|
152
|
+
include GridArea
|
|
153
|
+
|
|
154
|
+
attr_reader :pdf
|
|
155
|
+
|
|
156
|
+
def initialize(pdf, box1, box2)
|
|
157
|
+
@pdf = pdf
|
|
158
|
+
@boxes = [box1, box2]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def name
|
|
162
|
+
@boxes.map(&:name).join(":")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def total_height
|
|
166
|
+
@boxes[0].total_height
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def width
|
|
170
|
+
right_box.right - left_box.left
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def height
|
|
174
|
+
top_box.top - bottom_box.bottom
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def gutter
|
|
178
|
+
@boxes[0].gutter
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def left
|
|
182
|
+
left_box.left
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def right
|
|
186
|
+
right_box.right
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def top
|
|
190
|
+
top_box.top
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def bottom
|
|
194
|
+
bottom_box.bottom
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
def left_box
|
|
200
|
+
@left_box ||= @boxes.min_by(&:left)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def right_box
|
|
204
|
+
@right_box ||= @boxes.max_by(&:right)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def top_box
|
|
208
|
+
@top_box ||= @boxes.max_by(&:top)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def bottom_box
|
|
212
|
+
@bottom_box ||= @boxes.min_by(&:bottom)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
module Images
|
|
5
|
+
class Image
|
|
6
|
+
attr_accessor :scaled_width, :scaled_height
|
|
7
|
+
|
|
8
|
+
def calc_image_dimensions(options)
|
|
9
|
+
w = options[:width] || width
|
|
10
|
+
h = options[:height] || height
|
|
11
|
+
if options[:width] && !options[:height]
|
|
12
|
+
ratio = w / Float(width)
|
|
13
|
+
w = width * ratio
|
|
14
|
+
h = height * ratio
|
|
15
|
+
elsif options[:height] && !options[:width]
|
|
16
|
+
ratio = h / Float(height)
|
|
17
|
+
w = width * ratio
|
|
18
|
+
h = height * ratio
|
|
19
|
+
elsif options[:scale]
|
|
20
|
+
w = width * options[:scale]
|
|
21
|
+
h = height * options[:scale]
|
|
22
|
+
elsif options[:fit]
|
|
23
|
+
box_width, box_height = options[:fit]
|
|
24
|
+
if width / Float(height) > box_width / Float(box_height)
|
|
25
|
+
w = box_width
|
|
26
|
+
h = box_width / (width / Float(height))
|
|
27
|
+
else
|
|
28
|
+
h = box_height
|
|
29
|
+
w = box_height * (width / Float(height))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
self.scaled_width = w
|
|
33
|
+
self.scaled_height = h
|
|
34
|
+
[w, h]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class JPG < Image
|
|
39
|
+
class FormatError < StandardError
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
START_OF_FRAME_MARKERS = [0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF].freeze
|
|
43
|
+
MARKER_PREFIX = 0xFF
|
|
44
|
+
COLOR_SPACE_BY_CHANNELS = {1 => :DeviceGray, 3 => :DeviceRGB, 4 => :DeviceCMYK}.freeze
|
|
45
|
+
# Adobe's CMYK JPEGs store inverted values.
|
|
46
|
+
INVERTED_CMYK_DECODE = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0].freeze
|
|
47
|
+
|
|
48
|
+
attr_reader :width, :height, :bits, :channels
|
|
49
|
+
|
|
50
|
+
def self.can_render?(image_blob)
|
|
51
|
+
image_blob.byteslice(0, 3).bytes == [255, 216, 255]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize(data)
|
|
55
|
+
super()
|
|
56
|
+
@data = data
|
|
57
|
+
position = 2
|
|
58
|
+
loop do
|
|
59
|
+
marker, code, length = data.byteslice(position, 4)&.unpack("CCn")
|
|
60
|
+
raise FormatError, "JPEG marker not found!" if marker != MARKER_PREFIX
|
|
61
|
+
|
|
62
|
+
if START_OF_FRAME_MARKERS.include?(code)
|
|
63
|
+
@bits, @height, @width, @channels = data.unpack("@#{position + 4} CnnC")
|
|
64
|
+
break
|
|
65
|
+
end
|
|
66
|
+
position += 2 + length
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_pdf_object(document)
|
|
71
|
+
color_space = COLOR_SPACE_BY_CHANNELS.fetch(channels) { raise ArgumentError, "JPG uses an unsupported number of channels" }
|
|
72
|
+
dictionary = {Type: :XObject, Subtype: :Image, ColorSpace: color_space, BitsPerComponent: bits, Width: width, Height: height, Filter: :DCTDecode}
|
|
73
|
+
dictionary[:Decode] = INVERTED_CMYK_DECODE if color_space == :DeviceCMYK
|
|
74
|
+
document.ref!(dictionary, @data)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class PNG < Image
|
|
79
|
+
SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].freeze
|
|
80
|
+
GRAYSCALE = 0
|
|
81
|
+
TRUE_COLOR = 2
|
|
82
|
+
INDEXED = 3
|
|
83
|
+
GRAYSCALE_ALPHA = 4
|
|
84
|
+
TRUE_COLOR_ALPHA = 6
|
|
85
|
+
PNG_PREDICTORS = 15
|
|
86
|
+
# Compressed input fed at a time: at zlib's highest ratio, 4 KB inflate to about 4 MB before the size check.
|
|
87
|
+
INFLATE_CHUNK_BYTES = 4096
|
|
88
|
+
MAXIMUM_IMAGE_BYTES = 1 << 30
|
|
89
|
+
# Pixels split at a time: the unpack layout and its integers stay a few kilobytes, whatever the width.
|
|
90
|
+
ALPHA_SPLIT_CHUNK_PIXELS = 1024
|
|
91
|
+
# The bit depths the PNG specification allows for each color type.
|
|
92
|
+
BIT_DEPTHS = {GRAYSCALE => [1, 2, 4, 8, 16], TRUE_COLOR => [8, 16], INDEXED => [1, 2, 4, 8],
|
|
93
|
+
GRAYSCALE_ALPHA => [8, 16], TRUE_COLOR_ALPHA => [8, 16]}.freeze
|
|
94
|
+
|
|
95
|
+
attr_reader :palette, :transparency, :width, :height, :bits, :color_type, :compression_method,
|
|
96
|
+
:filter_method, :interlace_method, :alpha_channel
|
|
97
|
+
|
|
98
|
+
def self.can_render?(image_blob)
|
|
99
|
+
image_blob.byteslice(0, 8).bytes == SIGNATURE
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def initialize(data)
|
|
103
|
+
super()
|
|
104
|
+
@palette = +"".b
|
|
105
|
+
compressed = +"".b
|
|
106
|
+
@transparency = {}
|
|
107
|
+
position = 8
|
|
108
|
+
loop do
|
|
109
|
+
raise Errors::UnsupportedImageType, "Truncated PNG" if position + 8 > data.bytesize
|
|
110
|
+
|
|
111
|
+
length, section = data.unpack("@#{position} N a4")
|
|
112
|
+
raise Errors::UnsupportedImageType, "Truncated PNG #{section} chunk" if position + 8 + length > data.bytesize
|
|
113
|
+
|
|
114
|
+
chunk = data.byteslice(position + 8, length)
|
|
115
|
+
case section
|
|
116
|
+
when "IHDR"
|
|
117
|
+
@width, @height, @bits, @color_type, @compression_method, @filter_method, @interlace_method = chunk.unpack("NNCCCCC")
|
|
118
|
+
when "PLTE" then @palette << chunk
|
|
119
|
+
when "IDAT" then compressed << chunk
|
|
120
|
+
when "tRNS"
|
|
121
|
+
@transparency =
|
|
122
|
+
case @color_type
|
|
123
|
+
when INDEXED then {palette: chunk.unpack("C*")}
|
|
124
|
+
when GRAYSCALE then {grayscale: chunk.unpack1("n")}
|
|
125
|
+
when TRUE_COLOR then {rgb: chunk.unpack("nnn")}
|
|
126
|
+
else {}
|
|
127
|
+
end
|
|
128
|
+
when "IEND" then break
|
|
129
|
+
end
|
|
130
|
+
position += 12 + length
|
|
131
|
+
end
|
|
132
|
+
raise Errors::UnsupportedImageType, "PNG without a valid IHDR chunk" unless @width.to_i.positive? && @height.to_i.positive? && @bits.to_i.positive?
|
|
133
|
+
|
|
134
|
+
@compressed_data = compressed
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Inflated up to the size the header announces, never more: a few kilobytes of crafted data could otherwise
|
|
138
|
+
# inflate into gigabytes.
|
|
139
|
+
def img_data
|
|
140
|
+
@img_data ||= begin
|
|
141
|
+
samples = (colors || 1) + ((alpha_channel? && color_type != INDEXED) ? 1 : 0)
|
|
142
|
+
expected_bytes = height * (1 + (((width * bits * samples) + 7) / 8))
|
|
143
|
+
raise Errors::UnsupportedImageType, "PNG of #{width}x#{height} pixels is too large" if expected_bytes > MAXIMUM_IMAGE_BYTES
|
|
144
|
+
|
|
145
|
+
inflater = Zlib::Inflate.new
|
|
146
|
+
inflated = +"".b
|
|
147
|
+
(0...@compressed_data.bytesize).step(INFLATE_CHUNK_BYTES) do |offset|
|
|
148
|
+
inflated << inflater.inflate(@compressed_data.byteslice(offset, INFLATE_CHUNK_BYTES))
|
|
149
|
+
raise Errors::UnsupportedImageType, "PNG data inflates beyond its #{width}x#{height} size" if inflated.bytesize > expected_bytes
|
|
150
|
+
end
|
|
151
|
+
inflated << inflater.finish
|
|
152
|
+
inflated
|
|
153
|
+
rescue Zlib::Error => exception
|
|
154
|
+
raise Errors::UnsupportedImageType, "Corrupt PNG data: #{exception.message}"
|
|
155
|
+
ensure
|
|
156
|
+
# A refused stream is still mid-inflate: closing it as is warns under -w.
|
|
157
|
+
inflater&.reset
|
|
158
|
+
inflater&.close
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def colors
|
|
163
|
+
case color_type
|
|
164
|
+
when GRAYSCALE, INDEXED, GRAYSCALE_ALPHA then 1
|
|
165
|
+
when TRUE_COLOR, TRUE_COLOR_ALPHA then 3
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def alpha_channel?
|
|
170
|
+
return true if color_type == GRAYSCALE_ALPHA || color_type == TRUE_COLOR_ALPHA
|
|
171
|
+
return @transparency.any? if color_type == INDEXED
|
|
172
|
+
|
|
173
|
+
false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def min_pdf_version
|
|
177
|
+
if bits > 8 then 1.5
|
|
178
|
+
elsif alpha_channel? then 1.4
|
|
179
|
+
else 1.0
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def build_pdf_object(document)
|
|
184
|
+
raise Errors::UnsupportedImageType, "PNG uses an unsupported compression method" if compression_method != 0
|
|
185
|
+
raise Errors::UnsupportedImageType, "PNG uses an unsupported filter method" if filter_method != 0
|
|
186
|
+
raise Errors::UnsupportedImageType, "PNG uses unsupported interlace method" if interlace_method != 0
|
|
187
|
+
raise Errors::UnsupportedImageType, "PNG uses an invalid bit depth" if BIT_DEPTHS[color_type]&.include?(bits) == false
|
|
188
|
+
|
|
189
|
+
dictionary = {Type: :XObject, Subtype: :Image, Height: height, Width: width, BitsPerComponent: bits}
|
|
190
|
+
if color_type == INDEXED && alpha_channel?
|
|
191
|
+
index_samples, alpha_samples = split_palette_transparency
|
|
192
|
+
image = document.ref!(dictionary, index_samples)
|
|
193
|
+
image.data[:SMask] = document.ref!(alpha_dictionary(8), alpha_samples)
|
|
194
|
+
elsif alpha_channel?
|
|
195
|
+
color_rows, alpha_rows = split_alpha_channel
|
|
196
|
+
image = document.ref!(dictionary.merge(DecodeParms: predictors(colors)), color_rows)
|
|
197
|
+
image.data[:SMask] = document.ref!(alpha_dictionary(bits).merge(DecodeParms: predictors(1)), alpha_rows)
|
|
198
|
+
else
|
|
199
|
+
image = document.ref!(dictionary.merge(DecodeParms: predictors(colors)), img_data)
|
|
200
|
+
end
|
|
201
|
+
image.data[:ColorSpace] = if palette.empty?
|
|
202
|
+
(colors == 1) ? :DeviceGray : :DeviceRGB
|
|
203
|
+
else
|
|
204
|
+
[:Indexed, :DeviceRGB, (palette.bytesize / 3) - 1, document.ref!({}, palette)]
|
|
205
|
+
end
|
|
206
|
+
if transparency[:grayscale]
|
|
207
|
+
image.data[:Mask] = [transparency[:grayscale]] * 2
|
|
208
|
+
elsif transparency[:rgb]
|
|
209
|
+
image.data[:Mask] = transparency[:rgb].flat_map { [_1, _1] }
|
|
210
|
+
end
|
|
211
|
+
image
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
# The reader undoes the PNG filters itself; the stream gets compressed again when the document is written.
|
|
217
|
+
def predictors(color_count)
|
|
218
|
+
{Predictor: PNG_PREDICTORS, Colors: color_count, BitsPerComponent: bits, Columns: width}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def alpha_dictionary(bits_per_component)
|
|
222
|
+
{Type: :XObject, Subtype: :Image, Height: height, Width: width, BitsPerComponent: bits_per_component, ColorSpace: :DeviceGray, Decode: [0, 1]}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Color and alpha rows, each keeping its filter byte: PNG filters predict a byte from the same channel of
|
|
226
|
+
# the neighbouring pixels, which stays at the same relative place once the channels are apart.
|
|
227
|
+
def split_alpha_channel
|
|
228
|
+
sample_bytes = bits / 8
|
|
229
|
+
pixel_bytes = (colors + 1) * sample_bytes
|
|
230
|
+
row_bytes = (width * pixel_bytes) + 1
|
|
231
|
+
row_count = img_data.bytesize / row_bytes
|
|
232
|
+
chunk_pixels = [width, ALPHA_SPLIT_CHUNK_PIXELS].min
|
|
233
|
+
chunk_count, remainder_pixels = width.divmod(chunk_pixels)
|
|
234
|
+
chunk_layouts = alpha_split_layouts(sample_bytes, chunk_pixels)
|
|
235
|
+
remainder_layouts = alpha_split_layouts(sample_bytes, remainder_pixels)
|
|
236
|
+
color = +"".b
|
|
237
|
+
alpha = +"".b
|
|
238
|
+
row_count.times do |row_index|
|
|
239
|
+
start = row_index * row_bytes
|
|
240
|
+
filter = img_data.getbyte(start)
|
|
241
|
+
color << filter
|
|
242
|
+
alpha << filter
|
|
243
|
+
chunk_count.times { split_pixels(start + 1 + (_1 * chunk_pixels * pixel_bytes), chunk_layouts, color, alpha) }
|
|
244
|
+
split_pixels(start + 1 + (chunk_count * chunk_pixels * pixel_bytes), remainder_layouts, color, alpha) if remainder_pixels.positive?
|
|
245
|
+
end
|
|
246
|
+
[color, alpha]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def alpha_split_layouts(sample_bytes, pixels)
|
|
250
|
+
color_bytes = colors * sample_bytes
|
|
251
|
+
["C#{color_bytes}x#{sample_bytes}" * pixels, "x#{color_bytes}C#{sample_bytes}" * pixels]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def split_pixels(offset, (color_layout, alpha_layout), color, alpha)
|
|
255
|
+
img_data.unpack(color_layout, offset:).pack("C*", buffer: color)
|
|
256
|
+
img_data.unpack(alpha_layout, offset:).pack("C*", buffer: alpha)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Palette indices and their opacity: indices only mean something once the rows are unfiltered.
|
|
260
|
+
def split_palette_transparency
|
|
261
|
+
rows = PngDecoder.unfilter(img_data, ((width * bits) + 7) / 8, 1)
|
|
262
|
+
alpha_by_index = Array.new(256) { |index| transparency[:palette][index] || 0xFF }
|
|
263
|
+
alpha = rows.map do |row|
|
|
264
|
+
indices = (bits == 8) ? row.bytes : row.unpack1("B*").scan(/.{#{bits}}/).first(width).map! { _1.to_i(2) }
|
|
265
|
+
indices.map { alpha_by_index[_1] }.pack("C*")
|
|
266
|
+
end
|
|
267
|
+
[rows.join, alpha.join]
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|