diff --git a/README.md b/README.md index 0bbe2655..d0f19105 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ 0. [Image operations](#image-operations) 0. [Resize](#resize) 0. [Scale](#scale) + 0. [Contain](#contain) + 0. [Cover](#cover) 0. [Rotate](#rotate) 0. [Crop](#crop) 0. [Blur](#blur) @@ -31,14 +33,17 @@ 0. [Fade (adjust transparency)](#fade) 0. [Opacify](#opacify) 0. [Paste](#paste) + 0. [Set pixel](#set-pixel) 0. [Getters](#getters) 0. [Width](#width) 0. [Height](#height) + 0. [Pixel](#get-pixel) 0. [Clone](#clone) 0. [Extract / Copy](#extract) 0. [Get as a Buffer](#get-as-a-buffer) 0. [JPEG](#jpeg) 0. [PNG](#png) + 0. [GIF](#gif) 0. [Write to file](#write-to-file) 0. [Batch operations](#batch-operations) 0. [Copyrights](#copyrights) @@ -66,6 +71,7 @@ You can run tests with `npm test`. **Note:** Installation of this module involves compiling native code. If `npm install lwip` failes, you probably need to setup your system. [See instructions](https://github.com/TooTallNate/node-gyp#installation). +Building on Windows with Visual Studio requires version 2013 or higher. ### Usage @@ -80,7 +86,7 @@ If `npm install lwip` failes, you probably need to setup your system. ```Javascript // obtain an image object: require('lwip').open('image.jpg', function(err, image){ - + // check err... // define a batch of manipulations and save to disk as JPEG: image.batch() @@ -103,7 +109,7 @@ var lwip = require('lwip'); // obtain an image object: lwip.open('image.jpg', function(err, image){ - + // check err... // manipulate image: image.scale(0.5, function(err, image){ @@ -133,12 +139,15 @@ lwip.open('image.jpg', function(err, image){ **Decoding (reading):** - JPEG, 1 & 3 channels (grayscale & RGB). -- PNG, 1 & 3 channels (grayscale & RGB) + alpha (transparency) channel. +- PNG, transparency supported. +- GIF, transparency supported. Animated GIFs can be read, but only the first + frame will be retrieved. **Encoding (writing):** - JPEG, 3 channels (RGB). - PNG (lossless), 3 channels (RGB) or 4 channels (RGBA). +- GIF (no animations) Other formats may also be supported in the future, but are probably less urgent. Check the issues to see [which formats are planned to be supported](https://github.com/EyalAr/lwip/issues?labels=format+request&page=1&state=open). @@ -161,7 +170,7 @@ Colors are specified in one of three ways: "blue" // {r: 0, g: 0, b: 255, a: 100} "yellow" // {r: 255, g: 255, b: 0, a: 100} "cyan" // {r: 0, g: 255, b: 255, a: 100} - "magenta" // {r: 255, g: 0, b: 255, a: 100} + "magenta" // {r: 255, g: 0, b: 255, a: 100} ``` - As an array `[R, G, B, A]` where `R`, `G` and `B` are integers between 0 and @@ -197,11 +206,27 @@ by: `lwip.open(source, type, callback)` 0. `source {String/Buffer}`: The path to the image on disk or an image buffer. -0. `type {String}`: **Optional** type of the image. If omitted, the type will be - inferred from the file extension. If `source` is a buffer, `type` must be - specified. +0. `type {String/Object}`: **Optional** type of the image. If omitted, the type + will be inferred from the file extension. If `source` is a buffer, `type` + must be specified. If `source` is an encoded image buffer, `type` must be + a string of the image type (i.e. `"jpg"`). If `source` is a raw pixels buffer + `type` must be an object with `type.width` and `type.height` properties. 0. `callback {Function(err, image)}` +**Note about raw pixels buffers:** `source` may be a buffer of raw pixels. The +buffer may contain pixels of 1-4 channels, where: + +0. 1 channel is a grayscale image. +0. 2 channels is a grayscale image with an alpha channel. +0. 3 channels is an RGB image. +0. 4 channels is an RGBA image (with an alpha channel). + +In other words, if the image in the buffer has width `W` and height `H`, the +size of the buffer can be `W*H`, `2*W*H`, `3*W*H` or `4*W*H`. + +The channel values in the buffer must be stored sequentially. I.e. first all the +Red values, then all the Green values, etc. + #### Open file example ```Javascript @@ -285,6 +310,47 @@ lwip.create(500, 500, 'yellow', function(err, image){ - `"lanczos"` 0. `callback {Function(err, image)}` +#### Contain + +Contain the image in a colored canvas. The image will be resized to the largest +possible size such that it's fully contained inside the canvas. + +`image.contain(width, height, color, inter, callback)` + +0. `width {Integer}`: Canvas' width in pixels. +0. `height {Integer}`: Canvas' height in pixels. +0. `color {String / Array / Object}`: **Optional** Color of the canvas. See + [colors specification](#colors-specification). +0. `inter {String}`: **Optional** interpolation method. Defaults to `"lanczos"`. + Possible values: + - `"nearest-neighbor"` + - `"moving-average"` + - `"linear"` + - `"grid"` + - `"cubic"` + - `"lanczos"` +0. `callback {Function(err, image)}` + +#### Cover + +Cover a canvas with the image. The image will be resized to the smallest +possible size such that both its dimensions are bigger than the canvas's +dimensions. Margins of the image exceeding the canvas will be discarded. + +`image.cover(width, height, inter, callback)` + +0. `width {Integer}`: Canvas' width in pixels. +0. `height {Integer}`: Canvas' height in pixels. +0. `inter {String}`: **Optional** interpolation method. Defaults to `"lanczos"`. + Possible values: + - `"nearest-neighbor"` + - `"moving-average"` + - `"linear"` + - `"grid"` + - `"cubic"` + - `"lanczos"` +0. `callback {Function(err, image)}` + #### Rotate `image.rotate(degs, color, callback)` @@ -335,7 +401,7 @@ Mirror an image along the 'x' axis, 'y' axis or both. `image.mirror(axes, callback)` -0. `axes {String}`: `'x'`, `'y'` or `'xy'`. +0. `axes {String}`: `'x'`, `'y'` or `'xy'` (case sensitive). 0. `callback {Function(err, image)}` #### Flip @@ -468,6 +534,24 @@ Paste an image on top of this image. 0. Extra caution is required when using this method in batch mode, as the images may change by the time this operation is called. +#### Set Pixel + +Set the color of a pixel. + +`image.setPixel(left, top, color, callback)` + +0. `left, top {Integer}`: Coordinates of the pixel from the left-top corner of + the image. +0. `color {String / Array / Object}`: Color of the pixel to set. + See [colors specification](#colors-specification). +0. `callback {Function(err, image)}` + +**Notes:** + +0. If the coordinates exceed the bounds of the image, an exception is thrown. +0. Extra caution is required when using this method in batch mode, as the + dimensions of the image may change by the time this operation is called. + ### Getters #### Width @@ -478,6 +562,16 @@ Paste an image on top of this image. `image.height()` returns the image's height in pixels. +#### Get Pixel + +`image.getPixel(left, top)` returns the color of the pixel at the `(left, top)` +coordinate. + +0. `left {Integer>=0}` +0. `top {Integer>=0}` + +Color is returned as an object. See [colors specification](#colors-specification). + #### Clone Clone the image into a new image object. @@ -531,6 +625,7 @@ encoded data as a NodeJS Buffer object. 0. `format {String}`: Encoding format. Possible values: - `"jpg"` - `"png"` + - `"gif"` 0. `params {Object}`: **Optional** Format-specific parameters (See below). 0. `callback {Function(err, buffer)}` @@ -558,6 +653,23 @@ The `params` object should have the following fields: `'auto'`, the image will be encoded with 4 channels if it has transparent components, and 3 channels otherwise. +##### GIF + +The `params` object should have the following fields: + +- `colors {Integer}`: Defaults to `256`. Number of colors in the color table + (at most). Must be between 2 and 256. +- `interlaced {Boolean}`: Defaults to `false`. +- `transparency {true/false/'auto'}`: Preserve transparency? Defaults to + `'auto'`. Determines if the encoded image will have 3 or 4 channels. If + `'auto'`, the image will be encoded with 4 channels if it has transparent + components, and 3 channels otherwise. +- `threshold {Integer}` - Between 0 and 100. Pixels in a gif image are either + fully transparent or fully opaque. This value sets the alpha channel + threshold to determine if a pixel is opaque or transparent. If the alpha + channel of the pixel is above this threshold, this pixel will be considered + as opaque; otherwise it will be transparent. + #### Write to file Write encoded binary image data directly to a file. @@ -692,3 +804,6 @@ The native part of this module is compiled from source which uses the following: - The CImg Library - [Website](http://cimg.sourceforge.net/) - [Readme](https://github.com/EyalAr/lwip/blob/master/src/lib/cimg/README.txt) +- giflib + - [Website](http://giflib.sourceforge.net/) + - [Readme](https://github.com/EyalAr/lwip/blob/master/src/lib/gif/README) diff --git a/appveyor.yml b/appveyor.yml index e81024b9..98b2e13b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ init: environment: matrix: - - nodejs_version: "0.10.32" + - nodejs_version: "0.10" - nodejs_version: "0.11" matrix: @@ -13,13 +13,7 @@ matrix: install: - ps: Install-Product node $env:nodejs_version - - npm install - -test_script: - # Output useful info for debugging. - - node --version - - npm --version - - npm test + - npm install --msvs_version=2013 build: off diff --git a/binding.gyp b/binding.gyp index 6eef55e7..3ce4035d 100644 --- a/binding.gyp +++ b/binding.gyp @@ -2,13 +2,14 @@ "targets": [{ "target_name": "lwip_decoder", "sources": [ - # LWIP: + # LWIP: ####### "src/decoder/init.cpp", "src/decoder/util.cpp", "src/decoder/buffer_worker.cpp", "src/decoder/jpeg_decoder.cpp", "src/decoder/png_decoder.cpp", + "src/decoder/gif_decoder.cpp", # LIB JPEG: ########### "src/lib/jpeg/jmemnobs.c", @@ -66,7 +67,12 @@ "src/lib/zlib/inffast.c", "src/lib/zlib/uncompr.c", "src/lib/zlib/zutil.c", - "src/lib/zlib/trees.c" + "src/lib/zlib/trees.c", + # LIB GIF: + ########## + "src/lib/gif/dgif_lib.c", + "src/lib/gif/gif_err.c", + "src/lib/gif/gifalloc.c", ], 'include_dirs': [ ' 360) color.h = 0; + return hslToRgb(color); +} + +// create an empty image canvas +lwip.create(width, height, function(err, image){ + if (err) return console.log(err); + + var x, y, c, + batch = image.batch(); + + // set the same color for each columns in the image + for (x = 0; x < width ; x++){ + c = nextColor(); + for (y = 0; y < height; y++){ + batch.setPixel(x, y, c); + } + } + + batch.writeFile(output, function(err){ + if (err) console.log(err); + }); +}); + +// adapted from http://stackoverflow.com/a/9493060/1365324 +function hslToRgb(hsl){ + var h = hsl.h / 360, + s = hsl.s / 100, + l = hsl.l / 100; + + var r, g, b; + + if(s == 0){ + r = g = b = l; // achromatic + }else{ + function hue2rgb(p, q, t){ + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1/6) return p + (q - p) * 6 * t; + if(t < 1/2) return q; + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; +} diff --git a/index.js b/index.js index 93ce339a..5be8b4b1 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,4 @@ -require('./lib/Batch'); // Extend Image object with .batch() function +require('./lib/ImagePrototypeInit'); +require('./lib/BatchPrototypeInit'); + module.exports = require('./lib/obtain'); diff --git a/lib/Batch.js b/lib/Batch.js index 26fac8f3..9b8b10e3 100644 --- a/lib/Batch.js +++ b/lib/Batch.js @@ -1,263 +1,15 @@ (function(undefined) { - var path = require('path'), - fs = require('fs'), - async = require('async'), - decree = require('decree'), - defs = require('./defs'), - util = require('./util'), - Image = require('./Image'); - - var judges = { - // slice(0,-1) cuts the callback declaration, which is not needed in - // batch mode. - scale: decree(defs.args.scale.slice(0, -1)), - resize: decree(defs.args.resize.slice(0, -1)), - rotate: decree(defs.args.rotate.slice(0, -1)), - blur: decree(defs.args.blur.slice(0, -1)), - hslaAdjust: decree(defs.args.hslaAdjust.slice(0, -1)), - saturate: decree(defs.args.saturate.slice(0, -1)), - lighten: decree(defs.args.lighten.slice(0, -1)), - darken: decree(defs.args.darken.slice(0, -1)), - fade: decree(defs.args.fade.slice(0, -1)), - opacify: decree(defs.args.opacify.slice(0, -1)), - hue: decree(defs.args.hue.slice(0, -1)), - crop: decree(defs.args.crop.slice(0, -1)), - mirror: decree(defs.args.mirror.slice(0, -1)), - pad: decree(defs.args.pad.slice(0, -1)), - border: decree(defs.args.border.slice(0, -1)), - sharpen: decree(defs.args.sharpen.slice(0, -1)), - paste: decree(defs.args.paste.slice(0, -1)), - clone: decree(defs.args.clone.slice(0, -1)), - extract: decree(defs.args.extract.slice(0, -1)), - exec: decree(defs.args.exec), - toBuffer: decree(defs.args.toBuffer), - writeFile: decree(defs.args.writeFile) - }; - - var undefinedFilter = util.undefinedFilter, - normalizeColor = util.normalizeColor; - - // Extend Image with image.batch() - Image.prototype.batch = function() { - return new Batch(this); - }; - function Batch(image) { this.__image = image; this.__queue = []; this.__running = false; - this.__addOp = function(handle, args) { - this.__queue.push({ - handle: handle, - args: args - }); - }; } - Batch.prototype.exec = function() { - var that = this; - judges.exec(arguments, function(callback) { - if (that.__running) throw Error("Batch is already running"); - that.__running = true; - async.eachSeries(that.__queue, function(op, done) { - op.args.push(done); - // if an exception is thrown here, it should be caught (because we - // are in the middle of async process) and translated to an 'err' - // parameter. - try { - op.handle.apply(that.__image, op.args); - } catch (e) { - done(e); - } - }, function(err) { - that.__queue.length = 0; // queue is now empty - that.__running = false; - callback(err, that.__image); - }); - }); - }; - - Batch.prototype.scale = function() { - var that = this; - judges.scale(arguments, function(wRatio, hRatio, inter) { - if (!defs.interpolations[inter]) throw Error("Unknown interpolation " + inter); - that.__addOp(that.__image.scale, [wRatio, hRatio, inter].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.resize = function() { - var that = this; - judges.resize(arguments, function(width, height, inter) { - if (!defs.interpolations[inter]) throw Error("Unknown interpolation " + inter); - that.__addOp(that.__image.resize, [width, height, inter].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.rotate = function() { - var that = this; - judges.rotate(arguments, function(degs, color) { - color = normalizeColor(color); - that.__addOp(that.__image.rotate, [degs, color].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.blur = function() { - var that = this; - judges.blur(arguments, function(sigma) { - that.__addOp(that.__image.blur, [sigma].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.hslaAdjust = function() { - var that = this; - judges.hslaAdjust(arguments, function(hs, sd, ld, ad) { - that.__addOp(that.__image.hslaAdjust, [hs, sd, ld, ad].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.saturate = function() { - var that = this; - judges.saturate(arguments, function(delta) { - that.__addOp(that.__image.saturate, [delta].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.lighten = function() { - var that = this; - judges.lighten(arguments, function(delta) { - that.__addOp(that.__image.lighten, [delta].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.darken = function() { - var that = this; - judges.darken(arguments, function(delta) { - that.__addOp(that.__image.darken, [delta].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.fade = function() { - var that = this; - judges.fade(arguments, function(delta) { - that.__addOp(that.__image.fade, [delta].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.opacify = function() { - this.__addOp(this.__image.opacify, []); - return this; - }; - - Batch.prototype.hue = function() { - var that = this; - judges.hue(arguments, function(shift) { - that.__addOp(that.__image.hue, [shift].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.crop = function() { - var that = this; - judges.crop(arguments, function(left, top, right, bottom) { - that.__addOp(that.__image.crop, [left, top, right, bottom].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.mirror = function() { - var that = this; - judges.mirror(arguments, function(axes) { - axes = axes.toLowerCase(); - if (['x', 'y', 'xy', 'yx'].indexOf(axes) === -1) throw Error('Invalid axes'); - that.__addOp(that.__image.mirror, [axes].filter(undefinedFilter)); - }); - return this; - }; - - // mirror alias: - Batch.prototype.flip = Batch.prototype.mirror; - - Batch.prototype.pad = function() { - var that = this; - judges.pad(arguments, function(left, top, right, bottom, color) { - color = normalizeColor(color); - that.__addOp(that.__image.pad, [left, top, right, bottom, color].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.border = function() { - var that = this; - judges.border(arguments, function(width, color) { - color = normalizeColor(color); - that.__addOp(that.__image.border, [width, color].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.sharpen = function() { - var that = this; - judges.sharpen(arguments, function(amplitude) { - that.__addOp(that.__image.sharpen, [amplitude].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.paste = function() { - var that = this; - judges.paste(arguments, function(left, top, img) { - if (!(img instanceof Image)) - throw Error("Pasted image is not a valid Image object"); - that.__addOp(that.__image.paste, [left, top, img].filter(undefinedFilter)); - }); - return this; - }; - - Batch.prototype.toBuffer = function() { - var that = this; - judges.toBuffer(arguments, function(type, params, callback) { - if (type === 'jpg' || type === 'jpeg') { - if (params.quality != 0) - params.quality = params.quality || defs.defaults.DEF_JPEG_QUALITY; - if (params.quality != parseInt(params.quality) || params.quality < 0 || params.quality > 100) - throw Error('Invalid JPEG quality'); - } else if (type === 'png') { - params.compression = params.compression || defs.defaults.PNG_DEF_COMPRESSION; - if (['none', 'fast', 'high'].indexOf(params.compression) === -1) - throw Error('Invalid PNG compression'); - params.interlaced = params.interlaced || defs.defaults.PNG_DEF_INTERLACED; - if (typeof params.interlaced !== 'boolean') throw Error('PNG \'interlaced\' must be boolean'); - params.transparency = params.transparency || defs.defaults.PNG_DEF_TRANSPARENT; - if (typeof params.transparency !== 'boolean' && params.transparency.toLowerCase() !== 'auto') - throw Error('PNG \'transparency\' must be boolean or \'auto\''); - } else throw Error('Unknown type \'' + type + '\''); - that.exec(function(err, image) { - if (err) return callback(err); - image.toBuffer(type, params, callback); - }); - }); - }; - - Batch.prototype.writeFile = function(outpath, type, params, callback) { - var that = this; - judges.writeFile(arguments, function(outpath, type, params, callback) { - type = type || path.extname(outpath).slice(1).toLowerCase(); - that.toBuffer(type, params, function(err, buffer) { - if (err) return callback(err); - fs.writeFile(outpath, buffer, { - encoding: 'binary' - }, callback); - }); + Batch.prototype.__addOp = function(handle, args) { + this.__queue.push({ + handle: handle, + args: args }); }; diff --git a/lib/BatchPrototypeInit.js b/lib/BatchPrototypeInit.js new file mode 100644 index 00000000..75ee6c49 --- /dev/null +++ b/lib/BatchPrototypeInit.js @@ -0,0 +1,266 @@ +(function(undefined) { + + var path = require('path'), + fs = require('fs'), + async = require('async'), + decree = require('decree'), + defs = require('./defs'), + util = require('./util'), + Batch = require('./Batch'), + Image = require('./Image'); + + var judges = { + // slice(0,-1) cuts the callback declaration, which is not needed in + // batch mode. + scale: decree(defs.args.scale.slice(0, -1)), + resize: decree(defs.args.resize.slice(0, -1)), + contain: decree(defs.args.contain.slice(0, -1)), + cover: decree(defs.args.cover.slice(0, -1)), + rotate: decree(defs.args.rotate.slice(0, -1)), + blur: decree(defs.args.blur.slice(0, -1)), + hslaAdjust: decree(defs.args.hslaAdjust.slice(0, -1)), + saturate: decree(defs.args.saturate.slice(0, -1)), + lighten: decree(defs.args.lighten.slice(0, -1)), + darken: decree(defs.args.darken.slice(0, -1)), + fade: decree(defs.args.fade.slice(0, -1)), + opacify: decree(defs.args.opacify.slice(0, -1)), + hue: decree(defs.args.hue.slice(0, -1)), + crop: decree(defs.args.crop.slice(0, -1)), + mirror: decree(defs.args.mirror.slice(0, -1)), + pad: decree(defs.args.pad.slice(0, -1)), + border: decree(defs.args.border.slice(0, -1)), + sharpen: decree(defs.args.sharpen.slice(0, -1)), + paste: decree(defs.args.paste.slice(0, -1)), + clone: decree(defs.args.clone.slice(0, -1)), + extract: decree(defs.args.extract.slice(0, -1)), + setPixel: decree(defs.args.setPixel.slice(0, -1)), + exec: decree(defs.args.exec), + toBuffer: decree(defs.args.toBuffer), + writeFile: decree(defs.args.writeFile) + }; + + var undefinedFilter = util.undefinedFilter, + normalizeColor = util.normalizeColor; + + // Extend Image with image.batch() + Image.prototype.batch = function() { + return new Batch(this); + }; + + Batch.prototype.exec = function() { + var that = this; + judges.exec(arguments, function(callback) { + if (that.__running) throw Error("Batch is already running"); + that.__running = true; + async.eachSeries(that.__queue, function(op, done) { + op.args.push(done); + // if an exception is thrown here, it should be caught (because we + // are in the middle of async process) and translated to an 'err' + // parameter. + try { + op.handle.apply(that.__image, op.args); + } catch (e) { + done(e); + } + }, function(err) { + that.__queue.length = 0; // queue is now empty + that.__running = false; + callback(err, that.__image); + }); + }); + }; + + Batch.prototype.scale = function() { + var that = this; + judges.scale(arguments, function(wRatio, hRatio, inter) { + that.__addOp(that.__image.scale, [wRatio, hRatio, inter].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.resize = function() { + var that = this; + judges.resize(arguments, function(width, height, inter) { + that.__addOp(that.__image.resize, [width, height, inter].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.contain = function() { + var that = this; + judges.contain(arguments, function(width, height, color, inter) { + that.__addOp(that.__image.contain, [width, height, color, inter].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.cover = function() { + var that = this; + judges.cover(arguments, function(width, height, inter) { + that.__addOp(that.__image.cover, [width, height, inter].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.rotate = function() { + var that = this; + judges.rotate(arguments, function(degs, color) { + color = normalizeColor(color); + that.__addOp(that.__image.rotate, [degs, color].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.blur = function() { + var that = this; + judges.blur(arguments, function(sigma) { + that.__addOp(that.__image.blur, [sigma].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.hslaAdjust = function() { + var that = this; + judges.hslaAdjust(arguments, function(hs, sd, ld, ad) { + that.__addOp(that.__image.hslaAdjust, [hs, sd, ld, ad].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.saturate = function() { + var that = this; + judges.saturate(arguments, function(delta) { + that.__addOp(that.__image.saturate, [delta].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.lighten = function() { + var that = this; + judges.lighten(arguments, function(delta) { + that.__addOp(that.__image.lighten, [delta].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.darken = function() { + var that = this; + judges.darken(arguments, function(delta) { + that.__addOp(that.__image.darken, [delta].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.fade = function() { + var that = this; + judges.fade(arguments, function(delta) { + that.__addOp(that.__image.fade, [delta].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.opacify = function() { + this.__addOp(this.__image.opacify, []); + return this; + }; + + Batch.prototype.hue = function() { + var that = this; + judges.hue(arguments, function(shift) { + that.__addOp(that.__image.hue, [shift].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.crop = function() { + var that = this; + judges.crop(arguments, function(left, top, right, bottom) { + that.__addOp(that.__image.crop, [left, top, right, bottom].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.mirror = function() { + var that = this; + judges.mirror(arguments, function(axes) { + that.__addOp(that.__image.mirror, [axes].filter(undefinedFilter)); + }); + return this; + }; + + // mirror alias: + Batch.prototype.flip = Batch.prototype.mirror; + + Batch.prototype.pad = function() { + var that = this; + judges.pad(arguments, function(left, top, right, bottom, color) { + color = normalizeColor(color); + that.__addOp(that.__image.pad, [left, top, right, bottom, color].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.border = function() { + var that = this; + judges.border(arguments, function(width, color) { + color = normalizeColor(color); + that.__addOp(that.__image.border, [width, color].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.sharpen = function() { + var that = this; + judges.sharpen(arguments, function(amplitude) { + that.__addOp(that.__image.sharpen, [amplitude].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.paste = function() { + var that = this; + judges.paste(arguments, function(left, top, img) { + that.__addOp(that.__image.paste, [left, top, img].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.setPixel = function() { + var that = this; + judges.setPixel(arguments, function(left, top, color) { + that.__addOp(that.__image.setPixel, [left, top, color].filter(undefinedFilter)); + }); + return this; + }; + + Batch.prototype.toBuffer = function() { + var that = this; + judges.toBuffer(arguments, function(type, params, callback) { + if (type === 'jpg' || type === 'jpeg') { + util.normalizeJpegParams(params); + } else if (type === 'png') { + util.normalizePngParams(params); + } else if (type === 'gif') { + util.normalizeGifParams(params); + } else throw Error('Unknown type \'' + type + '\''); + that.exec(function(err, image) { + if (err) return callback(err); + image.toBuffer(type, params, callback); + }); + }); + }; + + Batch.prototype.writeFile = function(outpath, type, params, callback) { + var that = this; + judges.writeFile(arguments, function(outpath, type, params, callback) { + type = type || path.extname(outpath).slice(1).toLowerCase(); + that.toBuffer(type, params, function(err, buffer) { + if (err) return callback(err); + fs.writeFile(outpath, buffer, { + encoding: 'binary' + }, callback); + }); + }); + }; + +})(void 0); diff --git a/lib/Image.js b/lib/Image.js index 980f78f8..74083061 100644 --- a/lib/Image.js +++ b/lib/Image.js @@ -1,37 +1,6 @@ (function(undefined) { - var path = require('path'), - fs = require('fs'), - decree = require('decree'), - defs = require('./defs'), - util = require('./util'), - encoder = require('../build/Release/lwip_encoder'), - lwip_image = require('../build/Release/lwip_image'), - normalizeColor = util.normalizeColor; - - var judges = { - scale: decree(defs.args.scale), - resize: decree(defs.args.resize), - rotate: decree(defs.args.rotate), - blur: decree(defs.args.blur), - hslaAdjust: decree(defs.args.hslaAdjust), - saturate: decree(defs.args.saturate), - lighten: decree(defs.args.lighten), - darken: decree(defs.args.darken), - fade: decree(defs.args.fade), - opacify: decree(defs.args.opacify), - hue: decree(defs.args.hue), - crop: decree(defs.args.crop), - mirror: decree(defs.args.mirror), - pad: decree(defs.args.pad), - border: decree(defs.args.border), - sharpen: decree(defs.args.sharpen), - paste: decree(defs.args.paste), - clone: decree(defs.args.clone), - extract: decree(defs.args.extract), - toBuffer: decree(defs.args.toBuffer), - writeFile: decree(defs.args.writeFile) - }; + var lwip_image = require('../build/Release/lwip_image'); function Image(pixelsBuf, width, height, trans) { this.__lwip = new lwip_image.LwipImage(pixelsBuf, width, height); @@ -39,503 +8,6 @@ this.__trans = trans; } - Image.prototype.__lock = function() { - if (!this.__locked) this.__locked = true; - else throw Error("Another image operation already in progress"); - }; - - Image.prototype.__release = function() { - this.__locked = false; - }; - - Image.prototype.width = function() { - return this.__lwip.width(); - }; - - Image.prototype.height = function() { - return this.__lwip.height(); - }; - - Image.prototype.size = function() { - return { - width: this.__lwip.width(), - height: this.__lwip.height() - }; - }; - - Image.prototype.scale = function() { - this.__lock(); - var that = this; - judges.scale( - arguments, - function(wRatio, hRatio, inter, callback) { - if (!defs.interpolations[inter]) throw Error("Unknown interpolation " + inter); - hRatio = hRatio || wRatio; - var width = +wRatio * that.width(), - height = +hRatio * that.height(); - that.__lwip.resize(width, height, defs.interpolations[inter], function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.resize = function() { - this.__lock(); - var that = this; - judges.resize( - arguments, - function(width, height, inter, callback) { - if (!defs.interpolations[inter]) throw Error("Unknown interpolation " + inter); - height = height || width; - that.__lwip.resize(+width, +height, defs.interpolations[inter], function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.rotate = function() { - this.__lock(); - var that = this; - judges.rotate( - arguments, - function(degs, color, callback) { - color = normalizeColor(color); - if (color.a < 100) that.__trans = true; - that.__lwip.rotate(+degs, +color.r, +color.g, +color.b, +color.a, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.blur = function() { - this.__lock(); - var that = this; - judges.blur( - arguments, - function(sigma, callback) { - that.__lwip.blur(+sigma, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.hslaAdjust = function() { - this.__lock(); - var that = this; - judges.hslaAdjust( - arguments, - function(hs, sd, ld, ad, callback) { - that.__lwip.hslaAdj(+hs, +sd, +ld, +ad, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.saturate = function() { - this.__lock(); - var that = this; - judges.saturate( - arguments, - function(delta, callback) { - that.__lwip.hslaAdj(0, +delta, 0, 0, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.lighten = function() { - this.__lock(); - var that = this; - judges.lighten( - arguments, - function(delta, callback) { - that.__lwip.hslaAdj(0, 0, +delta, 0, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.darken = function() { - this.__lock(); - var that = this; - judges.darken( - arguments, - function(delta, callback) { - that.__lwip.hslaAdj(0, 0, -delta, 0, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.fade = function() { - this.__lock(); - var that = this; - judges.fade( - arguments, - function(delta, callback) { - that.__lwip.hslaAdj(0, 0, 0, -delta, function(err) { - if (+delta > 0) that.__trans = true; - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.opacify = function() { - this.__lock(); - var that = this; - judges.opacify( - arguments, - function(callback) { - that.__lwip.opacify(function(err) { - that.__trans = false; - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.hue = function() { - this.__lock(); - var that = this; - judges.hue( - arguments, - function(shift, callback) { - that.__lwip.hslaAdj(+shift, 0, 0, 0, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.crop = function() { - this.__lock(); - var that = this; - judges.crop( - arguments, - function(left, top, right, bottom, callback) { - if (!right && !bottom) { - var size = that.size(), - width = left, - height = top; - left = 0 | (size.width - width) / 2; - top = 0 | (size.height - height) / 2; - right = left + width - 1; - bottom = top + height - 1; - } - that.__lwip.crop(left, top, right, bottom, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.mirror = function() { - this.__lock(); - var that = this; - judges.mirror( - arguments, - function(axes, callback) { - var xaxis = false, - yaxis = false; - axes = axes.toLowerCase(); - if ('x' === axes) xaxis = true; - if ('y' === axes) yaxis = true; - if ('xy' === axes || 'yx' === axes) { - xaxis = true; - yaxis = true; - } - if (!(xaxis || yaxis)) { - that.__release(); - throw Error('Invalid axes'); - } - that.__lwip.mirror(xaxis, yaxis, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - // mirror alias: - Image.prototype.flip = Image.prototype.mirror; - - Image.prototype.pad = function() { - this.__lock(); - var that = this; - judges.pad( - arguments, - function(left, top, right, bottom, color, callback) { - color = normalizeColor(color); - if (color.a < 100) that.__trans = true; - that.__lwip.pad(+left, +top, +right, +bottom, +color.r, +color.g, +color.b, +color.a, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.border = function() { - this.__lock(); - var that = this; - judges.border( - arguments, - function(width, color, callback) { - color = normalizeColor(color); - if (color.a < 100) that.__trans = true; - // we can just use image.pad... - that.__lwip.pad(+width, +width, +width, +width, +color.r, +color.g, +color.b, +color.a, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.sharpen = function() { - this.__lock(); - var that = this; - judges.sharpen( - arguments, - function(amplitude, callback) { - that.__lwip.sharpen(+amplitude, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.paste = function() { - this.__lock(); - var that = this; - judges.paste( - arguments, - function(left, top, img, callback) { - if (!(img instanceof Image)) - throw Error("Pasted image is not a valid Image object"); - // first we retrieve what we need (buffer, dimensions, ...) - // synchronously so that the pasted image doesn't have a chance - // to be changed - var pixbuff = img.__lwip.buffer(), - width = img.__lwip.width(), - height = img.__lwip.height(); - if (left + width > that.__lwip.width() || top + height > that.__lwip.height()) - throw Error("Pasted image exceeds dimensions of base image"); - that.__lwip.paste(+left, +top, pixbuff, +width, +height, function(err) { - that.__release(); - callback(err, that); - }); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.clone = function() { - // no need to lock the image. we don't modify the memory buffer. - // just copy it. - var that = this; - judges.clone( - arguments, - function(callback) { - // first we retrieve what we need (buffer, dimensions, ...) - // synchronously so that the original image doesn't have a chance - // to be changed (remember, we don't lock it); and only then call - // the callback asynchronously. - var pixbuff = that.__lwip.buffer(), - width = that.__lwip.width(), - height = that.__lwip.height(), - trans = that.__trans; - setImmediate(function() { - callback(null, new Image(pixbuff, width, height, trans)); - }); - } - ); - }; - - Image.prototype.extract = function() { - // no need to lock the image. we don't modify the memory buffer. - // just copy it and then crop it. - var that = this; - judges.extract( - arguments, - function(left, top, right, bottom, callback) { - // first we retrieve what we need (buffer, dimensions, ...) - // synchronously so that the original image doesn't have a chance - // to be changed (remember, we don't lock it); then we crop it and - // only call the callback asynchronously. - var pixbuff = that.__lwip.buffer(), - width = that.__lwip.width(), - height = that.__lwip.height(), - trans = that.__trans, - eximg = new Image(pixbuff, width, height, trans); - eximg.__lwip.crop(left, top, right, bottom, function(err) { - callback(err, eximg); - }); - } - ); - }; - - Image.prototype.toBuffer = function() { - this.__lock(); - var that = this; - judges.toBuffer( - arguments, - function(type, params, callback) { - if (type === 'jpg' || type === 'jpeg') { - if (params.quality != 0) - params.quality = params.quality || defs.defaults.DEF_JPEG_QUALITY; - if (params.quality != parseInt(params.quality) || params.quality < 0 || params.quality > 100) - throw Error('Invalid JPEG quality'); - return encoder.jpeg( - that.__lwip.buffer(), - that.__lwip.width(), - that.__lwip.height(), - params.quality, - function(err, buffer) { - that.__release(); - callback(err, buffer); - } - ); - } else if (type === 'png') { - params.compression = params.compression || defs.defaults.PNG_DEF_COMPRESSION; - if (params.compression === 'none') params.compression = 0; - else if (params.compression === 'fast') params.compression = 1; - else if (params.compression === 'high') params.compression = 2; - else throw Error('Invalid PNG compression'); - params.interlaced = params.interlaced || defs.defaults.PNG_DEF_INTERLACED; - if (typeof params.interlaced !== 'boolean') throw Error('PNG \'interlaced\' must be boolean'); - params.transparency = params.transparency || defs.defaults.PNG_DEF_TRANSPARENT; - if (typeof params.transparency !== 'boolean'){ - if (typeof params.transparency === 'string' && params.transparency.toLowerCase() === 'auto') - params.transparency = that.__trans; - else throw Error('PNG \'transparency\' must be boolean or \'auto\''); - } - return encoder.png( - that.__lwip.buffer(), - that.__lwip.width(), - that.__lwip.height(), - params.compression, - params.interlaced, - params.transparency, - function(err, buffer) { - that.__release(); - callback(err, buffer); - } - ); - } else throw Error('Unknown type \'' + type + '\''); - }, - function(err) { - that.__release(); - throw err; - } - ); - }; - - Image.prototype.writeFile = function() { - var that = this; - judges.writeFile( - arguments, - function(outpath, type, params, callback) { - type = type || path.extname(outpath).slice(1).toLowerCase(); - that.toBuffer(type, params, function(err, buffer) { - if (err) return callback(err); - fs.writeFile(outpath, buffer, { - encoding: 'binary' - }, callback); - }); - } - ); - }; - // EXPORTS // ------- module.exports = Image; diff --git a/lib/ImagePrototypeInit.js b/lib/ImagePrototypeInit.js new file mode 100644 index 00000000..73649249 --- /dev/null +++ b/lib/ImagePrototypeInit.js @@ -0,0 +1,606 @@ +(function(undefined) { + + var Image = require('./Image'), + path = require('path'), + fs = require('fs'), + decree = require('decree'), + defs = require('./defs'), + util = require('./util'), + encoder = require('../build/Release/lwip_encoder'), + lwip_image = require('../build/Release/lwip_image'), + normalizeColor = util.normalizeColor; + + var judges = { + scale: decree(defs.args.scale), + resize: decree(defs.args.resize), + contain: decree(defs.args.contain), + cover: decree(defs.args.cover), + rotate: decree(defs.args.rotate), + blur: decree(defs.args.blur), + hslaAdjust: decree(defs.args.hslaAdjust), + saturate: decree(defs.args.saturate), + lighten: decree(defs.args.lighten), + darken: decree(defs.args.darken), + fade: decree(defs.args.fade), + opacify: decree(defs.args.opacify), + hue: decree(defs.args.hue), + crop: decree(defs.args.crop), + mirror: decree(defs.args.mirror), + pad: decree(defs.args.pad), + border: decree(defs.args.border), + sharpen: decree(defs.args.sharpen), + paste: decree(defs.args.paste), + clone: decree(defs.args.clone), + extract: decree(defs.args.extract), + toBuffer: decree(defs.args.toBuffer), + writeFile: decree(defs.args.writeFile), + setPixel: decree(defs.args.setPixel), + getPixel: decree(defs.args.getPixel) + }; + + Image.prototype.__lock = function() { + if (!this.__locked) this.__locked = true; + else throw Error("Another image operation already in progress"); + }; + + Image.prototype.__release = function() { + this.__locked = false; + }; + + Image.prototype.width = function() { + return this.__lwip.width(); + }; + + Image.prototype.height = function() { + return this.__lwip.height(); + }; + + Image.prototype.size = function() { + return { + width: this.__lwip.width(), + height: this.__lwip.height() + }; + }; + + Image.prototype.getPixel = function() { + var args = judges.getPixel(arguments), + left = args[0], + top = args[1]; + + if (left >= this.width() || top >= this.height()) + throw Error("Coordinates exceed dimensions of image"); + + var rgba = this.__lwip.getPixel(left, top); + + return { + r: rgba[0], + g: rgba[1], + b: rgba[2], + a: rgba[3] + }; + }; + + Image.prototype.scale = function() { + this.__lock(); + var that = this; + judges.scale( + arguments, + function(wRatio, hRatio, inter, callback) { + hRatio = hRatio || wRatio; + var width = +wRatio * that.width(), + height = +hRatio * that.height(); + that.__lwip.resize(width, height, defs.interpolations[inter], function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.resize = function() { + this.__lock(); + var that = this; + judges.resize( + arguments, + function(width, height, inter, callback) { + height = height || width; + that.__lwip.resize(+width, +height, defs.interpolations[inter], function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.contain = function() { + var that = this; + judges.contain( + arguments, + function(width, height, color, inter, callback) { + var s = Math.min(width / that.width(), height / that.height()); + that.scale(s, s, inter, function(err){ + if (err) return callback(err); + var padX = (width - that.width()) / 2, + padY = (height - that.height()) / 2; + that.pad( + Math.ceil(padX), + Math.ceil(padY), + Math.floor(padX), + Math.floor(padY), + color, + callback + ); + }); + } + ); + }; + + Image.prototype.cover = function() { + var that = this; + judges.cover( + arguments, + function(width, height, inter, callback) { + var s = Math.max(width / that.width(), height / that.height()); + that.scale(s, s, inter, function(err){ + if (err) return callback(err); + that.crop(width, height, callback); + }); + } + ); + }; + + Image.prototype.rotate = function() { + this.__lock(); + var that = this; + judges.rotate( + arguments, + function(degs, color, callback) { + color = normalizeColor(color); + if (color.a < 100) that.__trans = true; + that.__lwip.rotate(+degs, +color.r, +color.g, +color.b, +color.a, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.blur = function() { + this.__lock(); + var that = this; + judges.blur( + arguments, + function(sigma, callback) { + that.__lwip.blur(+sigma, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.hslaAdjust = function() { + this.__lock(); + var that = this; + judges.hslaAdjust( + arguments, + function(hs, sd, ld, ad, callback) { + that.__lwip.hslaAdj(+hs, +sd, +ld, +ad, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.saturate = function() { + this.__lock(); + var that = this; + judges.saturate( + arguments, + function(delta, callback) { + that.__lwip.hslaAdj(0, +delta, 0, 0, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.lighten = function() { + this.__lock(); + var that = this; + judges.lighten( + arguments, + function(delta, callback) { + that.__lwip.hslaAdj(0, 0, +delta, 0, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.darken = function() { + this.__lock(); + var that = this; + judges.darken( + arguments, + function(delta, callback) { + that.__lwip.hslaAdj(0, 0, -delta, 0, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.fade = function() { + this.__lock(); + var that = this; + judges.fade( + arguments, + function(delta, callback) { + that.__lwip.hslaAdj(0, 0, 0, -delta, function(err) { + if (+delta > 0) that.__trans = true; + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.opacify = function() { + this.__lock(); + var that = this; + judges.opacify( + arguments, + function(callback) { + that.__lwip.opacify(function(err) { + that.__trans = false; + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.hue = function() { + this.__lock(); + var that = this; + judges.hue( + arguments, + function(shift, callback) { + that.__lwip.hslaAdj(+shift, 0, 0, 0, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.crop = function() { + this.__lock(); + var that = this; + judges.crop( + arguments, + function(left, top, right, bottom, callback) { + if (!right && !bottom) { + var size = that.size(), + width = left, + height = top; + left = 0 | (size.width - width) / 2; + top = 0 | (size.height - height) / 2; + right = left + width - 1; + bottom = top + height - 1; + } + that.__lwip.crop(left, top, right, bottom, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.mirror = function() { + this.__lock(); + var that = this; + judges.mirror( + arguments, + function(axes, callback) { + var xaxis = false, + yaxis = false; + if ('x' === axes) xaxis = true; + if ('y' === axes) yaxis = true; + if ('xy' === axes || 'yx' === axes) { + xaxis = true; + yaxis = true; + } + that.__lwip.mirror(xaxis, yaxis, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + // mirror alias: + Image.prototype.flip = Image.prototype.mirror; + + Image.prototype.pad = function() { + this.__lock(); + var that = this; + judges.pad( + arguments, + function(left, top, right, bottom, color, callback) { + color = normalizeColor(color); + if (color.a < 100) that.__trans = true; + that.__lwip.pad(+left, +top, +right, +bottom, +color.r, +color.g, +color.b, +color.a, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.border = function() { + this.__lock(); + var that = this; + judges.border( + arguments, + function(width, color, callback) { + color = normalizeColor(color); + if (color.a < 100) that.__trans = true; + // we can just use image.pad... + that.__lwip.pad(+width, +width, +width, +width, +color.r, +color.g, +color.b, +color.a, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.sharpen = function() { + this.__lock(); + var that = this; + judges.sharpen( + arguments, + function(amplitude, callback) { + that.__lwip.sharpen(+amplitude, function(err) { + that.__release(); + callback(err, that); + }); + }, + function(err) { + that.__release(); + throw err; + } + ); + }; + + Image.prototype.paste = function() { + this.__lock(); + var that = this; + try{ + judges.paste( + arguments, + function(left, top, img, callback) { + // first we retrieve what we need (buffer, dimensions, ...) + // synchronously so that the pasted image doesn't have a chance + // to be changed + var pixbuff = img.__lwip.buffer(), + width = img.__lwip.width(), + height = img.__lwip.height(); + if (left + width > that.__lwip.width() || top + height > that.__lwip.height()) + throw Error("Pasted image exceeds dimensions of base image"); + that.__lwip.paste(+left, +top, pixbuff, +width, +height, function(err) { + that.__release(); + callback(err, that); + }); + } + ); + } catch(err){ + that.__release(); + throw err; + } + }; + + Image.prototype.setPixel = function() { + this.__lock(); + var that = this; + try{ + judges.setPixel( + arguments, + function(left, top, color, callback) { + if (left >= that.width() || top >= that.height()) + throw Error("Coordinates exceed dimensions of image"); + color = normalizeColor(color); + if (color.a < 100) that.__trans = true; + that.__lwip.setPixel(+left, +top, +color.r, +color.g, +color.b, +color.a, function(err) { + that.__release(); + callback(err, that); + }); + } + ); + } catch(err){ + that.__release(); + throw err; + } + }; + + Image.prototype.clone = function() { + // no need to lock the image. we don't modify the memory buffer. + // just copy it. + var that = this; + judges.clone( + arguments, + function(callback) { + // first we retrieve what we need (buffer, dimensions, ...) + // synchronously so that the original image doesn't have a chance + // to be changed (remember, we don't lock it); and only then call + // the callback asynchronously. + var pixbuff = that.__lwip.buffer(), + width = that.__lwip.width(), + height = that.__lwip.height(), + trans = that.__trans; + setImmediate(function() { + callback(null, new Image(pixbuff, width, height, trans)); + }); + } + ); + }; + + Image.prototype.extract = function() { + // no need to lock the image. we don't modify the memory buffer. + // just copy it and then crop it. + var that = this; + judges.extract( + arguments, + function(left, top, right, bottom, callback) { + // first we retrieve what we need (buffer, dimensions, ...) + // synchronously so that the original image doesn't have a chance + // to be changed (remember, we don't lock it); then we crop it and + // only call the callback asynchronously. + var pixbuff = that.__lwip.buffer(), + width = that.__lwip.width(), + height = that.__lwip.height(), + trans = that.__trans, + eximg = new Image(pixbuff, width, height, trans); + eximg.__lwip.crop(left, top, right, bottom, function(err) { + callback(err, eximg); + }); + } + ); + }; + + Image.prototype.toBuffer = function() { + this.__lock(); + var that = this; + try{ + judges.toBuffer( + arguments, + function(type, params, callback) { + if (type === 'jpg' || type === 'jpeg') { + util.normalizeJpegParams(params); + return encoder.jpeg( + that.__lwip.buffer(), + that.__lwip.width(), + that.__lwip.height(), + params.quality, + encoderCb + ); + } else if (type === 'png') { + util.normalizePngParams(params); + return encoder.png( + that.__lwip.buffer(), + that.__lwip.width(), + that.__lwip.height(), + params.compression, + params.interlaced, + params.transparency === 'auto' ? that.__trans : params.transparency, + encoderCb + ); + } else if (type === 'gif') { + util.normalizeGifParams(params); + return encoder.gif( + that.__lwip.buffer(), + that.__lwip.width(), + that.__lwip.height(), + util.getClosest2Exp(params.colors), + params.colors, + params.interlaced, + params.transparency === 'auto' ? that.__trans : params.transparency, + params.threshold, + encoderCb + ); + } else throw Error('Unknown type \'' + type + '\''); + + function encoderCb(err, buffer) { + that.__release(); + callback(err, buffer); + } + } + ); + } catch (err){ + that.__release(); + throw err; + } + }; + + Image.prototype.writeFile = function() { + var that = this; + judges.writeFile( + arguments, + function(outpath, type, params, callback) { + type = type || path.extname(outpath).slice(1).toLowerCase(); + that.toBuffer(type, params, function(err, buffer) { + if (err) return callback(err); + fs.writeFile(outpath, buffer, { + encoding: 'binary' + }, callback); + }); + } + ); + }; + +})(void 0); diff --git a/lib/defs.js b/lib/defs.js index ca820f07..47176214 100644 --- a/lib/defs.js +++ b/lib/defs.js @@ -10,10 +10,15 @@ PNG_DEF_COMPRESSION: 'fast', PNG_DEF_INTERLACED: false, PNG_DEF_TRANSPARENT: 'auto', - DEF_CREATE_COLOR: [0, 0, 0, 0] + GIF_DEF_COLORS: 256, + GIF_DEF_INTERLACED: false, + GIF_DEF_TRANSPARENT: 'auto', + GIF_DEF_THRESHOLD: 50, + DEF_CREATE_COLOR: [0, 0, 0, 0], + DEF_CONTAIN_COLOR: [0, 0, 0, 0] }; - exports.interpolations = { + var interpolations = exports.interpolations = { 'nearest-neighbor': 1, 'moving-average': 2, 'linear': 3, @@ -85,7 +90,7 @@ type: '*' }, { name: 'type', - type: 'string', + types: ['string', 'raw-buffer-properties'], optional: true }, { name: 'callback', @@ -99,7 +104,7 @@ type: 'p-number' }, { name: 'color', - types: ['string', 'array', 'hash'], + type: 'color', optional: true, default: defaults.DEF_CREATE_COLOR }, { @@ -114,8 +119,8 @@ type: 'p-number', optional: true }, { - name: 'inter', - type: 'string', + name: 'interpolation', + type: 'interpolation', optional: true, default: defaults.DEF_INTERPOLATION }, { @@ -130,8 +135,8 @@ type: 'p-number', optional: true }, { - name: 'inter', - type: 'string', + name: 'interpolation', + type: 'interpolation', optional: true, default: defaults.DEF_INTERPOLATION }, { @@ -143,7 +148,7 @@ type: 'number' }, { name: 'color', - types: ['string', 'array', 'hash'], + type: 'color', optional: true, default: defaults.DEF_ROTATE_COLOR }, { @@ -232,7 +237,7 @@ }], mirror: [{ name: 'axes', - type: 'string' + type: 'axes' }, { name: 'callback', type: 'function' @@ -253,12 +258,19 @@ name: 'callback', type: 'function' }], + getPixel: [{ + name: 'left', + type: 'nn-int' + }, { + name: 'top', + type: 'nn-int' + }], border: [{ name: 'width', type: 'nn-number' }, { name: 'color', - types: ['string', 'array', 'hash'], + type: 'color', optional: true, default: defaults.DEF_BORDER_COLOR }, { @@ -279,7 +291,7 @@ type: 'nn-number' }, { name: 'color', - types: ['string', 'array', 'hash'], + type: 'color', optional: true, default: defaults.DEF_PAD_COLOR }, { @@ -305,7 +317,7 @@ type: 'nn-number' }, { name: 'image', - type: '*' + type: 'image' }, { name: 'callback', type: 'function' @@ -341,7 +353,55 @@ }, { name: 'callback', type: 'function' - }] - } + }], + setPixel: [{ + name: 'left', + type: 'nn-int' + }, { + name: 'top', + type: 'nn-int' + }, { + name: 'color', + type: 'color' + }, { + name: 'callback', + type: 'function' + }], + contain: [{ + name: 'width', + type: 'p-number' + }, { + name: 'height', + type: 'p-number' + }, { + name: 'color', + type: 'color', + optional: true, + default: defaults.DEF_CONTAIN_COLOR + }, { + name: 'interpolation', + type: 'interpolation', + optional: true, + default: defaults.DEF_INTERPOLATION + }, { + name: 'callback', + type: 'function' + }], + cover: [{ + name: 'width', + type: 'p-number' + }, { + name: 'height', + type: 'p-number' + }, { + name: 'interpolation', + type: 'interpolation', + optional: true, + default: defaults.DEF_INTERPOLATION + }, { + name: 'callback', + type: 'function' + }], + }; })(); diff --git a/lib/obtain.js b/lib/obtain.js index 476fb168..23999c9c 100644 --- a/lib/obtain.js +++ b/lib/obtain.js @@ -14,13 +14,18 @@ }, { exts: ['png'], opener: decoder.png + }, { + exts: ['gif'], + opener: decoder.gif }]; function open() { decree(defs.args.open)(arguments, function(source, type, callback) { + var opener; if (typeof source === 'string') { type = type || path.extname(source).slice(1); - var opener = getOpener(type); + if (typeof type !== 'string') throw Error('Invalid type'); + opener = getOpener(type); fs.readFile(source, function(err, imbuff) { if (err) return callback(err); opener(imbuff, function(err, pixelsBuf, width, height, channels, trans) { @@ -28,10 +33,21 @@ }); }); } else if (source instanceof Buffer) { - var opener = getOpener(type); - opener(source, function(err, pixelsBuf, width, height, channels, trans) { - callback(err, err ? null : new Image(pixelsBuf, width, height, trans)); - }); + if(typeof type === 'object') { + // it's a raw pixels buffer + var channelSize = type.width * type.height; + var numChannels = source.length / channelSize; + if (numChannels !== parseInt(numChannels) || numChannels < 1 || numChannels > 4) { + throw Error("Buffer size does not match width and height"); + } + callback(null, new Image(source, type.width, type.height, (numChannels % 2 === 0))); + } else if (typeof type === 'string') { + // it's an encoded image + opener = getOpener(type); + opener(source, function(err, pixelsBuf, width, height, channels, trans) { + callback(err, err ? null : new Image(pixelsBuf, width, height, trans)); + }); + } else throw Error('Invalid type'); } else throw Error("Invalid source"); }); } diff --git a/lib/util.js b/lib/util.js index bc357ae6..45239f8d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,15 +1,34 @@ (function(undefined) { - var defs = require('./defs'); + var defs = require('./defs'), + decree = require('decree'), + Image = require('./Image'); + + decree.register('color', validateColor); + decree.register('interpolation', validateInterpolation); + decree.register('axes', validateAxes); + decree.register('image', validateImage); + decree.register('raw-buffer-properties', validateRawBufferProperties); function undefinedFilter(v) { return v !== undefined; } - function normalizeColor(color) { + function validateInterpolation(inter){ + return defs.interpolations.hasOwnProperty(inter); + } + + function validateAxes(axes){ + return ['x', 'y', 'xy', 'yx'].indexOf(axes) !== -1; + } + + function validateImage(img){ + return img instanceof Image; + } + + function validateColor(color) { if (typeof color === 'string') { - if (defs.colors[color]) color = defs.colors[color]; - else throw Error('Unknown color ' + color); + if (!defs.colors[color]) return false; } else { if (color instanceof Array) { color = { @@ -19,22 +38,109 @@ a: color[3] }; } - if (color.a !== 0) color.a = color.a || defs.defaults.DEF_COLOR_ALPHA; - if (color.r != parseInt(color.r) || color.r < 0 || color.r > 255) - throw Error('\'red\' color component is invalid'); - if (color.g != parseInt(color.g) || color.g < 0 || color.g > 255) - throw Error('\'green\' color component is invalid'); - if (color.b != parseInt(color.b) || color.b < 0 || color.b > 255) - throw Error('\'blue\' color component is invalid'); - if (color.a != parseInt(color.a) || color.a < 0 || color.a > 100) - throw Error('\'alpha\' color component is invalid'); + var a = color.a; + if (a !== 0) a = a || defs.defaults.DEF_COLOR_ALPHA; // (don't modify the original color object) + if (color.r != parseInt(color.r) || color.r < 0 || color.r > 255 || + color.g != parseInt(color.g) || color.g < 0 || color.g > 255 || + color.b != parseInt(color.b) || color.b < 0 || color.b > 255 || + a != parseInt(a) || a < 0 || a > 100) + return false; } + return true; + } + + function validateRawBufferProperties(p) { + if (p.width !== parseInt(p.width) || p.width <= 0 || + p.height !== parseInt(p.height) || p.height <= 0) + return false; + return true; + } + + function normalizeColor(color) { + if (typeof color === 'string') { + color = defs.colors[color]; + } else if (color instanceof Array) { + color = { + r: color[0], + g: color[1], + b: color[2], + a: color[3] + }; + } + if (color.a !== 0) color.a = color.a || defs.defaults.DEF_COLOR_ALPHA; return color; } + function normalizePngParams(params){ + params.compression = params.compression || defs.defaults.PNG_DEF_COMPRESSION; + if (params.compression === 'none') params.compression = 0; + else if (params.compression === 'fast') params.compression = 1; + else if (params.compression === 'high') params.compression = 2; + else if ([0, 1, 2].indexOf(params.compression) === -1) + throw Error('Invalid PNG compression'); + params.interlaced = params.interlaced || defs.defaults.PNG_DEF_INTERLACED; + if (typeof params.interlaced !== 'boolean') + throw Error('PNG \'interlaced\' must be boolean'); + if (params.transparency !== false) + params.transparency = params.transparency || defs.defaults.PNG_DEF_TRANSPARENT; + if (typeof params.transparency !== 'boolean'){ + if (typeof params.transparency !== 'string' || + params.transparency.toLowerCase() !== 'auto') + throw Error('PNG \'transparency\' must be boolean or \'auto\''); + } + } + + function normalizeGifParams(params){ + params.colors = params.colors || defs.defaults.GIF_DEF_COLORS; + if (params.colors != parseInt(params.colors) || params.colors < 2 || params.colors > 256) + throw Error('Invalid GIF color table size'); + params.interlaced = params.interlaced || defs.defaults.GIF_DEF_INTERLACED; + if (typeof params.interlaced !== 'boolean') + throw Error('GIF \'interlaced\' must be boolean'); + if (params.transparency !== false) + params.transparency = params.transparency || defs.defaults.GIF_DEF_TRANSPARENT; + if (typeof params.transparency !== 'boolean'){ + if (typeof params.transparency !== 'string' || + params.transparency.toLowerCase() !== 'auto') + throw Error('GIF \'transparency\' must be boolean or \'auto\''); + } + if (params.threshold !== 0) params.threshold = params.threshold || defs.defaults.GIF_DEF_THRESHOLD; + if (params.threshold != parseInt(params.threshold) || params.threshold < 0 || params.threshold > 100) + throw Error('Invalid GIF transparency threshold'); + } + + function normalizeJpegParams(params){ + if (params.quality !== 0) + params.quality = params.quality || defs.defaults.DEF_JPEG_QUALITY; + if (params.quality != parseInt(params.quality) || + params.quality < 0 || params.quality > 100) + throw Error('Invalid JPEG quality'); + } + + function getClosest2Exp(num){ + // num is assumed to be between 2 and 256 + // returned the closes 2 exponent which is **larger** than num + var e, n, min, r; + for (e = 1 ; e <= 8; e++){ + n = 1 << e; + if (num === n) return n; + if (!min) { min = num - n; r = e; } + else if (num - n < min){ + min = num - n; + r = e; + if (min < 0) return 1 << r; + } + } + return 256; + } + module.exports = { undefinedFilter: undefinedFilter, - normalizeColor: normalizeColor + normalizeColor: normalizeColor, + normalizePngParams: normalizePngParams, + normalizeGifParams: normalizeGifParams, + normalizeJpegParams: normalizeJpegParams, + getClosest2Exp: getClosest2Exp }; })(void 0); diff --git a/package.json b/package.json index ffc6ace4..6ffe6857 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.0.5", "main": "index.js", "dependencies": { - "async": "^0.9.0", + "async": "~0.9.0", "decree": "0.0.6", - "nan": "^1.3.0" + "nan": "~1.4.1" }, "scripts": { "install": "node-gyp rebuild", @@ -20,12 +20,12 @@ "example": "examples" }, "devDependencies": { - "coveralls": "^2.11.1", - "istanbul": "^0.3.2", + "coveralls": "^2.11.2", + "istanbul": "^0.3.5", "mkdirp": "^0.5.0", - "mocha": "^1.20.1", + "mocha": "^2.1.0", "mocha-lcov-reporter": "0.0.1", - "should": "^4.0.4" + "should": "^4.4.1" }, "repository": { "type": "git", @@ -42,6 +42,7 @@ "jpeg", "jpg", "png", + "gif", "crop", "blur", "sharpen", @@ -56,7 +57,9 @@ "alpha", "transparency", "fade", - "opacity" + "opacity", + "contain", + "cover" ], "author": "Eyal Arubas ", "license": "MIT", diff --git a/src/decoder/decoder.h b/src/decoder/decoder.h index 983e6bc6..3467765b 100644 --- a/src/decoder/decoder.h +++ b/src/decoder/decoder.h @@ -17,6 +17,7 @@ extern "C" { } #include #include +#include #include "CImg.h" using namespace cimg_library; @@ -53,6 +54,12 @@ typedef struct { size_t read; } pngReadCbData; +typedef struct { + unsigned char * src; + size_t size; + size_t read; +} gifReadCbData; + struct lwip_jpeg_error_mgr { struct jpeg_error_mgr pub; jmp_buf setjmp_buffer; @@ -72,10 +79,14 @@ string toRGBA(CImg ** img); string decode_jpeg_buffer(char * buffer, size_t size, CImg ** img); string decode_png_buffer(char * buffer, size_t size, CImg ** img); +string decode_gif_buffer(char * buffer, size_t size, CImg ** img); + void pngReadCB(png_structp png_ptr, png_bytep data, png_size_t length); +int gifReadCB(GifFileType * gif, GifByteType * buf, int length); NAN_METHOD(decodeJpegBuffer); NAN_METHOD(decodePngBuffer); +NAN_METHOD(decodeGifBuffer); void initAll(Handle exports); #endif diff --git a/src/decoder/gif_decoder.cpp b/src/decoder/gif_decoder.cpp new file mode 100644 index 00000000..e6a14ff8 --- /dev/null +++ b/src/decoder/gif_decoder.cpp @@ -0,0 +1,97 @@ +#include "decoder.h" + +#define ALPHA_TRANS 0 +#define ALPHA_OPAQUE 255 +#define C_TRANS 0 + +string decode_gif_buffer(char * buffer, size_t size, CImg ** cimg) { + + gifReadCbData buffinf = {(unsigned char *) buffer, size, 0}; + GifFileType * gif = NULL; + int errcode = 0; + + // buffinf will be available in gifReadCB as gif->userData + gif = DGifOpen((void *) &buffinf, gifReadCB, &errcode); + + if (NULL == gif) + return GifErrorString(errcode); + + if (GIF_ERROR == DGifSlurp(gif)) + return GifErrorString(gif->Error); + + GraphicsControlBlock gcb; + + // only for the first image + bool hasGCB = DGifSavedExtensionToGCB(gif, 0, &gcb) != GIF_ERROR; + // if may return GIF_ERROR, which means this image has no gcb. + // that's fine, as gcb's are optional + + SavedImage * gifimg = &gif->SavedImages[0]; + size_t width = (size_t) gifimg->ImageDesc.Width; + size_t height = (size_t) gifimg->ImageDesc.Height; + GifByteType * ipxls = gifimg->RasterBits; // pixels (indexed) + ColorMapObject * cmap = NULL != gifimg->ImageDesc.ColorMap + ? gifimg->ImageDesc.ColorMap + : gif->SColorMap; + + // allocate a CImg with the correct dimensions as the image + *cimg = new CImg(); + (*cimg)->assign(width, height, 1, 4); + + // pointers to RGBA sections in CImg + unsigned char *ptr_r = (*cimg)->data(0, 0, 0, 0), + *ptr_g = (*cimg)->data(0, 0, 0, 1), + *ptr_b = (*cimg)->data(0, 0, 0, 2), + *ptr_a = (*cimg)->data(0, 0, 0, 3); + + size_t i = 0, len = width * height; + GifByteType ci; + GifColorType c; + GifColorType c_trans = {C_TRANS, C_TRANS, C_TRANS}; + unsigned char alpha; + + for (; i < len; i++){ + ci = ipxls[i]; + if (hasGCB && ci == gcb.TransparentColor){ + c = c_trans; + alpha = ALPHA_TRANS; + } else { + c = cmap->Colors[ci]; + alpha = ALPHA_OPAQUE; + } + *(ptr_r++) = c.Red; + *(ptr_g++) = c.Green; + *(ptr_b++) = c.Blue; + *(ptr_a++) = alpha; + } + + if (GIF_ERROR == DGifCloseFile(gif, &errcode)){ + delete *cimg; + return GifErrorString(errcode); + } + + return ""; +} + +// 'gif' gives us access to our user data, which has a pointer to our +// buffinf struct. +// 'buf' is the buffer into which we need to copy the data +// 'length' is how many bytes we need to copy +// we should return how many bytes were actually copied +int gifReadCB(GifFileType * gif, GifByteType * buf, int length) { + gifReadCbData * buffinf = (gifReadCbData *) gif->UserData; + + // need to read 'length' bytes from the source buffer and copy them to buf. + + if (buffinf->read + length > buffinf->size) { + // no more bytes in source + memcpy(buf, buffinf->src + buffinf->read, buffinf->size - buffinf->read); + buffinf->read = buffinf->read; + return buffinf->size - buffinf->read; + } + + memcpy(buf, buffinf->src + buffinf->read, length); + buffinf->read += length; + + return length; +} diff --git a/src/decoder/init.cpp b/src/decoder/init.cpp index e85e8103..65ceae56 100644 --- a/src/decoder/init.cpp +++ b/src/decoder/init.cpp @@ -20,12 +20,24 @@ NAN_METHOD(decodePngBuffer) { NanReturnUndefined(); } +NAN_METHOD(decodeGifBuffer) { + NanScope(); + + Local gifBuff = args[0].As(); + NanCallback * callback = new NanCallback(args[1].As()); + + NanAsyncQueueWorker(new DecodeBufferWorker(callback, gifBuff, decode_gif_buffer)); + NanReturnUndefined(); +} + // create an init function for our node module void InitAll(Handle exports) { exports->Set(NanNew("jpeg"), NanNew(decodeJpegBuffer)->GetFunction()); exports->Set(NanNew("png"), NanNew(decodePngBuffer)->GetFunction()); + exports->Set(NanNew("gif"), + NanNew(decodeGifBuffer)->GetFunction()); } // use NODE_MODULE macro to register our module: diff --git a/src/encoder/encoder.h b/src/encoder/encoder.h index 29cd6ce6..a68468dc 100644 --- a/src/encoder/encoder.h +++ b/src/encoder/encoder.h @@ -15,6 +15,7 @@ extern "C" { } #include #include +#include #include "CImg.h" using namespace cimg_library; @@ -68,11 +69,45 @@ class EncodeToPngBufferWorker : public NanAsyncWorker { size_t _pngbufsize; }; +class EncodeToGifBufferWorker : public NanAsyncWorker { +public: + EncodeToGifBufferWorker( + Local & buff, + size_t width, + size_t height, + int cmapSize, + int colors, + bool interlaced, + bool trans, + int threshold, + NanCallback * callback + ); + ~EncodeToGifBufferWorker(); + void Execute (); + void HandleOKCallback (); +private: + unsigned char * _pixbuf; + size_t _width; + size_t _height; + int _cmapSize; + int _colors; + bool _interlaced; + bool _trans; + int _threshold; + char * _gifbuf; + size_t _gifbufsize; +}; + typedef struct { unsigned char * buff; size_t buffsize; } pngWriteCbData; +typedef struct { + unsigned char * buff; + size_t buffsize; +} gifWriteCbData; + struct lwip_jpeg_error_mgr { struct jpeg_error_mgr pub; jmp_buf setjmp_buffer; @@ -85,7 +120,10 @@ inline void lwip_jpeg_error_exit (j_common_ptr cinfo) { NAN_METHOD(encodeToJpegBuffer); NAN_METHOD(encodeToPngBuffer); +NAN_METHOD(encodeToGifBuffer); void pngWriteCB(png_structp png_ptr, png_bytep data, png_size_t length); +int gifWriteCB(GifFileType * gif, const GifByteType * chunk, int len); +void remapTransparentPixels(unsigned char * target, const unsigned char * map, size_t width, size_t height, int transColor, int threshold); void initAll(Handle exports); #endif diff --git a/src/encoder/gif_worker.cpp b/src/encoder/gif_worker.cpp new file mode 100644 index 00000000..d47e7752 --- /dev/null +++ b/src/encoder/gif_worker.cpp @@ -0,0 +1,173 @@ +#include "encoder.h" + +#define RGB_N_CHANNELS 3 +#define RGBA_N_CHANNELS 4 + +EncodeToGifBufferWorker::EncodeToGifBufferWorker( + Local & buff, + size_t width, + size_t height, + int cmapSize, + int colors, + bool interlaced, + bool trans, + int threshold, + NanCallback * callback +): NanAsyncWorker(callback), _width(width), _height(height), + _cmapSize(cmapSize), _colors(colors), _interlaced(interlaced), _trans(trans), + _threshold(threshold), _gifbuf(NULL), _gifbufsize(0) { + SaveToPersistent("buff", buff); // make sure buff isn't GC'ed + _pixbuf = (unsigned char *) Buffer::Data(buff); + if (_trans){ + // make room in the color table for a transparent color + if (_cmapSize == 256){ + if (_colors == 256) _colors--; + } else if (_cmapSize == _colors){ + _cmapSize *= 2; + } + } +} + +EncodeToGifBufferWorker::~EncodeToGifBufferWorker() {} + +void EncodeToGifBufferWorker::Execute () { + GifByteType + * redBuff = (GifByteType *) _pixbuf, + * greenBuff = (GifByteType *) _pixbuf + _width * _height, + * blueBuff = (GifByteType *) _pixbuf + 2 * _width * _height, + * alphaBuff = (GifByteType *) _pixbuf + 3 * _width * _height, + * gifimgbuf = (GifByteType *) malloc(_width * _height * sizeof(GifByteType)); // the indexed image + ColorMapObject *cmap; + SavedImage * simg; + + if (NULL == gifimgbuf){ + SetErrorMessage("Out of memory"); + return; + } + + cmap = GifMakeMapObject(_cmapSize, NULL); + + if (NULL == cmap){ + free(gifimgbuf); + SetErrorMessage("Out of memory"); + return; + } + + if (GIF_ERROR == GifQuantizeBuffer( + _width, _height, &_colors, + redBuff, greenBuff, blueBuff, + gifimgbuf, cmap->Colors + )){ + free(gifimgbuf); + GifFreeMapObject(cmap); + SetErrorMessage("Unable to quantize image"); + return; + } + + int errcode; + gifWriteCbData buffinf = {NULL, 0}; + GifFileType * gif; + + gif = EGifOpen((void *) &buffinf, gifWriteCB, &errcode); + + if (NULL == gif){ + free(gifimgbuf); + GifFreeMapObject(cmap); + SetErrorMessage(GifErrorString(errcode)); + return; + } + + gif->SWidth = _width; + gif->SHeight = _height; + gif->SColorResolution = _cmapSize; + + simg = GifMakeSavedImage(gif, NULL); + + if (NULL == simg){ + free(gifimgbuf); + EGifCloseFile(gif, &errcode); // will also free cmap + SetErrorMessage("Out of memory"); + return; + } + + simg->ImageDesc.Left = 0; + simg->ImageDesc.Top = 0; + simg->ImageDesc.Width = _width; + simg->ImageDesc.Height = _height; + simg->ImageDesc.Interlace = _interlaced; + simg->ImageDesc.ColorMap = cmap; + simg->RasterBits = gifimgbuf; + + // for some reason giflib sometimes creates an invalid file if the global + // color table is not set as well + gif->SColorMap = cmap; + + if (_trans){ + ExtensionBlock ext; + // 1. assign transparent color index in color table + GraphicsControlBlock gcb = {0, false, 0, _colors++}; + // 2. replace transparent pixels above threshold with this color + remapTransparentPixels(gifimgbuf, alphaBuff, _width, _height, gcb.TransparentColor, _threshold); + // 3. create a control block + size_t extlen = EGifGCBToExtension(&gcb, (GifByteType *) &ext); + if (GIF_ERROR == GifAddExtensionBlock( + &(simg->ExtensionBlockCount), + &(simg->ExtensionBlocks), + GRAPHICS_EXT_FUNC_CODE, + extlen, + (unsigned char *) &ext) + ) { + EGifCloseFile(gif, &errcode); + SetErrorMessage("Out of memory"); + return; + } + } + + + if (GIF_ERROR == EGifSpew(gif)){ + EGifCloseFile(gif, &errcode); + SetErrorMessage(GifErrorString(gif->Error)); + return; + } + + _gifbuf = (char *) buffinf.buff; + _gifbufsize = buffinf.buffsize; + + return; +} + +void EncodeToGifBufferWorker::HandleOKCallback () { + NanScope(); + Local argv[] = { + NanNull(), + NanBufferUse( + _gifbuf, + _gifbufsize + ) + }; + callback->Call(2, argv); +} + +int gifWriteCB(GifFileType * gif, const GifByteType * chunk, int len) { + gifWriteCbData * buffinf = (gifWriteCbData *) gif->UserData; + size_t size = buffinf->buffsize + len; + + if (NULL != buffinf->buff) + buffinf->buff = (unsigned char *) realloc(buffinf->buff, size * sizeof(GifByteType)); + else + buffinf->buff = (unsigned char *) malloc(size * sizeof(GifByteType)); + + if (!buffinf->buff) return 0; + + memcpy(buffinf->buff + buffinf->buffsize, chunk, len * sizeof(GifByteType)); + buffinf->buffsize += len; + + return len; +} + +void remapTransparentPixels(unsigned char * target, const unsigned char * map, size_t width, size_t height, int transColor, int threshold){ + size_t i = 0, len = width * height; + for (; i < len; i++){ + if (map[i] < threshold) *(target + i) = transColor; + } +} diff --git a/src/encoder/init.cpp b/src/encoder/init.cpp index 760b6b3a..5aad47e9 100644 --- a/src/encoder/init.cpp +++ b/src/encoder/init.cpp @@ -47,12 +47,44 @@ NAN_METHOD(encodeToPngBuffer) { NanReturnUndefined(); } +// encoder.gif(pixbuf, width, height, cmapSize, colors, interlaced, trans, threshold, callback) +NAN_METHOD(encodeToGifBuffer) { + NanScope(); + + Local buff = args[0].As(); + size_t width = args[1].As()->Value(); + size_t height = args[2].As()->Value(); + int cmapSize = args[3].As()->Value(); + int colors = args[4].As()->Value(); + bool interlaced = args[5]->BooleanValue(); + bool trans = args[6]->BooleanValue(); + int threshold = args[7].As()->Value(); + NanCallback * callback = new NanCallback(args[8].As()); + + NanAsyncQueueWorker( + new EncodeToGifBufferWorker( + buff, + width, + height, + cmapSize, + colors, + interlaced, + trans, + threshold, + callback + ) + ); + NanReturnUndefined(); +} + // create an init function for our node module void InitAll(Handle exports) { exports->Set(NanNew("jpeg"), NanNew(encodeToJpegBuffer)->GetFunction()); exports->Set(NanNew("png"), NanNew(encodeToPngBuffer)->GetFunction()); + exports->Set(NanNew("gif"), + NanNew(encodeToGifBuffer)->GetFunction()); } // use NODE_MODULE macro to register our module: diff --git a/src/image/image.cpp b/src/image/image.cpp index d06cacd4..2a614ae3 100644 --- a/src/image/image.cpp +++ b/src/image/image.cpp @@ -9,6 +9,7 @@ void LwipImage::Init(Handle exports) { tpl->SetClassName(NanNew("LwipImage")); NODE_SET_PROTOTYPE_METHOD(tpl, "width", width); NODE_SET_PROTOTYPE_METHOD(tpl, "height", height); + NODE_SET_PROTOTYPE_METHOD(tpl, "getPixel", getPixel); NODE_SET_PROTOTYPE_METHOD(tpl, "buffer", buffer); NODE_SET_PROTOTYPE_METHOD(tpl, "resize", resize); NODE_SET_PROTOTYPE_METHOD(tpl, "rotate", rotate); @@ -20,6 +21,7 @@ void LwipImage::Init(Handle exports) { NODE_SET_PROTOTYPE_METHOD(tpl, "hslaAdj", hslaAdj); NODE_SET_PROTOTYPE_METHOD(tpl, "opacify", opacify); NODE_SET_PROTOTYPE_METHOD(tpl, "paste", paste); + NODE_SET_PROTOTYPE_METHOD(tpl, "setPixel", setPixel); NanAssignPersistent(constructor, tpl); exports->Set( NanNew("LwipImage"), @@ -76,6 +78,21 @@ NAN_METHOD(LwipImage::height) { NanReturnValue(NanNew(obj->_cimg->height())); } +// image.getPixel(left, top): +// --------------- +NAN_METHOD(LwipImage::getPixel) { + NanScope(); + size_t left = (size_t) args[0].As()->Value(); + size_t top = (size_t) args[1].As()->Value(); + LwipImage * obj = ObjectWrap::Unwrap(args.Holder()); + Local rgba = NanNew(4); + rgba->Set(0, NanNew((*(obj->_cimg))(left, top, 0, 0))); // red + rgba->Set(1, NanNew((*(obj->_cimg))(left, top, 0, 1))); // green + rgba->Set(2, NanNew((*(obj->_cimg))(left, top, 0, 2))); // blue + rgba->Set(3, NanNew((*(obj->_cimg))(left, top, 0, 3))); // alpha + NanReturnValue(rgba); +} + // image.buffer(): // --------------- NAN_METHOD(LwipImage::buffer) { @@ -384,3 +401,41 @@ NAN_METHOD(LwipImage::paste) { NanReturnUndefined(); } + +// image.setPixel(left, top, color, callback): +// ------------------------------------------- + +// args[0] - left +// args[1] - top +// args[2] - red +// args[3] - green +// args[4] - blue +// args[5] - alpha +// args[6] - callback +NAN_METHOD(LwipImage::setPixel) { + NanScope(); + + size_t left = (size_t) args[0].As()->Value(); + size_t top = (size_t) args[1].As()->Value(); + unsigned char r = (unsigned char) args[2].As()->Value(); + unsigned char g = (unsigned char) args[3].As()->Value(); + unsigned char b = (unsigned char) args[4].As()->Value(); + unsigned char a = (unsigned char) args[5].As()->Value(); + NanCallback * callback = new NanCallback(args[6].As()); + CImg * cimg = ObjectWrap::Unwrap(args.This())->_cimg; + + NanAsyncQueueWorker( + new SetPixelWorker( + left, + top, + r, + g, + b, + a, + cimg, + callback + ) + ); + + NanReturnUndefined(); +} diff --git a/src/image/image.h b/src/image/image.h index d7cb9a13..7896d1f3 100644 --- a/src/image/image.h +++ b/src/image/image.h @@ -42,7 +42,9 @@ class LwipImage : public node::ObjectWrap { static NAN_METHOD(paste); static NAN_METHOD(width); static NAN_METHOD(height); + static NAN_METHOD(getPixel); static NAN_METHOD(buffer); + static NAN_METHOD(setPixel); LwipImage(unsigned char * data, size_t width, size_t height); ~LwipImage(); private: @@ -246,6 +248,31 @@ class PasteWorker : public NanAsyncWorker { CImg * _cimg; }; +class SetPixelWorker : public NanAsyncWorker { +public: + SetPixelWorker( + size_t left, + size_t top, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a, + CImg * cimg, + NanCallback * callback + ); + ~SetPixelWorker(); + void Execute (); + void HandleOKCallback (); +private: + size_t _left; + size_t _top; + unsigned char _r; + unsigned char _g; + unsigned char _b; + unsigned char _a; + CImg * _cimg; +}; + void rgb_to_hsl(unsigned char r, unsigned char g, unsigned char b, float * h, float * s, float * l); void hsl_to_rgb(float h, float s, float l, unsigned char * r, unsigned char * g, diff --git a/src/image/setpixel_worker.cpp b/src/image/setpixel_worker.cpp new file mode 100644 index 00000000..4e72862f --- /dev/null +++ b/src/image/setpixel_worker.cpp @@ -0,0 +1,33 @@ +#include "image.h" + +SetPixelWorker::SetPixelWorker( + size_t left, + size_t top, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a, + CImg * cimg, + NanCallback * callback +): NanAsyncWorker(callback), _left(left), _top(top), _r(r), _g(g), _b(b), _a(a), + _cimg(cimg) {} + +SetPixelWorker::~SetPixelWorker() {} + +void SetPixelWorker::Execute () { + try { + _cimg->fillC(_left, _top, 0, _r, _g, _b, _a); + } catch (CImgException e) { + SetErrorMessage("Unable to set pixel"); + return; + } + return; +} + +void SetPixelWorker::HandleOKCallback () { + NanScope(); + Local argv[] = { + NanNull() + }; + callback->Call(1, argv); +} diff --git a/src/lib/gif/AUTHORS b/src/lib/gif/AUTHORS new file mode 100644 index 00000000..78611cac --- /dev/null +++ b/src/lib/gif/AUTHORS @@ -0,0 +1,36 @@ +Michael Brown + callbacks to write data via user defined function + +Daniel Eisenbud + Fixes for crashes with invalid gif files and double freeing of + colormaps + +Gershon Elber + original giflib code + +Marc Ewing + spec file (for rpms) updates + +Toshio Kuratomi + uncompressed gif writing code + autoconf/automake process + former maintainer + +marek + Gif initialization fix + windows build code + +Peter Mehlitz + callbacks to read data from arbitrary sources (like libjpeg/libpng) + +Dick Porter + int/pointer fixes for Alpha + +Eric Raymond + current as well as long time former maintainer of giflib code + +Petter Reinholdtsen + Tru64 build fixs + +Georg Schwarz + IRIX fixes diff --git a/src/lib/gif/COPYING b/src/lib/gif/COPYING new file mode 100644 index 00000000..b9c0b501 --- /dev/null +++ b/src/lib/gif/COPYING @@ -0,0 +1,19 @@ +The GIFLIB distribution is Copyright (c) 1997 Eric S. Raymond + +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. diff --git a/src/lib/gif/README b/src/lib/gif/README new file mode 100644 index 00000000..38a9233c --- /dev/null +++ b/src/lib/gif/README @@ -0,0 +1,27 @@ += GIFLIB = + +This is the README file of GIFLIB, a library for manipulating GIF files. + +Latest versions of GIFLIB are currently hosted at: + http://sourceforge.net/projects/giflib + +== Overview == + +GIF is a legacy format; we recommend against generating new images in +it. For a cleaner, more extensible design with better color support +and compression, look up PNG. + +giflib provides code for reading GIF files and transforming them into +RGB bitmaps, and for writing RGB bitmaps as GIF files. + +The (permissive) open-source license is in the file COPYING. + +You will find build instructions in build.asc + +You will find full documentation of the API in doc/ and on the +project website. + +The project has a long and confusing history, described in history.asc + +The project to-do list is in TODO. + diff --git a/src/lib/gif/dgif_lib.c b/src/lib/gif/dgif_lib.c new file mode 100644 index 00000000..5bd5bf8b --- /dev/null +++ b/src/lib/gif/dgif_lib.c @@ -0,0 +1,1163 @@ +/****************************************************************************** + +dgif_lib.c - GIF decoding + +The functions here and in egif_lib.c are partitioned carefully so that +if you only require one of read and write capability, only one of these +two modules will be linked. Preserve this property! + +*****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif /* _WIN32 */ + +#include "gif_lib.h" +#include "gif_lib_private.h" + +/* compose unsigned little endian value */ +#define UNSIGNED_LITTLE_ENDIAN(lo, hi) ((lo) | ((hi) << 8)) + +/* avoid extra function call in case we use fread (TVT) */ +#define READ(_gif,_buf,_len) \ + (((GifFilePrivateType*)_gif->Private)->Read ? \ + ((GifFilePrivateType*)_gif->Private)->Read(_gif,_buf,_len) : \ + fread(_buf,1,_len,((GifFilePrivateType*)_gif->Private)->File)) + +static int DGifGetWord(GifFileType *GifFile, GifWord *Word); +static int DGifSetupDecompress(GifFileType *GifFile); +static int DGifDecompressLine(GifFileType *GifFile, GifPixelType *Line, + int LineLen); +static int DGifGetPrefixChar(GifPrefixType *Prefix, int Code, int ClearCode); +static int DGifDecompressInput(GifFileType *GifFile, int *Code); +static int DGifBufferedInput(GifFileType *GifFile, GifByteType *Buf, + GifByteType *NextByte); + +/****************************************************************************** + Open a new GIF file for read, given by its name. + Returns dynamically allocated GifFileType pointer which serves as the GIF + info record. +******************************************************************************/ +GifFileType * +DGifOpenFileName(const char *FileName, int *Error) +{ + int FileHandle; + GifFileType *GifFile; + + if ((FileHandle = open(FileName, O_RDONLY)) == -1) { + if (Error != NULL) + *Error = D_GIF_ERR_OPEN_FAILED; + return NULL; + } + + GifFile = DGifOpenFileHandle(FileHandle, Error); + return GifFile; +} + +/****************************************************************************** + Update a new GIF file, given its file handle. + Returns dynamically allocated GifFileType pointer which serves as the GIF + info record. +******************************************************************************/ +GifFileType * +DGifOpenFileHandle(int FileHandle, int *Error) +{ + char Buf[GIF_STAMP_LEN + 1]; + GifFileType *GifFile; + GifFilePrivateType *Private; + FILE *f; + + GifFile = (GifFileType *)malloc(sizeof(GifFileType)); + if (GifFile == NULL) { + if (Error != NULL) + *Error = D_GIF_ERR_NOT_ENOUGH_MEM; + (void)close(FileHandle); + return NULL; + } + + /*@i1@*/memset(GifFile, '\0', sizeof(GifFileType)); + + /* Belt and suspenders, in case the null pointer isn't zero */ + GifFile->SavedImages = NULL; + GifFile->SColorMap = NULL; + + Private = (GifFilePrivateType *)malloc(sizeof(GifFilePrivateType)); + if (Private == NULL) { + if (Error != NULL) + *Error = D_GIF_ERR_NOT_ENOUGH_MEM; + (void)close(FileHandle); + free((char *)GifFile); + return NULL; + } +#ifdef _WIN32 + _setmode(FileHandle, O_BINARY); /* Make sure it is in binary mode. */ +#endif /* _WIN32 */ + + f = fdopen(FileHandle, "rb"); /* Make it into a stream: */ + + /*@-mustfreeonly@*/ + GifFile->Private = (void *)Private; + Private->FileHandle = FileHandle; + Private->File = f; + Private->FileState = FILE_STATE_READ; + Private->Read = NULL; /* don't use alternate input method (TVT) */ + GifFile->UserData = NULL; /* TVT */ + /*@=mustfreeonly@*/ + + /* Let's see if this is a GIF file: */ + if (READ(GifFile, (unsigned char *)Buf, GIF_STAMP_LEN) != GIF_STAMP_LEN) { + if (Error != NULL) + *Error = D_GIF_ERR_READ_FAILED; + (void)fclose(f); + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + /* Check for GIF prefix at start of file */ + Buf[GIF_STAMP_LEN] = 0; + if (strncmp(GIF_STAMP, Buf, GIF_VERSION_POS) != 0) { + if (Error != NULL) + *Error = D_GIF_ERR_NOT_GIF_FILE; + (void)fclose(f); + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + if (DGifGetScreenDesc(GifFile) == GIF_ERROR) { + (void)fclose(f); + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + GifFile->Error = 0; + + /* What version of GIF? */ + Private->gif89 = (Buf[GIF_VERSION_POS] == '9'); + + return GifFile; +} + +/****************************************************************************** + GifFileType constructor with user supplied input function (TVT) +******************************************************************************/ +GifFileType * +DGifOpen(void *userData, InputFunc readFunc, int *Error) +{ + char Buf[GIF_STAMP_LEN + 1]; + GifFileType *GifFile; + GifFilePrivateType *Private; + + GifFile = (GifFileType *)malloc(sizeof(GifFileType)); + if (GifFile == NULL) { + if (Error != NULL) + *Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return NULL; + } + + memset(GifFile, '\0', sizeof(GifFileType)); + + /* Belt and suspenders, in case the null pointer isn't zero */ + GifFile->SavedImages = NULL; + GifFile->SColorMap = NULL; + + Private = (GifFilePrivateType *)malloc(sizeof(GifFilePrivateType)); + if (!Private) { + if (Error != NULL) + *Error = D_GIF_ERR_NOT_ENOUGH_MEM; + free((char *)GifFile); + return NULL; + } + + GifFile->Private = (void *)Private; + Private->FileHandle = 0; + Private->File = NULL; + Private->FileState = FILE_STATE_READ; + + Private->Read = readFunc; /* TVT */ + GifFile->UserData = userData; /* TVT */ + + /* Lets see if this is a GIF file: */ + if (READ(GifFile, (unsigned char *)Buf, GIF_STAMP_LEN) != GIF_STAMP_LEN) { + if (Error != NULL) + *Error = D_GIF_ERR_READ_FAILED; + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + /* Check for GIF prefix at start of file */ + Buf[GIF_STAMP_LEN] = '\0'; + if (strncmp(GIF_STAMP, Buf, GIF_VERSION_POS) != 0) { + if (Error != NULL) + *Error = D_GIF_ERR_NOT_GIF_FILE; + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + if (DGifGetScreenDesc(GifFile) == GIF_ERROR) { + free((char *)Private); + free((char *)GifFile); + *Error = D_GIF_ERR_NO_SCRN_DSCR; + return NULL; + } + + GifFile->Error = 0; + + /* What version of GIF? */ + Private->gif89 = (Buf[GIF_VERSION_POS] == '9'); + + return GifFile; +} + +/****************************************************************************** + This routine should be called before any other DGif calls. Note that + this routine is called automatically from DGif file open routines. +******************************************************************************/ +int +DGifGetScreenDesc(GifFileType *GifFile) +{ + int BitsPerPixel; + bool SortFlag; + GifByteType Buf[3]; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + /* Put the screen descriptor into the file: */ + if (DGifGetWord(GifFile, &GifFile->SWidth) == GIF_ERROR || + DGifGetWord(GifFile, &GifFile->SHeight) == GIF_ERROR) + return GIF_ERROR; + + if (READ(GifFile, Buf, 3) != 3) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + GifFreeMapObject(GifFile->SColorMap); + GifFile->SColorMap = NULL; + return GIF_ERROR; + } + GifFile->SColorResolution = (((Buf[0] & 0x70) + 1) >> 4) + 1; + SortFlag = (Buf[0] & 0x08) != 0; + BitsPerPixel = (Buf[0] & 0x07) + 1; + GifFile->SBackGroundColor = Buf[1]; + GifFile->AspectByte = Buf[2]; + if (Buf[0] & 0x80) { /* Do we have global color map? */ + int i; + + GifFile->SColorMap = GifMakeMapObject(1 << BitsPerPixel, NULL); + if (GifFile->SColorMap == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + + /* Get the global color map: */ + GifFile->SColorMap->SortFlag = SortFlag; + for (i = 0; i < GifFile->SColorMap->ColorCount; i++) { + if (READ(GifFile, Buf, 3) != 3) { + GifFreeMapObject(GifFile->SColorMap); + GifFile->SColorMap = NULL; + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + GifFile->SColorMap->Colors[i].Red = Buf[0]; + GifFile->SColorMap->Colors[i].Green = Buf[1]; + GifFile->SColorMap->Colors[i].Blue = Buf[2]; + } + } else { + GifFile->SColorMap = NULL; + } + + return GIF_OK; +} + +/****************************************************************************** + This routine should be called before any attempt to read an image. +******************************************************************************/ +int +DGifGetRecordType(GifFileType *GifFile, GifRecordType* Type) +{ + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + if (READ(GifFile, &Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + + switch (Buf) { + case DESCRIPTOR_INTRODUCER: + *Type = IMAGE_DESC_RECORD_TYPE; + break; + case EXTENSION_INTRODUCER: + *Type = EXTENSION_RECORD_TYPE; + break; + case TERMINATOR_INTRODUCER: + *Type = TERMINATE_RECORD_TYPE; + break; + default: + *Type = UNDEFINED_RECORD_TYPE; + GifFile->Error = D_GIF_ERR_WRONG_RECORD; + return GIF_ERROR; + } + + return GIF_OK; +} + +/****************************************************************************** + This routine should be called before any attempt to read an image. + Note it is assumed the Image desc. header has been read. +******************************************************************************/ +int +DGifGetImageDesc(GifFileType *GifFile) +{ + unsigned int BitsPerPixel; + GifByteType Buf[3]; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + SavedImage *sp; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + if (DGifGetWord(GifFile, &GifFile->Image.Left) == GIF_ERROR || + DGifGetWord(GifFile, &GifFile->Image.Top) == GIF_ERROR || + DGifGetWord(GifFile, &GifFile->Image.Width) == GIF_ERROR || + DGifGetWord(GifFile, &GifFile->Image.Height) == GIF_ERROR) + return GIF_ERROR; + if (READ(GifFile, Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Image.ColorMap = NULL; + return GIF_ERROR; + } + BitsPerPixel = (Buf[0] & 0x07) + 1; + GifFile->Image.Interlace = (Buf[0] & 0x40) ? true : false; + + /* Setup the colormap */ + if (GifFile->Image.ColorMap) { + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Image.ColorMap = NULL; + } + /* Does this image have local color map? */ + if (Buf[0] & 0x80) { + unsigned int i; + + GifFile->Image.ColorMap = GifMakeMapObject(1 << BitsPerPixel, NULL); + if (GifFile->Image.ColorMap == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + + /* Get the image local color map: */ + for (i = 0; i < GifFile->Image.ColorMap->ColorCount; i++) { + if (READ(GifFile, Buf, 3) != 3) { + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Error = D_GIF_ERR_READ_FAILED; + GifFile->Image.ColorMap = NULL; + return GIF_ERROR; + } + GifFile->Image.ColorMap->Colors[i].Red = Buf[0]; + GifFile->Image.ColorMap->Colors[i].Green = Buf[1]; + GifFile->Image.ColorMap->Colors[i].Blue = Buf[2]; + } + } + + if (GifFile->SavedImages) { + if ((GifFile->SavedImages = (SavedImage *)realloc(GifFile->SavedImages, + sizeof(SavedImage) * + (GifFile->ImageCount + 1))) == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + } else { + if ((GifFile->SavedImages = + (SavedImage *) malloc(sizeof(SavedImage))) == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + } + + sp = &GifFile->SavedImages[GifFile->ImageCount]; + memcpy(&sp->ImageDesc, &GifFile->Image, sizeof(GifImageDesc)); + if (GifFile->Image.ColorMap != NULL) { + sp->ImageDesc.ColorMap = GifMakeMapObject( + GifFile->Image.ColorMap->ColorCount, + GifFile->Image.ColorMap->Colors); + if (sp->ImageDesc.ColorMap == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + } + sp->RasterBits = (unsigned char *)NULL; + sp->ExtensionBlockCount = 0; + sp->ExtensionBlocks = (ExtensionBlock *) NULL; + + GifFile->ImageCount++; + + Private->PixelCount = (long)GifFile->Image.Width * + (long)GifFile->Image.Height; + + /* Reset decompress algorithm parameters. */ + return DGifSetupDecompress(GifFile); +} + +/****************************************************************************** + Get one full scanned line (Line) of length LineLen from GIF file. +******************************************************************************/ +int +DGifGetLine(GifFileType *GifFile, GifPixelType *Line, int LineLen) +{ + GifByteType *Dummy; + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + if (!LineLen) + LineLen = GifFile->Image.Width; + + if ((Private->PixelCount -= LineLen) > 0xffff0000UL) { + GifFile->Error = D_GIF_ERR_DATA_TOO_BIG; + return GIF_ERROR; + } + + if (DGifDecompressLine(GifFile, Line, LineLen) == GIF_OK) { + if (Private->PixelCount == 0) { + /* We probably won't be called any more, so let's clean up + * everything before we return: need to flush out all the + * rest of image until an empty block (size 0) + * detected. We use GetCodeNext. + */ + do + if (DGifGetCodeNext(GifFile, &Dummy) == GIF_ERROR) + return GIF_ERROR; + while (Dummy != NULL) ; + } + return GIF_OK; + } else + return GIF_ERROR; +} + +/****************************************************************************** + Put one pixel (Pixel) into GIF file. +******************************************************************************/ +int +DGifGetPixel(GifFileType *GifFile, GifPixelType Pixel) +{ + GifByteType *Dummy; + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + if (--Private->PixelCount > 0xffff0000UL) + { + GifFile->Error = D_GIF_ERR_DATA_TOO_BIG; + return GIF_ERROR; + } + + if (DGifDecompressLine(GifFile, &Pixel, 1) == GIF_OK) { + if (Private->PixelCount == 0) { + /* We probably won't be called any more, so let's clean up + * everything before we return: need to flush out all the + * rest of image until an empty block (size 0) + * detected. We use GetCodeNext. + */ + do + if (DGifGetCodeNext(GifFile, &Dummy) == GIF_ERROR) + return GIF_ERROR; + while (Dummy != NULL) ; + } + return GIF_OK; + } else + return GIF_ERROR; +} + +/****************************************************************************** + Get an extension block (see GIF manual) from GIF file. This routine only + returns the first data block, and DGifGetExtensionNext should be called + after this one until NULL extension is returned. + The Extension should NOT be freed by the user (not dynamically allocated). + Note it is assumed the Extension description header has been read. +******************************************************************************/ +int +DGifGetExtension(GifFileType *GifFile, int *ExtCode, GifByteType **Extension) +{ + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + if (READ(GifFile, &Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + *ExtCode = Buf; + + return DGifGetExtensionNext(GifFile, Extension); +} + +/****************************************************************************** + Get a following extension block (see GIF manual) from GIF file. This + routine should be called until NULL Extension is returned. + The Extension should NOT be freed by the user (not dynamically allocated). +******************************************************************************/ +int +DGifGetExtensionNext(GifFileType *GifFile, GifByteType ** Extension) +{ + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (READ(GifFile, &Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + if (Buf > 0) { + *Extension = Private->Buf; /* Use private unused buffer. */ + (*Extension)[0] = Buf; /* Pascal strings notation (pos. 0 is len.). */ + /* coverity[tainted_data] */ + if (READ(GifFile, &((*Extension)[1]), Buf) != Buf) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + } else + *Extension = NULL; + + return GIF_OK; +} + +/****************************************************************************** + Extract a Graphics Control Block from raw extension data +******************************************************************************/ + +int DGifExtensionToGCB(const size_t GifExtensionLength, + const GifByteType *GifExtension, + GraphicsControlBlock *GCB) +{ + if (GifExtensionLength != 4) { + return GIF_ERROR; + } + + GCB->DisposalMode = (GifExtension[0] >> 2) & 0x07; + GCB->UserInputFlag = (GifExtension[0] & 0x02) != 0; + GCB->DelayTime = UNSIGNED_LITTLE_ENDIAN(GifExtension[1], GifExtension[2]); + if (GifExtension[0] & 0x01) + GCB->TransparentColor = (int)GifExtension[3]; + else + GCB->TransparentColor = NO_TRANSPARENT_COLOR; + + return GIF_OK; +} + +/****************************************************************************** + Extract the Graphics Control Block for a saved image, if it exists. +******************************************************************************/ + +int DGifSavedExtensionToGCB(GifFileType *GifFile, + int ImageIndex, GraphicsControlBlock *GCB) +{ + int i; + + if (ImageIndex < 0 || ImageIndex > GifFile->ImageCount - 1) + return GIF_ERROR; + + GCB->DisposalMode = DISPOSAL_UNSPECIFIED; + GCB->UserInputFlag = false; + GCB->DelayTime = 0; + GCB->TransparentColor = NO_TRANSPARENT_COLOR; + + for (i = 0; i < GifFile->SavedImages[ImageIndex].ExtensionBlockCount; i++) { + ExtensionBlock *ep = &GifFile->SavedImages[ImageIndex].ExtensionBlocks[i]; + if (ep->Function == GRAPHICS_EXT_FUNC_CODE) + return DGifExtensionToGCB(ep->ByteCount, ep->Bytes, GCB); + } + + return GIF_ERROR; +} + +/****************************************************************************** + This routine should be called last, to close the GIF file. +******************************************************************************/ +int +DGifCloseFile(GifFileType *GifFile, int *ErrorCode) +{ + GifFilePrivateType *Private; + + if (GifFile == NULL || GifFile->Private == NULL) + return GIF_ERROR; + + if (GifFile->Image.ColorMap) { + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Image.ColorMap = NULL; + } + + if (GifFile->SColorMap) { + GifFreeMapObject(GifFile->SColorMap); + GifFile->SColorMap = NULL; + } + + if (GifFile->SavedImages) { + GifFreeSavedImages(GifFile); + GifFile->SavedImages = NULL; + } + + GifFreeExtensions(&GifFile->ExtensionBlockCount, &GifFile->ExtensionBlocks); + + Private = (GifFilePrivateType *) GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + if (ErrorCode != NULL) + *ErrorCode = D_GIF_ERR_NOT_READABLE; + free((char *)GifFile->Private); + free(GifFile); + return GIF_ERROR; + } + + if (Private->File && (fclose(Private->File) != 0)) { + if (ErrorCode != NULL) + *ErrorCode = D_GIF_ERR_CLOSE_FAILED; + free((char *)GifFile->Private); + free(GifFile); + return GIF_ERROR; + } + + free((char *)GifFile->Private); + free(GifFile); + if (ErrorCode != NULL) + *ErrorCode = D_GIF_SUCCEEDED; + return GIF_OK; +} + +/****************************************************************************** + Get 2 bytes (word) from the given file: +******************************************************************************/ +static int +DGifGetWord(GifFileType *GifFile, GifWord *Word) +{ + unsigned char c[2]; + + if (READ(GifFile, c, 2) != 2) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + + *Word = (GifWord)UNSIGNED_LITTLE_ENDIAN(c[0], c[1]); + return GIF_OK; +} + +/****************************************************************************** + Get the image code in compressed form. This routine can be called if the + information needed to be piped out as is. Obviously this is much faster + than decoding and encoding again. This routine should be followed by calls + to DGifGetCodeNext, until NULL block is returned. + The block should NOT be freed by the user (not dynamically allocated). +******************************************************************************/ +int +DGifGetCode(GifFileType *GifFile, int *CodeSize, GifByteType **CodeBlock) +{ + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + *CodeSize = Private->BitsPerPixel; + + return DGifGetCodeNext(GifFile, CodeBlock); +} + +/****************************************************************************** + Continue to get the image code in compressed form. This routine should be + called until NULL block is returned. + The block should NOT be freed by the user (not dynamically allocated). +******************************************************************************/ +int +DGifGetCodeNext(GifFileType *GifFile, GifByteType **CodeBlock) +{ + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + /* coverity[tainted_data_argument] */ + if (READ(GifFile, &Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + + /* coverity[lower_bounds] */ + if (Buf > 0) { + *CodeBlock = Private->Buf; /* Use private unused buffer. */ + (*CodeBlock)[0] = Buf; /* Pascal strings notation (pos. 0 is len.). */ + /* coverity[tainted_data] */ + if (READ(GifFile, &((*CodeBlock)[1]), Buf) != Buf) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + } else { + *CodeBlock = NULL; + Private->Buf[0] = 0; /* Make sure the buffer is empty! */ + Private->PixelCount = 0; /* And local info. indicate image read. */ + } + + return GIF_OK; +} + +/****************************************************************************** + Setup the LZ decompression for this image: +******************************************************************************/ +static int +DGifSetupDecompress(GifFileType *GifFile) +{ + int i, BitsPerPixel; + GifByteType CodeSize; + GifPrefixType *Prefix; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (READ(GifFile, &CodeSize, 1) < 1) { /* Read Code size from file. */ + return GIF_ERROR; /* Failed to read Code size. */ + } + BitsPerPixel = CodeSize; + + Private->Buf[0] = 0; /* Input Buffer empty. */ + Private->BitsPerPixel = BitsPerPixel; + Private->ClearCode = (1 << BitsPerPixel); + Private->EOFCode = Private->ClearCode + 1; + Private->RunningCode = Private->EOFCode + 1; + Private->RunningBits = BitsPerPixel + 1; /* Number of bits per code. */ + Private->MaxCode1 = 1 << Private->RunningBits; /* Max. code + 1. */ + Private->StackPtr = 0; /* No pixels on the pixel stack. */ + Private->LastCode = NO_SUCH_CODE; + Private->CrntShiftState = 0; /* No information in CrntShiftDWord. */ + Private->CrntShiftDWord = 0; + + Prefix = Private->Prefix; + for (i = 0; i <= LZ_MAX_CODE; i++) + Prefix[i] = NO_SUCH_CODE; + + return GIF_OK; +} + +/****************************************************************************** + The LZ decompression routine: + This version decompress the given GIF file into Line of length LineLen. + This routine can be called few times (one per scan line, for example), in + order the complete the whole image. +******************************************************************************/ +static int +DGifDecompressLine(GifFileType *GifFile, GifPixelType *Line, int LineLen) +{ + int i = 0; + int j, CrntCode, EOFCode, ClearCode, CrntPrefix, LastCode, StackPtr; + GifByteType *Stack, *Suffix; + GifPrefixType *Prefix; + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + + StackPtr = Private->StackPtr; + Prefix = Private->Prefix; + Suffix = Private->Suffix; + Stack = Private->Stack; + EOFCode = Private->EOFCode; + ClearCode = Private->ClearCode; + LastCode = Private->LastCode; + + if (StackPtr > LZ_MAX_CODE) { + return GIF_ERROR; + } + + if (StackPtr != 0) { + /* Let pop the stack off before continueing to read the GIF file: */ + while (StackPtr != 0 && i < LineLen) + Line[i++] = Stack[--StackPtr]; + } + + while (i < LineLen) { /* Decode LineLen items. */ + if (DGifDecompressInput(GifFile, &CrntCode) == GIF_ERROR) + return GIF_ERROR; + + if (CrntCode == EOFCode) { + /* Note however that usually we will not be here as we will stop + * decoding as soon as we got all the pixel, or EOF code will + * not be read at all, and DGifGetLine/Pixel clean everything. */ + GifFile->Error = D_GIF_ERR_EOF_TOO_SOON; + return GIF_ERROR; + } else if (CrntCode == ClearCode) { + /* We need to start over again: */ + for (j = 0; j <= LZ_MAX_CODE; j++) + Prefix[j] = NO_SUCH_CODE; + Private->RunningCode = Private->EOFCode + 1; + Private->RunningBits = Private->BitsPerPixel + 1; + Private->MaxCode1 = 1 << Private->RunningBits; + LastCode = Private->LastCode = NO_SUCH_CODE; + } else { + /* Its regular code - if in pixel range simply add it to output + * stream, otherwise trace to codes linked list until the prefix + * is in pixel range: */ + if (CrntCode < ClearCode) { + /* This is simple - its pixel scalar, so add it to output: */ + Line[i++] = CrntCode; + } else { + /* Its a code to needed to be traced: trace the linked list + * until the prefix is a pixel, while pushing the suffix + * pixels on our stack. If we done, pop the stack in reverse + * (thats what stack is good for!) order to output. */ + if (Prefix[CrntCode] == NO_SUCH_CODE) { + CrntPrefix = LastCode; + + /* Only allowed if CrntCode is exactly the running code: + * In that case CrntCode = XXXCode, CrntCode or the + * prefix code is last code and the suffix char is + * exactly the prefix of last code! */ + if (CrntCode == Private->RunningCode - 2) { + Suffix[Private->RunningCode - 2] = + Stack[StackPtr++] = DGifGetPrefixChar(Prefix, + LastCode, + ClearCode); + } else { + Suffix[Private->RunningCode - 2] = + Stack[StackPtr++] = DGifGetPrefixChar(Prefix, + CrntCode, + ClearCode); + } + } else + CrntPrefix = CrntCode; + + /* Now (if image is O.K.) we should not get a NO_SUCH_CODE + * during the trace. As we might loop forever, in case of + * defective image, we use StackPtr as loop counter and stop + * before overflowing Stack[]. */ + while (StackPtr < LZ_MAX_CODE && + CrntPrefix > ClearCode && CrntPrefix <= LZ_MAX_CODE) { + Stack[StackPtr++] = Suffix[CrntPrefix]; + CrntPrefix = Prefix[CrntPrefix]; + } + if (StackPtr >= LZ_MAX_CODE || CrntPrefix > LZ_MAX_CODE) { + GifFile->Error = D_GIF_ERR_IMAGE_DEFECT; + return GIF_ERROR; + } + /* Push the last character on stack: */ + Stack[StackPtr++] = CrntPrefix; + + /* Now lets pop all the stack into output: */ + while (StackPtr != 0 && i < LineLen) + Line[i++] = Stack[--StackPtr]; + } + if (LastCode != NO_SUCH_CODE && Prefix[Private->RunningCode - 2] == NO_SUCH_CODE) { + Prefix[Private->RunningCode - 2] = LastCode; + + if (CrntCode == Private->RunningCode - 2) { + /* Only allowed if CrntCode is exactly the running code: + * In that case CrntCode = XXXCode, CrntCode or the + * prefix code is last code and the suffix char is + * exactly the prefix of last code! */ + Suffix[Private->RunningCode - 2] = + DGifGetPrefixChar(Prefix, LastCode, ClearCode); + } else { + Suffix[Private->RunningCode - 2] = + DGifGetPrefixChar(Prefix, CrntCode, ClearCode); + } + } + LastCode = CrntCode; + } + } + + Private->LastCode = LastCode; + Private->StackPtr = StackPtr; + + return GIF_OK; +} + +/****************************************************************************** + Routine to trace the Prefixes linked list until we get a prefix which is + not code, but a pixel value (less than ClearCode). Returns that pixel value. + If image is defective, we might loop here forever, so we limit the loops to + the maximum possible if image O.k. - LZ_MAX_CODE times. +******************************************************************************/ +static int +DGifGetPrefixChar(GifPrefixType *Prefix, int Code, int ClearCode) +{ + int i = 0; + + while (Code > ClearCode && i++ <= LZ_MAX_CODE) { + if (Code > LZ_MAX_CODE) { + return NO_SUCH_CODE; + } + Code = Prefix[Code]; + } + return Code; +} + +/****************************************************************************** + Interface for accessing the LZ codes directly. Set Code to the real code + (12bits), or to -1 if EOF code is returned. +******************************************************************************/ +int +DGifGetLZCodes(GifFileType *GifFile, int *Code) +{ + GifByteType *CodeBlock; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + if (DGifDecompressInput(GifFile, Code) == GIF_ERROR) + return GIF_ERROR; + + if (*Code == Private->EOFCode) { + /* Skip rest of codes (hopefully only NULL terminating block): */ + do { + if (DGifGetCodeNext(GifFile, &CodeBlock) == GIF_ERROR) + return GIF_ERROR; + } while (CodeBlock != NULL) ; + + *Code = -1; + } else if (*Code == Private->ClearCode) { + /* We need to start over again: */ + Private->RunningCode = Private->EOFCode + 1; + Private->RunningBits = Private->BitsPerPixel + 1; + Private->MaxCode1 = 1 << Private->RunningBits; + } + + return GIF_OK; +} + +/****************************************************************************** + The LZ decompression input routine: + This routine is responsable for the decompression of the bit stream from + 8 bits (bytes) packets, into the real codes. + Returns GIF_OK if read successfully. +******************************************************************************/ +static int +DGifDecompressInput(GifFileType *GifFile, int *Code) +{ + static const unsigned short CodeMasks[] = { + 0x0000, 0x0001, 0x0003, 0x0007, + 0x000f, 0x001f, 0x003f, 0x007f, + 0x00ff, 0x01ff, 0x03ff, 0x07ff, + 0x0fff + }; + + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + GifByteType NextByte; + + /* The image can't contain more than LZ_BITS per code. */ + if (Private->RunningBits > LZ_BITS) { + GifFile->Error = D_GIF_ERR_IMAGE_DEFECT; + return GIF_ERROR; + } + + while (Private->CrntShiftState < Private->RunningBits) { + /* Needs to get more bytes from input stream for next code: */ + if (DGifBufferedInput(GifFile, Private->Buf, &NextByte) == GIF_ERROR) { + return GIF_ERROR; + } + Private->CrntShiftDWord |= + ((unsigned long)NextByte) << Private->CrntShiftState; + Private->CrntShiftState += 8; + } + *Code = Private->CrntShiftDWord & CodeMasks[Private->RunningBits]; + + Private->CrntShiftDWord >>= Private->RunningBits; + Private->CrntShiftState -= Private->RunningBits; + + /* If code cannot fit into RunningBits bits, must raise its size. Note + * however that codes above 4095 are used for special signaling. + * If we're using LZ_BITS bits already and we're at the max code, just + * keep using the table as it is, don't increment Private->RunningCode. + */ + if (Private->RunningCode < LZ_MAX_CODE + 2 && + ++Private->RunningCode > Private->MaxCode1 && + Private->RunningBits < LZ_BITS) { + Private->MaxCode1 <<= 1; + Private->RunningBits++; + } + return GIF_OK; +} + +/****************************************************************************** + This routines read one GIF data block at a time and buffers it internally + so that the decompression routine could access it. + The routine returns the next byte from its internal buffer (or read next + block in if buffer empty) and returns GIF_OK if succesful. +******************************************************************************/ +static int +DGifBufferedInput(GifFileType *GifFile, GifByteType *Buf, GifByteType *NextByte) +{ + if (Buf[0] == 0) { + /* Needs to read the next buffer - this one is empty: */ + if (READ(GifFile, Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + /* There shouldn't be any empty data blocks here as the LZW spec + * says the LZW termination code should come first. Therefore we + * shouldn't be inside this routine at that point. + */ + if (Buf[0] == 0) { + GifFile->Error = D_GIF_ERR_IMAGE_DEFECT; + return GIF_ERROR; + } + if (READ(GifFile, &Buf[1], Buf[0]) != Buf[0]) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + *NextByte = Buf[1]; + Buf[1] = 2; /* We use now the second place as last char read! */ + Buf[0]--; + } else { + *NextByte = Buf[Buf[1]++]; + Buf[0]--; + } + + return GIF_OK; +} + +/****************************************************************************** + This routine reads an entire GIF into core, hanging all its state info off + the GifFileType pointer. Call DGifOpenFileName() or DGifOpenFileHandle() + first to initialize I/O. Its inverse is EGifSpew(). +*******************************************************************************/ +int +DGifSlurp(GifFileType *GifFile) +{ + size_t ImageSize; + GifRecordType RecordType; + SavedImage *sp; + GifByteType *ExtData; + int ExtFunction; + + GifFile->ExtensionBlocks = NULL; + GifFile->ExtensionBlockCount = 0; + + do { + if (DGifGetRecordType(GifFile, &RecordType) == GIF_ERROR) + return (GIF_ERROR); + + switch (RecordType) { + case IMAGE_DESC_RECORD_TYPE: + if (DGifGetImageDesc(GifFile) == GIF_ERROR) + return (GIF_ERROR); + + sp = &GifFile->SavedImages[GifFile->ImageCount - 1]; + /* Allocate memory for the image */ + if (sp->ImageDesc.Width < 0 && sp->ImageDesc.Height < 0 && + sp->ImageDesc.Width > (INT_MAX / sp->ImageDesc.Height)) { + return GIF_ERROR; + } + ImageSize = sp->ImageDesc.Width * sp->ImageDesc.Height; + + if (ImageSize > (SIZE_MAX / sizeof(GifPixelType))) { + return GIF_ERROR; + } + sp->RasterBits = (unsigned char *)malloc(ImageSize * + sizeof(GifPixelType)); + + if (sp->RasterBits == NULL) { + return GIF_ERROR; + } + + if (sp->ImageDesc.Interlace) { + int i, j; + /* + * The way an interlaced image should be read - + * offsets and jumps... + */ + int InterlacedOffset[] = { 0, 4, 2, 1 }; + int InterlacedJumps[] = { 8, 8, 4, 2 }; + /* Need to perform 4 passes on the image */ + for (i = 0; i < 4; i++) + for (j = InterlacedOffset[i]; + j < sp->ImageDesc.Height; + j += InterlacedJumps[i]) { + if (DGifGetLine(GifFile, + sp->RasterBits+j*sp->ImageDesc.Width, + sp->ImageDesc.Width) == GIF_ERROR) + return GIF_ERROR; + } + } + else { + if (DGifGetLine(GifFile,sp->RasterBits,ImageSize)==GIF_ERROR) + return (GIF_ERROR); + } + + if (GifFile->ExtensionBlocks) { + sp->ExtensionBlocks = GifFile->ExtensionBlocks; + sp->ExtensionBlockCount = GifFile->ExtensionBlockCount; + + GifFile->ExtensionBlocks = NULL; + GifFile->ExtensionBlockCount = 0; + } + break; + + case EXTENSION_RECORD_TYPE: + if (DGifGetExtension(GifFile,&ExtFunction,&ExtData) == GIF_ERROR) + return (GIF_ERROR); + /* Create an extension block with our data */ + if (ExtData != NULL) { + if (GifAddExtensionBlock(&GifFile->ExtensionBlockCount, + &GifFile->ExtensionBlocks, + ExtFunction, ExtData[0], &ExtData[1]) + == GIF_ERROR) + return (GIF_ERROR); + } + while (ExtData != NULL) { + if (DGifGetExtensionNext(GifFile, &ExtData) == GIF_ERROR) + return (GIF_ERROR); + /* Continue the extension block */ + if (ExtData != NULL) + if (GifAddExtensionBlock(&GifFile->ExtensionBlockCount, + &GifFile->ExtensionBlocks, + CONTINUE_EXT_FUNC_CODE, + ExtData[0], &ExtData[1]) == GIF_ERROR) + return (GIF_ERROR); + } + break; + + case TERMINATE_RECORD_TYPE: + break; + + default: /* Should be trapped by DGifGetRecordType */ + break; + } + } while (RecordType != TERMINATE_RECORD_TYPE); + + return (GIF_OK); +} + +/* end */ diff --git a/src/lib/gif/egif_lib.c b/src/lib/gif/egif_lib.c new file mode 100644 index 00000000..39a62b2e --- /dev/null +++ b/src/lib/gif/egif_lib.c @@ -0,0 +1,1150 @@ +/****************************************************************************** + +egif_lib.c - GIF encoding + +The functions here and in dgif_lib.c are partitioned carefully so that +if you only require one of read and write capability, only one of these +two modules will be linked. Preserve this property! + +*****************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif /* _WIN32 */ +#include + +#include "gif_lib.h" +#include "gif_lib_private.h" + +/* Masks given codes to BitsPerPixel, to make sure all codes are in range: */ +/*@+charint@*/ +static const GifPixelType CodeMask[] = { + 0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff +}; +/*@-charint@*/ + +static int EGifPutWord(int Word, GifFileType * GifFile); +static int EGifSetupCompress(GifFileType * GifFile); +static int EGifCompressLine(GifFileType * GifFile, GifPixelType * Line, + int LineLen); +static int EGifCompressOutput(GifFileType * GifFile, int Code); +static int EGifBufferedOutput(GifFileType * GifFile, GifByteType * Buf, + int c); + +/* extract bytes from an unsigned word */ +#define LOBYTE(x) ((x) & 0xff) +#define HIBYTE(x) (((x) >> 8) & 0xff) + +/****************************************************************************** + Open a new GIF file for write, specified by name. If TestExistance then + if the file exists this routines fails (returns NULL). + Returns a dynamically allocated GifFileType pointer which serves as the GIF + info record. The Error member is cleared if successful. +******************************************************************************/ +GifFileType * +EGifOpenFileName(const char *FileName, const bool TestExistence, int *Error) +{ + + int FileHandle; + GifFileType *GifFile; + + if (TestExistence) + FileHandle = open(FileName, O_WRONLY | O_CREAT | O_EXCL, + S_IREAD | S_IWRITE); + else + FileHandle = open(FileName, O_WRONLY | O_CREAT | O_TRUNC, + S_IREAD | S_IWRITE); + + if (FileHandle == -1) { + if (Error != NULL) + *Error = E_GIF_ERR_OPEN_FAILED; + return NULL; + } + GifFile = EGifOpenFileHandle(FileHandle, Error); + if (GifFile == (GifFileType *) NULL) + (void)close(FileHandle); + return GifFile; +} + +/****************************************************************************** + Update a new GIF file, given its file handle, which must be opened for + write in binary mode. + Returns dynamically allocated a GifFileType pointer which serves as the GIF + info record. + Only fails on a memory allocation error. +******************************************************************************/ +GifFileType * +EGifOpenFileHandle(const int FileHandle, int *Error) +{ + GifFileType *GifFile; + GifFilePrivateType *Private; + FILE *f; + + GifFile = (GifFileType *) malloc(sizeof(GifFileType)); + if (GifFile == NULL) { + return NULL; + } + + memset(GifFile, '\0', sizeof(GifFileType)); + + Private = (GifFilePrivateType *)malloc(sizeof(GifFilePrivateType)); + if (Private == NULL) { + free(GifFile); + if (Error != NULL) + *Error = E_GIF_ERR_NOT_ENOUGH_MEM; + return NULL; + } + if ((Private->HashTable = _InitHashTable()) == NULL) { + free(GifFile); + free(Private); + if (Error != NULL) + *Error = E_GIF_ERR_NOT_ENOUGH_MEM; + return NULL; + } + +#ifdef _WIN32 + _setmode(FileHandle, O_BINARY); /* Make sure it is in binary mode. */ +#endif /* _WIN32 */ + + f = fdopen(FileHandle, "wb"); /* Make it into a stream: */ + + GifFile->Private = (void *)Private; + Private->FileHandle = FileHandle; + Private->File = f; + Private->FileState = FILE_STATE_WRITE; + + Private->Write = (OutputFunc) 0; /* No user write routine (MRB) */ + GifFile->UserData = (void *)NULL; /* No user write handle (MRB) */ + + GifFile->Error = 0; + + return GifFile; +} + +/****************************************************************************** + Output constructor that takes user supplied output function. + Basically just a copy of EGifOpenFileHandle. (MRB) +******************************************************************************/ +GifFileType * +EGifOpen(void *userData, OutputFunc writeFunc, int *Error) +{ + GifFileType *GifFile; + GifFilePrivateType *Private; + + GifFile = (GifFileType *)malloc(sizeof(GifFileType)); + if (GifFile == NULL) { + if (Error != NULL) + *Error = E_GIF_ERR_NOT_ENOUGH_MEM; + return NULL; + } + + memset(GifFile, '\0', sizeof(GifFileType)); + + Private = (GifFilePrivateType *)malloc(sizeof(GifFilePrivateType)); + if (Private == NULL) { + free(GifFile); + if (Error != NULL) + *Error = E_GIF_ERR_NOT_ENOUGH_MEM; + return NULL; + } + + Private->HashTable = _InitHashTable(); + if (Private->HashTable == NULL) { + free (GifFile); + free (Private); + if (Error != NULL) + *Error = E_GIF_ERR_NOT_ENOUGH_MEM; + return NULL; + } + + GifFile->Private = (void *)Private; + Private->FileHandle = 0; + Private->File = (FILE *) 0; + Private->FileState = FILE_STATE_WRITE; + + Private->Write = writeFunc; /* User write routine (MRB) */ + GifFile->UserData = userData; /* User write handle (MRB) */ + + Private->gif89 = false; /* initially, write GIF87 */ + + GifFile->Error = 0; + + return GifFile; +} + +/****************************************************************************** + Routine to compute the GIF version that will be written on output. +******************************************************************************/ +const char * +EGifGetGifVersion(GifFileType *GifFile) +{ + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + int i, j; + + /* + * Bulletproofing - always write GIF89 if we need to. + * Note, we don't clear the gif89 flag here because + * users of the sequential API might have called EGifSetGifVersion() + * in order to set that flag. + */ + for (i = 0; i < GifFile->ImageCount; i++) { + for (j = 0; j < GifFile->SavedImages[i].ExtensionBlockCount; j++) { + int function = + GifFile->SavedImages[i].ExtensionBlocks[j].Function; + + if (function == COMMENT_EXT_FUNC_CODE + || function == GRAPHICS_EXT_FUNC_CODE + || function == PLAINTEXT_EXT_FUNC_CODE + || function == APPLICATION_EXT_FUNC_CODE) + Private->gif89 = true; + } + } + for (i = 0; i < GifFile->ExtensionBlockCount; i++) { + int function = GifFile->ExtensionBlocks[i].Function; + + if (function == COMMENT_EXT_FUNC_CODE + || function == GRAPHICS_EXT_FUNC_CODE + || function == PLAINTEXT_EXT_FUNC_CODE + || function == APPLICATION_EXT_FUNC_CODE) + Private->gif89 = true; + } + + if (Private->gif89) + return GIF89_STAMP; + else + return GIF87_STAMP; +} + +/****************************************************************************** + Set the GIF version. In the extremely unlikely event that there is ever + another version, replace the bool argument with an enum in which the + GIF87 value is 0 (numerically the same as bool false) and the GIF89 value + is 1 (numerically the same as bool true). That way we'll even preserve + object-file compatibility! +******************************************************************************/ +void EGifSetGifVersion(GifFileType *GifFile, const bool gif89) +{ + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + + Private->gif89 = gif89; +} + +/****************************************************************************** + All writes to the GIF should go through this. +******************************************************************************/ +static int InternalWrite(GifFileType *GifFileOut, + const unsigned char *buf, size_t len) +{ + GifFilePrivateType *Private = (GifFilePrivateType*)GifFileOut->Private; + if (Private->Write) + return Private->Write(GifFileOut,buf,len); + else + return fwrite(buf, 1, len, Private->File); +} + +/****************************************************************************** + This routine should be called before any other EGif calls, immediately + following the GIF file opening. +******************************************************************************/ +int +EGifPutScreenDesc(GifFileType *GifFile, + const int Width, + const int Height, + const int ColorRes, + const int BackGround, + const ColorMapObject *ColorMap) +{ + GifByteType Buf[3]; + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + const char *write_version; + + if (Private->FileState & FILE_STATE_SCREEN) { + /* If already has screen descriptor - something is wrong! */ + GifFile->Error = E_GIF_ERR_HAS_SCRN_DSCR; + return GIF_ERROR; + } + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + GifFile->Error = E_GIF_ERR_NOT_WRITEABLE; + return GIF_ERROR; + } + + write_version = EGifGetGifVersion(GifFile); + + /* First write the version prefix into the file. */ + if (InternalWrite(GifFile, (unsigned char *)write_version, + strlen(write_version)) != strlen(write_version)) { + GifFile->Error = E_GIF_ERR_WRITE_FAILED; + return GIF_ERROR; + } + + GifFile->SWidth = Width; + GifFile->SHeight = Height; + GifFile->SColorResolution = ColorRes; + GifFile->SBackGroundColor = BackGround; + if (ColorMap) { + GifFile->SColorMap = GifMakeMapObject(ColorMap->ColorCount, + ColorMap->Colors); + if (GifFile->SColorMap == NULL) { + GifFile->Error = E_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + } else + GifFile->SColorMap = NULL; + + /* + * Put the logical screen descriptor into the file: + */ + /* Logical Screen Descriptor: Dimensions */ + (void)EGifPutWord(Width, GifFile); + (void)EGifPutWord(Height, GifFile); + + /* Logical Screen Descriptor: Packed Fields */ + /* Note: We have actual size of the color table default to the largest + * possible size (7+1 == 8 bits) because the decoder can use it to decide + * how to display the files. + */ + Buf[0] = (ColorMap ? 0x80 : 0x00) | /* Yes/no global colormap */ + ((ColorRes - 1) << 4) | /* Bits allocated to each primary color */ + (ColorMap ? ColorMap->BitsPerPixel - 1 : 0x07 ); /* Actual size of the + color table. */ + if (ColorMap != NULL && ColorMap->SortFlag) + Buf[0] |= 0x08; + Buf[1] = BackGround; /* Index into the ColorTable for background color */ + Buf[2] = GifFile->AspectByte; /* Pixel Aspect Ratio */ + InternalWrite(GifFile, Buf, 3); + + /* If we have Global color map - dump it also: */ + if (ColorMap != NULL) { + int i; + for (i = 0; i < ColorMap->ColorCount; i++) { + /* Put the ColorMap out also: */ + Buf[0] = ColorMap->Colors[i].Red; + Buf[1] = ColorMap->Colors[i].Green; + Buf[2] = ColorMap->Colors[i].Blue; + if (InternalWrite(GifFile, Buf, 3) != 3) { + GifFile->Error = E_GIF_ERR_WRITE_FAILED; + return GIF_ERROR; + } + } + } + + /* Mark this file as has screen descriptor, and no pixel written yet: */ + Private->FileState |= FILE_STATE_SCREEN; + + return GIF_OK; +} + +/****************************************************************************** + This routine should be called before any attempt to dump an image - any + call to any of the pixel dump routines. +******************************************************************************/ +int +EGifPutImageDesc(GifFileType *GifFile, + const int Left, + const int Top, + const int Width, + const int Height, + const bool Interlace, + const ColorMapObject *ColorMap) +{ + GifByteType Buf[3]; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (Private->FileState & FILE_STATE_IMAGE && + Private->PixelCount > 0xffff0000UL) { + /* If already has active image descriptor - something is wrong! */ + GifFile->Error = E_GIF_ERR_HAS_IMAG_DSCR; + return GIF_ERROR; + } + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + GifFile->Error = E_GIF_ERR_NOT_WRITEABLE; + return GIF_ERROR; + } + GifFile->Image.Left = Left; + GifFile->Image.Top = Top; + GifFile->Image.Width = Width; + GifFile->Image.Height = Height; + GifFile->Image.Interlace = Interlace; + if (ColorMap) { + if (GifFile->Image.ColorMap != NULL) { + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Image.ColorMap = NULL; + } + GifFile->Image.ColorMap = GifMakeMapObject(ColorMap->ColorCount, + ColorMap->Colors); + if (GifFile->Image.ColorMap == NULL) { + GifFile->Error = E_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + } else { + GifFile->Image.ColorMap = NULL; + } + + /* Put the image descriptor into the file: */ + Buf[0] = DESCRIPTOR_INTRODUCER; /* Image separator character. */ + InternalWrite(GifFile, Buf, 1); + (void)EGifPutWord(Left, GifFile); + (void)EGifPutWord(Top, GifFile); + (void)EGifPutWord(Width, GifFile); + (void)EGifPutWord(Height, GifFile); + Buf[0] = (ColorMap ? 0x80 : 0x00) | + (Interlace ? 0x40 : 0x00) | + (ColorMap ? ColorMap->BitsPerPixel - 1 : 0); + InternalWrite(GifFile, Buf, 1); + + /* If we have Global color map - dump it also: */ + if (ColorMap != NULL) { + int i; + for (i = 0; i < ColorMap->ColorCount; i++) { + /* Put the ColorMap out also: */ + Buf[0] = ColorMap->Colors[i].Red; + Buf[1] = ColorMap->Colors[i].Green; + Buf[2] = ColorMap->Colors[i].Blue; + if (InternalWrite(GifFile, Buf, 3) != 3) { + GifFile->Error = E_GIF_ERR_WRITE_FAILED; + return GIF_ERROR; + } + } + } + if (GifFile->SColorMap == NULL && GifFile->Image.ColorMap == NULL) { + GifFile->Error = E_GIF_ERR_NO_COLOR_MAP; + return GIF_ERROR; + } + + /* Mark this file as has screen descriptor: */ + Private->FileState |= FILE_STATE_IMAGE; + Private->PixelCount = (long)Width *(long)Height; + + /* Reset compress algorithm parameters. */ + (void)EGifSetupCompress(GifFile); + + return GIF_OK; +} + +/****************************************************************************** + Put one full scanned line (Line) of length LineLen into GIF file. +******************************************************************************/ +int +EGifPutLine(GifFileType * GifFile, GifPixelType *Line, int LineLen) +{ + int i; + GifPixelType Mask; + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + GifFile->Error = E_GIF_ERR_NOT_WRITEABLE; + return GIF_ERROR; + } + + if (!LineLen) + LineLen = GifFile->Image.Width; + if (Private->PixelCount < (unsigned)LineLen) { + GifFile->Error = E_GIF_ERR_DATA_TOO_BIG; + return GIF_ERROR; + } + Private->PixelCount -= LineLen; + + /* Make sure the codes are not out of bit range, as we might generate + * wrong code (because of overflow when we combine them) in this case: */ + Mask = CodeMask[Private->BitsPerPixel]; + for (i = 0; i < LineLen; i++) + Line[i] &= Mask; + + return EGifCompressLine(GifFile, Line, LineLen); +} + +/****************************************************************************** + Put one pixel (Pixel) into GIF file. +******************************************************************************/ +int +EGifPutPixel(GifFileType *GifFile, GifPixelType Pixel) +{ + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + GifFile->Error = E_GIF_ERR_NOT_WRITEABLE; + return GIF_ERROR; + } + + if (Private->PixelCount == 0) { + GifFile->Error = E_GIF_ERR_DATA_TOO_BIG; + return GIF_ERROR; + } + --Private->PixelCount; + + /* Make sure the code is not out of bit range, as we might generate + * wrong code (because of overflow when we combine them) in this case: */ + Pixel &= CodeMask[Private->BitsPerPixel]; + + return EGifCompressLine(GifFile, &Pixel, 1); +} + +/****************************************************************************** + Put a comment into GIF file using the GIF89 comment extension block. +******************************************************************************/ +int +EGifPutComment(GifFileType *GifFile, const char *Comment) +{ + unsigned int length; + char *buf; + + length = strlen(Comment); + if (length <= 255) { + return EGifPutExtension(GifFile, COMMENT_EXT_FUNC_CODE, + length, Comment); + } else { + buf = (char *)Comment; + if (EGifPutExtensionLeader(GifFile, COMMENT_EXT_FUNC_CODE) + == GIF_ERROR) { + return GIF_ERROR; + } + + /* Break the comment into 255 byte sub blocks */ + while (length > 255) { + if (EGifPutExtensionBlock(GifFile, 255, buf) == GIF_ERROR) { + return GIF_ERROR; + } + buf = buf + 255; + length -= 255; + } + /* Output any partial block and the clear code. */ + if (length > 0) { + if (EGifPutExtensionBlock(GifFile, length, buf) == GIF_ERROR) { + return GIF_ERROR; + } + } + if (EGifPutExtensionTrailer(GifFile) == GIF_ERROR) { + return GIF_ERROR; + } + } + return GIF_OK; +} + +/****************************************************************************** + Begin an extension block (see GIF manual). More + extensions can be dumped using EGifPutExtensionBlock until + EGifPutExtensionTrailer is invoked. +******************************************************************************/ +int +EGifPutExtensionLeader(GifFileType *GifFile, const int ExtCode) +{ + GifByteType Buf[3]; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + GifFile->Error = E_GIF_ERR_NOT_WRITEABLE; + return GIF_ERROR; + } + + Buf[0] = EXTENSION_INTRODUCER; + Buf[1] = ExtCode; + InternalWrite(GifFile, Buf, 2); + + return GIF_OK; +} + +/****************************************************************************** + Put extension block data (see GIF manual) into a GIF file. +******************************************************************************/ +int +EGifPutExtensionBlock(GifFileType *GifFile, + const int ExtLen, + const void *Extension) +{ + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + GifFile->Error = E_GIF_ERR_NOT_WRITEABLE; + return GIF_ERROR; + } + + Buf = ExtLen; + InternalWrite(GifFile, &Buf, 1); + InternalWrite(GifFile, Extension, ExtLen); + + return GIF_OK; +} + +/****************************************************************************** + Put a terminating block (see GIF manual) into a GIF file. +******************************************************************************/ +int +EGifPutExtensionTrailer(GifFileType *GifFile) { + + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + GifFile->Error = E_GIF_ERR_NOT_WRITEABLE; + return GIF_ERROR; + } + + /* Write the block terminator */ + Buf = 0; + InternalWrite(GifFile, &Buf, 1); + + return GIF_OK; +} + +/****************************************************************************** + Put an extension block (see GIF manual) into a GIF file. + Warning: This function is only useful for Extension blocks that have at + most one subblock. Extensions with more than one subblock need to use the + EGifPutExtension{Leader,Block,Trailer} functions instead. +******************************************************************************/ +int +EGifPutExtension(GifFileType *GifFile, + const int ExtCode, + const int ExtLen, + const void *Extension) { + + GifByteType Buf[3]; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + GifFile->Error = E_GIF_ERR_NOT_WRITEABLE; + return GIF_ERROR; + } + + if (ExtCode == 0) + InternalWrite(GifFile, (GifByteType *)&ExtLen, 1); + else { + Buf[0] = EXTENSION_INTRODUCER; + Buf[1] = ExtCode; /* Extension Label */ + Buf[2] = ExtLen; /* Extension length */ + InternalWrite(GifFile, Buf, 3); + } + InternalWrite(GifFile, Extension, ExtLen); + Buf[0] = 0; + InternalWrite(GifFile, Buf, 1); + + return GIF_OK; +} + +/****************************************************************************** + Render a Graphics Control Block as raw extension data +******************************************************************************/ + +size_t EGifGCBToExtension(const GraphicsControlBlock *GCB, + GifByteType *GifExtension) +{ + GifExtension[0] = 0; + GifExtension[0] |= (GCB->TransparentColor == NO_TRANSPARENT_COLOR) ? 0x00 : 0x01; + GifExtension[0] |= GCB->UserInputFlag ? 0x02 : 0x00; + GifExtension[0] |= ((GCB->DisposalMode & 0x07) << 2); + GifExtension[1] = LOBYTE(GCB->DelayTime); + GifExtension[2] = HIBYTE(GCB->DelayTime); + GifExtension[3] = (char)GCB->TransparentColor; + return 4; +} + +/****************************************************************************** + Replace the Graphics Control Block for a saved image, if it exists. +******************************************************************************/ + +int EGifGCBToSavedExtension(const GraphicsControlBlock *GCB, + GifFileType *GifFile, int ImageIndex) +{ + int i; + size_t Len; + GifByteType buf[sizeof(GraphicsControlBlock)]; /* a bit dodgy... */ + + if (ImageIndex < 0 || ImageIndex > GifFile->ImageCount - 1) + return GIF_ERROR; + + for (i = 0; i < GifFile->SavedImages[ImageIndex].ExtensionBlockCount; i++) { + ExtensionBlock *ep = &GifFile->SavedImages[ImageIndex].ExtensionBlocks[i]; + if (ep->Function == GRAPHICS_EXT_FUNC_CODE) { + EGifGCBToExtension(GCB, ep->Bytes); + return GIF_OK; + } + } + + Len = EGifGCBToExtension(GCB, (GifByteType *)buf); + if (GifAddExtensionBlock(&GifFile->SavedImages[ImageIndex].ExtensionBlockCount, + &GifFile->SavedImages[ImageIndex].ExtensionBlocks, + GRAPHICS_EXT_FUNC_CODE, + Len, + (unsigned char *)buf) == GIF_ERROR) + return (GIF_ERROR); + + return (GIF_OK); +} + +/****************************************************************************** + Put the image code in compressed form. This routine can be called if the + information needed to be piped out as is. Obviously this is much faster + than decoding and encoding again. This routine should be followed by calls + to EGifPutCodeNext, until NULL block is given. + The block should NOT be freed by the user (not dynamically allocated). +******************************************************************************/ +int +EGifPutCode(GifFileType *GifFile, int CodeSize, const GifByteType *CodeBlock) +{ + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + GifFile->Error = E_GIF_ERR_NOT_WRITEABLE; + return GIF_ERROR; + } + + /* No need to dump code size as Compression set up does any for us: */ + /* + * Buf = CodeSize; + * if (InternalWrite(GifFile, &Buf, 1) != 1) { + * GifFile->Error = E_GIF_ERR_WRITE_FAILED; + * return GIF_ERROR; + * } + */ + + return EGifPutCodeNext(GifFile, CodeBlock); +} + +/****************************************************************************** + Continue to put the image code in compressed form. This routine should be + called with blocks of code as read via DGifGetCode/DGifGetCodeNext. If + given buffer pointer is NULL, empty block is written to mark end of code. +******************************************************************************/ +int +EGifPutCodeNext(GifFileType *GifFile, const GifByteType *CodeBlock) +{ + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (CodeBlock != NULL) { + if (InternalWrite(GifFile, CodeBlock, CodeBlock[0] + 1) + != (unsigned)(CodeBlock[0] + 1)) { + GifFile->Error = E_GIF_ERR_WRITE_FAILED; + return GIF_ERROR; + } + } else { + Buf = 0; + if (InternalWrite(GifFile, &Buf, 1) != 1) { + GifFile->Error = E_GIF_ERR_WRITE_FAILED; + return GIF_ERROR; + } + Private->PixelCount = 0; /* And local info. indicate image read. */ + } + + return GIF_OK; +} + +/****************************************************************************** + This routine should be called last, to close the GIF file. +******************************************************************************/ +int +EGifCloseFile(GifFileType *GifFile, int *ErrorCode) +{ + GifByteType Buf; + GifFilePrivateType *Private; + FILE *File; + + if (GifFile == NULL) + return GIF_ERROR; + + Private = (GifFilePrivateType *) GifFile->Private; + if (Private == NULL) + return GIF_ERROR; + if (!IS_WRITEABLE(Private)) { + /* This file was NOT open for writing: */ + if (ErrorCode != NULL) + *ErrorCode = E_GIF_ERR_NOT_WRITEABLE; + free(GifFile); + return GIF_ERROR; + } + + File = Private->File; + + Buf = TERMINATOR_INTRODUCER; + InternalWrite(GifFile, &Buf, 1); + + if (GifFile->Image.ColorMap) { + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Image.ColorMap = NULL; + } + if (GifFile->SColorMap) { + GifFreeMapObject(GifFile->SColorMap); + GifFile->SColorMap = NULL; + } + if (Private) { + if (Private->HashTable) { + free((char *) Private->HashTable); + } + free((char *) Private); + } + + if (File && fclose(File) != 0) { + if (ErrorCode != NULL) + *ErrorCode = E_GIF_ERR_CLOSE_FAILED; + free(GifFile); + return GIF_ERROR; + } + + free(GifFile); + if (ErrorCode != NULL) + *ErrorCode = E_GIF_SUCCEEDED; + return GIF_OK; +} + +/****************************************************************************** + Put 2 bytes (a word) into the given file in little-endian order: +******************************************************************************/ +static int +EGifPutWord(int Word, GifFileType *GifFile) +{ + unsigned char c[2]; + + c[0] = LOBYTE(Word); + c[1] = HIBYTE(Word); + if (InternalWrite(GifFile, c, 2) == 2) + return GIF_OK; + else + return GIF_ERROR; +} + +/****************************************************************************** + Setup the LZ compression for this image: +******************************************************************************/ +static int +EGifSetupCompress(GifFileType *GifFile) +{ + int BitsPerPixel; + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + + /* Test and see what color map to use, and from it # bits per pixel: */ + if (GifFile->Image.ColorMap) + BitsPerPixel = GifFile->Image.ColorMap->BitsPerPixel; + else if (GifFile->SColorMap) + BitsPerPixel = GifFile->SColorMap->BitsPerPixel; + else { + GifFile->Error = E_GIF_ERR_NO_COLOR_MAP; + return GIF_ERROR; + } + + Buf = BitsPerPixel = (BitsPerPixel < 2 ? 2 : BitsPerPixel); + InternalWrite(GifFile, &Buf, 1); /* Write the Code size to file. */ + + Private->Buf[0] = 0; /* Nothing was output yet. */ + Private->BitsPerPixel = BitsPerPixel; + Private->ClearCode = (1 << BitsPerPixel); + Private->EOFCode = Private->ClearCode + 1; + Private->RunningCode = Private->EOFCode + 1; + Private->RunningBits = BitsPerPixel + 1; /* Number of bits per code. */ + Private->MaxCode1 = 1 << Private->RunningBits; /* Max. code + 1. */ + Private->CrntCode = FIRST_CODE; /* Signal that this is first one! */ + Private->CrntShiftState = 0; /* No information in CrntShiftDWord. */ + Private->CrntShiftDWord = 0; + + /* Clear hash table and send Clear to make sure the decoder do the same. */ + _ClearHashTable(Private->HashTable); + + if (EGifCompressOutput(GifFile, Private->ClearCode) == GIF_ERROR) { + GifFile->Error = E_GIF_ERR_DISK_IS_FULL; + return GIF_ERROR; + } + return GIF_OK; +} + +/****************************************************************************** + The LZ compression routine: + This version compresses the given buffer Line of length LineLen. + This routine can be called a few times (one per scan line, for example), in + order to complete the whole image. +******************************************************************************/ +static int +EGifCompressLine(GifFileType *GifFile, + GifPixelType *Line, + const int LineLen) +{ + int i = 0, CrntCode, NewCode; + unsigned long NewKey; + GifPixelType Pixel; + GifHashTableType *HashTable; + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + + HashTable = Private->HashTable; + + if (Private->CrntCode == FIRST_CODE) /* Its first time! */ + CrntCode = Line[i++]; + else + CrntCode = Private->CrntCode; /* Get last code in compression. */ + + while (i < LineLen) { /* Decode LineLen items. */ + Pixel = Line[i++]; /* Get next pixel from stream. */ + /* Form a new unique key to search hash table for the code combines + * CrntCode as Prefix string with Pixel as postfix char. + */ + NewKey = (((uint32_t) CrntCode) << 8) + Pixel; + if ((NewCode = _ExistsHashTable(HashTable, NewKey)) >= 0) { + /* This Key is already there, or the string is old one, so + * simple take new code as our CrntCode: + */ + CrntCode = NewCode; + } else { + /* Put it in hash table, output the prefix code, and make our + * CrntCode equal to Pixel. + */ + if (EGifCompressOutput(GifFile, CrntCode) == GIF_ERROR) { + GifFile->Error = E_GIF_ERR_DISK_IS_FULL; + return GIF_ERROR; + } + CrntCode = Pixel; + + /* If however the HashTable if full, we send a clear first and + * Clear the hash table. + */ + if (Private->RunningCode >= LZ_MAX_CODE) { + /* Time to do some clearance: */ + if (EGifCompressOutput(GifFile, Private->ClearCode) + == GIF_ERROR) { + GifFile->Error = E_GIF_ERR_DISK_IS_FULL; + return GIF_ERROR; + } + Private->RunningCode = Private->EOFCode + 1; + Private->RunningBits = Private->BitsPerPixel + 1; + Private->MaxCode1 = 1 << Private->RunningBits; + _ClearHashTable(HashTable); + } else { + /* Put this unique key with its relative Code in hash table: */ + _InsertHashTable(HashTable, NewKey, Private->RunningCode++); + } + } + + } + + /* Preserve the current state of the compression algorithm: */ + Private->CrntCode = CrntCode; + + if (Private->PixelCount == 0) { + /* We are done - output last Code and flush output buffers: */ + if (EGifCompressOutput(GifFile, CrntCode) == GIF_ERROR) { + GifFile->Error = E_GIF_ERR_DISK_IS_FULL; + return GIF_ERROR; + } + if (EGifCompressOutput(GifFile, Private->EOFCode) == GIF_ERROR) { + GifFile->Error = E_GIF_ERR_DISK_IS_FULL; + return GIF_ERROR; + } + if (EGifCompressOutput(GifFile, FLUSH_OUTPUT) == GIF_ERROR) { + GifFile->Error = E_GIF_ERR_DISK_IS_FULL; + return GIF_ERROR; + } + } + + return GIF_OK; +} + +/****************************************************************************** + The LZ compression output routine: + This routine is responsible for the compression of the bit stream into + 8 bits (bytes) packets. + Returns GIF_OK if written successfully. +******************************************************************************/ +static int +EGifCompressOutput(GifFileType *GifFile, + const int Code) +{ + GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; + int retval = GIF_OK; + + if (Code == FLUSH_OUTPUT) { + while (Private->CrntShiftState > 0) { + /* Get Rid of what is left in DWord, and flush it. */ + if (EGifBufferedOutput(GifFile, Private->Buf, + Private->CrntShiftDWord & 0xff) == GIF_ERROR) + retval = GIF_ERROR; + Private->CrntShiftDWord >>= 8; + Private->CrntShiftState -= 8; + } + Private->CrntShiftState = 0; /* For next time. */ + if (EGifBufferedOutput(GifFile, Private->Buf, + FLUSH_OUTPUT) == GIF_ERROR) + retval = GIF_ERROR; + } else { + Private->CrntShiftDWord |= ((long)Code) << Private->CrntShiftState; + Private->CrntShiftState += Private->RunningBits; + while (Private->CrntShiftState >= 8) { + /* Dump out full bytes: */ + if (EGifBufferedOutput(GifFile, Private->Buf, + Private->CrntShiftDWord & 0xff) == GIF_ERROR) + retval = GIF_ERROR; + Private->CrntShiftDWord >>= 8; + Private->CrntShiftState -= 8; + } + } + + /* If code cannt fit into RunningBits bits, must raise its size. Note */ + /* however that codes above 4095 are used for special signaling. */ + if (Private->RunningCode >= Private->MaxCode1 && Code <= 4095) { + Private->MaxCode1 = 1 << ++Private->RunningBits; + } + + return retval; +} + +/****************************************************************************** + This routines buffers the given characters until 255 characters are ready + to be output. If Code is equal to -1 the buffer is flushed (EOF). + The buffer is Dumped with first byte as its size, as GIF format requires. + Returns GIF_OK if written successfully. +******************************************************************************/ +static int +EGifBufferedOutput(GifFileType *GifFile, + GifByteType *Buf, + int c) +{ + if (c == FLUSH_OUTPUT) { + /* Flush everything out. */ + if (Buf[0] != 0 + && InternalWrite(GifFile, Buf, Buf[0] + 1) != (unsigned)(Buf[0] + 1)) { + GifFile->Error = E_GIF_ERR_WRITE_FAILED; + return GIF_ERROR; + } + /* Mark end of compressed data, by an empty block (see GIF doc): */ + Buf[0] = 0; + if (InternalWrite(GifFile, Buf, 1) != 1) { + GifFile->Error = E_GIF_ERR_WRITE_FAILED; + return GIF_ERROR; + } + } else { + if (Buf[0] == 255) { + /* Dump out this buffer - it is full: */ + if (InternalWrite(GifFile, Buf, Buf[0] + 1) != (unsigned)(Buf[0] + 1)) { + GifFile->Error = E_GIF_ERR_WRITE_FAILED; + return GIF_ERROR; + } + Buf[0] = 0; + } + Buf[++Buf[0]] = c; + } + + return GIF_OK; +} + +/****************************************************************************** + This routine writes to disk an in-core representation of a GIF previously + created by DGifSlurp(). +******************************************************************************/ + +static int +EGifWriteExtensions(GifFileType *GifFileOut, + ExtensionBlock *ExtensionBlocks, + int ExtensionBlockCount) +{ + if (ExtensionBlocks) { + ExtensionBlock *ep; + int j; + + for (j = 0; j < ExtensionBlockCount; j++) { + ep = &ExtensionBlocks[j]; + if (ep->Function != CONTINUE_EXT_FUNC_CODE) + if (EGifPutExtensionLeader(GifFileOut, ep->Function) == GIF_ERROR) + return (GIF_ERROR); + if (EGifPutExtensionBlock(GifFileOut, ep->ByteCount, ep->Bytes) == GIF_ERROR) + return (GIF_ERROR); + if (j == ExtensionBlockCount - 1 || (ep+1)->Function != CONTINUE_EXT_FUNC_CODE) + if (EGifPutExtensionTrailer(GifFileOut) == GIF_ERROR) + return (GIF_ERROR); + } + } + + return (GIF_OK); +} + +int +EGifSpew(GifFileType *GifFileOut) +{ + int i, j; + + if (EGifPutScreenDesc(GifFileOut, + GifFileOut->SWidth, + GifFileOut->SHeight, + GifFileOut->SColorResolution, + GifFileOut->SBackGroundColor, + GifFileOut->SColorMap) == GIF_ERROR) { + return (GIF_ERROR); + } + + for (i = 0; i < GifFileOut->ImageCount; i++) { + SavedImage *sp = &GifFileOut->SavedImages[i]; + int SavedHeight = sp->ImageDesc.Height; + int SavedWidth = sp->ImageDesc.Width; + + /* this allows us to delete images by nuking their rasters */ + if (sp->RasterBits == NULL) + continue; + + if (EGifWriteExtensions(GifFileOut, + sp->ExtensionBlocks, + sp->ExtensionBlockCount) == GIF_ERROR) + return (GIF_ERROR); + + if (EGifPutImageDesc(GifFileOut, + sp->ImageDesc.Left, + sp->ImageDesc.Top, + SavedWidth, + SavedHeight, + sp->ImageDesc.Interlace, + sp->ImageDesc.ColorMap) == GIF_ERROR) + return (GIF_ERROR); + + if (sp->ImageDesc.Interlace) { + /* + * The way an interlaced image should be written - + * offsets and jumps... + */ + int InterlacedOffset[] = { 0, 4, 2, 1 }; + int InterlacedJumps[] = { 8, 8, 4, 2 }; + int k; + /* Need to perform 4 passes on the images: */ + for (k = 0; k < 4; k++) + for (j = InterlacedOffset[k]; + j < SavedHeight; + j += InterlacedJumps[k]) { + if (EGifPutLine(GifFileOut, + sp->RasterBits + j * SavedWidth, + SavedWidth) == GIF_ERROR) + return (GIF_ERROR); + } + } else { + for (j = 0; j < SavedHeight; j++) { + if (EGifPutLine(GifFileOut, + sp->RasterBits + j * SavedWidth, + SavedWidth) == GIF_ERROR) + return (GIF_ERROR); + } + } + } + + if (EGifWriteExtensions(GifFileOut, + GifFileOut->ExtensionBlocks, + GifFileOut->ExtensionBlockCount) == GIF_ERROR) + return (GIF_ERROR); + + if (EGifCloseFile(GifFileOut, NULL) == GIF_ERROR) + return (GIF_ERROR); + + return (GIF_OK); +} + +/* end */ diff --git a/src/lib/gif/gif_err.c b/src/lib/gif/gif_err.c new file mode 100644 index 00000000..3ec2a56f --- /dev/null +++ b/src/lib/gif/gif_err.c @@ -0,0 +1,97 @@ +/***************************************************************************** + +gif_err.c - handle error reporting for the GIF library. + +****************************************************************************/ + +#include + +#include "gif_lib.h" +#include "gif_lib_private.h" + +/***************************************************************************** + Return a string description of the last GIF error +*****************************************************************************/ +const char * +GifErrorString(int ErrorCode) +{ + const char *Err; + + switch (ErrorCode) { + case E_GIF_ERR_OPEN_FAILED: + Err = "Failed to open given file"; + break; + case E_GIF_ERR_WRITE_FAILED: + Err = "Failed to write to given file"; + break; + case E_GIF_ERR_HAS_SCRN_DSCR: + Err = "Screen descriptor has already been set"; + break; + case E_GIF_ERR_HAS_IMAG_DSCR: + Err = "Image descriptor is still active"; + break; + case E_GIF_ERR_NO_COLOR_MAP: + Err = "Neither global nor local color map"; + break; + case E_GIF_ERR_DATA_TOO_BIG: + Err = "Number of pixels bigger than width * height"; + break; + case E_GIF_ERR_NOT_ENOUGH_MEM: + Err = "Failed to allocate required memory"; + break; + case E_GIF_ERR_DISK_IS_FULL: + Err = "Write failed (disk full?)"; + break; + case E_GIF_ERR_CLOSE_FAILED: + Err = "Failed to close given file"; + break; + case E_GIF_ERR_NOT_WRITEABLE: + Err = "Given file was not opened for write"; + break; + case D_GIF_ERR_OPEN_FAILED: + Err = "Failed to open given file"; + break; + case D_GIF_ERR_READ_FAILED: + Err = "Failed to read from given file"; + break; + case D_GIF_ERR_NOT_GIF_FILE: + Err = "Data is not in GIF format"; + break; + case D_GIF_ERR_NO_SCRN_DSCR: + Err = "No screen descriptor detected"; + break; + case D_GIF_ERR_NO_IMAG_DSCR: + Err = "No Image Descriptor detected"; + break; + case D_GIF_ERR_NO_COLOR_MAP: + Err = "Neither global nor local color map"; + break; + case D_GIF_ERR_WRONG_RECORD: + Err = "Wrong record type detected"; + break; + case D_GIF_ERR_DATA_TOO_BIG: + Err = "Number of pixels bigger than width * height"; + break; + case D_GIF_ERR_NOT_ENOUGH_MEM: + Err = "Failed to allocate required memory"; + break; + case D_GIF_ERR_CLOSE_FAILED: + Err = "Failed to close given file"; + break; + case D_GIF_ERR_NOT_READABLE: + Err = "Given file was not opened for read"; + break; + case D_GIF_ERR_IMAGE_DEFECT: + Err = "Image is defective, decoding aborted"; + break; + case D_GIF_ERR_EOF_TOO_SOON: + Err = "Image EOF detected before image complete"; + break; + default: + Err = NULL; + break; + } + return Err; +} + +/* end */ diff --git a/src/lib/gif/gif_font.c b/src/lib/gif/gif_font.c new file mode 100644 index 00000000..ba47b29e --- /dev/null +++ b/src/lib/gif/gif_font.c @@ -0,0 +1,252 @@ +/***************************************************************************** + +gif_font.c - utility font handling and simple drawing for the GIF library + +****************************************************************************/ + +#include + +#include "gif_lib.h" + +/***************************************************************************** + Ascii 8 by 8 regular font - only first 128 characters are supported. +*****************************************************************************/ + +/* + * Each array entry holds the bits for 8 horizontal scan lines, topmost + * first. The most significant bit of each constant is the leftmost bit of + * the scan line. + */ +/*@+charint@*/ +const unsigned char GifAsciiTable8x8[][GIF_FONT_WIDTH] = { + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /* Ascii 0 */ + {0x3c, 0x42, 0xa5, 0x81, 0xbd, 0x42, 0x3c, 0x00}, /* Ascii 1 */ + {0x3c, 0x7e, 0xdb, 0xff, 0xc3, 0x7e, 0x3c, 0x00}, /* Ascii 2 */ + {0x00, 0xee, 0xfe, 0xfe, 0x7c, 0x38, 0x10, 0x00}, /* Ascii 3 */ + {0x10, 0x38, 0x7c, 0xfe, 0x7c, 0x38, 0x10, 0x00}, /* Ascii 4 */ + {0x00, 0x3c, 0x18, 0xff, 0xff, 0x08, 0x18, 0x00}, /* Ascii 5 */ + {0x10, 0x38, 0x7c, 0xfe, 0xfe, 0x10, 0x38, 0x00}, /* Ascii 6 */ + {0x00, 0x00, 0x18, 0x3c, 0x18, 0x00, 0x00, 0x00}, /* Ascii 7 */ + {0xff, 0xff, 0xe7, 0xc3, 0xe7, 0xff, 0xff, 0xff}, /* Ascii 8 */ + {0x00, 0x3c, 0x42, 0x81, 0x81, 0x42, 0x3c, 0x00}, /* Ascii 9 */ + {0xff, 0xc3, 0xbd, 0x7e, 0x7e, 0xbd, 0xc3, 0xff}, /* Ascii 10 */ + {0x1f, 0x07, 0x0d, 0x7c, 0xc6, 0xc6, 0x7c, 0x00}, /* Ascii 11 */ + {0x00, 0x7e, 0xc3, 0xc3, 0x7e, 0x18, 0x7e, 0x18}, /* Ascii 12 */ + {0x04, 0x06, 0x07, 0x04, 0x04, 0xfc, 0xf8, 0x00}, /* Ascii 13 */ + {0x0c, 0x0a, 0x0d, 0x0b, 0xf9, 0xf9, 0x1f, 0x1f}, /* Ascii 14 */ + {0x00, 0x92, 0x7c, 0x44, 0xc6, 0x7c, 0x92, 0x00}, /* Ascii 15 */ + {0x00, 0x00, 0x60, 0x78, 0x7e, 0x78, 0x60, 0x00}, /* Ascii 16 */ + {0x00, 0x00, 0x06, 0x1e, 0x7e, 0x1e, 0x06, 0x00}, /* Ascii 17 */ + {0x18, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x18}, /* Ascii 18 */ + {0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x66, 0x00}, /* Ascii 19 */ + {0xff, 0xb6, 0x76, 0x36, 0x36, 0x36, 0x36, 0x00}, /* Ascii 20 */ + {0x7e, 0xc1, 0xdc, 0x22, 0x22, 0x1f, 0x83, 0x7e}, /* Ascii 21 */ + {0x00, 0x00, 0x00, 0x7e, 0x7e, 0x00, 0x00, 0x00}, /* Ascii 22 */ + {0x18, 0x7e, 0x18, 0x18, 0x7e, 0x18, 0x00, 0xff}, /* Ascii 23 */ + {0x18, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00}, /* Ascii 24 */ + {0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x18, 0x00}, /* Ascii 25 */ + {0x00, 0x04, 0x06, 0xff, 0x06, 0x04, 0x00, 0x00}, /* Ascii 26 */ + {0x00, 0x20, 0x60, 0xff, 0x60, 0x20, 0x00, 0x00}, /* Ascii 27 */ + {0x00, 0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xff, 0x00}, /* Ascii 28 */ + {0x00, 0x24, 0x66, 0xff, 0x66, 0x24, 0x00, 0x00}, /* Ascii 29 */ + {0x00, 0x00, 0x10, 0x38, 0x7c, 0xfe, 0x00, 0x00}, /* Ascii 30 */ + {0x00, 0x00, 0x00, 0xfe, 0x7c, 0x38, 0x10, 0x00}, /* Ascii 31 */ + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /* */ + {0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, 0x00}, /* ! */ + {0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /* " */ + {0x6c, 0x6c, 0xfe, 0x6c, 0xfe, 0x6c, 0x6c, 0x00}, /* # */ + {0x10, 0x7c, 0xd2, 0x7c, 0x86, 0x7c, 0x10, 0x00}, /* $ */ + {0xf0, 0x96, 0xfc, 0x18, 0x3e, 0x72, 0xde, 0x00}, /* % */ + {0x30, 0x48, 0x30, 0x78, 0xce, 0xcc, 0x78, 0x00}, /* & */ + {0x0c, 0x0c, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, /* ' */ + {0x10, 0x60, 0xc0, 0xc0, 0xc0, 0x60, 0x10, 0x00}, /* ( */ + {0x10, 0x0c, 0x06, 0x06, 0x06, 0x0c, 0x10, 0x00}, /* ) */ + {0x00, 0x54, 0x38, 0xfe, 0x38, 0x54, 0x00, 0x00}, /* * */ + {0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00}, /* + */ + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x70}, /* , */ + {0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00}, /* - */ + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00}, /* . */ + {0x02, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x00}, /* / */ + {0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, /* 0 */ + {0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x3c, 0x00}, /* 1 */ + {0x7c, 0xc6, 0x06, 0x0c, 0x30, 0x60, 0xfe, 0x00}, /* 2 */ + {0x7c, 0xc6, 0x06, 0x3c, 0x06, 0xc6, 0x7c, 0x00}, /* 3 */ + {0x0e, 0x1e, 0x36, 0x66, 0xfe, 0x06, 0x06, 0x00}, /* 4 */ + {0xfe, 0xc0, 0xc0, 0xfc, 0x06, 0x06, 0xfc, 0x00}, /* 5 */ + {0x7c, 0xc6, 0xc0, 0xfc, 0xc6, 0xc6, 0x7c, 0x00}, /* 6 */ + {0xfe, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x60, 0x00}, /* 7 */ + {0x7c, 0xc6, 0xc6, 0x7c, 0xc6, 0xc6, 0x7c, 0x00}, /* 8 */ + {0x7c, 0xc6, 0xc6, 0x7e, 0x06, 0xc6, 0x7c, 0x00}, /* 9 */ + {0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00}, /* : */ + {0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x20, 0x00}, /* }, */ + {0x00, 0x1c, 0x30, 0x60, 0x30, 0x1c, 0x00, 0x00}, /* < */ + {0x00, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x00, 0x00}, /* = */ + {0x00, 0x70, 0x18, 0x0c, 0x18, 0x70, 0x00, 0x00}, /* > */ + {0x7c, 0xc6, 0x0c, 0x18, 0x30, 0x00, 0x30, 0x00}, /* ? */ + {0x7c, 0x82, 0x9a, 0xaa, 0xaa, 0x9e, 0x7c, 0x00}, /* @ */ + {0x38, 0x6c, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0x00}, /* A */ + {0xfc, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xfc, 0x00}, /* B */ + {0x7c, 0xc6, 0xc6, 0xc0, 0xc0, 0xc6, 0x7c, 0x00}, /* C */ + {0xf8, 0xcc, 0xc6, 0xc6, 0xc6, 0xcc, 0xf8, 0x00}, /* D */ + {0xfe, 0xc0, 0xc0, 0xfc, 0xc0, 0xc0, 0xfe, 0x00}, /* E */ + {0xfe, 0xc0, 0xc0, 0xfc, 0xc0, 0xc0, 0xc0, 0x00}, /* F */ + {0x7c, 0xc6, 0xc0, 0xce, 0xc6, 0xc6, 0x7e, 0x00}, /* G */ + {0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0x00}, /* H */ + {0x78, 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00}, /* I */ + {0x1e, 0x06, 0x06, 0x06, 0xc6, 0xc6, 0x7c, 0x00}, /* J */ + {0xc6, 0xcc, 0xd8, 0xf0, 0xd8, 0xcc, 0xc6, 0x00}, /* K */ + {0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xfe, 0x00}, /* L */ + {0xc6, 0xee, 0xfe, 0xd6, 0xc6, 0xc6, 0xc6, 0x00}, /* M */ + {0xc6, 0xe6, 0xf6, 0xde, 0xce, 0xc6, 0xc6, 0x00}, /* N */ + {0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, /* O */ + {0xfc, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0x00}, /* P */ + {0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x06}, /* Q */ + {0xfc, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0x00}, /* R */ + {0x78, 0xcc, 0x60, 0x30, 0x18, 0xcc, 0x78, 0x00}, /* S */ + {0xfc, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00}, /* T */ + {0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, /* U */ + {0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x00}, /* V */ + {0xc6, 0xc6, 0xc6, 0xd6, 0xfe, 0xee, 0xc6, 0x00}, /* W */ + {0xc6, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0xc6, 0x00}, /* X */ + {0xc3, 0xc3, 0x66, 0x3c, 0x18, 0x18, 0x18, 0x00}, /* Y */ + {0xfe, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0xfe, 0x00}, /* Z */ + {0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00}, /* [ */ + {0xc0, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x03, 0x00}, /* \ */ + {0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00}, /* ] */ + {0x00, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00}, /* ^ */ + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff}, /* _ */ + {0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, /* ` */ + {0x00, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0x7e, 0x00}, /* a */ + {0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xe6, 0xdc, 0x00}, /* b */ + {0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0x7e, 0x00}, /* c */ + {0x06, 0x06, 0x7e, 0xc6, 0xc6, 0xce, 0x76, 0x00}, /* d */ + {0x00, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0x7e, 0x00}, /* e */ + {0x1e, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x00}, /* f */ + {0x00, 0x00, 0x7e, 0xc6, 0xce, 0x76, 0x06, 0x7c}, /* g */ + {0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00}, /* */ + {0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00}, /* i */ + {0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0xf0}, /* j */ + {0xc0, 0xc0, 0xcc, 0xd8, 0xf0, 0xd8, 0xcc, 0x00}, /* k */ + {0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00}, /* l */ + {0x00, 0x00, 0xcc, 0xfe, 0xd6, 0xc6, 0xc6, 0x00}, /* m */ + {0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00}, /* n */ + {0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, /* o */ + {0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xe6, 0xdc, 0xc0}, /* p */ + {0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xce, 0x76, 0x06}, /* q */ + {0x00, 0x00, 0x6e, 0x70, 0x60, 0x60, 0x60, 0x00}, /* r */ + {0x00, 0x00, 0x7c, 0xc0, 0x7c, 0x06, 0xfc, 0x00}, /* s */ + {0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x1c, 0x00}, /* t */ + {0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00}, /* u */ + {0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x00}, /* v */ + {0x00, 0x00, 0xc6, 0xc6, 0xd6, 0xfe, 0x6c, 0x00}, /* w */ + {0x00, 0x00, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0x00}, /* x */ + {0x00, 0x00, 0xc6, 0xc6, 0xce, 0x76, 0x06, 0x7c}, /* y */ + {0x00, 0x00, 0xfc, 0x18, 0x30, 0x60, 0xfc, 0x00}, /* z */ + {0x0e, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0e, 0x00}, /* { */ + {0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, /* | */ + {0xe0, 0x30, 0x30, 0x1c, 0x30, 0x30, 0xe0, 0x00}, /* } */ + {0x00, 0x00, 0x70, 0x9a, 0x0e, 0x00, 0x00, 0x00}, /* ~ */ + {0x00, 0x00, 0x18, 0x3c, 0x66, 0xff, 0x00, 0x00} /* Ascii 127 */ +}; +/*@=charint@*/ + +void +GifDrawText8x8(SavedImage *Image, + const int x, const int y, + const char *legend, + const int color) +{ + int i, j; + int base; + const char *cp; + + for (i = 0; i < GIF_FONT_HEIGHT; i++) { + base = Image->ImageDesc.Width * (y + i) + x; + + for (cp = legend; *cp; cp++) + for (j = 0; j < GIF_FONT_WIDTH; j++) { + if (GifAsciiTable8x8[(short)(*cp)][i] & (1 << (GIF_FONT_WIDTH - j))) + Image->RasterBits[base] = color; + base++; + } + } +} + +void +GifDrawBox(SavedImage *Image, + const int x, const int y, + const int w, const int d, + const int color) +{ + int j, base = Image->ImageDesc.Width * y + x; + + for (j = 0; j < w; j++) + Image->RasterBits[base + j] = + Image->RasterBits[base + (d * Image->ImageDesc.Width) + j] = color; + + for (j = 0; j < d; j++) + Image->RasterBits[base + j * Image->ImageDesc.Width] = + Image->RasterBits[base + j * Image->ImageDesc.Width + w] = color; +} + +void +GifDrawRectangle(SavedImage *Image, + const int x, const int y, + const int w, const int d, + const int color) +{ + unsigned char *bp = Image->RasterBits + Image->ImageDesc.Width * y + x; + int i; + + for (i = 0; i < d; i++) + memset(bp + (i * Image->ImageDesc.Width), color, (size_t)w); +} + +void +GifDrawBoxedText8x8(SavedImage *Image, + const int x, const int y, + const char *legend, + const int border, + const int bg, const int fg) +{ + int i, j = 0, LineCount = 0, TextWidth = 0; + const char *cp; + + /* compute size of text to box */ + for (cp = legend; *cp; cp++) + if (*cp == '\r') { + if (j > TextWidth) + TextWidth = j; + j = 0; + LineCount++; + } else if (*cp != '\t') + ++j; + LineCount++; /* count last line */ + if (j > TextWidth) /* last line might be longer than any previous */ + TextWidth = j; + + /* fill the box */ + GifDrawRectangle(Image, x + 1, y + 1, + border + TextWidth * GIF_FONT_WIDTH + border - 1, + border + LineCount * GIF_FONT_HEIGHT + border - 1, bg); + + /* draw the text */ + i = 0; + cp = strtok((char *)legend, "\r\n"); + do { + int leadspace = 0; + + if (cp[0] == '\t') + leadspace = (TextWidth - strlen(++cp)) / 2; + + GifDrawText8x8(Image, x + border + (leadspace * GIF_FONT_WIDTH), + y + border + (GIF_FONT_HEIGHT * i++), cp, fg); + cp = strtok((char *)NULL, "\r\n"); + } while (cp); + + /* outline the box */ + GifDrawBox(Image, x, y, border + TextWidth * GIF_FONT_WIDTH + border, + border + LineCount * GIF_FONT_HEIGHT + border, fg); +} + +/* end */ diff --git a/src/lib/gif/gif_hash.c b/src/lib/gif/gif_hash.c new file mode 100644 index 00000000..61a4d139 --- /dev/null +++ b/src/lib/gif/gif_hash.c @@ -0,0 +1,132 @@ +/***************************************************************************** + +gif_hash.c -- module to support the following operations: + +1. InitHashTable - initialize hash table. +2. ClearHashTable - clear the hash table to an empty state. +2. InsertHashTable - insert one item into data structure. +3. ExistsHashTable - test if item exists in data structure. + +This module is used to hash the GIF codes during encoding. + +*****************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include "gif_lib.h" +#include "gif_hash.h" +#include "gif_lib_private.h" + +/* #define DEBUG_HIT_RATE Debug number of misses per hash Insert/Exists. */ + +#ifdef DEBUG_HIT_RATE +static long NumberOfTests = 0, + NumberOfMisses = 0; +#endif /* DEBUG_HIT_RATE */ + +static int KeyItem(uint32_t Item); + +/****************************************************************************** + Initialize HashTable - allocate the memory needed and clear it. * +******************************************************************************/ +GifHashTableType *_InitHashTable(void) +{ + GifHashTableType *HashTable; + + if ((HashTable = (GifHashTableType *) malloc(sizeof(GifHashTableType))) + == NULL) + return NULL; + + _ClearHashTable(HashTable); + + return HashTable; +} + +/****************************************************************************** + Routine to clear the HashTable to an empty state. * + This part is a little machine depended. Use the commented part otherwise. * +******************************************************************************/ +void _ClearHashTable(GifHashTableType *HashTable) +{ + memset(HashTable -> HTable, 0xFF, HT_SIZE * sizeof(uint32_t)); +} + +/****************************************************************************** + Routine to insert a new Item into the HashTable. The data is assumed to be * + new one. * +******************************************************************************/ +void _InsertHashTable(GifHashTableType *HashTable, uint32_t Key, int Code) +{ + int HKey = KeyItem(Key); + uint32_t *HTable = HashTable -> HTable; + +#ifdef DEBUG_HIT_RATE + NumberOfTests++; + NumberOfMisses++; +#endif /* DEBUG_HIT_RATE */ + + while (HT_GET_KEY(HTable[HKey]) != 0xFFFFFL) { +#ifdef DEBUG_HIT_RATE + NumberOfMisses++; +#endif /* DEBUG_HIT_RATE */ + HKey = (HKey + 1) & HT_KEY_MASK; + } + HTable[HKey] = HT_PUT_KEY(Key) | HT_PUT_CODE(Code); +} + +/****************************************************************************** + Routine to test if given Key exists in HashTable and if so returns its code * + Returns the Code if key was found, -1 if not. * +******************************************************************************/ +int _ExistsHashTable(GifHashTableType *HashTable, uint32_t Key) +{ + int HKey = KeyItem(Key); + uint32_t *HTable = HashTable -> HTable, HTKey; + +#ifdef DEBUG_HIT_RATE + NumberOfTests++; + NumberOfMisses++; +#endif /* DEBUG_HIT_RATE */ + + while ((HTKey = HT_GET_KEY(HTable[HKey])) != 0xFFFFFL) { +#ifdef DEBUG_HIT_RATE + NumberOfMisses++; +#endif /* DEBUG_HIT_RATE */ + if (Key == HTKey) return HT_GET_CODE(HTable[HKey]); + HKey = (HKey + 1) & HT_KEY_MASK; + } + + return -1; +} + +/****************************************************************************** + Routine to generate an HKey for the hashtable out of the given unique key. * + The given Key is assumed to be 20 bits as follows: lower 8 bits are the * + new postfix character, while the upper 12 bits are the prefix code. * + Because the average hit ratio is only 2 (2 hash references per entry), * + evaluating more complex keys (such as twin prime keys) does not worth it! * +******************************************************************************/ +static int KeyItem(uint32_t Item) +{ + return ((Item >> 12) ^ Item) & HT_KEY_MASK; +} + +#ifdef DEBUG_HIT_RATE +/****************************************************************************** + Debugging routine to print the hit ratio - number of times the hash table * + was tested per operation. This routine was used to test the KeyItem routine * +******************************************************************************/ +void HashTablePrintHitRatio(void) +{ + printf("Hash Table Hit Ratio is %ld/%ld = %ld%%.\n", + NumberOfMisses, NumberOfTests, + NumberOfMisses * 100 / NumberOfTests); +} +#endif /* DEBUG_HIT_RATE */ + +/* end */ diff --git a/src/lib/gif/gif_hash.h b/src/lib/gif/gif_hash.h new file mode 100644 index 00000000..ac20a43c --- /dev/null +++ b/src/lib/gif/gif_hash.h @@ -0,0 +1,39 @@ +/****************************************************************************** + +gif_hash.h - magfic constants and declarations for GIF LZW + +******************************************************************************/ + +#ifndef _GIF_HASH_H_ +#define _GIF_HASH_H_ + +#include +#include + +#define HT_SIZE 8192 /* 12bits = 4096 or twice as big! */ +#define HT_KEY_MASK 0x1FFF /* 13bits keys */ +#define HT_KEY_NUM_BITS 13 /* 13bits keys */ +#define HT_MAX_KEY 8191 /* 13bits - 1, maximal code possible */ +#define HT_MAX_CODE 4095 /* Biggest code possible in 12 bits. */ + +/* The 32 bits of the long are divided into two parts for the key & code: */ +/* 1. The code is 12 bits as our compression algorithm is limited to 12bits */ +/* 2. The key is 12 bits Prefix code + 8 bit new char or 20 bits. */ +/* The key is the upper 20 bits. The code is the lower 12. */ +#define HT_GET_KEY(l) (l >> 12) +#define HT_GET_CODE(l) (l & 0x0FFF) +#define HT_PUT_KEY(l) (l << 12) +#define HT_PUT_CODE(l) (l & 0x0FFF) + +typedef struct GifHashTableType { + uint32_t HTable[HT_SIZE]; +} GifHashTableType; + +GifHashTableType *_InitHashTable(void); +void _ClearHashTable(GifHashTableType *HashTable); +void _InsertHashTable(GifHashTableType *HashTable, uint32_t Key, int Code); +int _ExistsHashTable(GifHashTableType *HashTable, uint32_t Key); + +#endif /* _GIF_HASH_H_ */ + +/* end */ diff --git a/src/lib/gif/gif_lib.h b/src/lib/gif/gif_lib.h new file mode 100644 index 00000000..b556bc69 --- /dev/null +++ b/src/lib/gif/gif_lib.h @@ -0,0 +1,309 @@ +/****************************************************************************** + +gif_lib.h - service library for decoding and encoding GIF images + +*****************************************************************************/ + +#ifndef _GIF_LIB_H_ +#define _GIF_LIB_H_ 1 + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#define GIFLIB_MAJOR 5 +#define GIFLIB_MINOR 1 +#define GIFLIB_RELEASE 0 + +#define GIF_ERROR 0 +#define GIF_OK 1 + +#include +#include + +#define GIF_STAMP "GIFVER" /* First chars in file - GIF stamp. */ +#define GIF_STAMP_LEN sizeof(GIF_STAMP) - 1 +#define GIF_VERSION_POS 3 /* Version first character in stamp. */ +#define GIF87_STAMP "GIF87a" /* First chars in file - GIF stamp. */ +#define GIF89_STAMP "GIF89a" /* First chars in file - GIF stamp. */ + +typedef unsigned char GifPixelType; +typedef unsigned char *GifRowType; +typedef unsigned char GifByteType; +typedef unsigned int GifPrefixType; +typedef int GifWord; + +typedef struct GifColorType { + GifByteType Red, Green, Blue; +} GifColorType; + +typedef struct ColorMapObject { + int ColorCount; + int BitsPerPixel; + bool SortFlag; + GifColorType *Colors; /* on malloc(3) heap */ +} ColorMapObject; + +typedef struct GifImageDesc { + GifWord Left, Top, Width, Height; /* Current image dimensions. */ + bool Interlace; /* Sequential/Interlaced lines. */ + ColorMapObject *ColorMap; /* The local color map */ +} GifImageDesc; + +typedef struct ExtensionBlock { + int ByteCount; + GifByteType *Bytes; /* on malloc(3) heap */ + int Function; /* The block function code */ +#define CONTINUE_EXT_FUNC_CODE 0x00 /* continuation subblock */ +#define COMMENT_EXT_FUNC_CODE 0xfe /* comment */ +#define GRAPHICS_EXT_FUNC_CODE 0xf9 /* graphics control (GIF89) */ +#define PLAINTEXT_EXT_FUNC_CODE 0x01 /* plaintext */ +#define APPLICATION_EXT_FUNC_CODE 0xff /* application block */ +} ExtensionBlock; + +typedef struct SavedImage { + GifImageDesc ImageDesc; + GifByteType *RasterBits; /* on malloc(3) heap */ + int ExtensionBlockCount; /* Count of extensions before image */ + ExtensionBlock *ExtensionBlocks; /* Extensions before image */ +} SavedImage; + +typedef struct GifFileType { + GifWord SWidth, SHeight; /* Size of virtual canvas */ + GifWord SColorResolution; /* How many colors can we generate? */ + GifWord SBackGroundColor; /* Background color for virtual canvas */ + GifByteType AspectByte; /* Used to compute pixel aspect ratio */ + ColorMapObject *SColorMap; /* Global colormap, NULL if nonexistent. */ + int ImageCount; /* Number of current image (both APIs) */ + GifImageDesc Image; /* Current image (low-level API) */ + SavedImage *SavedImages; /* Image sequence (high-level API) */ + int ExtensionBlockCount; /* Count extensions past last image */ + ExtensionBlock *ExtensionBlocks; /* Extensions past last image */ + int Error; /* Last error condition reported */ + void *UserData; /* hook to attach user data (TVT) */ + void *Private; /* Don't mess with this! */ +} GifFileType; + +#define GIF_ASPECT_RATIO(n) ((n)+15.0/64.0) + +typedef enum { + UNDEFINED_RECORD_TYPE, + SCREEN_DESC_RECORD_TYPE, + IMAGE_DESC_RECORD_TYPE, /* Begin with ',' */ + EXTENSION_RECORD_TYPE, /* Begin with '!' */ + TERMINATE_RECORD_TYPE /* Begin with ';' */ +} GifRecordType; + +/* func type to read gif data from arbitrary sources (TVT) */ +typedef int (*InputFunc) (GifFileType *, GifByteType *, int); + +/* func type to write gif data to arbitrary targets. + * Returns count of bytes written. (MRB) + */ +typedef int (*OutputFunc) (GifFileType *, const GifByteType *, int); + +/****************************************************************************** + GIF89 structures +******************************************************************************/ + +typedef struct GraphicsControlBlock { + int DisposalMode; +#define DISPOSAL_UNSPECIFIED 0 /* No disposal specified. */ +#define DISPOSE_DO_NOT 1 /* Leave image in place */ +#define DISPOSE_BACKGROUND 2 /* Set area too background color */ +#define DISPOSE_PREVIOUS 3 /* Restore to previous content */ + bool UserInputFlag; /* User confirmation required before disposal */ + int DelayTime; /* pre-display delay in 0.01sec units */ + int TransparentColor; /* Palette index for transparency, -1 if none */ +#define NO_TRANSPARENT_COLOR -1 +} GraphicsControlBlock; + +/****************************************************************************** + GIF encoding routines +******************************************************************************/ + +/* Main entry points */ +GifFileType *EGifOpenFileName(const char *GifFileName, + const bool GifTestExistence, int *Error); +GifFileType *EGifOpenFileHandle(const int GifFileHandle, int *Error); +GifFileType *EGifOpen(void *userPtr, OutputFunc writeFunc, int *Error); +int EGifSpew(GifFileType * GifFile); +const char *EGifGetGifVersion(GifFileType *GifFile); /* new in 5.x */ +int EGifCloseFile(GifFileType *GifFile, int *ErrorCode); + +#define E_GIF_SUCCEEDED 0 +#define E_GIF_ERR_OPEN_FAILED 1 /* And EGif possible errors. */ +#define E_GIF_ERR_WRITE_FAILED 2 +#define E_GIF_ERR_HAS_SCRN_DSCR 3 +#define E_GIF_ERR_HAS_IMAG_DSCR 4 +#define E_GIF_ERR_NO_COLOR_MAP 5 +#define E_GIF_ERR_DATA_TOO_BIG 6 +#define E_GIF_ERR_NOT_ENOUGH_MEM 7 +#define E_GIF_ERR_DISK_IS_FULL 8 +#define E_GIF_ERR_CLOSE_FAILED 9 +#define E_GIF_ERR_NOT_WRITEABLE 10 + +/* These are legacy. You probably do not want to call them directly */ +int EGifPutScreenDesc(GifFileType *GifFile, + const int GifWidth, const int GifHeight, + const int GifColorRes, + const int GifBackGround, + const ColorMapObject *GifColorMap); +int EGifPutImageDesc(GifFileType *GifFile, + const int GifLeft, const int GifTop, + const int GifWidth, const int GifHeight, + const bool GifInterlace, + const ColorMapObject *GifColorMap); +void EGifSetGifVersion(GifFileType *GifFile, const bool gif89); +int EGifPutLine(GifFileType *GifFile, GifPixelType *GifLine, + int GifLineLen); +int EGifPutPixel(GifFileType *GifFile, const GifPixelType GifPixel); +int EGifPutComment(GifFileType *GifFile, const char *GifComment); +int EGifPutExtensionLeader(GifFileType *GifFile, const int GifExtCode); +int EGifPutExtensionBlock(GifFileType *GifFile, + const int GifExtLen, const void *GifExtension); +int EGifPutExtensionTrailer(GifFileType *GifFile); +int EGifPutExtension(GifFileType *GifFile, const int GifExtCode, + const int GifExtLen, + const void *GifExtension); +int EGifPutCode(GifFileType *GifFile, int GifCodeSize, + const GifByteType *GifCodeBlock); +int EGifPutCodeNext(GifFileType *GifFile, + const GifByteType *GifCodeBlock); + +/****************************************************************************** + GIF decoding routines +******************************************************************************/ + +/* Main entry points */ +GifFileType *DGifOpenFileName(const char *GifFileName, int *Error); +GifFileType *DGifOpenFileHandle(int GifFileHandle, int *Error); +int DGifSlurp(GifFileType * GifFile); +GifFileType *DGifOpen(void *userPtr, InputFunc readFunc, int *Error); /* new one (TVT) */ + int DGifCloseFile(GifFileType * GifFile, int *ErrorCode); + +#define D_GIF_SUCCEEDED 0 +#define D_GIF_ERR_OPEN_FAILED 101 /* And DGif possible errors. */ +#define D_GIF_ERR_READ_FAILED 102 +#define D_GIF_ERR_NOT_GIF_FILE 103 +#define D_GIF_ERR_NO_SCRN_DSCR 104 +#define D_GIF_ERR_NO_IMAG_DSCR 105 +#define D_GIF_ERR_NO_COLOR_MAP 106 +#define D_GIF_ERR_WRONG_RECORD 107 +#define D_GIF_ERR_DATA_TOO_BIG 108 +#define D_GIF_ERR_NOT_ENOUGH_MEM 109 +#define D_GIF_ERR_CLOSE_FAILED 110 +#define D_GIF_ERR_NOT_READABLE 111 +#define D_GIF_ERR_IMAGE_DEFECT 112 +#define D_GIF_ERR_EOF_TOO_SOON 113 + +/* These are legacy. You probably do not want to call them directly */ +int DGifGetScreenDesc(GifFileType *GifFile); +int DGifGetRecordType(GifFileType *GifFile, GifRecordType *GifType); +int DGifGetImageDesc(GifFileType *GifFile); +int DGifGetLine(GifFileType *GifFile, GifPixelType *GifLine, int GifLineLen); +int DGifGetPixel(GifFileType *GifFile, GifPixelType GifPixel); +int DGifGetComment(GifFileType *GifFile, char *GifComment); +int DGifGetExtension(GifFileType *GifFile, int *GifExtCode, + GifByteType **GifExtension); +int DGifGetExtensionNext(GifFileType *GifFile, GifByteType **GifExtension); +int DGifGetCode(GifFileType *GifFile, int *GifCodeSize, + GifByteType **GifCodeBlock); +int DGifGetCodeNext(GifFileType *GifFile, GifByteType **GifCodeBlock); +int DGifGetLZCodes(GifFileType *GifFile, int *GifCode); + + +/****************************************************************************** + Color table quantization (deprecated) +******************************************************************************/ +int GifQuantizeBuffer(unsigned int Width, unsigned int Height, + int *ColorMapSize, GifByteType * RedInput, + GifByteType * GreenInput, GifByteType * BlueInput, + GifByteType * OutputBuffer, + GifColorType * OutputColorMap); + +/****************************************************************************** + Error handling and reporting. +******************************************************************************/ +extern const char *GifErrorString(int ErrorCode); /* new in 2012 - ESR */ + +/***************************************************************************** + Everything below this point is new after version 1.2, supporting `slurp + mode' for doing I/O in two big belts with all the image-bashing in core. +******************************************************************************/ + +/****************************************************************************** + Color map handling from gif_alloc.c +******************************************************************************/ + +extern ColorMapObject *GifMakeMapObject(int ColorCount, + const GifColorType *ColorMap); +extern void GifFreeMapObject(ColorMapObject *Object); +extern ColorMapObject *GifUnionColorMap(const ColorMapObject *ColorIn1, + const ColorMapObject *ColorIn2, + GifPixelType ColorTransIn2[]); +extern int GifBitSize(int n); + +/****************************************************************************** + Support for the in-core structures allocation (slurp mode). +******************************************************************************/ + +extern void GifApplyTranslation(SavedImage *Image, GifPixelType Translation[]); +extern int GifAddExtensionBlock(int *ExtensionBlock_Count, + ExtensionBlock **ExtensionBlocks, + int Function, + unsigned int Len, unsigned char ExtData[]); +extern void GifFreeExtensions(int *ExtensionBlock_Count, + ExtensionBlock **ExtensionBlocks); +extern SavedImage *GifMakeSavedImage(GifFileType *GifFile, + const SavedImage *CopyFrom); +extern void GifFreeSavedImages(GifFileType *GifFile); + +/****************************************************************************** + 5.x functions for GIF89 graphics control blocks +******************************************************************************/ + +int DGifExtensionToGCB(const size_t GifExtensionLength, + const GifByteType *GifExtension, + GraphicsControlBlock *GCB); +size_t EGifGCBToExtension(const GraphicsControlBlock *GCB, + GifByteType *GifExtension); + +int DGifSavedExtensionToGCB(GifFileType *GifFile, + int ImageIndex, + GraphicsControlBlock *GCB); +int EGifGCBToSavedExtension(const GraphicsControlBlock *GCB, + GifFileType *GifFile, + int ImageIndex); + +/****************************************************************************** + The library's internal utility font +******************************************************************************/ + +#define GIF_FONT_WIDTH 8 +#define GIF_FONT_HEIGHT 8 +extern const unsigned char GifAsciiTable8x8[][GIF_FONT_WIDTH]; + +extern void GifDrawText8x8(SavedImage *Image, + const int x, const int y, + const char *legend, const int color); + +extern void GifDrawBox(SavedImage *Image, + const int x, const int y, + const int w, const int d, const int color); + +extern void GifDrawRectangle(SavedImage *Image, + const int x, const int y, + const int w, const int d, const int color); + +extern void GifDrawBoxedText8x8(SavedImage *Image, + const int x, const int y, + const char *legend, + const int border, const int bg, const int fg); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* _GIF_LIB_H */ + +/* end */ diff --git a/src/lib/gif/gif_lib_private.h b/src/lib/gif/gif_lib_private.h new file mode 100644 index 00000000..adaf5571 --- /dev/null +++ b/src/lib/gif/gif_lib_private.h @@ -0,0 +1,59 @@ +/**************************************************************************** + +gif_lib_private.h - internal giflib routines and structures + +****************************************************************************/ + +#ifndef _GIF_LIB_PRIVATE_H +#define _GIF_LIB_PRIVATE_H + +#include "gif_lib.h" +#include "gif_hash.h" + +#define EXTENSION_INTRODUCER 0x21 +#define DESCRIPTOR_INTRODUCER 0x2c +#define TERMINATOR_INTRODUCER 0x3b + +#define LZ_MAX_CODE 4095 /* Biggest code possible in 12 bits. */ +#define LZ_BITS 12 + +#define FLUSH_OUTPUT 4096 /* Impossible code, to signal flush. */ +#define FIRST_CODE 4097 /* Impossible code, to signal first. */ +#define NO_SUCH_CODE 4098 /* Impossible code, to signal empty. */ + +#define FILE_STATE_WRITE 0x01 +#define FILE_STATE_SCREEN 0x02 +#define FILE_STATE_IMAGE 0x04 +#define FILE_STATE_READ 0x08 + +#define IS_READABLE(Private) (Private->FileState & FILE_STATE_READ) +#define IS_WRITEABLE(Private) (Private->FileState & FILE_STATE_WRITE) + +typedef struct GifFilePrivateType { + GifWord FileState, FileHandle, /* Where all this data goes to! */ + BitsPerPixel, /* Bits per pixel (Codes uses at least this + 1). */ + ClearCode, /* The CLEAR LZ code. */ + EOFCode, /* The EOF LZ code. */ + RunningCode, /* The next code algorithm can generate. */ + RunningBits, /* The number of bits required to represent RunningCode. */ + MaxCode1, /* 1 bigger than max. possible code, in RunningBits bits. */ + LastCode, /* The code before the current code. */ + CrntCode, /* Current algorithm code. */ + StackPtr, /* For character stack (see below). */ + CrntShiftState; /* Number of bits in CrntShiftDWord. */ + unsigned long CrntShiftDWord; /* For bytes decomposition into codes. */ + unsigned long PixelCount; /* Number of pixels in image. */ + FILE *File; /* File as stream. */ + InputFunc Read; /* function to read gif input (TVT) */ + OutputFunc Write; /* function to write gif output (MRB) */ + GifByteType Buf[256]; /* Compressed input is buffered here. */ + GifByteType Stack[LZ_MAX_CODE]; /* Decoded pixels are stacked here. */ + GifByteType Suffix[LZ_MAX_CODE + 1]; /* So we can trace the codes. */ + GifPrefixType Prefix[LZ_MAX_CODE + 1]; + GifHashTableType *HashTable; + bool gif89; +} GifFilePrivateType; + +#endif /* _GIF_LIB_PRIVATE_H */ + +/* end */ diff --git a/src/lib/gif/gifalloc.c b/src/lib/gif/gifalloc.c new file mode 100644 index 00000000..9fe3edf8 --- /dev/null +++ b/src/lib/gif/gifalloc.c @@ -0,0 +1,401 @@ +/***************************************************************************** + + GIF construction tools + +****************************************************************************/ + +#include +#include +#include + +#include "gif_lib.h" + +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) + +/****************************************************************************** + Miscellaneous utility functions +******************************************************************************/ + +/* return smallest bitfield size n will fit in */ +int +GifBitSize(int n) +{ + register int i; + + for (i = 1; i <= 8; i++) + if ((1 << i) >= n) + break; + return (i); +} + +/****************************************************************************** + Color map object functions +******************************************************************************/ + +/* + * Allocate a color map of given size; initialize with contents of + * ColorMap if that pointer is non-NULL. + */ +ColorMapObject * +GifMakeMapObject(int ColorCount, const GifColorType *ColorMap) +{ + ColorMapObject *Object; + + /*** FIXME: Our ColorCount has to be a power of two. Is it necessary to + * make the user know that or should we automatically round up instead? */ + if (ColorCount != (1 << GifBitSize(ColorCount))) { + return ((ColorMapObject *) NULL); + } + + Object = (ColorMapObject *)malloc(sizeof(ColorMapObject)); + if (Object == (ColorMapObject *) NULL) { + return ((ColorMapObject *) NULL); + } + + Object->Colors = (GifColorType *)calloc(ColorCount, sizeof(GifColorType)); + if (Object->Colors == (GifColorType *) NULL) { + free(Object); + return ((ColorMapObject *) NULL); + } + + Object->ColorCount = ColorCount; + Object->BitsPerPixel = GifBitSize(ColorCount); + Object->SortFlag = false; + + if (ColorMap != NULL) { + memcpy((char *)Object->Colors, + (char *)ColorMap, ColorCount * sizeof(GifColorType)); + } + + return (Object); +} + +/******************************************************************************* +Free a color map object +*******************************************************************************/ +void +GifFreeMapObject(ColorMapObject *Object) +{ + if (Object != NULL) { + (void)free(Object->Colors); + (void)free(Object); + } +} + +#ifdef DEBUG +void +DumpColorMap(ColorMapObject *Object, + FILE * fp) +{ + if (Object != NULL) { + int i, j, Len = Object->ColorCount; + + for (i = 0; i < Len; i += 4) { + for (j = 0; j < 4 && j < Len; j++) { + (void)fprintf(fp, "%3d: %02x %02x %02x ", i + j, + Object->Colors[i + j].Red, + Object->Colors[i + j].Green, + Object->Colors[i + j].Blue); + } + (void)fprintf(fp, "\n"); + } + } +} +#endif /* DEBUG */ + +/******************************************************************************* + Compute the union of two given color maps and return it. If result can't + fit into 256 colors, NULL is returned, the allocated union otherwise. + ColorIn1 is copied as is to ColorUnion, while colors from ColorIn2 are + copied iff they didn't exist before. ColorTransIn2 maps the old + ColorIn2 into the ColorUnion color map table./ +*******************************************************************************/ +ColorMapObject * +GifUnionColorMap(const ColorMapObject *ColorIn1, + const ColorMapObject *ColorIn2, + GifPixelType ColorTransIn2[]) +{ + int i, j, CrntSlot, RoundUpTo, NewGifBitSize; + ColorMapObject *ColorUnion; + + /* + * We don't worry about duplicates within either color map; if + * the caller wants to resolve those, he can perform unions + * with an empty color map. + */ + + /* Allocate table which will hold the result for sure. */ + ColorUnion = GifMakeMapObject(MAX(ColorIn1->ColorCount, + ColorIn2->ColorCount) * 2, NULL); + + if (ColorUnion == NULL) + return (NULL); + + /* + * Copy ColorIn1 to ColorUnion. + */ + for (i = 0; i < ColorIn1->ColorCount; i++) + ColorUnion->Colors[i] = ColorIn1->Colors[i]; + CrntSlot = ColorIn1->ColorCount; + + /* + * Potentially obnoxious hack: + * + * Back CrntSlot down past all contiguous {0, 0, 0} slots at the end + * of table 1. This is very useful if your display is limited to + * 16 colors. + */ + while (ColorIn1->Colors[CrntSlot - 1].Red == 0 + && ColorIn1->Colors[CrntSlot - 1].Green == 0 + && ColorIn1->Colors[CrntSlot - 1].Blue == 0) + CrntSlot--; + + /* Copy ColorIn2 to ColorUnion (use old colors if they exist): */ + for (i = 0; i < ColorIn2->ColorCount && CrntSlot <= 256; i++) { + /* Let's see if this color already exists: */ + for (j = 0; j < ColorIn1->ColorCount; j++) + if (memcmp (&ColorIn1->Colors[j], &ColorIn2->Colors[i], + sizeof(GifColorType)) == 0) + break; + + if (j < ColorIn1->ColorCount) + ColorTransIn2[i] = j; /* color exists in Color1 */ + else { + /* Color is new - copy it to a new slot: */ + ColorUnion->Colors[CrntSlot] = ColorIn2->Colors[i]; + ColorTransIn2[i] = CrntSlot++; + } + } + + if (CrntSlot > 256) { + GifFreeMapObject(ColorUnion); + return ((ColorMapObject *) NULL); + } + + NewGifBitSize = GifBitSize(CrntSlot); + RoundUpTo = (1 << NewGifBitSize); + + if (RoundUpTo != ColorUnion->ColorCount) { + register GifColorType *Map = ColorUnion->Colors; + + /* + * Zero out slots up to next power of 2. + * We know these slots exist because of the way ColorUnion's + * start dimension was computed. + */ + for (j = CrntSlot; j < RoundUpTo; j++) + Map[j].Red = Map[j].Green = Map[j].Blue = 0; + + /* perhaps we can shrink the map? */ + if (RoundUpTo < ColorUnion->ColorCount) + ColorUnion->Colors = (GifColorType *)realloc(Map, + sizeof(GifColorType) * RoundUpTo); + } + + ColorUnion->ColorCount = RoundUpTo; + ColorUnion->BitsPerPixel = NewGifBitSize; + + return (ColorUnion); +} + +/******************************************************************************* + Apply a given color translation to the raster bits of an image +*******************************************************************************/ +void +GifApplyTranslation(SavedImage *Image, GifPixelType Translation[]) +{ + register int i; + register int RasterSize = Image->ImageDesc.Height * Image->ImageDesc.Width; + + for (i = 0; i < RasterSize; i++) + Image->RasterBits[i] = Translation[Image->RasterBits[i]]; +} + +/****************************************************************************** + Extension record functions +******************************************************************************/ +int +GifAddExtensionBlock(int *ExtensionBlockCount, + ExtensionBlock **ExtensionBlocks, + int Function, + unsigned int Len, + unsigned char ExtData[]) +{ + ExtensionBlock *ep; + + if (*ExtensionBlocks == NULL) + *ExtensionBlocks=(ExtensionBlock *)malloc(sizeof(ExtensionBlock)); + else + *ExtensionBlocks = (ExtensionBlock *)realloc(*ExtensionBlocks, + sizeof(ExtensionBlock) * + (*ExtensionBlockCount + 1)); + + if (*ExtensionBlocks == NULL) + return (GIF_ERROR); + + ep = &(*ExtensionBlocks)[(*ExtensionBlockCount)++]; + + ep->Function = Function; + ep->ByteCount=Len; + ep->Bytes = (GifByteType *)malloc(ep->ByteCount); + if (ep->Bytes == NULL) + return (GIF_ERROR); + + if (ExtData != NULL) { + memcpy(ep->Bytes, ExtData, Len); + } + + return (GIF_OK); +} + +void +GifFreeExtensions(int *ExtensionBlockCount, + ExtensionBlock **ExtensionBlocks) +{ + ExtensionBlock *ep; + + if (*ExtensionBlocks == NULL) + return; + + for (ep = *ExtensionBlocks; + ep < (*ExtensionBlocks + *ExtensionBlockCount); + ep++) + (void)free((char *)ep->Bytes); + (void)free((char *)*ExtensionBlocks); + *ExtensionBlocks = NULL; + *ExtensionBlockCount = 0; +} + +/****************************************************************************** + Image block allocation functions +******************************************************************************/ + +/* Private Function: + * Frees the last image in the GifFile->SavedImages array + */ +void +FreeLastSavedImage(GifFileType *GifFile) +{ + SavedImage *sp; + + if ((GifFile == NULL) || (GifFile->SavedImages == NULL)) + return; + + /* Remove one SavedImage from the GifFile */ + GifFile->ImageCount--; + sp = &GifFile->SavedImages[GifFile->ImageCount]; + + /* Deallocate its Colormap */ + if (sp->ImageDesc.ColorMap != NULL) { + GifFreeMapObject(sp->ImageDesc.ColorMap); + sp->ImageDesc.ColorMap = NULL; + } + + /* Deallocate the image data */ + if (sp->RasterBits != NULL) + free((char *)sp->RasterBits); + + /* Deallocate any extensions */ + GifFreeExtensions(&sp->ExtensionBlockCount, &sp->ExtensionBlocks); + + /*** FIXME: We could realloc the GifFile->SavedImages structure but is + * there a point to it? Saves some memory but we'd have to do it every + * time. If this is used in GifFreeSavedImages then it would be inefficient + * (The whole array is going to be deallocated.) If we just use it when + * we want to free the last Image it's convenient to do it here. + */ +} + +/* + * Append an image block to the SavedImages array + */ +SavedImage * +GifMakeSavedImage(GifFileType *GifFile, const SavedImage *CopyFrom) +{ + if (GifFile->SavedImages == NULL) + GifFile->SavedImages = (SavedImage *)malloc(sizeof(SavedImage)); + else + GifFile->SavedImages = (SavedImage *)realloc(GifFile->SavedImages, + sizeof(SavedImage) * (GifFile->ImageCount + 1)); + + if (GifFile->SavedImages == NULL) + return ((SavedImage *)NULL); + else { + SavedImage *sp = &GifFile->SavedImages[GifFile->ImageCount++]; + memset((char *)sp, '\0', sizeof(SavedImage)); + + if (CopyFrom != NULL) { + memcpy((char *)sp, CopyFrom, sizeof(SavedImage)); + + /* + * Make our own allocated copies of the heap fields in the + * copied record. This guards against potential aliasing + * problems. + */ + + /* first, the local color map */ + if (sp->ImageDesc.ColorMap != NULL) { + sp->ImageDesc.ColorMap = GifMakeMapObject( + CopyFrom->ImageDesc.ColorMap->ColorCount, + CopyFrom->ImageDesc.ColorMap->Colors); + if (sp->ImageDesc.ColorMap == NULL) { + FreeLastSavedImage(GifFile); + return (SavedImage *)(NULL); + } + } + + /* next, the raster */ + sp->RasterBits = (unsigned char *)malloc(sizeof(GifPixelType) * + CopyFrom->ImageDesc.Height * + CopyFrom->ImageDesc.Width); + if (sp->RasterBits == NULL) { + FreeLastSavedImage(GifFile); + return (SavedImage *)(NULL); + } + memcpy(sp->RasterBits, CopyFrom->RasterBits, + sizeof(GifPixelType) * CopyFrom->ImageDesc.Height * + CopyFrom->ImageDesc.Width); + + /* finally, the extension blocks */ + if (sp->ExtensionBlocks != NULL) { + sp->ExtensionBlocks = (ExtensionBlock *)malloc( + sizeof(ExtensionBlock) * + CopyFrom->ExtensionBlockCount); + if (sp->ExtensionBlocks == NULL) { + FreeLastSavedImage(GifFile); + return (SavedImage *)(NULL); + } + memcpy(sp->ExtensionBlocks, CopyFrom->ExtensionBlocks, + sizeof(ExtensionBlock) * CopyFrom->ExtensionBlockCount); + } + } + + return (sp); + } +} + +void +GifFreeSavedImages(GifFileType *GifFile) +{ + SavedImage *sp; + + if ((GifFile == NULL) || (GifFile->SavedImages == NULL)) { + return; + } + for (sp = GifFile->SavedImages; + sp < GifFile->SavedImages + GifFile->ImageCount; sp++) { + if (sp->ImageDesc.ColorMap != NULL) { + GifFreeMapObject(sp->ImageDesc.ColorMap); + sp->ImageDesc.ColorMap = NULL; + } + + if (sp->RasterBits != NULL) + free((char *)sp->RasterBits); + + GifFreeExtensions(&sp->ExtensionBlockCount, &sp->ExtensionBlocks); + } + free((char *)GifFile->SavedImages); + GifFile->SavedImages = NULL; +} + +/* end */ diff --git a/src/lib/gif/quantize.c b/src/lib/gif/quantize.c new file mode 100644 index 00000000..e5d4a590 --- /dev/null +++ b/src/lib/gif/quantize.c @@ -0,0 +1,330 @@ +/***************************************************************************** + + quantize.c - quantize a high resolution image into lower one + + Based on: "Color Image Quantization for frame buffer Display", by + Paul Heckbert SIGGRAPH 1982 page 297-307. + + This doesn't really belong in the core library, was undocumented, + and was removed in 4.2. Then it turned out some client apps were + actually using it, so it was restored in 5.0. + +******************************************************************************/ + +#include +#include +#include "gif_lib.h" +#include "gif_lib_private.h" + +#define ABS(x) ((x) > 0 ? (x) : (-(x))) + +#define COLOR_ARRAY_SIZE 32768 +#define BITS_PER_PRIM_COLOR 5 +#define MAX_PRIM_COLOR 0x1f + +static int SortRGBAxis; + +typedef struct QuantizedColorType { + GifByteType RGB[3]; + GifByteType NewColorIndex; + long Count; + struct QuantizedColorType *Pnext; +} QuantizedColorType; + +typedef struct NewColorMapType { + GifByteType RGBMin[3], RGBWidth[3]; + unsigned int NumEntries; /* # of QuantizedColorType in linked list below */ + unsigned long Count; /* Total number of pixels in all the entries */ + QuantizedColorType *QuantizedColors; +} NewColorMapType; + +static int SubdivColorMap(NewColorMapType * NewColorSubdiv, + unsigned int ColorMapSize, + unsigned int *NewColorMapSize); +static int SortCmpRtn(const void *Entry1, const void *Entry2); + +/****************************************************************************** + Quantize high resolution image into lower one. Input image consists of a + 2D array for each of the RGB colors with size Width by Height. There is no + Color map for the input. Output is a quantized image with 2D array of + indexes into the output color map. + Note input image can be 24 bits at the most (8 for red/green/blue) and + the output has 256 colors at the most (256 entries in the color map.). + ColorMapSize specifies size of color map up to 256 and will be updated to + real size before returning. + Also non of the parameter are allocated by this routine. + This function returns GIF_OK if successful, GIF_ERROR otherwise. +******************************************************************************/ +int +GifQuantizeBuffer(unsigned int Width, + unsigned int Height, + int *ColorMapSize, + GifByteType * RedInput, + GifByteType * GreenInput, + GifByteType * BlueInput, + GifByteType * OutputBuffer, + GifColorType * OutputColorMap) { + + unsigned int Index, NumOfEntries; + int i, j, MaxRGBError[3]; + unsigned int NewColorMapSize; + long Red, Green, Blue; + NewColorMapType NewColorSubdiv[256]; + QuantizedColorType *ColorArrayEntries, *QuantizedColor; + + ColorArrayEntries = (QuantizedColorType *)malloc( + sizeof(QuantizedColorType) * COLOR_ARRAY_SIZE); + if (ColorArrayEntries == NULL) { + return GIF_ERROR; + } + + for (i = 0; i < COLOR_ARRAY_SIZE; i++) { + ColorArrayEntries[i].RGB[0] = i >> (2 * BITS_PER_PRIM_COLOR); + ColorArrayEntries[i].RGB[1] = (i >> BITS_PER_PRIM_COLOR) & + MAX_PRIM_COLOR; + ColorArrayEntries[i].RGB[2] = i & MAX_PRIM_COLOR; + ColorArrayEntries[i].Count = 0; + } + + /* Sample the colors and their distribution: */ + for (i = 0; i < (int)(Width * Height); i++) { + Index = ((RedInput[i] >> (8 - BITS_PER_PRIM_COLOR)) << + (2 * BITS_PER_PRIM_COLOR)) + + ((GreenInput[i] >> (8 - BITS_PER_PRIM_COLOR)) << + BITS_PER_PRIM_COLOR) + + (BlueInput[i] >> (8 - BITS_PER_PRIM_COLOR)); + ColorArrayEntries[Index].Count++; + } + + /* Put all the colors in the first entry of the color map, and call the + * recursive subdivision process. */ + for (i = 0; i < 256; i++) { + NewColorSubdiv[i].QuantizedColors = NULL; + NewColorSubdiv[i].Count = NewColorSubdiv[i].NumEntries = 0; + for (j = 0; j < 3; j++) { + NewColorSubdiv[i].RGBMin[j] = 0; + NewColorSubdiv[i].RGBWidth[j] = 255; + } + } + + /* Find the non empty entries in the color table and chain them: */ + for (i = 0; i < COLOR_ARRAY_SIZE; i++) + if (ColorArrayEntries[i].Count > 0) + break; + QuantizedColor = NewColorSubdiv[0].QuantizedColors = &ColorArrayEntries[i]; + NumOfEntries = 1; + while (++i < COLOR_ARRAY_SIZE) + if (ColorArrayEntries[i].Count > 0) { + QuantizedColor->Pnext = &ColorArrayEntries[i]; + QuantizedColor = &ColorArrayEntries[i]; + NumOfEntries++; + } + QuantizedColor->Pnext = NULL; + + NewColorSubdiv[0].NumEntries = NumOfEntries; /* Different sampled colors */ + NewColorSubdiv[0].Count = ((long)Width) * Height; /* Pixels */ + NewColorMapSize = 1; + if (SubdivColorMap(NewColorSubdiv, *ColorMapSize, &NewColorMapSize) != + GIF_OK) { + free((char *)ColorArrayEntries); + return GIF_ERROR; + } + if (NewColorMapSize < *ColorMapSize) { + /* And clear rest of color map: */ + for (i = NewColorMapSize; i < *ColorMapSize; i++) + OutputColorMap[i].Red = OutputColorMap[i].Green = + OutputColorMap[i].Blue = 0; + } + + /* Average the colors in each entry to be the color to be used in the + * output color map, and plug it into the output color map itself. */ + for (i = 0; i < NewColorMapSize; i++) { + if ((j = NewColorSubdiv[i].NumEntries) > 0) { + QuantizedColor = NewColorSubdiv[i].QuantizedColors; + Red = Green = Blue = 0; + while (QuantizedColor) { + QuantizedColor->NewColorIndex = i; + Red += QuantizedColor->RGB[0]; + Green += QuantizedColor->RGB[1]; + Blue += QuantizedColor->RGB[2]; + QuantizedColor = QuantizedColor->Pnext; + } + OutputColorMap[i].Red = (Red << (8 - BITS_PER_PRIM_COLOR)) / j; + OutputColorMap[i].Green = (Green << (8 - BITS_PER_PRIM_COLOR)) / j; + OutputColorMap[i].Blue = (Blue << (8 - BITS_PER_PRIM_COLOR)) / j; + } + } + + /* Finally scan the input buffer again and put the mapped index in the + * output buffer. */ + MaxRGBError[0] = MaxRGBError[1] = MaxRGBError[2] = 0; + for (i = 0; i < (int)(Width * Height); i++) { + Index = ((RedInput[i] >> (8 - BITS_PER_PRIM_COLOR)) << + (2 * BITS_PER_PRIM_COLOR)) + + ((GreenInput[i] >> (8 - BITS_PER_PRIM_COLOR)) << + BITS_PER_PRIM_COLOR) + + (BlueInput[i] >> (8 - BITS_PER_PRIM_COLOR)); + Index = ColorArrayEntries[Index].NewColorIndex; + OutputBuffer[i] = Index; + if (MaxRGBError[0] < ABS(OutputColorMap[Index].Red - RedInput[i])) + MaxRGBError[0] = ABS(OutputColorMap[Index].Red - RedInput[i]); + if (MaxRGBError[1] < ABS(OutputColorMap[Index].Green - GreenInput[i])) + MaxRGBError[1] = ABS(OutputColorMap[Index].Green - GreenInput[i]); + if (MaxRGBError[2] < ABS(OutputColorMap[Index].Blue - BlueInput[i])) + MaxRGBError[2] = ABS(OutputColorMap[Index].Blue - BlueInput[i]); + } + +#ifdef DEBUG + fprintf(stderr, + "Quantization L(0) errors: Red = %d, Green = %d, Blue = %d.\n", + MaxRGBError[0], MaxRGBError[1], MaxRGBError[2]); +#endif /* DEBUG */ + + free((char *)ColorArrayEntries); + + *ColorMapSize = NewColorMapSize; + + return GIF_OK; +} + +/****************************************************************************** + Routine to subdivide the RGB space recursively using median cut in each + axes alternatingly until ColorMapSize different cubes exists. + The biggest cube in one dimension is subdivide unless it has only one entry. + Returns GIF_ERROR if failed, otherwise GIF_OK. +*******************************************************************************/ +static int +SubdivColorMap(NewColorMapType * NewColorSubdiv, + unsigned int ColorMapSize, + unsigned int *NewColorMapSize) { + + int MaxSize; + unsigned int i, j, Index = 0, NumEntries, MinColor, MaxColor; + long Sum, Count; + QuantizedColorType *QuantizedColor, **SortArray; + + while (ColorMapSize > *NewColorMapSize) { + /* Find candidate for subdivision: */ + MaxSize = -1; + for (i = 0; i < *NewColorMapSize; i++) { + for (j = 0; j < 3; j++) { + if ((((int)NewColorSubdiv[i].RGBWidth[j]) > MaxSize) && + (NewColorSubdiv[i].NumEntries > 1)) { + MaxSize = NewColorSubdiv[i].RGBWidth[j]; + Index = i; + SortRGBAxis = j; + } + } + } + + if (MaxSize == -1) + return GIF_OK; + + /* Split the entry Index into two along the axis SortRGBAxis: */ + + /* Sort all elements in that entry along the given axis and split at + * the median. */ + SortArray = (QuantizedColorType **)malloc( + sizeof(QuantizedColorType *) * + NewColorSubdiv[Index].NumEntries); + if (SortArray == NULL) + return GIF_ERROR; + for (j = 0, QuantizedColor = NewColorSubdiv[Index].QuantizedColors; + j < NewColorSubdiv[Index].NumEntries && QuantizedColor != NULL; + j++, QuantizedColor = QuantizedColor->Pnext) + SortArray[j] = QuantizedColor; + + /* + * Because qsort isn't stable, this can produce differing + * results for the order of tuples depending on platform + * details of how qsort() is implemented. + * + * We mitigate this problem by sorting on all three axes rather + * than only the one specied by SortRGBAxis; that way the instability + * can only become an issue if there are multiple color indices + * referring to identical RGB tuples. Older versions of this + * sorted on only the one axis. + */ + qsort(SortArray, NewColorSubdiv[Index].NumEntries, + sizeof(QuantizedColorType *), SortCmpRtn); + + /* Relink the sorted list into one: */ + for (j = 0; j < NewColorSubdiv[Index].NumEntries - 1; j++) + SortArray[j]->Pnext = SortArray[j + 1]; + SortArray[NewColorSubdiv[Index].NumEntries - 1]->Pnext = NULL; + NewColorSubdiv[Index].QuantizedColors = QuantizedColor = SortArray[0]; + free((char *)SortArray); + + /* Now simply add the Counts until we have half of the Count: */ + Sum = NewColorSubdiv[Index].Count / 2 - QuantizedColor->Count; + NumEntries = 1; + Count = QuantizedColor->Count; + while (QuantizedColor->Pnext != NULL && + (Sum -= QuantizedColor->Pnext->Count) >= 0 && + QuantizedColor->Pnext->Pnext != NULL) { + QuantizedColor = QuantizedColor->Pnext; + NumEntries++; + Count += QuantizedColor->Count; + } + /* Save the values of the last color of the first half, and first + * of the second half so we can update the Bounding Boxes later. + * Also as the colors are quantized and the BBoxes are full 0..255, + * they need to be rescaled. + */ + MaxColor = QuantizedColor->RGB[SortRGBAxis]; /* Max. of first half */ + /* coverity[var_deref_op] */ + MinColor = QuantizedColor->Pnext->RGB[SortRGBAxis]; /* of second */ + MaxColor <<= (8 - BITS_PER_PRIM_COLOR); + MinColor <<= (8 - BITS_PER_PRIM_COLOR); + + /* Partition right here: */ + NewColorSubdiv[*NewColorMapSize].QuantizedColors = + QuantizedColor->Pnext; + QuantizedColor->Pnext = NULL; + NewColorSubdiv[*NewColorMapSize].Count = Count; + NewColorSubdiv[Index].Count -= Count; + NewColorSubdiv[*NewColorMapSize].NumEntries = + NewColorSubdiv[Index].NumEntries - NumEntries; + NewColorSubdiv[Index].NumEntries = NumEntries; + for (j = 0; j < 3; j++) { + NewColorSubdiv[*NewColorMapSize].RGBMin[j] = + NewColorSubdiv[Index].RGBMin[j]; + NewColorSubdiv[*NewColorMapSize].RGBWidth[j] = + NewColorSubdiv[Index].RGBWidth[j]; + } + NewColorSubdiv[*NewColorMapSize].RGBWidth[SortRGBAxis] = + NewColorSubdiv[*NewColorMapSize].RGBMin[SortRGBAxis] + + NewColorSubdiv[*NewColorMapSize].RGBWidth[SortRGBAxis] - MinColor; + NewColorSubdiv[*NewColorMapSize].RGBMin[SortRGBAxis] = MinColor; + + NewColorSubdiv[Index].RGBWidth[SortRGBAxis] = + MaxColor - NewColorSubdiv[Index].RGBMin[SortRGBAxis]; + + (*NewColorMapSize)++; + } + + return GIF_OK; +} + +/**************************************************************************** + Routine called by qsort to compare two entries. +*****************************************************************************/ + +static int +SortCmpRtn(const void *Entry1, + const void *Entry2) { + QuantizedColorType *entry1 = (*((QuantizedColorType **) Entry1)); + QuantizedColorType *entry2 = (*((QuantizedColorType **) Entry2)); + + /* sort on all axes of the color space! */ + int hash1 = entry1->RGB[SortRGBAxis] * 256 * 256 + + entry1->RGB[(SortRGBAxis+1) % 3] * 256 + + entry1->RGB[(SortRGBAxis+2) % 3]; + int hash2 = entry2->RGB[SortRGBAxis] * 256 * 256 + + entry2->RGB[(SortRGBAxis+1) % 3] * 256 + + entry2->RGB[(SortRGBAxis+2) % 3]; + + return hash1 - hash2; +} + +/* end */ diff --git a/src/win/unistd.h b/src/win/unistd.h new file mode 100644 index 00000000..8655b1d5 --- /dev/null +++ b/src/win/unistd.h @@ -0,0 +1 @@ +#include diff --git a/tests/00.argsValidation/001.open.js b/tests/00.argsValidation/001.open.js index a5ce9b22..c0bd291e 100644 --- a/tests/00.argsValidation/001.open.js +++ b/tests/00.argsValidation/001.open.js @@ -39,6 +39,11 @@ describe('lwip.open arguments validation', function() { lwip.open.bind(lwip, imgs.jpg.rgb, 'jjpg', function() {}).should.throwError(); }); }); + describe('with invalid type (raw buffer properties)', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, imgs.jpg.rgb, {width: 120, height: 120}, function() {}).should.throwError(); + }); + }); }); @@ -65,4 +70,107 @@ describe('lwip.open arguments validation', function() { }); + describe('pixelbuffer', function() { + + var buffer; + before(function(done) { + buffer = new Buffer(120 * 120); + done(); + }); + + describe('without raw buffer properties', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, function() {}).should.throwError(); + }); + }); + + describe('without width', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { height: 120 }, function() {}).should.throwError(); + }); + }); + + describe('without height', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { width: 120 }, function() {}).should.throwError(); + }); + }); + + describe('without width and height', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { }, function() {}).should.throwError(); + }); + }); + + describe('with non numeric width', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { width: "lorem", height: 120 }, function() {}).should.throwError(); + }); + }); + + describe('with non numeric height', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { width: 120, height: "lorem" }, function() {}).should.throwError(); + }); + }); + + describe('with non numeric width and height', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { width: "lorem", height: "ipsum" }, function() {}).should.throwError(); + }); + }); + + describe('with negative width', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { width: -120, height: 120 }, function() {}).should.throwError(); + }); + }); + + describe('with negative height', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { width: 120, height: -120 }, function() {}).should.throwError(); + }); + }); + + describe('with negative width and height', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { width: -120, height: -120 }, function() {}).should.throwError(); + }); + }); + + describe('with incorrect width and height', function() { + it('should throw an error', function() { + lwip.open.bind(lwip, buffer, { width: 123, height: 321 }, function() {}).should.throwError(); + }); + }); + + describe('with correct width and height for 1 channel', function() { + it('should succeed', function() { + lwip.open.bind(lwip, buffer, { width: 120, height: 120 }, function() {}).should.not.throw(); + }); + }); + + describe('with correct width and height for 2 channels', function() { + var newBuffer = new Buffer(120 * 120 * 2); + it('should succeed', function() { + lwip.open.bind(lwip, newBuffer, { width: 120, height: 120 }, function() {}).should.not.throw(); + }); + }); + + describe('with correct width and height for 3 channels', function() { + var newBuffer = new Buffer(120 * 120 * 3); + it('should succeed', function() { + lwip.open.bind(lwip, newBuffer, { width: 120, height: 120 }, function() {}).should.not.throw(); + }); + }); + + describe('with correct width and height for 4 channels', function() { + var newBuffer = new Buffer(120 * 120 * 4); + it('should succeed', function() { + lwip.open.bind(lwip, newBuffer, { width: 120, height: 120 }, function() {}).should.not.throw(); + }); + }); + + }); + }); diff --git a/tests/00.argsValidation/104.image.toBuffer.js b/tests/00.argsValidation/104.image.toBuffer.js index 7554b632..e65917a8 100644 --- a/tests/00.argsValidation/104.image.toBuffer.js +++ b/tests/00.argsValidation/104.image.toBuffer.js @@ -254,4 +254,133 @@ describe('image.toBuffer arguments validation', function() { }); + describe('GIF params', function() { + + describe("valid params", function(){ + + describe('defaults', function() { + it('should succeed', function(done) { + image.toBuffer.bind(image, 'gif', done).should.not.throwError(); + }); + }); + + describe('120, false, false, 50', function() { + it('should succeed', function(done) { + image.toBuffer.bind(image, 'gif', { + colors: 120, + interlaced: false, + transparency: false, + threshold: 50 + }, done).should.not.throwError(); + }); + }); + + describe('256, true, false, 50', function() { + it('should succeed', function(done) { + image.toBuffer.bind(image, 'gif', { + colors: 256, + interlaced: true, + transparency: false, + threshold: 50 + }, done).should.not.throwError(); + }); + }); + + describe('2, false, true, 0', function() { + it('should succeed', function(done) { + image.toBuffer.bind(image, 'gif', { + colors: 2, + interlaced: false, + transparency: true, + threshold: 0 + }, done).should.not.throwError(); + }); + }); + + describe('120, true, true, 100', function() { + it('should succeed', function(done) { + image.toBuffer.bind(image, 'gif', { + colors: 120, + interlaced: true, + transparency: true, + threshold: 100 + }, done).should.not.throwError(); + }); + }); + + }); + + describe("invalid params", function(){ + + describe('invalid colors (wrong type)', function() { + it('should throw an error', function() { + image.toBuffer.bind(image, 'gif', { + colors: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid colors (<2)', function() { + it('should throw an error', function() { + image.toBuffer.bind(image, 'gif', { + colors: 1 + }, function() {}).should.throwError(); + }); + }); + + describe('invalid colors (>256)', function() { + it('should throw an error', function() { + image.toBuffer.bind(image, 'gif', { + colors: 257 + }, function() {}).should.throwError(); + }); + }); + + describe('invalid interlaced', function() { + it('should throw an error', function() { + image.toBuffer.bind(image, 'gif', { + interlaced: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid transparency', function() { + it('should throw an error', function() { + image.toBuffer.bind(image, 'gif', { + transparency: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid threshold (wrong type)', function() { + it('should throw an error', function() { + image.toBuffer.bind(image, 'gif', { + transparency: true, + threshold: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid threshold (<0)', function() { + it('should throw an error', function() { + image.toBuffer.bind(image, 'gif', { + transparency: true, + threshold: -1 + }, function() {}).should.throwError(); + }); + }); + + describe('invalid threshold (>100)', function() { + it('should throw an error', function() { + image.toBuffer.bind(image, 'gif', { + transparency: true, + threshold: 101 + }, function() {}).should.throwError(); + }); + }); + + }); + + }); + }); diff --git a/tests/00.argsValidation/105.image.writeFile.js b/tests/00.argsValidation/105.image.writeFile.js index 7adf8442..ae429d2a 100644 --- a/tests/00.argsValidation/105.image.writeFile.js +++ b/tests/00.argsValidation/105.image.writeFile.js @@ -59,4 +59,80 @@ describe('image.writeFile arguments validation', function() { }); }); + + describe('GIF params', function() { + + describe("invalid params", function(){ + + describe('invalid colors (wrong type)', function() { + it('should throw an error', function() { + image.writeFile.bind(image, 'res.gif', 'gif', { + colors: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid colors (<2)', function() { + it('should throw an error', function() { + image.writeFile.bind(image, 'res.gif', 'gif', { + colors: 1 + }, function() {}).should.throwError(); + }); + }); + + describe('invalid colors (>256)', function() { + it('should throw an error', function() { + image.writeFile.bind(image, 'res.gif', 'gif', { + colors: 257 + }, function() {}).should.throwError(); + }); + }); + + describe('invalid interlaced', function() { + it('should throw an error', function() { + image.writeFile.bind(image, 'res.gif', 'gif', { + interlaced: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid transparency', function() { + it('should throw an error', function() { + image.writeFile.bind(image, 'res.gif', 'gif', { + transparency: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid threshold (wrong type)', function() { + it('should throw an error', function() { + image.writeFile.bind(image, 'res.gif', 'gif', { + transparency: true, + threshold: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid threshold (<0)', function() { + it('should throw an error', function() { + image.writeFile.bind(image, 'res.gif', 'gif', { + transparency: true, + threshold: -1 + }, function() {}).should.throwError(); + }); + }); + + describe('invalid threshold (>100)', function() { + it('should throw an error', function() { + image.writeFile.bind(image, 'res.gif', 'gif', { + transparency: true, + threshold: 101 + }, function() {}).should.throwError(); + }); + }); + + }); + + }); + }); diff --git a/tests/00.argsValidation/110.image.getPixel.js b/tests/00.argsValidation/110.image.getPixel.js new file mode 100644 index 00000000..803fadee --- /dev/null +++ b/tests/00.argsValidation/110.image.getPixel.js @@ -0,0 +1,25 @@ +// methods should throw errors when arguments are invalid + +var should = require("should"), + lwip = require('../../'), + imgs = require('../imgs'); + +describe('image.getPixel arguments validation', function() { + + var image; + before(function(done) { + lwip.open(imgs.jpg.rgb, function(err, img) { + image = img; + done(err); + }); + }); + + describe('coordinates exceeding image dimensions', function() { + it('should throw an error', function() { + image.getPixel.bind(image, 99999, 0).should.throwError(); + image.getPixel.bind(image, 0, 99999).should.throwError(); + image.getPixel.bind(image, 99999, 99999).should.throwError(); + }); + }); + +}); diff --git a/tests/00.argsValidation/110.image.setPixel.js b/tests/00.argsValidation/110.image.setPixel.js new file mode 100644 index 00000000..41e57ecb --- /dev/null +++ b/tests/00.argsValidation/110.image.setPixel.js @@ -0,0 +1,25 @@ +// methods should throw errors when arguments are invalid + +var should = require("should"), + lwip = require('../../'), + imgs = require('../imgs'); + +describe('image.setPixel arguments validation', function() { + + var image; + before(function(done) { + lwip.open(imgs.jpg.rgb, function(err, img) { + image = img; + done(err); + }); + }); + + describe("pixel coordinates out of bounds", function(){ + it("should throw an error", function(done){ + image.setPixel.bind(image, 9999, 0, 'yellow', function(){}).should.throwError(); + image.setPixel.bind(image, 0, 9999, 'yellow', function(){}).should.throwError(); + image.setPixel.bind(image, 9999, 9999, 'yellow', function(){}).should.throwError(); + image.setPixel.bind(image, 0, 0, 'yellow', done).should.not.throwError(); // sanity check + }); + }); +}); diff --git a/tests/00.argsValidation/203.batch.rotate.js b/tests/00.argsValidation/203.batch.rotate.js index 97216f78..01d3cbc4 100644 --- a/tests/00.argsValidation/203.batch.rotate.js +++ b/tests/00.argsValidation/203.batch.rotate.js @@ -8,7 +8,7 @@ describe('batch.rotate arguments validation', function() { var batch; before(function(done) { - lwip.open(imgs.jpg.rgb, function(err, img) { + lwip.open(imgs.gif.rgb, function(err, img) { batch = img.batch(); done(err); }); diff --git a/tests/00.argsValidation/204.batch.toBuffer.js b/tests/00.argsValidation/204.batch.toBuffer.js index 9359f4dc..f8d79cae 100644 --- a/tests/00.argsValidation/204.batch.toBuffer.js +++ b/tests/00.argsValidation/204.batch.toBuffer.js @@ -278,4 +278,133 @@ describe('batch.toBuffer arguments validation', function() { }); + describe('GIF params', function() { + + describe("valid params", function(){ + + describe('defaults', function() { + it('should succeed', function(done) { + batch.toBuffer.bind(batch, 'gif', done).should.not.throwError(); + }); + }); + + describe('120, false, false, 50', function() { + it('should succeed', function(done) { + batch.toBuffer.bind(batch, 'gif', { + colors: 120, + interlaced: false, + transparency: false, + threshold: 50 + }, done).should.not.throwError(); + }); + }); + + describe('256, true, false, 50', function() { + it('should succeed', function(done) { + batch.toBuffer.bind(batch, 'gif', { + colors: 256, + interlaced: true, + transparency: false, + threshold: 50 + }, done).should.not.throwError(); + }); + }); + + describe('2, false, true, 0', function() { + it('should succeed', function(done) { + batch.toBuffer.bind(batch, 'gif', { + colors: 2, + interlaced: false, + transparency: true, + threshold: 0 + }, done).should.not.throwError(); + }); + }); + + describe('120, true, true, 100', function() { + it('should succeed', function(done) { + batch.toBuffer.bind(batch, 'gif', { + colors: 120, + interlaced: true, + transparency: true, + threshold: 100 + }, done).should.not.throwError(); + }); + }); + + }); + + describe("invalid params", function(){ + + describe('invalid colors (wrong type)', function() { + it('should throw an error', function() { + batch.toBuffer.bind(batch, 'gif', { + colors: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid colors (<2)', function() { + it('should throw an error', function() { + batch.toBuffer.bind(batch, 'gif', { + colors: 1 + }, function() {}).should.throwError(); + }); + }); + + describe('invalid colors (>256)', function() { + it('should throw an error', function() { + batch.toBuffer.bind(batch, 'gif', { + colors: 257 + }, function() {}).should.throwError(); + }); + }); + + describe('invalid interlaced', function() { + it('should throw an error', function() { + batch.toBuffer.bind(batch, 'gif', { + interlaced: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid transparency', function() { + it('should throw an error', function() { + batch.toBuffer.bind(batch, 'gif', { + transparency: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid threshold (wrong type)', function() { + it('should throw an error', function() { + batch.toBuffer.bind(batch, 'gif', { + transparency: true, + threshold: 'foo' + }, function() {}).should.throwError(); + }); + }); + + describe('invalid threshold (<0)', function() { + it('should throw an error', function() { + batch.toBuffer.bind(batch, 'gif', { + transparency: true, + threshold: -1 + }, function() {}).should.throwError(); + }); + }); + + describe('invalid threshold (>100)', function() { + it('should throw an error', function() { + batch.toBuffer.bind(batch, 'gif', { + transparency: true, + threshold: 101 + }, function() {}).should.throwError(); + }); + }); + + }); + + }); + }); diff --git a/tests/00.argsValidation/206.batch.mirror.js b/tests/00.argsValidation/206.batch.mirror.js index 7e7856e6..8989b7c7 100644 --- a/tests/00.argsValidation/206.batch.mirror.js +++ b/tests/00.argsValidation/206.batch.mirror.js @@ -8,7 +8,7 @@ describe('batch.mirror arguments validation', function() { var batch; before(function(done) { - lwip.open(imgs.jpg.rgb, function(err, img) { + lwip.open(imgs.gif.trans, function(err, img) { batch = img.batch(); done(err); }); diff --git a/tests/00.argsValidation/210.batch.setPixel.js b/tests/00.argsValidation/210.batch.setPixel.js new file mode 100644 index 00000000..3eaa1660 --- /dev/null +++ b/tests/00.argsValidation/210.batch.setPixel.js @@ -0,0 +1,43 @@ +// methods should throw errors when arguments are invalid + +var should = require("should"), + assert = require('assert'), + lwip = require('../../'), + imgs = require('../imgs'); + +describe('batch.setPixel arguments validation', function() { + + var batch; + beforeEach(function(done) { + lwip.open(imgs.jpg.rgb, function(err, img) { + batch = img.batch(); + done(err); + }); + }); + + describe('pixel exceeds dimensions', function() { + + describe('at the time of exec', function() { + it('should return an error', function(done) { + batch.setPixel.bind(batch, 999, 999, 'yellow').should.not.throwError(); + batch.exec(function(err) { + // there should be an error message + assert(!!err); + done(); + }); + }); + }); + + describe('before exec', function() { + it('should not return an error', function(done) { + batch.resize(1000, 1000); + batch.setPixel.bind(batch, 999, 999, 'yellow').should.not.throwError(); + batch.exec(function(err) { + // there should not be an error message + assert(!err); + done(); + }); + }); + }); + }); +}); diff --git a/tests/01.getters/index.js b/tests/01.getters/index.js index 9e9e47ca..854cf7b1 100644 --- a/tests/01.getters/index.js +++ b/tests/01.getters/index.js @@ -33,6 +33,32 @@ describe('lwip.size', function() { }); }); +describe('lwip.getPixel', function() { + it('should return correct color at 0,0', function() { + var color = image.getPixel(0, 0); + assert(color.r === 48); + assert(color.g === 86); + assert(color.b === 151); + assert(color.a === 100); + }); + + it('should return correct color at 418, 242', function() { + var color = image.getPixel(418, 242); + assert(color.r === 208); + assert(color.g === 228); + assert(color.b === 237); + assert(color.a === 100); + }); + + it('should return correct color at 499, 332', function() { + var color = image.getPixel(499, 332); + assert(color.r === 31); + assert(color.g === 27); + assert(color.b === 0); + assert(color.a === 100); + }); +}); + describe('lwip.clone', function() { it('should return a new image object', function(done) { image.clone(function(err, clonedImage) { diff --git a/tests/02.operations/001.open.js b/tests/02.operations/001.open.js index 74af5d62..f4f460c5 100644 --- a/tests/02.operations/001.open.js +++ b/tests/02.operations/001.open.js @@ -109,6 +109,59 @@ describe('lwip.open', function() { }); + describe('gif file', function() { + + describe('rgb image (with gif extension)', function() { + it('should succeed', function(done) { + lwip.open(imgs.gif.rgb, function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + describe('rgb image (no extension)', function() { + it('should succeed', function(done) { + lwip.open(imgs.gif.noex, 'gif', function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + describe('grayscale image', function() { + it('should succeed', function(done) { + lwip.open(imgs.gif.gs, function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + describe('transparent image', function() { + it('should succeed', function(done) { + lwip.open(imgs.gif.trans, function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + describe('invalid image', function() { + it('should fail', function(done) { + lwip.open(imgs.gif.inv, function(err, img) { + should(err).be.Error; + done(); + }); + }); + }); + + }); + describe('jpeg buffer', function() { describe('rgb image', function() { @@ -188,4 +241,112 @@ describe('lwip.open', function() { }); }); + + describe('gif buffer', function() { + + describe('rgb image', function() { + var buffer; + before(function(done) { + fs.readFile(imgs.gif.rgb, function(err, imbuf) { + buffer = imbuf; + done(err); + }); + }); + + it('should succeed', function(done) { + lwip.open(buffer, 'gif', function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + describe('grayscale image', function() { + var buffer; + before(function(done) { + fs.readFile(imgs.gif.gs, function(err, imbuf) { + buffer = imbuf; + done(err); + }); + }); + + it('should succeed', function(done) { + lwip.open(buffer, 'gif', function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + }); + + describe('raw pixel buffer', function() { + + describe('grayscale image', function(){ + var buffer; + before(function(done) { + buffer = new Buffer(100 * 100); + done(); + }); + + it('should succeed', function(done) { + lwip.open(buffer, { width: 100, height: 100 }, function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + describe('grayscale image with alpha', function(){ + var buffer; + before(function(done) { + buffer = new Buffer(100 * 100 * 2); + done(); + }); + + it('should succeed', function(done) { + lwip.open(buffer, { width: 100, height: 100 }, function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + describe('rgb image', function(){ + var buffer; + before(function(done) { + buffer = new Buffer(100 * 100 * 3); + done(); + }); + + it('should succeed', function(done) { + lwip.open(buffer, { width: 100, height: 100 }, function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + describe('rgb image with alpha', function(){ + var buffer; + before(function(done) { + buffer = new Buffer(100 * 100 * 4); + done(); + }); + + it('should succeed', function(done) { + lwip.open(buffer, { width: 100, height: 100 }, function(err, img) { + should(err).not.be.Error; + img.should.be.OK; + done(); + }); + }); + }); + + }); }); diff --git a/tests/02.operations/101.writeFile.js b/tests/02.operations/101.writeFile.js index 2d1a8b9c..cf881abd 100644 --- a/tests/02.operations/101.writeFile.js +++ b/tests/02.operations/101.writeFile.js @@ -7,8 +7,10 @@ var join = require('path').join, var tmpDir = join(__dirname, '../results'), outJpeg = 'write_test.jpg', outPng = 'write_test.png', + outGif = 'write_test.gif', outpathJpeg = join(tmpDir, outJpeg), - outpathPng = join(tmpDir, outPng); + outpathPng = join(tmpDir, outPng), + outpathGif = join(tmpDir, outGif); describe('lwip.writeFile', function() { @@ -149,4 +151,72 @@ describe('lwip.writeFile', function() { }); }); + + describe('gif', function() { + + describe('with type unspecified', function() { + + describe('params unspecified', function() { + it('should succeed', function(done) { + image.writeFile(outpathGif, done); + }); + }); + + describe('params specified - 256, not interlaced, not transparent, 50', function() { + it('should succeed', function(done) { + image.writeFile(outpathGif, { + colors: 256, + interlaced: false, + transparent: false, + threshold: 100 + }, done); + }); + }); + + describe('params specified - 99, interlaced, not transparent, 50', function() { + it('should succeed', function(done) { + image.writeFile(outpathGif, { + colors: 99, + interlaced: true, + transparent: false, + threshold: 100 + }, done); + }); + }); + + describe('params specified - 256, not interlaced, transparent, 30', function() { + it('should succeed', function(done) { + image.writeFile(outpathGif, { + colors: 256, + interlaced: false, + transparent: true, + threshold: 30 + }, done); + }); + }); + + describe('params specified - 16, interlaced, transparent, 88', function() { + it('should succeed', function(done) { + image.writeFile(outpathGif, { + colors: 16, + interlaced: true, + transparent: true, + threshold: 88 + }, done); + }); + }); + + }); + + describe('with type specified', function() { + + describe('params unspecified', function() { + it('should succeed', function(done) { + image.writeFile(outpathGif, 'gif', done); + }); + }); + + }); + + }); }); diff --git a/tests/02.operations/104.rotate.js b/tests/02.operations/104.rotate.js index bf3957bd..2e494e72 100644 --- a/tests/02.operations/104.rotate.js +++ b/tests/02.operations/104.rotate.js @@ -17,7 +17,7 @@ describe('lwip.rotate', function() { }); beforeEach(function(done) { - lwip.open(imgs.jpg.rgb, function(err, img) { + lwip.open(imgs.gif.trans, function(err, img) { image = img; done(err); }); @@ -113,6 +113,17 @@ describe('lwip.rotate', function() { }); }); + describe('-5 degs, {r:200,g:110,b:220} fill', function() { + it('should succeed', function(done) { + current.push(-5, 'degs', 'r-200,g-110,b-220'); + image.rotate(-5, { + r: 200, + g: 110, + b: 220 + }, done); + }); + }); + describe('-5 degs, {r:200,g:110,b:220,a:50} fill', function() { it('should succeed', function(done) { current.push(-5, 'degs', 'r-200,g-110,b-220,a-50'); diff --git a/tests/02.operations/107.mirror.js b/tests/02.operations/107.mirror.js index d0a7ac2e..3db952a7 100644 --- a/tests/02.operations/107.mirror.js +++ b/tests/02.operations/107.mirror.js @@ -25,8 +25,8 @@ describe('lwip.mirror (/flip)', function() { }); afterEach(function(done) { - image.writeFile(join(tmpDir, current.join('_') + '.jpg'), 'jpeg', { - quality: 100 + image.writeFile(join(tmpDir, current.join('_') + '.gif'), 'gif', { + colors: 256 }, done); }); @@ -43,10 +43,10 @@ describe('lwip.mirror (/flip)', function() { }); }); - describe('Y', function() { + describe('y', function() { it('should succeed', function(done) { - current.push('Y'); - image.flip('Y', done); + current.push('y'); + image.flip('y', done); }); }); @@ -58,17 +58,17 @@ describe('lwip.mirror (/flip)', function() { current = [basename, 'axes']; }); - describe('Xy', function() { + describe('xy', function() { it('should succeed', function(done) { - current.push('Xy'); - image.flip('Xy', done); + current.push('xy'); + image.flip('xy', done); }); }); - describe('YX', function() { + describe('yx', function() { it('should succeed', function(done) { - current.push('YX'); - image.mirror('YX', done); + current.push('yx'); + image.mirror('yx', done); }); }); diff --git a/tests/02.operations/109.border.js b/tests/02.operations/109.border.js index 329ec890..5b8ddaca 100644 --- a/tests/02.operations/109.border.js +++ b/tests/02.operations/109.border.js @@ -7,7 +7,7 @@ var join = require('path').join, var tmpDir = join(__dirname, '../results'), basename = 'border', - current = [basename]; + current; describe('lwip.border', function() { @@ -17,16 +17,20 @@ describe('lwip.border', function() { mkdirp(tmpDir, done); }); + beforeEach(function(){ + current = [basename]; + }); + beforeEach(function(done) { - lwip.open(imgs.png.rgb, function(err, img) { + lwip.open(imgs.png.trans, function(err, img) { image = img; done(err); }); }); afterEach(function(done) { - image.writeFile(join(tmpDir, current.join('_') + '.png'), 'png', { - compression: 'none' + image.writeFile(join(tmpDir, current.join('_') + '.gif'), 'gif', { + colors: 256 }, done); }); diff --git a/tests/02.operations/118.setPixel.js b/tests/02.operations/118.setPixel.js new file mode 100644 index 00000000..932c7671 --- /dev/null +++ b/tests/02.operations/118.setPixel.js @@ -0,0 +1,71 @@ +var join = require('path').join, + should = require('should'), + assert = require('assert'), + mkdirp = require('mkdirp'), + lwip = require('../../'), + imgs = require('../imgs'); + +var tmpDir = join(__dirname, '../results'), + basename = 'setPixel', + current; + +describe('lwip.setPixel', function() { + + var image; + + before(function(done) { + mkdirp(tmpDir, done); + }); + + beforeEach(function(done) { + lwip.open(imgs.png.trans, function(err, img) { + image = img; + done(err); + }); + }); + + afterEach(function(done) { + image.writeFile(join(tmpDir, current.join('_') + '.jpg'), 'jpeg', { + quality: 100 + }, done); + }); + + beforeEach(function() { + current = [basename]; + }); + + describe('red pixel at 0,0', function() { + + it('pixel should have the correct color', function(done) { + current.push('0,0-red'); + image.setPixel(0, 0, 'red', function(err, im) { + if (err) return done(err); + var color = im.getPixel(0, 0); + assert(color.r === 255); + assert(color.g === 0); + assert(color.b === 0); + assert(color.a === 100); + done(); + }); + }); + + }); + + describe('red pixel at 100,100', function() { + + it('pixel should have the correct color', function(done) { + current.push('100,100-red'); + image.setPixel(100, 100, 'red', function(err, im) { + if (err) return done(err); + var color = im.getPixel(100, 100); + assert(color.r === 255); + assert(color.g === 0); + assert(color.b === 0); + assert(color.a === 100); + done(); + }); + }); + + }); + +}); diff --git a/tests/02.operations/119.contain.js b/tests/02.operations/119.contain.js new file mode 100644 index 00000000..6f868b8c --- /dev/null +++ b/tests/02.operations/119.contain.js @@ -0,0 +1,86 @@ +var join = require('path').join, + should = require('should'), + assert = require('assert'), + mkdirp = require('mkdirp'), + lwip = require('../../'), + imgs = require('../imgs'); + +var tmpDir = join(__dirname, '../results'), + basename = 'contain', + current; + +describe('lwip.contain', function() { + + var image; + + before(function(done) { + mkdirp(tmpDir, done); + }); + + beforeEach(function(done) { + lwip.open(imgs.png.rgb, function(err, img) { + image = img; + done(err); + }); + }); + + afterEach(function(done) { + image.writeFile(join(tmpDir, current.join('_') + '.gif'), 'gif', { + colors: 256, + interlaced: true + }, done); + }); + + beforeEach(function(){ + current = [ basename ]; + }); + + describe('800X100, no color, unspecified interpolation', function() { + it('image should have the correct size', function(done) { + current.push('800X100','no_color','unspecified_inter'); + image.contain(800, 100, function(err, im) { + if (err) return done(err); + assert(im.width() === 800); + assert(im.height() === 100); + done(); + }); + }); + }); + + describe('100X800, no color, lanczos interpolation', function() { + it('image should have the correct size', function(done) { + current.push('100X800','no_color','lanczos'); + image.contain(100, 800, 'lanczos', function(err, im) { + if (err) return done(err); + assert(im.width() === 100); + assert(im.height() === 800); + done(); + }); + }); + }); + + describe('100X400, gray, unspecified interpolation', function() { + it('image should have the correct size', function(done) { + current.push('100X400','gray','unspecified_inter'); + image.contain(100, 400, 'gray', function(err, im) { + if (err) return done(err); + assert(im.width() === 100); + assert(im.height() === 400); + done(); + }); + }); + }); + + describe('400X100, gray, lanczos interpolation', function() { + it('image should have the correct size', function(done) { + current.push('400X100','gray','lanczos'); + image.contain(400, 100, 'gray', 'lanczos', function(err, im) { + if (err) return done(err); + assert(im.width() === 400); + assert(im.height() === 100); + done(); + }); + }); + }); + +}); diff --git a/tests/02.operations/120.cover.js b/tests/02.operations/120.cover.js new file mode 100644 index 00000000..c69a40f7 --- /dev/null +++ b/tests/02.operations/120.cover.js @@ -0,0 +1,61 @@ +var join = require('path').join, + should = require('should'), + assert = require('assert'), + mkdirp = require('mkdirp'), + lwip = require('../../'), + imgs = require('../imgs'); + +var tmpDir = join(__dirname, '../results'), + basename = 'cover', + current; + +describe('lwip.cover', function() { + + var image; + + before(function(done) { + mkdirp(tmpDir, done); + }); + + beforeEach(function(done) { + lwip.open(imgs.png.rgb, function(err, img) { + image = img; + done(err); + }); + }); + + afterEach(function(done) { + image.writeFile(join(tmpDir, current.join('_') + '.jpg'), 'jpg', { + quality: 90 + }, done); + }); + + beforeEach(function(){ + current = [ basename ]; + }); + + describe('800X300, unspecified interpolation', function() { + it('image should have the correct size', function(done) { + current.push('800X300','unspecified_inter'); + image.cover(800, 300, function(err, im) { + if (err) return done(err); + assert(im.width() === 800); + assert(im.height() === 300); + done(); + }); + }); + }); + + describe('300X800, lanczos interpolation', function() { + it('image should have the correct size', function(done) { + current.push('300X800','lanczos'); + image.cover(300, 800, 'lanczos', function(err, im) { + if (err) return done(err); + assert(im.width() === 300); + assert(im.height() === 800); + done(); + }); + }); + }); + +}); diff --git a/tests/03.safety/00.locks.js b/tests/03.safety/00.locks.js index d88d87d6..66ea7f80 100644 --- a/tests/03.safety/00.locks.js +++ b/tests/03.safety/00.locks.js @@ -5,7 +5,7 @@ var join = require('path').join, describe('simultaneous operations locks', function() { - var image; + var image, tmpImage; beforeEach(function(done) { lwip.open(imgs.jpg.rgb, function(err, img) { @@ -14,6 +14,13 @@ describe('simultaneous operations locks', function() { }); }); + before(function(done){ + lwip.create(10, 10, function(err, img){ + tmpImage = img; + done(err); + }); + }); + describe('image.resize lock', function() { it('should lock image', function() { image.resize.bind(image, 100, 100, function() {}).should.not.throwError(); @@ -122,6 +129,34 @@ describe('simultaneous operations locks', function() { describe('image.hslaAdjust lock', function() { it('should lock image', function() { image.hslaAdjust.bind(image, 100, 1, 1, 0, function() {}).should.not.throwError(); + image.setPixel.bind(image, 0, 0, 'yellow', function() {}).should.throwError(); + }); + }); + + describe('image.setPixel lock', function() { + it('should lock image', function() { + image.setPixel.bind(image, 0, 0, 'yellow', function() {}).should.not.throwError(); + image.paste.bind(image, 0, 0, tmpImage, function() {}).should.throwError(); + }); + }); + + describe('image.paste lock', function() { + it('should lock image', function() { + image.paste.bind(image, 0, 0, tmpImage, function() {}).should.not.throwError(); + image.contain.bind(image, 100, 100, function() {}).should.throwError(); + }); + }); + + describe('image.contain lock', function() { + it('should lock image', function() { + image.contain.bind(image, 100, 200, function() {}).should.not.throwError(); + image.cover.bind(image, 100, 100, function() {}).should.throwError(); + }); + }); + + describe('image.cover lock', function() { + it('should lock image', function() { + image.cover.bind(image, 200, 300, function() {}).should.not.throwError(); image.resize.bind(image, 100, 100, function() {}).should.throwError(); }); }); diff --git a/tests/03.safety/01.releases.js b/tests/03.safety/01.releases.js index d7045953..224c93f7 100644 --- a/tests/03.safety/01.releases.js +++ b/tests/03.safety/01.releases.js @@ -5,7 +5,7 @@ var join = require('path').join, describe('failed ops lock release', function() { - var image; + var image, tmpImage; beforeEach(function(done) { lwip.open(imgs.jpg.rgb, function(err, img) { @@ -14,6 +14,13 @@ describe('failed ops lock release', function() { }); }); + before(function(done){ + lwip.create(10, 10, function(err, img){ + tmpImage = img; + done(err); + }); + }); + describe('image.resize release', function() { it('should release image lock', function() { image.resize.bind(image, 'foo', 'bar', function() {}).should.throwError(); @@ -122,6 +129,34 @@ describe('failed ops lock release', function() { describe('image.hslaAdjust release', function() { it('should release image lock', function() { image.hslaAdjust.bind(image, 'foo', 'foo', 'foo', 'foo', function() {}).should.throwError(); + image.setPixel.bind(image, 0, 0, 'yellow', function() {}).should.not.throwError(); + }); + }); + + describe('image.setPixel release', function() { + it('should release image lock', function() { + image.setPixel.bind(image, 'foo', 'foo', 'foo', function() {}).should.throwError(); + image.paste.bind(image, 0, 0, tmpImage, function() {}).should.not.throwError(); + }); + }); + + describe('image.paste release', function() { + it('should release image lock', function() { + image.paste.bind(image, 'foo', 'foo', 'foo', function() {}).should.throwError(); + image.contain.bind(image, 100, 200, function() {}).should.not.throwError(); + }); + }); + + describe('image.contain release', function() { + it('should release image lock', function() { + image.contain.bind(image, 'foo', 'foo', 'foo', function() {}).should.throwError(); + image.cover.bind(image, 100, 100, function() {}).should.not.throwError(); + }); + }); + + describe('image.cover release', function() { + it('should release image lock', function() { + image.cover.bind(image, 'foo', 'foo', function() {}).should.throwError(); image.resize.bind(image, 100, 100, function() {}).should.not.throwError(); }); }); diff --git a/tests/04.batch/index.js b/tests/04.batch/index.js index 0001ee7f..d03e342d 100644 --- a/tests/04.batch/index.js +++ b/tests/04.batch/index.js @@ -131,6 +131,66 @@ describe('image.batch', function() { }); + describe('gif', function() { + + describe('non interlaced', function() { + + describe('no transparency', function() { + it('should succeed', function(done) { + batch.toBuffer('gif', { + colors: 122, + interlaced: false, + transparency: false + }, function(err, buffer) { + done(err); + }); + }); + }); + + describe('with transparency', function() { + it('should succeed', function(done) { + batch.toBuffer('gif', { + interlaced: false, + transparency: true, + threshold: 55 + }, function(err, buffer) { + done(err); + }); + }); + }); + + }); + + describe('interlaced', function() { + + describe('no transparency', function() { + it('should succeed', function(done) { + batch.toBuffer('gif', { + colors: 122, + interlaced: true, + transparency: false + }, function(err, buffer) { + done(err); + }); + }); + }); + + describe('with transparency', function() { + it('should succeed', function(done) { + batch.toBuffer('gif', { + interlaced: true, + transparency: true, + threshold: 55 + }, function(err, buffer) { + done(err); + }); + }); + }); + + }); + + }); + }); describe('writeFile', function() { @@ -227,6 +287,54 @@ describe('image.batch', function() { }); + describe('gif', function() { + + describe('non interlaced', function() { + + describe('no transparency', function() { + it('should succeed', function(done) { + batch.writeFile(join(tmpDir, 'btch--noint#notrn--' + ops.join('#') + '.gif'), 'gif', { + interlaced: false, + transparency: false, + }, done); + }); + }); + + describe('with transparency', function() { + it('should succeed', function(done) { + batch.writeFile(join(tmpDir, 'btch--noint#trn--' + ops.join('#') + '.gif'), 'gif', { + interlaced: false, + transparency: true, + }, done); + }); + }); + + }); + + describe('interlaced', function() { + + describe('no transparency', function() { + it('should succeed', function(done) { + batch.writeFile(join(tmpDir, 'btch--noint#notrn--' + ops.join('#') + '.gif'), 'gif', { + interlaced: true, + transparency: false, + }, done); + }); + }); + + describe('with transparency', function() { + it('should succeed', function(done) { + batch.writeFile(join(tmpDir, 'btch--noint#trn--' + ops.join('#') + '.gif'), 'gif', { + interlaced: true, + transparency: true, + }, done); + }); + }); + + }); + + }); + }); }); diff --git a/tests/05.stress/index.js b/tests/05.stress/index.js index 371e1d4f..f4ba2395 100644 --- a/tests/05.stress/index.js +++ b/tests/05.stress/index.js @@ -9,7 +9,8 @@ var join = require('path').join, var tmpDir = join(__dirname, '../results'), outpathJpeg = join(tmpDir, 'stress.jpg'), - outpathPng = join(tmpDir, 'stress.png'); + outpathPng = join(tmpDir, 'stress.png'), + outpathGif = join(tmpDir, 'stress.gif'); describe('stress tests', function() { @@ -49,7 +50,7 @@ describe('stress tests', function() { describe('open image 300 times (in parallel) and save to disk as png (fast compression, not interlaced)', function() { it('should succeed', function(done) { async.times(300, function(i, done) { - lwip.open(imgs.jpg.rgb, 'jpeg', function(err, image) { + lwip.open(imgs.gif.rgb, 'gif', function(err, image) { if (err) return done(err); image.writeFile(outpathPng, 'png', { compression: 'fast', @@ -60,6 +61,21 @@ describe('stress tests', function() { }); }); + describe('open image 300 times (in parallel) and save to disk as gif (128 colors, not interlaced, transparent)', function() { + it('should succeed', function(done) { + async.times(300, function(i, done) { + lwip.open(imgs.png.trans, 'png', function(err, image) { + if (err) return done(err); + image.writeFile(outpathGif, 'gif', { + colors: 128, + transparency: true, + threshold: 60 + }, done); + }); + }, done); + }); + }); + describe('7 random manipulations for 50 images (in parallel)', function() { it('should succeed', function(done) { async.times(50, function(i, done) { diff --git a/tests/images/gs.gif b/tests/images/gs.gif new file mode 100644 index 00000000..614d1531 Binary files /dev/null and b/tests/images/gs.gif differ diff --git a/tests/images/invalid.gif b/tests/images/invalid.gif new file mode 100644 index 00000000..aef3cf0d Binary files /dev/null and b/tests/images/invalid.gif differ diff --git a/tests/images/rgb.gif b/tests/images/rgb.gif new file mode 100644 index 00000000..0462fc29 Binary files /dev/null and b/tests/images/rgb.gif differ diff --git a/tests/images/rgbgif b/tests/images/rgbgif new file mode 100644 index 00000000..0462fc29 Binary files /dev/null and b/tests/images/rgbgif differ diff --git a/tests/images/trans.gif b/tests/images/trans.gif new file mode 100644 index 00000000..83766c26 Binary files /dev/null and b/tests/images/trans.gif differ diff --git a/tests/imgs.js b/tests/imgs.js index 582e1947..fa957508 100644 --- a/tests/imgs.js +++ b/tests/imgs.js @@ -15,5 +15,12 @@ module.exports = { noex: join(__dirname, imbase, 'rgbpng'), trans: join(__dirname, imbase, 'trans.png'), inv: join(__dirname, imbase, 'invalid.png') + }, + gif: { + gs: join(__dirname, imbase, 'gs.gif'), + rgb: join(__dirname, imbase, 'rgb.gif'), + noex: join(__dirname, imbase, 'rgbgif'), + trans: join(__dirname, imbase, 'trans.gif'), + inv: join(__dirname, imbase, 'invalid.gif') } }; diff --git a/tests/utils.js b/tests/utils.js index 2f9b44f0..275ebc9a 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -6,7 +6,7 @@ module.exports = { function generateRandomBatch(batch, n) { var ops = []; for (var i = 0; i < n; i++) { - var r = Math.floor(Math.random() * 14); + var r = Math.floor(Math.random() * 16); switch (r) { case 0: var sd = Math.floor(Math.random() * 20); @@ -81,6 +81,18 @@ function generateRandomBatch(batch, n) { batch = batch.opacify(); ops.push('opc'); break; + case 14: + var w = Math.floor(Math.random() * 1000) + 10; + var h = Math.floor(Math.random() * 1000) + 10; + batch = batch.contain(w, h, getRandomColor()); + ops.push('cnt' + w + 'X' + h); + break; + case 15: + var w = Math.floor(Math.random() * 1000) + 10; + var h = Math.floor(Math.random() * 1000) + 10; + batch = batch.cover(w, h); + ops.push('cvr' + w + 'X' + h); + break; } } return ops;