You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
typesetting/pitfall/pdfkit/node_modules/png-js/png.coffee

392 lines
15 KiB
CoffeeScript

###
# MIT LICENSE
# Copyright (c) 2011 Devon Govett
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
# to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
###
class PNG
@load: (url, canvas, callback) ->
callback = canvas if typeof canvas is 'function'
xhr = new XMLHttpRequest
xhr.open("GET", url, true)
xhr.responseType = "arraybuffer"
xhr.onload = =>
data = new Uint8Array(xhr.response or xhr.mozResponseArrayBuffer)
png = new PNG(data)
png.render(canvas) if typeof canvas?.getContext is 'function'
callback?(png)
xhr.send(null)
APNG_DISPOSE_OP_NONE = 0
APNG_DISPOSE_OP_BACKGROUND = 1
APNG_DISPOSE_OP_PREVIOUS = 2
APNG_BLEND_OP_SOURCE = 0
APNG_BLEND_OP_OVER = 1
constructor: (@data) ->
@pos = 8 # Skip the default header
@palette = []
@imgData = []
@transparency = {}
@animation = null
@text = {}
frame = null
loop
chunkSize = @readUInt32()
section = (String.fromCharCode @data[@pos++] for i in [0...4]).join('')
switch section
when 'IHDR'
# we can grab interesting values from here (like width, height, etc)
@width = @readUInt32()
@height = @readUInt32()
@bits = @data[@pos++]
@colorType = @data[@pos++]
@compressionMethod = @data[@pos++]
@filterMethod = @data[@pos++]
@interlaceMethod = @data[@pos++]
when 'acTL'
# we have an animated PNG
@animation =
numFrames: @readUInt32()
numPlays: @readUInt32() or Infinity
frames: []
when 'PLTE'
@palette = @read(chunkSize)
when 'fcTL'
@animation.frames.push(frame) if frame
@pos += 4 # skip sequence number
frame =
width: @readUInt32()
height: @readUInt32()
xOffset: @readUInt32()
yOffset: @readUInt32()
delayNum = @readUInt16()
delayDen = @readUInt16() or 100
frame.delay = 1000 * delayNum / delayDen
frame.disposeOp = @data[@pos++]
frame.blendOp = @data[@pos++]
frame.data = []
when 'IDAT', 'fdAT'
if section is 'fdAT'
@pos += 4 # skip sequence number
chunkSize -= 4
data = frame?.data or @imgData
for i in [0...chunkSize]
data.push @data[@pos++]
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 = @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 = @read(chunkSize)[0]
when 2
# True color with proper alpha channel.
@transparency.rgb = @read(chunkSize)
when 'tEXt'
text = @read(chunkSize)
index = text.indexOf(0)
key = String.fromCharCode text.slice(0, index)...
@text[key] = String.fromCharCode text.slice(index + 1)...
when 'IEND'
@animation.frames.push(frame) if frame
# 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'
#console.log("imgdata")
#console.log(@imgData)
@imgData = new Uint8Array @imgData
return
else
# unknown (or unimportant) section, skip it
@pos += chunkSize
@pos += 4 # Skip the CRC
if @pos > @data.length
throw new Error "Incomplete or corrupt PNG file"
#console.log("done parsing PNG")
return
read: (bytes) ->
(@data[@pos++] for i in [0...bytes])
readUInt32: ->
b1 = @data[@pos++] << 24
b2 = @data[@pos++] << 16
b3 = @data[@pos++] << 8
b4 = @data[@pos++]
b1 | b2 | b3 | b4
readUInt16: ->
b1 = @data[@pos++] << 8
b2 = @data[@pos++]
b1 | b2
decodePixels: (data = @imgData) ->
return new Uint8Array(0) if data.length is 0
data = new FlateStream(data)
data = data.getBytes()
pixelBytes = @pixelBitlength / 8
scanlineLength = pixelBytes * @width
pixels = new Uint8Array(scanlineLength * @height)
length = data.length
row = 0
pos = 0
c = 0
while pos < length
switch data[pos++]
when 0 # None
for i in [0...scanlineLength] by 1
pixels[c++] = data[pos++]
when 1 # Sub
for i in [0...scanlineLength] by 1
byte = data[pos++]
left = if i < pixelBytes then 0 else pixels[c - pixelBytes]
pixels[c++] = (byte + left) % 256
when 2 # Up
for i in [0...scanlineLength] by 1
byte = data[pos++]
col = (i - (i % pixelBytes)) / pixelBytes
upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)]
pixels[c++] = (upper + byte) % 256
when 3 # Average
for i in [0...scanlineLength] by 1
byte = data[pos++]
col = (i - (i % pixelBytes)) / pixelBytes
left = if i < pixelBytes then 0 else pixels[c - pixelBytes]
upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)]
pixels[c++] = (byte + Math.floor((left + upper) / 2)) % 256
when 4 # Paeth
for i in [0...scanlineLength] by 1
byte = data[pos++]
col = (i - (i % pixelBytes)) / pixelBytes
left = if i < pixelBytes then 0 else pixels[c - pixelBytes]
if row is 0
upper = upperLeft = 0
else
upper = pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)]
upperLeft = col && pixels[(row - 1) * scanlineLength + (col - 1) * pixelBytes + (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
pixels[c++] = (byte + paeth) % 256
else
throw new Error "Invalid filter algorithm: " + data[pos - 1]
row++
return pixels
decodePalette: ->
palette = @palette
transparency = @transparency.indexed or []
ret = new Uint8Array((transparency.length or 0) + palette.length)
pos = 0
length = palette.length
c = 0
for i in [0...palette.length] by 3
ret[pos++] = palette[i]
ret[pos++] = palette[i + 1]
ret[pos++] = palette[i + 2]
ret[pos++] = transparency[c++] ? 255
return ret
copyToImageData: (imageData, pixels) ->
colors = @colors
palette = null
alpha = @hasAlphaChannel
if @palette.length
palette = @_decodedPalette ?= @decodePalette()
colors = 4
alpha = true
data = imageData.data
length = data.length
input = palette or pixels
i = j = 0
if colors is 1
while i < length
k = if palette then pixels[i / 4] * 4 else j
v = input[k++]
data[i++] = v
data[i++] = v
data[i++] = v
data[i++] = if alpha then input[k++] else 255
j = k
else
while i < length
k = if palette then pixels[i / 4] * 4 else j
data[i++] = input[k++]
data[i++] = input[k++]
data[i++] = input[k++]
data[i++] = if alpha then input[k++] else 255
j = k
return
decode: ->
ret = new Uint8Array(@width * @height * 4)
@copyToImageData ret, @decodePixels()
return ret
scratchCanvas = document.createElement 'canvas'
scratchCtx = scratchCanvas.getContext '2d'
makeImage = (imageData) ->
scratchCtx.width = imageData.width
scratchCtx.height = imageData.height
scratchCtx.clearRect(0, 0, imageData.width, imageData.height)
scratchCtx.putImageData(imageData, 0, 0)
img = new Image
img.src = scratchCanvas.toDataURL()
return img
decodeFrames: (ctx) ->
return unless @animation
for frame, i in @animation.frames
imageData = ctx.createImageData(frame.width, frame.height)
pixels = @decodePixels(new Uint8Array(frame.data))
@copyToImageData(imageData, pixels)
frame.imageData = imageData
frame.image = makeImage(imageData)
renderFrame: (ctx, number) ->
frames = @animation.frames
frame = frames[number]
prev = frames[number - 1]
# if we're on the first frame, clear the canvas
if number is 0
ctx.clearRect(0, 0, @width, @height)
# check the previous frame's dispose operation
if prev?.disposeOp is APNG_DISPOSE_OP_BACKGROUND
ctx.clearRect(prev.xOffset, prev.yOffset, prev.width, prev.height)
else if prev?.disposeOp is APNG_DISPOSE_OP_PREVIOUS
ctx.putImageData(prev.imageData, prev.xOffset, prev.yOffset)
# APNG_BLEND_OP_SOURCE overwrites the previous data
if frame.blendOp is APNG_BLEND_OP_SOURCE
ctx.clearRect(frame.xOffset, frame.yOffset, frame.width, frame.height)
# draw the current frame
ctx.drawImage(frame.image, frame.xOffset, frame.yOffset)
animate: (ctx) ->
frameNumber = 0
{numFrames, frames, numPlays} = @animation
do doFrame = =>
f = frameNumber++ % numFrames
frame = frames[f]
@renderFrame(ctx, f)
if numFrames > 1 and frameNumber / numFrames < numPlays
@animation._timeout = setTimeout(doFrame, frame.delay)
stopAnimation: ->
clearTimeout @animation?._timeout
render: (canvas) ->
# if this canvas was displaying another image before,
# stop the animation on it
if canvas._png
canvas._png.stopAnimation()
canvas._png = this
canvas.width = @width
canvas.height = @height
ctx = canvas.getContext "2d"
if @animation
@decodeFrames(ctx)
@animate(ctx)
else
data = ctx.createImageData @width, @height
@copyToImageData data, @decodePixels()
ctx.putImageData data, 0, 0
window.PNG = PNG