Parent

Prawn::Images::PNG

A convenience class that wraps the logic for extracting the parts of a PNG image that we need to embed them in a PDF

Attributes

palette[R]
img_data[R]
transparency[R]
width[R]
height[R]
bits[R]
color_type[R]
compression_method[R]
filter_method[R]
interlace_method[R]
alpha_channel[R]
scaled_width[RW]
scaled_height[RW]

Public Class Methods

new(data) click to toggle source

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

Public Instance Methods

alpha_channel?() click to toggle source
     # File lib/prawn/images/png.rb, line 121
121:       def alpha_channel?
122:         @color_type == 4 || @color_type == 6
123:       end
alpha_channel_bits() click to toggle source

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_pdf_object(document) click to toggle source

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
colors() click to toggle source

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
min_pdf_version() click to toggle source

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
pixel_bitlength() click to toggle source

number of bits used per pixel

     # File lib/prawn/images/png.rb, line 106
106:       def pixel_bitlength
107:         if alpha_channel?
108:           self.bits * (self.colors + 1)
109:         else
110:           self.bits * self.colors
111:         end
112:       end
split_alpha_channel!() click to toggle source

split the alpha channel data from the raw image data in images where it’s required.

     # File lib/prawn/images/png.rb, line 117
117:       def split_alpha_channel!
118:         unfilter_image_data if alpha_channel?
119:       end

Private Instance Methods

unfilter_image_data() click to toggle source
     # 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.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.