Class: Prawn::Images::PNG
Overview
A convenience class that wraps the logic for extracting the parts of a PNG image that we need to embed them in a PDF.
Extension API collapse
-
#alpha_channel ⇒ String?
readonly
Extracted alpha-channel.
-
#bits ⇒ Integer
readonly
Bits per sample or per palette index.
-
#color_type ⇒ Integer
readonly
Color type.
-
#compression_method ⇒ Integer
readonly
Compression method.
-
#filter_method ⇒ Integer
readonly
Filter method.
-
#height ⇒ Integer
readonly
Image height in pixels.
-
#img_data ⇒ String
readonly
Image data.
-
#interlace_method ⇒ Integer
readonly
Interlace method.
-
#palette ⇒ String
readonly
Palette data.
-
#scaled_height ⇒ Number
Scaled height of the image in PDF points.
-
#scaled_width ⇒ Number
Scaled width of the image in PDF points.
-
#transparency ⇒ Hash{Symbol => String}
readonly
Transparency data.
-
#width ⇒ Integer
readonly
Image width in pixels.
Extension API collapse
-
.can_render?(image_blob) ⇒ Boolean
Can this image handler process this image?.
-
#alpha_channel? ⇒ Boolean
Is there an alpha-channel in this image?.
-
#build_pdf_object(document) ⇒ PDF::Core::Reference
Build a PDF object representing this image in
document
, and return a Reference to it. -
#colors ⇒ Integer
Number of color components to each pixel.
-
#initialize(data) ⇒ PNG
constructor
Process a new PNG image.
-
#min_pdf_version ⇒ Float
Returns the minimum PDF version required to support this image.
Constructor Details
#initialize(data) ⇒ PNG
Process a new PNG image
Source Code
75 | def initialize(data) |
76 | super() |
77 | data = StringIO.new(data.dup) |
78 | |
79 | data.read(8) # Skip the default header |
80 | |
81 | @palette = +'' |
82 | @img_data = +'' |
83 | @transparency = {} |
84 | |
85 | loop do |
86 | chunk_size = data.read(4).unpack1('N') |
87 | section = data.read(4) |
88 | case section |
89 | when 'IHDR' |
90 | # we can grab other interesting values from here (like width,
|
91 | # height, etc)
|
92 | values = data.read(chunk_size).unpack('NNCCCCC') |
93 | |
94 | @width = values[0] |
95 | @height = values[1] |
96 | @bits = values[2] |
97 | @color_type = values[3] |
98 | @compression_method = values[4] |
99 | @filter_method = values[5] |
100 | @interlace_method = values[6] |
101 | when 'PLTE' |
102 | @palette << data.read(chunk_size) |
103 | when 'IDAT' |
104 | @img_data << data.read(chunk_size) |
105 | when 'tRNS' |
106 | # This chunk can only occur once and it must occur after the
|
107 | # PLTE chunk and before the IDAT chunk
|
108 | @transparency = {} |
109 | case @color_type |
110 | when 3 |
111 | @transparency[:palette] = data.read(chunk_size).unpack('C*') |
112 | when 0 |
113 | # Greyscale. Corresponding to entries in the PLTE chunk.
|
114 | # Grey is two bytes, range 0 .. (2 ^ bit-depth) - 1
|
115 | grayval = data.read(chunk_size).unpack1('n') |
116 | @transparency[:grayscale] = grayval |
117 | when 2 |
118 | # True colour with proper alpha channel.
|
119 | @transparency[:rgb] = data.read(chunk_size).unpack('nnn') |
120 | end
|
121 | when 'IEND' |
122 | # we've got everything we need, exit the loop
|
123 | break
|
124 | else
|
125 | # unknown (or un-important) section, skip over it
|
126 | data.seek(data.pos + chunk_size) |
127 | end
|
128 | |
129 | data.read(4) # Skip the CRC |
130 | end
|
131 | |
132 | @img_data = Zlib::Inflate.inflate(@img_data) |
133 | end
|
Instance Attribute Details
#alpha_channel ⇒ String? (readonly)
Extracted alpha-channel.
Source Code
54 | def alpha_channel |
55 | @alpha_channel
|
56 | end
|
#bits ⇒ Integer (readonly)
Bits per sample or per palette index.
Source Code
34 | def bits |
35 | @bits
|
36 | end
|
#color_type ⇒ Integer (readonly)
Color type.
Source Code
38 | def color_type |
39 | @color_type
|
40 | end
|
#compression_method ⇒ Integer (readonly)
Compression method.
Source Code
42 | def compression_method |
43 | @compression_method
|
44 | end
|
#filter_method ⇒ Integer (readonly)
Filter method.
Source Code
46 | def filter_method |
47 | @filter_method
|
48 | end
|
#height ⇒ Integer (readonly)
Image height in pixels.
Source Code
30 | def height |
31 | @height
|
32 | end
|
#img_data ⇒ String (readonly)
Image data.
Source Code
18 | def img_data |
19 | @img_data
|
20 | end
|
#interlace_method ⇒ Integer (readonly)
Interlace method.
Source Code
50 | def interlace_method |
51 | @interlace_method
|
52 | end
|
#palette ⇒ String (readonly)
Palette data.
Source Code
14 | def palette |
15 | @palette
|
16 | end
|
#scaled_height ⇒ Number
Scaled height of the image in PDF points.
Source Code
62 | def scaled_height |
63 | @scaled_height
|
64 | end
|
#scaled_width ⇒ Number
Scaled width of the image in PDF points.
Source Code
58 | def scaled_width |
59 | @scaled_width
|
60 | end
|
#transparency ⇒ Hash{Symbol => String} (readonly)
Transparency data.
Source Code
22 | def transparency |
23 | @transparency
|
24 | end
|
#width ⇒ Integer (readonly)
Image width in pixels.
Source Code
26 | def width |
27 | @width
|
28 | end
|
Class Method Details
.can_render?(image_blob) ⇒ Boolean
Can this image handler process this image?
Source Code
68 | def self.can_render?(image_blob) |
69 | image_blob[0, 8].unpack('C*') == [137, 80, 78, 71, 13, 10, 26, 10] |
70 | end
|
Instance Method Details
#alpha_channel? ⇒ Boolean
Is there an alpha-channel in this image?
Source Code
165 | def alpha_channel? |
166 | return true if color_type == 4 || color_type == 6 |
167 | return @transparency.any? if color_type == 3 |
168 | |
169 | false
|
170 | end
|
#build_pdf_object(document) ⇒ PDF::Core::Reference
Build a PDF object representing this image in document
, and return
a Reference to it.
Source Code
177 | def build_pdf_object(document) |
178 | if compression_method != 0 |
179 | raise Errors::UnsupportedImageType, |
180 | 'PNG uses an unsupported compression method'
|
181 | end
|
182 | |
183 | if filter_method != 0 |
184 | raise Errors::UnsupportedImageType, |
185 | 'PNG uses an unsupported filter method'
|
186 | end
|
187 | |
188 | if interlace_method != 0 |
189 | raise Errors::UnsupportedImageType, |
190 | 'PNG uses unsupported interlace method'
|
191 | end
|
192 | |
193 | # some PNG types store the colour and alpha channel data together,
|
194 | # which the PDF spec doesn't like, so split it out.
|
195 | split_alpha_channel!
|
196 | |
197 | case colors |
198 | when 1 |
199 | color = :DeviceGray |
200 | when 3 |
201 | color = :DeviceRGB |
202 | else
|
203 | raise Errors::UnsupportedImageType, |
204 | "PNG uses an unsupported number of colors (#{png.colors})" |
205 | end
|
206 | |
207 | # build the image dict
|
208 | obj = document.ref!( |
209 | Type: :XObject, |
210 | Subtype: :Image, |
211 | Height: height, |
212 | Width: width, |
213 | BitsPerComponent: bits, |
214 | )
|
215 | |
216 | # append the actual image data to the object as a stream
|
217 | obj << img_data |
218 | |
219 | obj.stream.filters << { |
220 | FlateDecode: { |
221 | Predictor: 15, |
222 | Colors: colors, |
223 | BitsPerComponent: bits, |
224 | Columns: width, |
225 | },
|
226 | }
|
227 | |
228 | # sort out the colours of the image
|
229 | if palette.empty? |
230 | obj.data[:ColorSpace] = color |
231 | else
|
232 | # embed the colour palette in the PDF as a object stream
|
233 | palette_obj = document.ref!({}) |
234 | palette_obj << palette |
235 | |
236 | # build the color space array for the image
|
237 | obj.data[:ColorSpace] = [ |
238 | :Indexed, |
239 | :DeviceRGB, |
240 | (palette.size / 3) - 1, |
241 | palette_obj, |
242 | ]
|
243 | end
|
244 | |
245 | # *************************************
|
246 | # add transparency data if necessary
|
247 | # *************************************
|
248 | |
249 | # For PNG color types 0, 2 and 3, the transparency data is stored in
|
250 | # a dedicated PNG chunk, and is exposed via the transparency attribute
|
251 | # of the PNG class.
|
252 | if transparency[:grayscale] |
253 | # Use Color Key Masking (spec section 4.8.5)
|
254 | # - An array with N elements, where N is two times the number of color
|
255 | # components.
|
256 | val = transparency[:grayscale] |
257 | obj.data[:Mask] = [val, val] |
258 | elsif transparency[:rgb] |
259 | # Use Color Key Masking (spec section 4.8.5)
|
260 | # - An array with N elements, where N is two times the number of color
|
261 | # components.
|
262 | rgb = transparency[:rgb] |
263 | obj.data[:Mask] = rgb.map { |x| [x, x] }.flatten |
264 | end
|
265 | |
266 | # For PNG color types 4 and 6, the transparency data is stored as
|
267 | # a alpha channel mixed in with the main image data. The PNG class
|
268 | # separates it out for us and makes it available via the alpha_channel
|
269 | # attribute
|
270 | if alpha_channel? |
271 | smask_obj = document.ref!( |
272 | Type: :XObject, |
273 | Subtype: :Image, |
274 | Height: height, |
275 | Width: width, |
276 | BitsPerComponent: bits, |
277 | ColorSpace: :DeviceGray, |
278 | Decode: [0, 1], |
279 | )
|
280 | smask_obj.stream << alpha_channel |
281 | |
282 | smask_obj.stream.filters << { |
283 | FlateDecode: { |
284 | Predictor: 15, |
285 | Colors: 1, |
286 | BitsPerComponent: bits, |
287 | Columns: width, |
288 | },
|
289 | }
|
290 | obj.data[:SMask] = smask_obj |
291 | end
|
292 | |
293 | obj
|
294 | end
|
#colors ⇒ Integer
Number of color components to each pixel.
Source Code
138 | def colors |
139 | case color_type |
140 | when 0, 3, 4 |
141 | 1
|
142 | when 2, 6 |
143 | 3
|
144 | end
|
145 | end
|
#min_pdf_version ⇒ Float
Returns the minimum PDF version required to support this image.
Source Code
299 | def min_pdf_version |
300 | if bits > 8 |
301 | # 16-bit color only supported in 1.5+ (ISO 32000-1:2008 8.9.5.1)
|
302 | 1.5
|
303 | elsif alpha_channel? |
304 | # Need transparency for SMask
|
305 | 1.4
|
306 | else
|
307 | 1.0
|
308 | end
|
309 | end
|