Object
A convenience class that wraps the logic for extracting the parts of a PNG image that we need to embed them in a PDF
Process a new PNG image
| data | A binary string of PNG data |
# File lib/prawn/images/png.rb, line 30
30: def initialize(data)
31: data = StringIO.new(data.dup)
32:
33: data.read(8) # Skip the default header
34:
35: @palette = ""
36: @img_data = ""
37: @transparency = {}
38:
39: loop do
40: chunk_size = data.read(4).unpack("N")[0]
41: section = data.read(4)
42: case section
43: when 'IHDR'
44: # we can grab other interesting values from here (like width,
45: # height, etc)
46: values = data.read(chunk_size).unpack("NNCCCCC")
47:
48: @width = values[0]
49: @height = values[1]
50: @bits = values[2]
51: @color_type = values[3]
52: @compression_method = values[4]
53: @filter_method = values[5]
54: @interlace_method = values[6]
55: when 'PLTE'
56: @palette << data.read(chunk_size)
57: when 'IDAT'
58: @img_data << data.read(chunk_size)
59: when 'tRNS'
60: # This chunk can only occur once and it must occur after the
61: # PLTE chunk and before the IDAT chunk
62: @transparency = {}
63: case @color_type
64: when 3
65: # Indexed colour, RGB. Each byte in this chunk is an alpha for
66: # the palette index in the PLTE ("palette") chunk up until the
67: # last non-opaque entry. Set up an array, stretching over all
68: # palette entries which will be 0 (opaque) or 1 (transparent).
69: @transparency[:indexed] = data.read(chunk_size).unpack("C*")
70: short = 255 - @transparency[:indexed].size
71: @transparency[:indexed] += ([255] * short) if short > 0
72: when 0
73: # Greyscale. Corresponding to entries in the PLTE chunk.
74: # Grey is two bytes, range 0 .. (2 ^ bit-depth) - 1
75: grayval = data.read(chunk_size).unpack("n").first
76: @transparency[:grayscale] = grayval
77: when 2
78: # True colour with proper alpha channel.
79: @transparency[:rgb] = data.read(chunk_size).unpack("nnn")
80: end
81: when 'IEND'
82: # we've got everything we need, exit the loop
83: break
84: else
85: # unknown (or un-important) section, skip over it
86: data.seek(data.pos + chunk_size)
87: end
88:
89: data.read(4) # Skip the CRC
90: end
91: end
# File lib/prawn/images/png.rb, line 121
121: def alpha_channel?
122: @color_type == 4 || @color_type == 6
123: end
Adobe Reader can’t handle 16-bit png channels — chop off the second byte (least significant)
# File lib/prawn/images/png.rb, line 128
128: def alpha_channel_bits
129: 8
130: end
Build a PDF object representing this image in document, and return a Reference to it.
# File lib/prawn/images/png.rb, line 135
135: def build_pdf_object(document)
136: if compression_method != 0
137: raise Errors::UnsupportedImageType,
138: 'PNG uses an unsupported compression method'
139: end
140:
141: if filter_method != 0
142: raise Errors::UnsupportedImageType,
143: 'PNG uses an unsupported filter method'
144: end
145:
146: if interlace_method != 0
147: raise Errors::UnsupportedImageType,
148: 'PNG uses unsupported interlace method'
149: end
150:
151: # some PNG types store the colour and alpha channel data together,
152: # which the PDF spec doesn't like, so split it out.
153: split_alpha_channel!
154:
155: case colors
156: when 1
157: color = :DeviceGray
158: when 3
159: color = :DeviceRGB
160: else
161: raise Errors::UnsupportedImageType,
162: "PNG uses an unsupported number of colors (#{png.colors})"
163: end
164:
165: # build the image dict
166: obj = document.ref!(
167: :Type => :XObject,
168: :Subtype => :Image,
169: :Height => height,
170: :Width => width,
171: :BitsPerComponent => bits,
172: :Length => img_data.size,
173: :Filter => :FlateDecode
174: )
175:
176: unless alpha_channel
177: obj.data[:DecodeParms] = {:Predictor => 15,
178: :Colors => colors,
179: :BitsPerComponent => bits,
180: :Columns => width}
181: end
182:
183: # append the actual image data to the object as a stream
184: obj << img_data
185:
186: # sort out the colours of the image
187: if palette.empty?
188: obj.data[:ColorSpace] = color
189: else
190: # embed the colour palette in the PDF as a object stream
191: palette_obj = document.ref!(:Length => palette.size)
192: palette_obj << palette
193:
194: # build the color space array for the image
195: obj.data[:ColorSpace] = [:Indexed,
196: :DeviceRGB,
197: (palette.size / 3) -1,
198: palette_obj]
199: end
200:
201: # *************************************
202: # add transparency data if necessary
203: # *************************************
204:
205: # For PNG color types 0, 2 and 3, the transparency data is stored in
206: # a dedicated PNG chunk, and is exposed via the transparency attribute
207: # of the PNG class.
208: if transparency[:grayscale]
209: # Use Color Key Masking (spec section 4.8.5)
210: # - An array with N elements, where N is two times the number of color
211: # components.
212: val = transparency[:grayscale]
213: obj.data[:Mask] = [val, val]
214: elsif transparency[:rgb]
215: # Use Color Key Masking (spec section 4.8.5)
216: # - An array with N elements, where N is two times the number of color
217: # components.
218: rgb = transparency[:rgb]
219: obj.data[:Mask] = rgb.collect { |x| [x,x] }.flatten
220: elsif transparency[:indexed]
221: # TODO: broken. I was attempting to us Color Key Masking, but I think
222: # we need to construct an SMask i think. Maybe do it inside
223: # the PNG class, and store it in alpha_channel
224: #obj.data[:Mask] = transparency[:indexed]
225: end
226:
227: # For PNG color types 4 and 6, the transparency data is stored as a alpha
228: # channel mixed in with the main image data. The PNG class seperates
229: # it out for us and makes it available via the alpha_channel attribute
230: if alpha_channel?
231: smask_obj = document.ref!(
232: :Type => :XObject,
233: :Subtype => :Image,
234: :Height => height,
235: :Width => width,
236: :BitsPerComponent => alpha_channel_bits,
237: :Length => alpha_channel.size,
238: :Filter => :FlateDecode,
239: :ColorSpace => :DeviceGray,
240: :Decode => [0, 1]
241: )
242: smask_obj << alpha_channel
243: obj.data[:SMask] = smask_obj
244: end
245:
246: obj
247: end
number of color components to each pixel
# File lib/prawn/images/png.rb, line 95
95: def colors
96: case self.color_type
97: when 0, 3, 4
98: return 1
99: when 2, 6
100: return 3
101: end
102: end
Returns the minimum PDF version required to support this image.
# File lib/prawn/images/png.rb, line 250
250: def min_pdf_version
251: if bits > 8
252: # 16-bit color only supported in 1.5+ (ISO 32000-1:2008 8.9.5.1)
253: 1.5
254: elsif alpha_channel?
255: # Need transparency for SMask
256: 1.4
257: else
258: 1.0
259: end
260: end
# File lib/prawn/images/png.rb, line 264
264: def unfilter_image_data
265: data = Zlib::Inflate.inflate(@img_data).unpack 'C*'
266: @img_data = ""
267: @alpha_channel = ""
268:
269: pixel_bytes = pixel_bitlength / 8
270: scanline_length = pixel_bytes * self.width + 1
271: row = 0
272: pixels = []
273: paeth, pa, pb, pc = nil
274: until data.empty? do
275: row_data = data.slice! 0, scanline_length
276: filter = row_data.shift
277: case filter
278: when 0 # None
279: when 1 # Sub
280: row_data.each_with_index do |byte, index|
281: left = index < pixel_bytes ? 0 : row_data[index - pixel_bytes]
282: row_data[index] = (byte + left) % 256
283: #p [byte, left, row_data[index]]
284: end
285: when 2 # Up
286: row_data.each_with_index do |byte, index|
287: col = index / pixel_bytes
288: upper = row == 0 ? 0 : pixels[row-1][col][index % pixel_bytes]
289: row_data[index] = (upper + byte) % 256
290: end
291: when 3 # Average
292: row_data.each_with_index do |byte, index|
293: col = index / pixel_bytes
294: upper = row == 0 ? 0 : pixels[row-1][col][index % pixel_bytes]
295: left = index < pixel_bytes ? 0 : row_data[index - pixel_bytes]
296:
297: row_data[index] = (byte + ((left + upper)/2).floor) % 256
298: end
299: when 4 # Paeth
300: left = upper = upper_left = nil
301: row_data.each_with_index do |byte, index|
302: col = index / pixel_bytes
303:
304: left = index < pixel_bytes ? 0 : row_data[index - pixel_bytes]
305: if row.zero?
306: upper = upper_left = 0
307: else
308: upper = pixels[row-1][col][index % pixel_bytes]
309: upper_left = col.zero? ? 0 :
310: pixels[row-1][col-1][index % pixel_bytes]
311: end
312:
313: p = left + upper - upper_left
314: pa = (p - left).abs
315: pb = (p - upper).abs
316: pc = (p - upper_left).abs
317:
318: paeth = if pa <= pb && pa <= pc
319: left
320: elsif pb <= pc
321: upper
322: else
323: upper_left
324: end
325:
326: row_data[index] = (byte + paeth) % 256
327: end
328: else
329: raise ArgumentError, "Invalid filter algorithm #{filter}"
330: end
331:
332: s = []
333: row_data.each_slice pixel_bytes do |slice|
334: s << slice
335: end
336: pixels << s
337: row += 1
338: end
339:
340: # convert the pixel data to seperate strings for colours and alpha
341: color_byte_size = self.colors * self.bits / 8
342: alpha_byte_size = alpha_channel_bits / 8
343: pixels.each do |this_row|
344: this_row.each do |pixel|
345: @img_data << pixel[0, color_byte_size].pack("C*")
346: @alpha_channel << pixel[color_byte_size, alpha_byte_size].pack("C*")
347: end
348: end
349:
350: # compress the data
351: @img_data = Zlib::Deflate.deflate(@img_data)
352: @alpha_channel = Zlib::Deflate.deflate(@alpha_channel)
353: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.