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