### # 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