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.
Files changed (163) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/COPYING +2 -0
  4. data/GPLv2 +339 -0
  5. data/GPLv3 +674 -0
  6. data/LICENSE +65 -0
  7. data/LICENSE-prawn-svg +21 -0
  8. data/README.md +148 -0
  9. data/data/fonts/Courier-Bold.afm +342 -0
  10. data/data/fonts/Courier-BoldOblique.afm +342 -0
  11. data/data/fonts/Courier-Oblique.afm +342 -0
  12. data/data/fonts/Courier.afm +342 -0
  13. data/data/fonts/Helvetica-Bold.afm +2827 -0
  14. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  15. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  16. data/data/fonts/Helvetica.afm +3051 -0
  17. data/data/fonts/MustRead.html +19 -0
  18. data/data/fonts/Symbol.afm +213 -0
  19. data/data/fonts/Times-Bold.afm +2588 -0
  20. data/data/fonts/Times-BoldItalic.afm +2384 -0
  21. data/data/fonts/Times-Italic.afm +2667 -0
  22. data/data/fonts/Times-Roman.afm +2419 -0
  23. data/data/fonts/ZapfDingbats.afm +225 -0
  24. data/lib/gamba/annotations.rb +21 -0
  25. data/lib/gamba/combine.rb +307 -0
  26. data/lib/gamba/document/bounding_box.rb +193 -0
  27. data/lib/gamba/document/column_box.rb +91 -0
  28. data/lib/gamba/document/internals.rb +264 -0
  29. data/lib/gamba/document/span.rb +25 -0
  30. data/lib/gamba/document.rb +268 -0
  31. data/lib/gamba/encoding.rb +13 -0
  32. data/lib/gamba/errors.rb +39 -0
  33. data/lib/gamba/font.rb +228 -0
  34. data/lib/gamba/fonts/afm.rb +171 -0
  35. data/lib/gamba/fonts/otf.rb +50 -0
  36. data/lib/gamba/fonts/ttf.rb +413 -0
  37. data/lib/gamba/graphic_state.rb +84 -0
  38. data/lib/gamba/graphics/color.rb +174 -0
  39. data/lib/gamba/graphics/patterns.rb +100 -0
  40. data/lib/gamba/graphics/style.rb +159 -0
  41. data/lib/gamba/graphics.rb +212 -0
  42. data/lib/gamba/grid.rb +216 -0
  43. data/lib/gamba/images/image.rb +271 -0
  44. data/lib/gamba/images/png_decoder.rb +65 -0
  45. data/lib/gamba/images.rb +115 -0
  46. data/lib/gamba/measurement_extensions.rb +40 -0
  47. data/lib/gamba/measurements.rb +62 -0
  48. data/lib/gamba/outline.rb +148 -0
  49. data/lib/gamba/page.rb +125 -0
  50. data/lib/gamba/page_geometry.rb +27 -0
  51. data/lib/gamba/pdf/decryption.rb +165 -0
  52. data/lib/gamba/pdf/outline_node.rb +53 -0
  53. data/lib/gamba/pdf/reader.rb +616 -0
  54. data/lib/gamba/pdf/reference.rb +31 -0
  55. data/lib/gamba/pdf/writer.rb +105 -0
  56. data/lib/gamba/pdf.rb +87 -0
  57. data/lib/gamba/pdf_a/srgb_profile.rb +61 -0
  58. data/lib/gamba/pdf_a.rb +132 -0
  59. data/lib/gamba/repeater.rb +79 -0
  60. data/lib/gamba/security.rb +111 -0
  61. data/lib/gamba/stamp.rb +76 -0
  62. data/lib/gamba/table/cell/kinds.rb +246 -0
  63. data/lib/gamba/table/cell.rb +331 -0
  64. data/lib/gamba/table/cells.rb +208 -0
  65. data/lib/gamba/table.rb +298 -0
  66. data/lib/gamba/text/box.rb +16 -0
  67. data/lib/gamba/text/formatted/arranger.rb +217 -0
  68. data/lib/gamba/text/formatted/box.rb +312 -0
  69. data/lib/gamba/text/formatted/fragment.rb +205 -0
  70. data/lib/gamba/text/formatted/line_wrap.rb +205 -0
  71. data/lib/gamba/text/formatted/parser.rb +150 -0
  72. data/lib/gamba/text/formatted/wrap.rb +103 -0
  73. data/lib/gamba/text.rb +310 -0
  74. data/lib/gamba/transformation_stack.rb +43 -0
  75. data/lib/gamba/true_type/cff_subset.rb +157 -0
  76. data/lib/gamba/true_type/file.rb +372 -0
  77. data/lib/gamba/true_type/open_type_layout.rb +189 -0
  78. data/lib/gamba/true_type/simple_subsets.rb +108 -0
  79. data/lib/gamba/true_type/subset.rb +206 -0
  80. data/lib/gamba/version.rb +10 -0
  81. data/lib/gamba/view.rb +28 -0
  82. data/lib/gamba.rb +124 -0
  83. data/lib/pdf/core.rb +4 -0
  84. data/lib/prawn/measurement_extensions.rb +4 -0
  85. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  86. data/lib/prawn/svg/attributes/mask.rb +16 -0
  87. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  88. data/lib/prawn/svg/attributes/space.rb +10 -0
  89. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  90. data/lib/prawn/svg/attributes/transform.rb +10 -0
  91. data/lib/prawn/svg/attributes.rb +6 -0
  92. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  93. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  94. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  95. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  96. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  97. data/lib/prawn/svg/color.rb +240 -0
  98. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  99. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  100. data/lib/prawn/svg/css/font_parser.rb +46 -0
  101. data/lib/prawn/svg/css/selector_parser.rb +180 -0
  102. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  103. data/lib/prawn/svg/css/values_parser.rb +68 -0
  104. data/lib/prawn/svg/document.rb +169 -0
  105. data/lib/prawn/svg/elements/anchor.rb +10 -0
  106. data/lib/prawn/svg/elements/base.rb +315 -0
  107. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  108. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  109. data/lib/prawn/svg/elements/circle.rb +19 -0
  110. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  111. data/lib/prawn/svg/elements/container.rb +10 -0
  112. data/lib/prawn/svg/elements/direct_render_base.rb +28 -0
  113. data/lib/prawn/svg/elements/ellipse.rb +20 -0
  114. data/lib/prawn/svg/elements/gradient.rb +183 -0
  115. data/lib/prawn/svg/elements/ignored.rb +5 -0
  116. data/lib/prawn/svg/elements/image.rb +178 -0
  117. data/lib/prawn/svg/elements/line.rb +27 -0
  118. data/lib/prawn/svg/elements/marker.rb +72 -0
  119. data/lib/prawn/svg/elements/mask.rb +65 -0
  120. data/lib/prawn/svg/elements/path.rb +294 -0
  121. data/lib/prawn/svg/elements/pattern.rb +150 -0
  122. data/lib/prawn/svg/elements/polygon.rb +25 -0
  123. data/lib/prawn/svg/elements/polyline.rb +23 -0
  124. data/lib/prawn/svg/elements/rect.rb +64 -0
  125. data/lib/prawn/svg/elements/root.rb +22 -0
  126. data/lib/prawn/svg/elements/switch.rb +92 -0
  127. data/lib/prawn/svg/elements/text.rb +99 -0
  128. data/lib/prawn/svg/elements/text_component.rb +352 -0
  129. data/lib/prawn/svg/elements/text_node.rb +308 -0
  130. data/lib/prawn/svg/elements/text_path.rb +153 -0
  131. data/lib/prawn/svg/elements/use.rb +142 -0
  132. data/lib/prawn/svg/elements/viewport.rb +29 -0
  133. data/lib/prawn/svg/elements.rb +46 -0
  134. data/lib/prawn/svg/extension.rb +25 -0
  135. data/lib/prawn/svg/font.rb +20 -0
  136. data/lib/prawn/svg/font_metrics.rb +144 -0
  137. data/lib/prawn/svg/font_registry.rb +315 -0
  138. data/lib/prawn/svg/funciri.rb +14 -0
  139. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  140. data/lib/prawn/svg/gradients.rb +53 -0
  141. data/lib/prawn/svg/interface.rb +72 -0
  142. data/lib/prawn/svg/length.rb +43 -0
  143. data/lib/prawn/svg/link_renderer.rb +40 -0
  144. data/lib/prawn/svg/loaders/data.rb +17 -0
  145. data/lib/prawn/svg/loaders/file.rb +111 -0
  146. data/lib/prawn/svg/loaders/web.rb +24 -0
  147. data/lib/prawn/svg/paint.rb +67 -0
  148. data/lib/prawn/svg/pathable.rb +153 -0
  149. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  150. data/lib/prawn/svg/pdfmatrix.rb +39 -0
  151. data/lib/prawn/svg/percentage.rb +24 -0
  152. data/lib/prawn/svg/properties.rb +329 -0
  153. data/lib/prawn/svg/renderer.rb +249 -0
  154. data/lib/prawn/svg/state.rb +24 -0
  155. data/lib/prawn/svg/transform_parser.rb +78 -0
  156. data/lib/prawn/svg/ttc.rb +35 -0
  157. data/lib/prawn/svg/ttf.rb +95 -0
  158. data/lib/prawn/svg/url_loader.rb +46 -0
  159. data/lib/prawn/svg/version.rb +5 -0
  160. data/lib/prawn/table.rb +4 -0
  161. data/lib/prawn-svg.rb +52 -0
  162. data/lib/prawn.rb +3 -0
  163. 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