mirror of
https://github.com/foliojs/pdfkit.git
synced 2026-02-01 16:56:57 +00:00
291 lines
11 KiB
CoffeeScript
291 lines
11 KiB
CoffeeScript
fs = require 'fs'
|
|
Data = '../data'
|
|
zlib = require 'zlib'
|
|
|
|
class PNG
|
|
@open: (filename) ->
|
|
contents = fs.readFileSync filename
|
|
data = new Data(contents)
|
|
new PNG(data)
|
|
|
|
constructor: (@data) ->
|
|
data.pos = 8 # Skip the default header
|
|
|
|
@palette = []
|
|
@imgData = []
|
|
@transparency = {}
|
|
|
|
loop
|
|
chunkSize = data.readUInt32()
|
|
section = data.readString(4)
|
|
|
|
switch section
|
|
when 'IHDR'
|
|
# we can grab interesting values from here (like width, height, etc)
|
|
@width = data.readUInt32()
|
|
@height = data.readUInt32()
|
|
@bits = data.readByte()
|
|
@colorType = data.readByte()
|
|
@compressionMethod = data.readByte()
|
|
@filterMethod = data.readByte()
|
|
@interlaceMethod = data.readByte()
|
|
|
|
when 'PLTE'
|
|
@palette = data.read(chunkSize)
|
|
|
|
when 'IDAT'
|
|
for i in [0...chunkSize]
|
|
@imgData.push data.readByte()
|
|
|
|
when 'tRNS'
|
|
# This chunk can only occur once and it must occur after the
|
|
# PLTE chunk and before the IDAT chunk.
|
|
@transparency = {}
|
|
switch @colorType
|
|
when 3
|
|
# Indexed color, 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(chunkSize)
|
|
short = 255 - @transparency.indexed.length
|
|
if short > 0
|
|
@transparency.indexed.push 255 for i in [0...short]
|
|
when 0
|
|
# Greyscale. Corresponding to entries in the PLTE chunk.
|
|
# Grey is two bytes, range 0 .. (2 ^ bit-depth) - 1
|
|
@transparency.grayscale = data.read(chunkSize)[0]
|
|
when 2
|
|
# True color with proper alpha channel.
|
|
@transparency.rgb = data.read(chunkSize)
|
|
|
|
when 'IEND'
|
|
# we've got everything we need!
|
|
@colors = switch @colorType
|
|
when 0, 3, 4 then 1
|
|
when 2, 6 then 3
|
|
|
|
@hasAlphaChannel = @colorType in [4, 6]
|
|
colors = @colors + if @hasAlphaChannel then 1 else 0
|
|
@pixelBitlength = @bits * colors
|
|
|
|
@colorSpace = switch @colors
|
|
when 1 then 'DeviceGray'
|
|
when 3 then 'DeviceRGB'
|
|
|
|
@imgData = new Buffer @imgData
|
|
return
|
|
|
|
else
|
|
# unknown (or unimportant) section, skip it
|
|
data.pos += chunkSize
|
|
|
|
data.pos += 4 # Skip the CRC
|
|
|
|
return
|
|
|
|
object: (document) ->
|
|
obj = document.ref
|
|
Type: 'XObject'
|
|
Subtype: 'Image'
|
|
BitsPerComponent: @bits
|
|
Width: @width
|
|
Height: @height
|
|
Length: @imgData.length
|
|
Filter: 'FlateDecode'
|
|
|
|
unless @hasAlphaChannel
|
|
obj.data['DecodeParms'] =
|
|
Predictor: 15
|
|
Colors: @colors
|
|
BitsPerComponent: @bits
|
|
Columns: @width
|
|
|
|
if @palette.length is 0
|
|
obj.data['ColorSpace'] = @colorSpace
|
|
else
|
|
# embed the color palette in the PDF as an object stream
|
|
palette = document.ref
|
|
Length: @palette.length
|
|
|
|
palette.add new Buffer(@palette)
|
|
|
|
# build the color space array for the image
|
|
obj.data['ColorSpace'] = ['Indexed', 'DeviceRGB', (@palette.length / 3) - 1, palette]
|
|
|
|
# For PNG color types 0, 2 and 3, the transparency data is stored in
|
|
# a dedicated PNG chunk.
|
|
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.greyscale
|
|
obj.data['Mask'] = [val, val]
|
|
|
|
else if @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
|
|
mask = []
|
|
for x in rgb
|
|
mask.push x, x
|
|
|
|
obj.data['Mask'] = mask
|
|
|
|
else if @transparency.indexed
|
|
# Create a transparency SMask for the image based on the data
|
|
# in the PLTE and tRNS sections. See below for details on SMasks.
|
|
@loadIndexedAlphaChannel()
|
|
|
|
# For PNG color types 4 and 6, the transparency data is stored as a alpha
|
|
# channel mixed in with the main image data. Separate this data out into an
|
|
# SMask object and store it separately in the PDF.
|
|
if @hasAlphaChannel
|
|
@splitAlphaChannel()
|
|
obj.data['Length'] = @imgData.length
|
|
|
|
if @alphaChannel
|
|
sMask = document.ref
|
|
Type: 'XObject'
|
|
Subtype: 'Image'
|
|
Height: @height
|
|
Width: @width
|
|
BitsPerComponent: 8
|
|
Length: @alphaChannel.length
|
|
Filter: 'FlateDecode'
|
|
ColorSpace: 'DeviceGray'
|
|
Decode: [0, 1]
|
|
|
|
sMask.add @alphaChannel
|
|
obj.data['SMask'] = sMask
|
|
|
|
# add the actual image data
|
|
obj.add @imgData
|
|
return obj
|
|
|
|
decodePixels: ->
|
|
data = zlib.inflate @imgData
|
|
pixelBytes = @pixelBitlength / 8
|
|
scanlineLength = pixelBytes * @width
|
|
|
|
row = 0
|
|
pixels = []
|
|
length = data.length
|
|
pos = 0
|
|
|
|
while pos < length
|
|
filter = data[pos++]
|
|
i = 0
|
|
rowData = []
|
|
|
|
switch filter
|
|
when 0 # None
|
|
while i < scanlineLength
|
|
rowData[i++] = data[pos++]
|
|
|
|
when 1 # Sub
|
|
while i < scanlineLength
|
|
byte = data[pos++]
|
|
left = if i < pixelBytes then 0 else rowData[i - pixelBytes]
|
|
rowData[i++] = (byte + left) % 256
|
|
|
|
when 2 # Up
|
|
while i < scanlineLength
|
|
byte = data[pos++]
|
|
col = (i - (i % pixelBytes)) / pixelBytes
|
|
upper = if row is 0 then 0 else pixels[row - 1][col][i % pixelBytes]
|
|
rowData[i++] = (upper + byte) % 256
|
|
|
|
when 3 # Average
|
|
while i < scanlineLength
|
|
byte = data[pos++]
|
|
col = (i - (i % pixelBytes)) / pixelBytes
|
|
left = if i < pixelBytes then 0 else rowData[i - pixelBytes]
|
|
upper = if row is 0 then 0 else pixels[row - 1][col][i % pixelBytes]
|
|
rowData[i++] = (byte + Math.floor((left + upper) / 2)) % 256
|
|
|
|
when 4 # Paeth
|
|
while i < scanlineLength
|
|
byte = data[pos++]
|
|
col = (i - (i % pixelBytes)) / pixelBytes
|
|
left = if i < pixelBytes then 0 else rowData[i - pixelBytes]
|
|
|
|
if row is 0
|
|
upper = upperLeft = 0
|
|
else
|
|
upper = pixels[row - 1][col][i % pixelBytes]
|
|
upperLeft = if col is 0 then 0 else pixels[row - 1][col - 1][i % pixelBytes]
|
|
|
|
p = left + upper - upperLeft
|
|
pa = Math.abs(p - left)
|
|
pb = Math.abs(p - upper)
|
|
pc = Math.abs(p - upperLeft)
|
|
|
|
if pa <= pb and pa <= pc
|
|
paeth = left
|
|
else if pb <= pc
|
|
paeth = upper
|
|
else
|
|
paeth = upperLeft
|
|
|
|
rowData[i++] = (byte + paeth) % 256
|
|
|
|
else
|
|
throw new Error "Invalid filter algorithm: " + filter
|
|
|
|
s = []
|
|
for i in [0...rowData.length] by pixelBytes
|
|
s.push rowData.slice(i, i + pixelBytes)
|
|
|
|
pixels.push(s)
|
|
row += 1
|
|
|
|
return pixels
|
|
|
|
splitAlphaChannel: ->
|
|
pixels = @decodePixels()
|
|
|
|
colorByteSize = @colors * @bits / 8
|
|
alphaByteSize = 1
|
|
|
|
pixelCount = @width * @height
|
|
imgData = new Buffer(pixelCount * colorByteSize)
|
|
alphaChannel = new Buffer(pixelCount)
|
|
|
|
p = a = 0
|
|
for row in pixels
|
|
for pixel in row
|
|
imgData[p++] = pixel[i] for i in [0...colorByteSize]
|
|
alphaChannel[a++] = pixel[colorByteSize]
|
|
|
|
@imgData = zlib.deflate imgData
|
|
@alphaChannel = zlib.deflate alphaChannel
|
|
|
|
decodingPalette: ->
|
|
palette = @palette
|
|
transparency = @transparency.indexed
|
|
decodingMap = []
|
|
index = 0
|
|
|
|
for i in [0...palette.length] by 3
|
|
alpha = transparency[index++] ? 255
|
|
pixel = palette.slice(i, i + 3).concat(alpha)
|
|
decodingMap.push pixel
|
|
|
|
return decodingMap
|
|
|
|
loadIndexedAlphaChannel: ->
|
|
palette = @decodingPalette()
|
|
pixels = @decodePixels()
|
|
|
|
pixelCount = @width * @height
|
|
alphaChannel = new Buffer(pixelCount)
|
|
|
|
i = 0
|
|
for row in pixels
|
|
for pixel in row
|
|
pixel = pixel[0]
|
|
alphaChannel[i++] = palette[pixel][3]
|
|
|
|
@alphaChannel = zlib.deflate alphaChannel
|
|
|
|
module.exports = PNG |