├── docs └── _config.yml ├── test ├── output │ └── .gitignore ├── fixtures │ ├── input.bmp │ ├── input.jpg │ ├── input.png │ ├── input.tif │ ├── FreeSans.ttf │ ├── input.heif │ ├── node-gd.gif │ └── input-transparent.png ├── dirname.mjs ├── image-pointer.test.mjs ├── destroy.test.mjs ├── openfile.test.mjs ├── colormatch.test.mjs ├── tiff.test.mjs ├── query-image-info.test.mjs ├── gifanim.test.mjs ├── file-types.test.mjs ├── image-creation.test.mjs ├── main.test.mjs ├── heif-avif.test.mjs └── fonts-and-images.test.mjs ├── .prebuildrc ├── .gitignore ├── index.js ├── util.sh ├── Dockerfile ├── .github └── workflows │ ├── run-tests.yml │ └── prebuild.yml ├── CONTRIBUTORS.md ├── LICENSE ├── src ├── addon.cc ├── node_gd.h └── node_gd_workers.cc ├── lib ├── bindings.js ├── node-gd.js └── GifAnim.js ├── package.json ├── CHANGELOG.md ├── CLAUDE.md ├── docker-test.sh ├── binding.gyp ├── README.md └── index.d.ts /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /test/output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | ~.gitignore 3 | -------------------------------------------------------------------------------- /test/fixtures/input.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/HEAD/test/fixtures/input.bmp -------------------------------------------------------------------------------- /test/fixtures/input.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/HEAD/test/fixtures/input.jpg -------------------------------------------------------------------------------- /test/fixtures/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/HEAD/test/fixtures/input.png -------------------------------------------------------------------------------- /test/fixtures/input.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/HEAD/test/fixtures/input.tif -------------------------------------------------------------------------------- /test/fixtures/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/HEAD/test/fixtures/FreeSans.ttf -------------------------------------------------------------------------------- /test/fixtures/input.heif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/HEAD/test/fixtures/input.heif -------------------------------------------------------------------------------- /test/fixtures/node-gd.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/HEAD/test/fixtures/node-gd.gif -------------------------------------------------------------------------------- /test/fixtures/input-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/HEAD/test/fixtures/input-transparent.png -------------------------------------------------------------------------------- /.prebuildrc: -------------------------------------------------------------------------------- 1 | { 2 | "napi": true, 3 | "strip": true, 4 | "arch": ["x64", "arm64"], 5 | "target": [ 6 | "node@16.0.0", 7 | "node@18.0.0", 8 | "node@20.0.0", 9 | "node@22.0.0" 10 | ], 11 | "debug": false 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | prebuilds 4 | .DS_Store 5 | test/fixtures/.DS_Store 6 | DS_Store 7 | test/dev/* 8 | .cache 9 | .npm 10 | .node_repl_history 11 | .bash_history 12 | .python_history 13 | .vscode 14 | 15 | # Prebuild artifacts 16 | *.tar.gz 17 | *.tgz -------------------------------------------------------------------------------- /test/dirname.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const dirname = (url) => { 5 | const filePath = fileURLToPath(url); 6 | const dirname = path.dirname(filePath); 7 | return dirname; 8 | }; 9 | 10 | export default dirname; 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Set up module 3 | * Copyright (c) 2020 Vincent Bruijn 4 | * 5 | * MIT Licensed 6 | */ 7 | 8 | import gd from './lib/node-gd.js'; 9 | import GifAnim from './lib/GifAnim.js'; 10 | 11 | gd.GifAnim = GifAnim; 12 | 13 | export default gd; 14 | -------------------------------------------------------------------------------- /util.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig 3 | LIST=`pkg-config --static --libs-only-l gdlib | sed s/-l//g` 4 | PRESENT=0 5 | 6 | for i in $LIST; do 7 | if test "$i" = "$1"; then 8 | PRESENT=1 9 | fi 10 | done 11 | 12 | if test $PRESENT -eq 0; then 13 | echo false 14 | else 15 | echo true 16 | fi 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Map /usr/src to /Users/vincentb/Projects/node-gd 2 | # docker run -it -v $(pwd):/usr/src y-a-v-a:node-gd bash 3 | FROM node:22 4 | 5 | USER root 6 | 7 | ENV HOME=/usr/src 8 | 9 | RUN apt-get update && \ 10 | apt-get install build-essential pkg-config python3 libgd-dev libheif-dev libavif-dev -y && \ 11 | npm i -g npm && \ 12 | npm i -g node-gyp && \ 13 | mkdir $HOME/.cache && \ 14 | chown -R node:node $HOME 15 | 16 | USER node 17 | 18 | WORKDIR $HOME 19 | 20 | ENTRYPOINT [] 21 | 22 | CMD ["bash"] 23 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | on: [push, pull_request] 3 | jobs: 4 | build-and-test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node: [18, 20, 22] 9 | name: Node ${{ matrix.node }} sample 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Node setup 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: ${{ matrix.node }} 17 | cache: 'npm' 18 | - name: Setup GCC 19 | uses: egor-tensin/setup-gcc@v1 20 | with: 21 | platform: x64 22 | - name: Install packages 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get -y install libgd-dev libheif-dev libavif-dev libaom-dev libdav1d-dev 26 | - name: Run npm tasks 27 | run: npm install && npm run test 28 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | * 267 Vincent Bruijn 2 | * 39 Vincent Bruijn 3 | * 29 Mike Smullin 4 | * 11 andris9 5 | * 8 Ilya Sheershoff 6 | * 7 taggon 7 | * 4 Andris Reinman 8 | * 3 Svetlozar Argirov 9 | * 3 Damian Senn 10 | * 2 Yun Lai 11 | * 2 Andris Reinman 12 | * 2 Burak Tamturk 13 | * 2 Christophe BENOIT 14 | * 2 Vladislav Veluga 15 | * 1 dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 16 | * 1 kmpm 17 | * 1 Tim Smart 18 | * 1 Farrin Reid 19 | * 1 Gabriele D'Arrigo 20 | * 1 Holixus 21 | * 1 Josh Dawkins 22 | * 1 Thomas de Barochez 23 | * 1 Dany Shaanan 24 | * 1 Carlos Rodriguez 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012 the contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/addon.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2009-2011, Taegon Kim 3 | * Copyright (c) 2014-2021, Vincent Bruijn 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | #include 19 | #include "node_gd.h" 20 | #include "node_gd.cc" 21 | 22 | Napi::Object Init(Napi::Env env, Napi::Object exports) 23 | { 24 | Gd::Init(env, exports); 25 | 26 | return exports; 27 | } 28 | 29 | NODE_API_MODULE(node_gd, Init); 30 | -------------------------------------------------------------------------------- /lib/bindings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Load C++ bindings for libgd 3 | * Copyright (c) 2014-2021 Vincent Bruijn 4 | * 5 | * MIT Licensed 6 | */ 7 | 8 | import path from 'path'; 9 | import { fileURLToPath } from 'url'; 10 | import { createRequire } from 'module'; 11 | const require = createRequire(import.meta.url); 12 | 13 | const filePath = fileURLToPath(import.meta.url); 14 | const dirname = path.dirname(filePath); 15 | 16 | let bindings; 17 | 18 | try { 19 | // Try to load using node-gyp-build (will load prebuilds if available) 20 | bindings = require('node-gyp-build')(path.resolve(dirname, '..')); 21 | } catch (e) { 22 | // Fallback to manual path resolution for backward compatibility 23 | const libPaths = [ 24 | path.normalize(`${dirname}/../build/Release/node_gd.node`), 25 | path.normalize(`${dirname}/../build/default/node_gd.node`), 26 | ]; 27 | 28 | try { 29 | bindings = require(libPaths.shift()); 30 | } catch (e) { 31 | console.log(e.message); 32 | try { 33 | bindings = require(libPaths.shift()); 34 | } catch (e) { 35 | console.log(e.message); 36 | console.log('Unable to find addon node_gd.node in build directory.'); 37 | process.exit(1); 38 | } 39 | } 40 | } 41 | 42 | export default bindings; 43 | -------------------------------------------------------------------------------- /test/image-pointer.test.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import gd from '../index.js'; 4 | import { assert } from 'chai'; 5 | 6 | import dirname from './dirname.mjs'; 7 | 8 | const currentDir = dirname(import.meta.url); 9 | 10 | var source = currentDir + '/fixtures'; 11 | 12 | const s = source + '/input.jpg'; 13 | 14 | describe('gd.createFromJpegPtr - Creating image from Buffer', function () { 15 | it('should not accept a String', function (done) { 16 | const imageAsString = fs.readFile(s, function (error, data) { 17 | if (error) { 18 | throw error; 19 | } 20 | 21 | assert.throws( 22 | function () { 23 | gd.createFromJpegPtr(data.toString('utf8')); 24 | }, 25 | TypeError, 26 | /Argument not a Buffer/ 27 | ); 28 | 29 | done(); 30 | }); 31 | }); 32 | 33 | it('should accept a Buffer', function (done) { 34 | const imageData = fs.readFile(s, function (error, data) { 35 | if (error) { 36 | throw error; 37 | } 38 | 39 | const img = gd.createFromJpegPtr(data); 40 | 41 | assert.ok(img instanceof gd.Image); 42 | img.destroy(); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('should not accept a Number', function (done) { 48 | assert.throws( 49 | function () { 50 | gd.createFromJpegPtr(1234567890); 51 | }, 52 | TypeError, 53 | /Argument not a Buffer/ 54 | ); 55 | done(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/destroy.test.mjs: -------------------------------------------------------------------------------- 1 | import gd from '../index.js'; 2 | import { assert } from 'chai'; 3 | 4 | describe('Image destroy', function () { 5 | it("gd.Image#destroy() -- accessing 'width' property after destroy throws an Error", async function () { 6 | const img = await gd.create(200, 200); 7 | assert.strictEqual(img.width, 200); 8 | assert.strictEqual(img.height, 200); 9 | assert.instanceOf(img, gd.Image, 'Object not instance of gd.Image'); 10 | img.destroy(); 11 | 12 | try { 13 | img.width; 14 | } catch (e) { 15 | assert.instanceOf(e, Error); 16 | } 17 | }); 18 | 19 | it("gd.Image#destroy() -- accessing 'height' property after destroy throws an Error", async function () { 20 | const img = await gd.create(200, 200); 21 | assert.strictEqual(img.height, 200); 22 | img.destroy(); 23 | 24 | try { 25 | img.height; 26 | } catch (e) { 27 | assert.ok(e instanceof Error); 28 | } 29 | }); 30 | 31 | it("gd.Image#destroy() -- accessing 'trueColor' property after destroy throws an Error", async function () { 32 | const img = await gd.create(200, 200); 33 | assert.strictEqual(img.trueColor, 0); 34 | img.destroy(); 35 | 36 | try { 37 | img.trueColor; 38 | } catch (e) { 39 | assert.ok(e instanceof Error); 40 | } 41 | }); 42 | 43 | it("gd.Image#destroy() -- accessing 'trueColor' property after destroy throws an Error", async function () { 44 | const img = await gd.create(200, 200); 45 | assert.strictEqual(img.trueColor, 0); 46 | img.destroy(); 47 | 48 | try { 49 | img.getPixel(1, 1); 50 | } catch (e) { 51 | assert.ok(e instanceof Error); 52 | } 53 | }); 54 | 55 | // it("gd.Image#destroy() -- ", async function() { 56 | 57 | // }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/openfile.test.mjs: -------------------------------------------------------------------------------- 1 | import gd from '../index.js'; 2 | import { assert } from 'chai'; 3 | 4 | import dirname from './dirname.mjs'; 5 | 6 | const currentDir = dirname(import.meta.url); 7 | 8 | var source = currentDir + '/fixtures'; 9 | var target = currentDir + '/output'; 10 | 11 | describe('gd.openFile', () => { 12 | it('returns a Promise', () => { 13 | const imagePromise = gd.openFile(`${source}/input.jpg`); 14 | 15 | assert.ok(imagePromise.constructor === Promise); 16 | imagePromise.then((image) => image.destroy()); 17 | }); 18 | 19 | it('opens a file', async () => { 20 | const img = await gd.openFile(`${source}/input.jpg`); 21 | 22 | assert.ok(img.width === 100); 23 | 24 | img.destroy(); 25 | }); 26 | 27 | it('throws an exception when file does not exist', async () => { 28 | try { 29 | await gd.openFile(`${source}/abcxyz.jpg`); 30 | } catch (exception) { 31 | assert.ok(exception instanceof Error); 32 | } 33 | }); 34 | }); 35 | 36 | describe('gd.file', () => { 37 | it('returns a Promise', async () => { 38 | const img = await gd.openFile(`${source}/input.jpg`); 39 | 40 | var a = img.file(`${target}/test.jpg`); 41 | assert.isTrue(a.constructor === Promise); 42 | }); 43 | 44 | it('returns a Promise which resolves to boolean', async () => { 45 | const img = await gd.openFile(`${source}/input.jpg`); 46 | 47 | var a = await img.file(`${target}/test1.jpg`); 48 | 49 | assert.isTrue(a === true); 50 | }); 51 | 52 | it('saves files of different extensions', async function () { 53 | const image = await gd.openFile(`${source}/input.jpg`); 54 | 55 | const success1 = await image.file(`${target}/test.bmp`); 56 | const success2 = await image.file(`${target}/test.png`); 57 | 58 | assert.isTrue(success1 === true); 59 | assert.isTrue(success2 === true); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/colormatch.test.mjs: -------------------------------------------------------------------------------- 1 | import gd from '../index.js'; 2 | import { assert } from 'chai'; 3 | 4 | import dirname from './dirname.mjs'; 5 | 6 | describe('gd.Image#colormatch', function () { 7 | it('throws error when `this` image is not truecolor', async function () { 8 | const baseImage = await gd.create(100, 100); 9 | const paletteImage = await gd.create(100, 100); 10 | 11 | try { 12 | baseImage.colorMatch(paletteImage); 13 | } catch (e) { 14 | assert.instanceOf(e, Error); 15 | } 16 | }); 17 | 18 | it('throws an Error when argument image is not palette', async function () { 19 | const baseImage = await gd.create(100, 100); 20 | const trueColorImg = await gd.createTrueColor(100, 100); 21 | 22 | try { 23 | baseImage.colorMatch(trueColorImg); 24 | } catch (e) { 25 | assert.instanceOf(e, Error); 26 | } 27 | }); 28 | 29 | it('expects images to have same dimensions', async function () { 30 | const baseImage = await gd.createTrueColor(100, 100); 31 | const paletteImage = await gd.create(90, 90); 32 | 33 | try { 34 | baseImage.colorMatch(paletteImage); 35 | } catch (e) { 36 | baseImage.destroy(); 37 | paletteImage.destroy(); 38 | assert.instanceOf(e, Error); 39 | } 40 | }); 41 | 42 | it('expects the palette iamge to have at least one color allocated', async function () { 43 | const baseImage = await gd.createTrueColor(100, 100); 44 | const paletteImage = await gd.create(100, 100); 45 | 46 | try { 47 | baseImage.colorMatch(paletteImage); 48 | } catch (e) { 49 | baseImage.destroy(); 50 | paletteImage.destroy(); 51 | assert.instanceOf(e, Error); 52 | } 53 | }); 54 | 55 | it('can match palette colors to truecolor image', async function () { 56 | const currentDir = dirname(import.meta.url); 57 | const baseImage = await gd.openJpeg(`${currentDir}/fixtures/input.jpg`); 58 | const paletteImage = await gd.openGif(`${currentDir}/fixtures/node-gd.gif`); 59 | 60 | const result = baseImage.colorMatch(paletteImage); 61 | assert.equal(result, 0); 62 | 63 | await paletteImage.saveGif(`${currentDir}/output/colorMatch.gif`); 64 | baseImage.destroy(); 65 | paletteImage.destroy(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/tiff.test.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import gd from '../index.js'; 4 | import { assert } from 'chai'; 5 | 6 | import dirname from './dirname.mjs'; 7 | 8 | const currentDir = dirname(import.meta.url); 9 | 10 | var source = currentDir + '/fixtures/'; 11 | var target = currentDir + '/output/'; 12 | 13 | describe('Section Handling TIFF files', function () { 14 | it('gd.openTiff() -- can open a tiff and save it as a jpg', async function () { 15 | var s; 16 | var t; 17 | if (!gd.GD_TIFF) { 18 | return this.skip(); 19 | } 20 | s = source + 'input.tif'; 21 | t = target + 'output-from-tiff.jpg'; 22 | 23 | const img = await gd.openTiff(s); 24 | await img.saveJpeg(t, 100); 25 | assert.ok(fs.existsSync(t)); 26 | img.destroy(); 27 | }); 28 | 29 | it('gd.Image#saveTiff() -- can open a jpg file and save it as a tiff', async function () { 30 | var s; 31 | var t; 32 | if (!gd.GD_TIFF) { 33 | return this.skip(); 34 | } 35 | s = source + 'input.jpg'; 36 | t = target + 'output-from-jpg.tiff'; 37 | 38 | const img = await gd.openJpeg(s); 39 | await img.saveTiff(t); 40 | assert.ok(fs.existsSync(t)); 41 | img.destroy(); 42 | }); 43 | 44 | it('gd.createFromTiff() -- can open a tiff and save it as a tiff', async function () { 45 | var s; 46 | var t; 47 | if (!gd.GD_TIFF) { 48 | return this.skip(); 49 | } 50 | s = source + 'input.tif'; 51 | t = target + 'output-from-tiff.tif'; 52 | 53 | var image = await gd.createFromTiff(s); 54 | await image.saveTiff(t); 55 | assert.ok(fs.existsSync(t)); 56 | image.destroy(); 57 | }); 58 | 59 | it('gd.createFromTiffPtr() -- can open a tif and store it in a pointer and save a tiff from the pointer', async function () { 60 | if (!gd.GD_TIFF) { 61 | return this.skip(); 62 | } 63 | var s = source + 'input.tif'; 64 | var t = target + 'output-from-tiff-ptr.tif'; 65 | 66 | var imageData = fs.readFileSync(s); 67 | var image = gd.createFromTiffPtr(imageData); 68 | await image.saveTiff(t); 69 | assert.ok(fs.existsSync(t)); 70 | image.destroy(); 71 | }); 72 | 73 | it('gd.Image#saveTiff() -- can create a truecolor Tiff image with text', async function () { 74 | var f, img, t, txtColor; 75 | if (!gd.GD_TIFF) { 76 | return this.skip(); 77 | } 78 | f = source + 'FreeSans.ttf'; 79 | t = target + 'output-truecolor-string.tif'; 80 | img = await gd.createTrueColor(120, 20); 81 | txtColor = img.colorAllocate(255, 255, 0); 82 | img.stringFT(txtColor, f, 16, 0, 8, 18, 'Hello world!'); 83 | await img.saveTiff(t); 84 | assert.ok(fs.existsSync(t)); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/query-image-info.test.mjs: -------------------------------------------------------------------------------- 1 | import gd from '../index.js'; 2 | import { assert } from 'chai'; 3 | 4 | import dirname from './dirname.mjs'; 5 | 6 | const currentDir = dirname(import.meta.url); 7 | 8 | var source = currentDir + '/fixtures/'; 9 | 10 | describe('Section querying image information', function () { 11 | it('gd.trueColorAlpha() -- can query true color alpha values of a color', async () => { 12 | var image = await gd.createTrueColor(100, 100); 13 | var someColor = gd.trueColorAlpha(63, 255, 191, 63); 14 | 15 | assert.equal(image.red(someColor), 63); 16 | assert.equal(image.green(someColor), 255); 17 | assert.equal(image.blue(someColor), 191); 18 | assert.equal(image.alpha(someColor), 63); 19 | }); 20 | 21 | it('gd.trueColor() -- can query true color values of a color', async () => { 22 | var image = await gd.createTrueColor(100, 100); 23 | var someColor = gd.trueColor(63, 255, 191, 63); 24 | 25 | assert.equal(image.red(someColor), 63); 26 | assert.equal(image.green(someColor), 255); 27 | assert.equal(image.blue(someColor), 191); 28 | }); 29 | 30 | it('gd.Image#colorAllocateAlpha() -- can query palette color values of a color with alpha', async () => { 31 | var image = await gd.create(100, 100); 32 | var someColor = image.colorAllocateAlpha(63, 255, 191, 63); 33 | 34 | assert.equal(image.red(someColor), 63); 35 | assert.equal(image.green(someColor), 255); 36 | assert.equal(image.blue(someColor), 191); 37 | assert.equal(image.alpha(someColor), 63); 38 | }); 39 | 40 | it('gd.Image#colorAllocate() -- can query palette color values of a color', async () => { 41 | var image = await gd.create(100, 100); 42 | var someColor = image.colorAllocate(63, 255, 191); 43 | 44 | assert.equal(image.red(someColor), 63); 45 | assert.equal(image.green(someColor), 255); 46 | assert.equal(image.blue(someColor), 191); 47 | }); 48 | 49 | it('gd.Image#getTrueColorPixel() -- can query the color of a pixel within image bounds', async function () { 50 | var s = source + 'input.png'; 51 | const image = await gd.openPng(s); 52 | var color = image.getTrueColorPixel(0, 0); 53 | assert.isNumber(color, 'got Number for getTrueColorPixel'); 54 | }); 55 | 56 | it('gd.Image#getTrueColorPixel() -- will throw an error when quering the color of a pixel outside of image bounds', async function () { 57 | var s = source + 'input.png'; 58 | const image = await gd.openPng(s); 59 | assert.throws(function () { 60 | var color = image.getTrueColorPixel(-1, -1); 61 | }, 'Value for x and y must be greater than 0'); 62 | 63 | var color = image.getTrueColorPixel(image.width + 1, 1); 64 | assert.equal( 65 | 0, 66 | color, 67 | '0 should be returned when querying above upper bounds' 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /lib/node-gd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend C++ bindings with some convenient sugar 3 | * Copyright 2014-2021 Vincent Bruijn 4 | */ 5 | 6 | import fs from 'fs'; 7 | import path from 'path'; 8 | import gd from './bindings.js'; 9 | 10 | const version = gd.getGDVersion(); 11 | 12 | const formats = ['Jpeg', 'Png', 'Gif', 'WBMP', 'Bmp']; 13 | 14 | if (version >= '2.3.2') { 15 | if (gd.GD_HEIF) { 16 | formats.push('Heif'); 17 | } 18 | if (gd.GD_AVIF) { 19 | formats.push('Avif'); 20 | } 21 | } 22 | 23 | if (gd.GD_TIFF) { 24 | formats.push('Tiff'); 25 | } 26 | 27 | if (gd.GD_WEBP) { 28 | formats.push('Webp'); 29 | } 30 | 31 | /** 32 | * Create convenience functions for opening 33 | * specific image formats 34 | * 35 | * @param {string} format 36 | * @returns {function} The function that will be called 37 | * when gd.openJpeg is called 38 | */ 39 | function openFormatFn(format) { 40 | return function (path = '') { 41 | return gd[`createFrom${format}`].call(gd, path); 42 | }; 43 | } 44 | 45 | /** 46 | * Create convience functions for saving 47 | * specific image formats 48 | * 49 | * @param {string} format 50 | * @returns {function} 51 | */ 52 | function saveFormatFn(format) { 53 | return { 54 | [`save${format}`]() { 55 | const args = [...arguments]; 56 | const filename = args.shift(); 57 | 58 | return new Promise((resolve, reject) => { 59 | const data = this[`${format.toLowerCase()}Ptr`].apply(this, args); 60 | 61 | fs.writeFile(filename, data, 'latin1', error => { 62 | if (error) { 63 | return reject(error); 64 | } 65 | resolve(true); 66 | }); 67 | }); 68 | }, 69 | }[`save${format}`]; 70 | } 71 | 72 | /** 73 | * Add convenience functions to gd 74 | */ 75 | formats.forEach(format => { 76 | gd[`open${format}`] = openFormatFn(format); 77 | 78 | if (!gd.Image.prototype[`save${format}`]) { 79 | Object.defineProperty(gd.Image.prototype, `save${format}`, { 80 | value: saveFormatFn(format), 81 | }); 82 | } 83 | }); 84 | 85 | /** 86 | * Wrapper around gdImageCreateFromFile 87 | * With safety check for file existence to mitigate 88 | * uninformative segmentation faults from libgd 89 | * 90 | * @param {string} file Path of file to open 91 | * @returns {Promise} 92 | */ 93 | function openFile(file) { 94 | return new Promise((resolve, reject) => { 95 | const filePath = path.normalize(file); 96 | 97 | fs.access(filePath, fs.constants.F_OK, error => { 98 | if (error) { 99 | return reject(error); 100 | } 101 | 102 | resolve(gd.createFromFile(filePath)); 103 | }); 104 | }); 105 | } 106 | 107 | gd.openFile = openFile; 108 | 109 | gd.toString = function toString() { 110 | return '[object Gd]'; 111 | }; 112 | 113 | gd.Image.prototype.toString = function toString() { 114 | return '[object Image]'; 115 | }; 116 | 117 | export default gd; 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-gd", 3 | "version": "3.0.0", 4 | "description": "GD graphics library (libgd) C++ bindings for Node.js", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "type": "module", 8 | "directories": { 9 | "test": "test", 10 | "doc": "docs" 11 | }, 12 | "files": [ 13 | "lib", 14 | "src", 15 | "binding.gyp", 16 | "util.sh", 17 | "CONTRIBUTORS.md", 18 | "index.d.ts", 19 | "test/*.test.js", 20 | "test/fixtures/*", 21 | "prebuilds" 22 | ], 23 | "os": [ 24 | "!win32" 25 | ], 26 | "engines": { 27 | "node": ">=18" 28 | }, 29 | "binary": { 30 | "napi_versions": [3, 4, 5, 6, 7, 8, 9] 31 | }, 32 | "homepage": "https://github.com/y-a-v-a/node-gd", 33 | "bugs": "https://github.com/y-a-v-a/node-gd/issues", 34 | "scripts": { 35 | "clean": "rm -rf test/output/* build/ prebuilds/", 36 | "rebuild": "node-gyp rebuild -j max", 37 | "pretest": "node-gyp build -j max", 38 | "test": "./node_modules/.bin/mocha --reporter spec --bail --ui bdd --colors --file ./test/main.test.mjs", 39 | "install": "node-gyp-build || node-gyp rebuild -j max", 40 | "prebuild": "prebuildify --napi --strip", 41 | "prebuild-all": "prebuildify --napi --strip --arch x64 --arch arm64", 42 | "prebuild-upload": "prebuildify --napi --strip --upload-all", 43 | "update-contributors": "git shortlog -sen | sed 's/^ /*/' > CONTRIBUTORS.md", 44 | "docker-build": "docker build --progress=plain -t yava:node-gd .", 45 | "docker-run": "docker run -it -v $PWD:/usr/src yava:node-gd" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git://github.com/y-a-v-a/node-gd.git" 50 | }, 51 | "keywords": [ 52 | "libgd", 53 | "libgd2", 54 | "gd", 55 | "image", 56 | "png", 57 | "jpg", 58 | "jpeg", 59 | "gif", 60 | "graphics", 61 | "library" 62 | ], 63 | "author": "Taegon Kim ", 64 | "license": "MIT", 65 | "contributors": [ 66 | { 67 | "name": "Dudochkin Victor", 68 | "email": "blacksmith@gogoo.ru" 69 | }, 70 | { 71 | "name": "Andris Reinman", 72 | "email": "andris@node.ee" 73 | }, 74 | { 75 | "name": "Peter Magnusson" 76 | }, 77 | { 78 | "name": "Damian Senn", 79 | "email": "damian.senn@adfinis-sygroup.ch" 80 | }, 81 | { 82 | "name": "Farrin Reid" 83 | }, 84 | { 85 | "name": "Josh (zer0x304)" 86 | }, 87 | { 88 | "name": "Mike Smullin", 89 | "email": "mike@smullindesign.com" 90 | }, 91 | { 92 | "name": "Vincent Bruijn (y_a_v_a)", 93 | "email": "vebruijn@gmail.com" 94 | } 95 | ], 96 | "gypfile": true, 97 | "readmeFilename": "README.md", 98 | "devDependencies": { 99 | "chai": "^5.2.1", 100 | "mocha": "^11.7.1", 101 | "prebuildify": "^6.0.0" 102 | }, 103 | "dependencies": { 104 | "node-addon-api": "^8.4.0", 105 | "node-gyp": "^11.2.0", 106 | "node-gyp-build": "^4.8.0" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | # 3.0.0 - 2023-06-05 (current) 9 | 10 | - Only support libgd 2.3.0 and up 11 | 12 | ### Added 13 | 14 | - A lot of error messages with a sane message 15 | - Added gdImageColorExactAlpha 16 | - Added gdImagePixelate 17 | - Added getter for interpolation_id 18 | - Added gdImageScale 19 | - Added gdImageSetInterpolationMethod 20 | - Added resX and resY getters 21 | - Added gdImageRotateInterpolated 22 | - Added homebrew paths e.g. `/opt/homebrew/include` 23 | 24 | ### Updated 25 | 26 | - Updated dependencies to latest versions 27 | - Updated Github actions dependencies 28 | - C++ code formatting 29 | 30 | ### Removed 31 | 32 | - Removed gd and gd2 image formats as libgd turned them off by default since 2.3.0 33 | - Removed many libgd version conditions since decision is made to support libgd 2.3.0 and up only per node-gd 3.x.x 34 | 35 | # 2.1.1 - 2020-09-10 36 | 37 | ### Added 38 | 39 | - TypeScript types file, as mentioned in #81 (thanks to [vladislav805](https://github.com/vladislav805)) 40 | 41 | ### Fixed 42 | 43 | - Package size of eventual npm package tgz file by being more specific about what it should contain in `package.json`'s `file` property. 44 | 45 | # 2.1.0 - 2020-06-19 46 | 47 | ### Added 48 | 49 | - Support for libgd 2.3.0 50 | 51 | ### Fixed 52 | 53 | - Tests with regard to font boundary coordinates 54 | 55 | # 2.0.1 - 2020-05-26 56 | 57 | ### Added 58 | 59 | - Added `files` property in `package.json`. 60 | - Added test files to `files` property 61 | 62 | ### Changed 63 | 64 | - Upgraded dependencies in package.json 65 | - Typo fixed in documentation (thanks [gabrieledarrigo](https://github.com/gabrieledarrigo)) 66 | - Updated test to create `output` directory if not present 67 | 68 | ### Removed 69 | 70 | - Remove `.npmignore` in favour of `files` property in `package.json`. 71 | 72 | # 2.0.0 - 2020-01-19 73 | 74 | ### Added 75 | 76 | - Added multiple `AsyncWorker` classes. 77 | - Added a license file. 78 | - Added a changelog file. 79 | - Added a lot of new tests. 80 | 81 | ### Changed 82 | 83 | - Changed the workging of Gif animation creation. 84 | - Moved from Nan to Napi. 85 | - Changed `gd.create` and `gd.createTruecolor` and let them return a `Promise`. 86 | - Moved macros to header file. 87 | - Updated documentation 88 | - Changed custom prototype functions unwritable functions with a [proper name](https://stackoverflow.com/questions/9479046/is-there-any-non-eval-way-to-create-a-function-with-a-runtime-determined-name/9479081#9479081) 89 | 90 | ### Removed 91 | 92 | - No longer supports image creation from `String`, only from `Buffer` from now on. 93 | 94 | ### Breaking 95 | 96 | - Dropped support for Node <6.x 97 | 98 | # 1.5.4 - 2018-02-06 99 | 100 | ### Fixed 101 | 102 | - Fixed creating image from `String` or `Buffer`. 103 | 104 | ### Added 105 | 106 | - Extended documentation for `gd.Image#crop()` 107 | 108 | # 1.5.3 - 2018-01-21 109 | 110 | ### Fixed 111 | 112 | - Fixed #59 where a value of `0` was considered out of range. 113 | -------------------------------------------------------------------------------- /test/gifanim.test.mjs: -------------------------------------------------------------------------------- 1 | import gd from '../index.js'; 2 | 3 | describe('Gif animation creation', function () { 4 | it('gd.Image#gifAnimBegin -- returns a Promise', async function () { 5 | this.skip(); 6 | var anim = './test/output/anim.gif'; 7 | 8 | // create first frame 9 | var firstFrame = await gd.create(200, 200); 10 | 11 | // allocate some colors 12 | var whiteBackground = firstFrame.colorAllocate(255, 255, 255); 13 | var pink = firstFrame.colorAllocate(255, 0, 255); 14 | var trans = firstFrame.colorAllocate(1, 1, 1); 15 | 16 | // // create first frame and draw an ellipse 17 | firstFrame.ellipse(100, -50, 100, 100, pink); 18 | // // await firstFrame.gif('./test.gif'); 19 | 20 | // // start animation 21 | firstFrame.gifAnimBegin(anim, 1, -1); 22 | firstFrame.gifAnimAdd(anim, 0, 0, 0, 5, 1, null); 23 | 24 | var totalFrames = []; 25 | for (var i = 0; i < 30; i++) { 26 | totalFrames.push(gd.create(200, 200)); 27 | } 28 | 29 | Promise.all(totalFrames) 30 | .then((frames) => { 31 | return frames.map(function (frame, idx, arr) { 32 | frame.colorAllocate(255, 255, 255); 33 | let pink = frame.colorAllocate(255, 0, 255); 34 | firstFrame.paletteCopy(frame); 35 | frame.colorTransparent(trans); 36 | // const frame = await gd.create(200, 200); 37 | // arr[idx] = frame; 38 | frame.ellipse(100, idx * 10 - 40, 100, 100, pink); 39 | var lastFrame = i === 0 ? firstFrame : arr[i - 1]; 40 | 41 | // await frame.gifAnimAdd(anim, 0, 0, 0, 5, 1, null); 42 | frame.gifAnimAdd(anim, 0, 0, 0, 5, 1, lastFrame); 43 | // frame.destroy(); 44 | 45 | // frame.ellipse(100, (1 * 10 - 40), 100, 100, pink); 46 | // await frame.file(`./test-1.jpg`); 47 | // frame.gifAnimAdd(anim, 0, 0, 0, 5, 1, null); 48 | return frame; 49 | }); 50 | }) 51 | .then(async function (frames) { 52 | // Promise.all(frames).then(async frames => { 53 | 54 | frames.map(async (frame, idx) => { 55 | await frame.gif(`./test/output/anim-${idx}.gif`); 56 | frame.destroy(); 57 | }); 58 | firstFrame.gifAnimEnd(anim); 59 | firstFrame.destroy(); 60 | // }); 61 | }); 62 | }); 63 | 64 | it('has a new implementation', async function () { 65 | const image = await gd.create(200, 200); 66 | image.colorAllocate(255, 255, 255); 67 | const pink = image.colorAllocate(255, 0, 255); 68 | image.ellipse(100, 100, 100, 100, pink); 69 | const image2 = await gd.create(200, 200); 70 | image2.colorAllocate(255, 255, 255); 71 | image.paletteCopy(image2); 72 | image2.ellipse(100, 100, 80, 80, pink); 73 | const image3 = await gd.create(200, 200); 74 | image3.ellipse(100, 100, 60, 60, pink); 75 | const image4 = await gd.create(200, 200); 76 | image4.ellipse(100, 100, 70, 70, pink); 77 | const image5 = await gd.create(200, 200); 78 | image5.ellipse(100, 100, 90, 90, pink); 79 | 80 | const anim = new gd.GifAnim(image, { delay: 10 }); 81 | 82 | anim.add(image2, { delay: 10 }); 83 | anim.add(image3, { delay: 10 }); 84 | anim.add(image4, { delay: 10 }); 85 | anim.add(image5, { delay: 10 }); 86 | 87 | await anim.end('./test/output/output-animation.gif'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is node-gd, a Node.js binding for the libgd C graphics library. It provides high-level JavaScript APIs for creating, manipulating, and saving images in various formats (PNG, JPEG, GIF, BMP, TIFF, WebP, etc.). 8 | 9 | ## Key Architecture 10 | 11 | ### Core Components 12 | 13 | - **Native C++ binding** (`src/`) - Contains the core N-API wrapper around libgd 14 | - `node_gd.cc` - Main implementation with all image manipulation functions 15 | - `node_gd.h` - Header file with class definitions and macros 16 | - `addon.cc` - Module initialization 17 | - `node_gd_workers.cc` - Async worker implementations for file I/O 18 | 19 | - **JavaScript wrapper** (`lib/`) - Adds convenience methods and Promise support 20 | - `node-gd.js` - Main wrapper that adds format-specific convenience functions 21 | - `bindings.js` - Handles the native binding loading 22 | - `GifAnim.js` - Animated GIF functionality 23 | 24 | - **Entry point** (`index.js`) - Simple module setup that exports the main API 25 | 26 | ### Build System 27 | 28 | - Uses `node-gyp` for compiling the native C++ code 29 | - `binding.gyp` - Defines build configuration with conditional compilation based on available libraries 30 | - `util.sh` - Shell script that detects available libgd features (AVIF, HEIF, TIFF, etc.) 31 | 32 | ## Common Commands 33 | 34 | ### Development 35 | ```bash 36 | # Build the native module 37 | npm run rebuild 38 | 39 | # Clean build artifacts 40 | npm run clean 41 | 42 | # Install (triggers rebuild) 43 | npm install 44 | ``` 45 | 46 | ### Testing 47 | ```bash 48 | # Run all tests (builds first via pretest) 49 | npm test 50 | 51 | # Just build without testing 52 | npm run pretest 53 | ``` 54 | 55 | ### Docker Development 56 | ```bash 57 | # Build Docker image 58 | npm run docker-build 59 | 60 | # Run in Docker 61 | npm run docker-run 62 | ``` 63 | 64 | ## Development Notes 65 | 66 | ### Image Types 67 | - **Palette images** - Created with `gd.create()`, max 256 colors, white background 68 | - **True color images** - Created with `gd.createTrueColor()`, millions of colors, black background 69 | 70 | ### Memory Management 71 | - All images must be explicitly destroyed with `image.destroy()` to free C-level memory 72 | - Failing to destroy images will cause memory leaks 73 | 74 | ### Async vs Sync 75 | - Version 2.x removed sync functions (`createSync`, `createTrueColorSync`) 76 | - All file I/O operations return Promises and use N-API AsyncWorkers 77 | - Drawing operations are synchronous 78 | 79 | ### Platform Support 80 | - **Does not build on Windows** - explicitly excluded in package.json 81 | - Requires libgd development headers to be installed on the system 82 | - Uses pkg-config to find libgd 83 | 84 | ### Testing 85 | - Uses Mocha with ES modules 86 | - Tests are in `test/` directory with `.test.mjs` extension 87 | - Test fixtures in `test/fixtures/` 88 | - Output images saved to `test/output/` 89 | 90 | ### Conditional Features 91 | The build system detects available libgd features and compiles accordingly: 92 | - AVIF support (requires libavif) 93 | - HEIF support (requires libheif) 94 | - TIFF support (requires libtiff) 95 | - WebP support (requires libwebp) 96 | - FreeType font support 97 | - Fontconfig support 98 | 99 | Check available features with `gd.getGDVersion()` and feature flags like `gd.GD_TIFF`. -------------------------------------------------------------------------------- /docker-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Docker test script for node-gd 4 | # This script builds the Docker image and runs all tests within the container 5 | 6 | set -e # Exit on any error 7 | 8 | # Colors for output 9 | RED='\033[0;31m' 10 | GREEN='\033[0;32m' 11 | YELLOW='\033[1;33m' 12 | BLUE='\033[0;34m' 13 | NC='\033[0m' # No Color 14 | 15 | # Function to log messages with timestamp 16 | log() { 17 | echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" 18 | } 19 | 20 | log_success() { 21 | echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] ✓${NC} $1" 22 | } 23 | 24 | log_error() { 25 | echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ✗${NC} $1" 26 | } 27 | 28 | log_warning() { 29 | echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] ⚠${NC} $1" 30 | } 31 | 32 | # Docker image name 33 | IMAGE_NAME="yava:node-gd" 34 | 35 | echo "================================================" 36 | echo " Node-GD Docker Test Suite" 37 | echo "================================================" 38 | echo "" 39 | 40 | # Step 1: Build Docker image 41 | log "Building Docker image: $IMAGE_NAME" 42 | if docker build --progress=plain -t "$IMAGE_NAME" . > build.log 2>&1; then 43 | log_success "Docker image built successfully" 44 | else 45 | log_error "Docker image build failed" 46 | echo "Build log:" 47 | cat build.log 48 | exit 1 49 | fi 50 | 51 | echo "" 52 | 53 | # Step 2: Run tests in container 54 | log "Running tests in Docker container..." 55 | echo "================================================" 56 | 57 | # Create a temporary script to run inside the container 58 | cat > /tmp/test_runner.sh << 'EOF' 59 | #!/bin/bash 60 | set -e 61 | 62 | echo "Node.js version: $(node --version)" 63 | echo "NPM version: $(npm --version)" 64 | echo "Working directory: $(pwd)" 65 | echo "Directory contents:" 66 | ls -la 67 | 68 | echo "" 69 | echo "Installing dependencies..." 70 | npm ci 71 | 72 | echo "" 73 | echo "Running tests..." 74 | npm test 75 | 76 | echo "" 77 | echo "All tests completed successfully!" 78 | EOF 79 | 80 | chmod +x /tmp/test_runner.sh 81 | 82 | # Run the container with volume mapping and execute tests 83 | if docker run --rm \ 84 | -v "$(pwd):/usr/src" \ 85 | -v "/tmp/test_runner.sh:/tmp/test_runner.sh" \ 86 | "$IMAGE_NAME" \ 87 | /tmp/test_runner.sh > test_output.log 2>&1; then 88 | 89 | log_success "All tests passed successfully!" 90 | echo "" 91 | echo "Test output:" 92 | echo "================================================" 93 | cat test_output.log 94 | echo "================================================" 95 | 96 | else 97 | log_error "Tests failed!" 98 | echo "" 99 | echo "Test output:" 100 | echo "================================================" 101 | cat test_output.log 102 | echo "================================================" 103 | exit 1 104 | fi 105 | 106 | echo "" 107 | log_success "Docker test suite completed successfully!" 108 | 109 | # Cleanup 110 | rm -f build.log test_output.log /tmp/test_runner.sh 111 | 112 | echo "" 113 | echo "================================================" 114 | echo " Test Suite Summary" 115 | echo "================================================" 116 | echo "✓ Docker image built with Node.js 22 LTS" 117 | echo "✓ All tests executed in containerized environment" 118 | echo "✓ Test results logged and displayed" 119 | echo "================================================" -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "conditions": [ 3 | ['OS!="win"', { 4 | 'variables': { 5 | 'with_avif%': ' 4 | * 5 | * MIT Licensed 6 | */ 7 | 8 | import bindings from './bindings.js'; 9 | import fs from 'fs'; 10 | 11 | export default class GifAnim { 12 | constructor(image, options = {}) { 13 | if (!image || image.constructor !== bindings.Image) { 14 | throw new Error( 15 | 'Constructor requires an instance of gd.Image as first parameter' 16 | ); 17 | } 18 | options = Object.assign( 19 | { 20 | globalColorMap: 1, 21 | loops: -1, 22 | localColorMap: 0, 23 | leftOffset: 0, 24 | topOffset: 0, 25 | delay: 100, 26 | disposal: 1, 27 | }, 28 | options 29 | ); 30 | this.isEnded = false; 31 | this.frames = []; 32 | 33 | const animationMetaData = image.gifAnimBegin( 34 | options.globalColorMap, 35 | options.loops 36 | ); 37 | if (!animationMetaData) { 38 | throw new Error('Unable to begin Gif animation'); 39 | } 40 | this.frameBuffers = [animationMetaData]; 41 | 42 | const firstFrame = image.gifAnimAdd( 43 | options.localColorMap, 44 | options.leftOffset, 45 | options.topOffset, 46 | options.delay, 47 | options.disposal, 48 | null 49 | ); 50 | 51 | if (!firstFrame) { 52 | throw new Error('Unable to add frame to Gif animation'); 53 | } 54 | // keep reference to images because 55 | // they are used by gd.Image.prototype.gifAnimAdd() 56 | this.frameBuffers.push(firstFrame); 57 | this.frames.push(image); 58 | } 59 | 60 | /** 61 | * Add a new frame to the animation 62 | * @param {gd.Image} image Image to add as next frame 63 | * @param {object} options Object containing meta data for the frame 64 | */ 65 | add(image, options = {}) { 66 | if (!image || image.constructor !== bindings.Image) { 67 | throw new Error( 68 | 'Only instances of gd.Image can be added as additional frames.' 69 | ); 70 | } 71 | 72 | if (this.isEnded) { 73 | throw new Error( 74 | 'No more frames can be added to this animation, gd.GifAnim#end() has been called earlier for this instance.' 75 | ); 76 | } 77 | 78 | options = Object.assign( 79 | { 80 | localColorMap: 0, 81 | leftOffset: 0, 82 | topOffset: 0, 83 | delay: 100, 84 | disposal: 1, 85 | }, 86 | options 87 | ); 88 | 89 | const newFrame = image.gifAnimAdd( 90 | options.localColorMap, 91 | options.leftOffset, 92 | options.topOffset, 93 | options.delay, 94 | options.disposal, 95 | this.frames[this.lastIndex] 96 | ); 97 | 98 | if (!newFrame) { 99 | throw new Error('Unable to add frame to Gif animation'); 100 | } 101 | 102 | this.frameBuffers.push(newFrame); 103 | this.frames.push(image); 104 | } 105 | 106 | get lastIndex() { 107 | return this.frames.length - 1; 108 | } 109 | 110 | end(outName) { 111 | return new Promise((resolve, reject) => { 112 | if (this.isEnded) { 113 | reject('gd.GifAnim#end() already called'); 114 | } 115 | this.frames[this.lastIndex].gifAnimEnd(); 116 | if (typeof outName === 'string' && outName.length) { 117 | fs.writeFile(outName, Buffer.concat(this.frameBuffers), (error) => { 118 | if (error) reject('Unable to save animation'); 119 | 120 | this.isEnded = true; 121 | resolve(true); 122 | }); 123 | } else { 124 | this.isEnded = true; 125 | resolve(Buffer.concat(this.frameBuffers)); 126 | } 127 | }); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /.github/workflows/prebuild.yml: -------------------------------------------------------------------------------- 1 | name: Prebuild 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | prebuild: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest] 18 | node-version: [18, 20, 22] 19 | architecture: [x64] 20 | include: 21 | # Add arm64 for macOS (Apple Silicon) 22 | - os: macos-latest 23 | node-version: 18 24 | architecture: arm64 25 | - os: macos-latest 26 | node-version: 20 27 | architecture: arm64 28 | - os: macos-latest 29 | node-version: 22 30 | architecture: arm64 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Use Node.js ${{ matrix.node-version }} 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | architecture: ${{ matrix.architecture }} 40 | 41 | - name: Install system dependencies (Ubuntu) 42 | if: matrix.os == 'ubuntu-latest' 43 | run: | 44 | sudo apt-get update 45 | sudo apt-get install -y build-essential pkg-config libgd-dev 46 | # Install optional dependencies for full feature set 47 | sudo apt-get install -y libheif-dev libavif-dev libtiff-dev libwebp-dev \ 48 | libfontconfig1-dev libfreetype6-dev libpng-dev libjpeg-dev \ 49 | libaom-dev libdav1d-dev 50 | 51 | - name: Install system dependencies (macOS) 52 | if: matrix.os == 'macos-latest' 53 | run: | 54 | brew install pkg-config gd 55 | # Install optional dependencies for full feature set 56 | brew install libheif libavif libtiff webp fontconfig freetype 57 | 58 | - name: Cache npm dependencies 59 | uses: actions/cache@v3 60 | with: 61 | path: ~/.npm 62 | key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} 63 | restore-keys: | 64 | ${{ runner.os }}-node-${{ matrix.node-version }}- 65 | 66 | - name: Install dependencies 67 | run: npm ci 68 | 69 | - name: Build and test 70 | run: | 71 | npm run rebuild 72 | npm test 73 | 74 | - name: Generate prebuilds 75 | run: | 76 | npx prebuildify --napi --strip 77 | 78 | - name: Upload prebuilds 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: prebuilds-${{ matrix.os }}-${{ matrix.node-version }}-${{ matrix.architecture }} 82 | path: prebuilds/ 83 | retention-days: 30 84 | 85 | - name: Verify prebuild works 86 | run: | 87 | # Test that the prebuild can be loaded 88 | node -e " 89 | const gd = require('./'); 90 | console.log('Version:', gd.getGDVersion()); 91 | console.log('Prebuild loaded successfully'); 92 | " 93 | 94 | # Job to collect all prebuilds and prepare for release 95 | collect-prebuilds: 96 | needs: prebuild 97 | runs-on: ubuntu-latest 98 | if: github.event_name == 'release' && github.event.action == 'published' 99 | 100 | steps: 101 | - uses: actions/checkout@v4 102 | 103 | - name: Download all prebuilds 104 | uses: actions/download-artifact@v4 105 | with: 106 | path: all-prebuilds 107 | pattern: prebuilds-* 108 | merge-multiple: true 109 | 110 | - name: Reorganize prebuilds 111 | run: | 112 | mkdir -p prebuilds 113 | # Copy all prebuilds to the prebuilds directory 114 | find all-prebuilds -name "*.node" -exec cp {} prebuilds/ \; 115 | ls -la prebuilds/ 116 | 117 | - name: Create release package 118 | run: | 119 | tar -czf prebuilds.tar.gz prebuilds/ 120 | 121 | - name: Upload to GitHub Release 122 | uses: softprops/action-gh-release@v1 123 | with: 124 | files: prebuilds.tar.gz 125 | env: 126 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 127 | -------------------------------------------------------------------------------- /test/file-types.test.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import gd from '../index.js'; 4 | import { assert } from 'chai'; 5 | 6 | import dirname from './dirname.mjs'; 7 | 8 | const currentDir = dirname(import.meta.url); 9 | 10 | var source = currentDir + '/fixtures/'; 11 | var target = currentDir + '/output/'; 12 | 13 | describe('Section Handling file types', function () { 14 | it('gd.Image#jpeg() -- returns a Promise', async function () { 15 | const s = `${source}input.png`; 16 | const t = `${target}output-gd.Image.jpeg.jpg`; 17 | const img = await gd.openPng(s); 18 | const successPromise = img.jpeg(t, 0); 19 | 20 | assert.ok(successPromise.constructor === Promise); 21 | }); 22 | 23 | it('gd.Image#jpeg() -- returns true when save is succesfull', async function () { 24 | const s = `${source}input.png`; 25 | const t = `${target}output-success-gd.Image.jpeg.jpg`; 26 | const img = await gd.openPng(s); 27 | 28 | assert.ok(img instanceof gd.Image); 29 | const success = await img.jpeg(t, 0); 30 | 31 | assert.ok(success === true); 32 | }); 33 | 34 | it('gd.Image#jpeg() -- returns "Cannot save JPEG file" in catch when failing', async function () { 35 | const s = `${source}input.png`; 36 | const img = await gd.openPng(s); 37 | assert.ok(img instanceof gd.Image); 38 | 39 | img.jpeg('', 100).catch(function (reason) { 40 | assert.ok(reason === 'Cannot save JPEG file'); 41 | }); 42 | }); 43 | 44 | it('gd.Image#saveJpeg() -- can copy a png into a jpeg', async () => { 45 | var s, t; 46 | s = source + 'input.png'; 47 | t = target + 'output.jpg'; 48 | const img = await gd.openPng(s); 49 | 50 | var canvas; 51 | canvas = await gd.createTrueColor(100, 100); 52 | img.copyResampled(canvas, 0, 0, 0, 0, 100, 100, img.width, img.height); 53 | await canvas.saveJpeg(t, 10); 54 | assert.ok(fs.existsSync(t)); 55 | img.destroy(); 56 | canvas.destroy(); 57 | }); 58 | 59 | it('gd.Image#saveGif() -- can copy a png into gif', async () => { 60 | var s, t; 61 | s = source + 'input.png'; 62 | t = target + 'output.gif'; 63 | const img = await gd.openPng(s); 64 | var canvas; 65 | canvas = await gd.createTrueColor(img.width, img.height); 66 | img.copyResampled(canvas, 0, 0, 0, 0, img.width, img.height, img.width, img.height); 67 | await canvas.saveGif(t); 68 | assert.ok(fs.existsSync(t)); 69 | img.destroy(); 70 | canvas.destroy(); 71 | }); 72 | 73 | it('gd.Image#saveWBMP() -- can copy a png into WBMP', async function () { 74 | var s, t; 75 | s = source + 'input.png'; 76 | t = target + 'output.wbmp'; 77 | const img = await gd.openPng(s); 78 | var canvas, fg; 79 | canvas = await gd.createTrueColor(img.width, img.height); 80 | img.copyResampled(canvas, 0, 0, 0, 0, img.width, img.height, img.width, img.height); 81 | fg = img.getPixel(5, 5); 82 | await canvas.saveWBMP(t, fg); 83 | assert.ok(fs.existsSync(t)); 84 | img.destroy(); 85 | canvas.destroy(); 86 | }); 87 | 88 | it('gd.Image#savePng() -- can open a jpeg file and save it as png', async function () { 89 | var s, t; 90 | s = source + 'input.jpg'; 91 | t = target + 'output-from-jpeg.png'; 92 | const img = await gd.openJpeg(s); 93 | 94 | await img.savePng(t, -1); 95 | assert.ok(fs.existsSync(t)); 96 | img.destroy(); 97 | }); 98 | 99 | it('gd.Image#saveAvif() -- can open a jpeg file and save it as avif', async function () { 100 | if (gd.getGDVersion() < '2.3.2' || !gd.GD_AVIF) { 101 | this.skip(); 102 | return; 103 | } 104 | 105 | // Skip this test in CI environments where AVIF encoder may not be available 106 | if (process.env.CI || process.env.GITHUB_ACTIONS) { 107 | this.skip(); 108 | return; 109 | } 110 | 111 | var s, t; 112 | s = source + 'input.jpg'; 113 | t = target + 'output-from-jpeg.avif'; 114 | const img = await gd.openJpeg(s); 115 | 116 | try { 117 | await img.saveAvif(t, -1); 118 | assert.ok(fs.existsSync(t)); 119 | } catch (error) { 120 | // If AVIF codec is not available at runtime, skip the test 121 | if (error.message && (error.message.includes('codec') || error.message.includes('encoder') || error.message.includes('AVIF'))) { 122 | this.skip(); 123 | return; 124 | } 125 | throw error; 126 | } finally { 127 | img.destroy(); 128 | } 129 | }); 130 | 131 | it('gd.Image#saveHeif() -- can open a jpeg file and save it as heif', async function () { 132 | if (!gd.GD_HEIF) { 133 | this.skip(); 134 | return; 135 | } 136 | var s, t; 137 | s = source + 'input.jpg'; 138 | t = target + 'output-from-jpeg.heif'; 139 | const img = await gd.openJpeg(s); 140 | 141 | await img.saveHeif(t, -1); 142 | assert.ok(fs.existsSync(t)); 143 | img.destroy(); 144 | }); 145 | 146 | it('gd.openHeif() -- can open a heif file and save it as png', async function () { 147 | if (!gd.GD_HEIF) { 148 | this.skip(); 149 | return; 150 | } 151 | var s, t; 152 | s = source + 'input.heif'; 153 | t = target + 'output-from-heif.png'; 154 | const img = await gd.openHeif(s); 155 | 156 | await img.savePng(t, -1); 157 | assert.ok(fs.existsSync(t)); 158 | img.destroy(); 159 | }); 160 | 161 | it('gd.Image#savePng() -- can open a bmp and save it as png', async function () { 162 | var s; 163 | var t; 164 | s = source + 'input.bmp'; 165 | t = target + 'output-from-bmp.png'; 166 | const img = await gd.openBmp(s); 167 | await img.savePng(t, -1); 168 | assert.ok(fs.existsSync(t)); 169 | img.destroy(); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![node-gd logo](https://raw.githubusercontent.com/y-a-v-a/node-gd-artwork/master/node-gd-mini.png)](https://github.com/y-a-v-a/node-gd) 2 | 3 | # node-gd 4 | 5 | GD graphics library, [libgd](http://www.libgd.org/), C++ bindings for Node.js. This version is the community-maintained [official NodeJS node-gd repo](https://npmjs.org/package/node-gd). With `node-gd` you can easily create, manipulate, open and save paletted and true color images from and to a variety of image formats including JPEG, PNG, GIF and BMP. 6 | 7 | ## Installation 8 | 9 | ### Quick Installation (Recommended) 10 | 11 | For most users, installation is now much simpler thanks to prebuilt binaries: 12 | 13 | ```bash 14 | $ npm install node-gd 15 | ``` 16 | 17 | This will automatically download and install a precompiled binary for your platform (macOS x64/arm64, Linux x64/arm64) and Node.js version. No build tools or system dependencies required! 18 | 19 | ### Manual Installation 20 | 21 | If prebuilt binaries are not available for your platform or you want to build from source: 22 | 23 | #### Preconditions 24 | 25 | Have environment-specific build tools available. Next to that: to take full advantage of node-gd, best is to ensure you install the latest version of libgd2, which can be found at the [libgd github repository](https://github.com/libgd/libgd/releases). 26 | 27 | #### On Debian/Ubuntu 28 | 29 | ```bash 30 | $ sudo apt-get install libgd-dev # libgd 31 | $ npm install node-gd 32 | ``` 33 | 34 | #### On RHEL/CentOS 35 | 36 | ```bash 37 | $ sudo yum install gd-devel 38 | $ npm install node-gd 39 | ``` 40 | 41 | #### On Mac OS/X 42 | 43 | Using Homebrew 44 | 45 | ```bash 46 | $ brew install pkg-config gd 47 | $ npm install node-gd 48 | ``` 49 | 50 | ...or using MacPorts 51 | 52 | ```bash 53 | $ sudo port install pkgconfig gd2 54 | $ npm install node-gd 55 | ``` 56 | 57 | ### Platform Support 58 | 59 | - **✅ macOS** - x64 and arm64 (Apple Silicon) - prebuilt binaries available 60 | - **✅ Linux** - x64 and arm64 - prebuilt binaries available 61 | - **✅ FreeBSD** - manual build only 62 | - **❌ Windows** - Not supported 63 | 64 | ### Build from Source 65 | 66 | If you need to build from source or want to customize the build: 67 | 68 | ```bash 69 | $ git clone https://github.com/y-a-v-a/node-gd.git 70 | $ cd node-gd 71 | $ npm install # This will fallback to building from source if no prebuilt binary 72 | ``` 73 | 74 | ### Supported Node.js Versions 75 | 76 | Prebuilt binaries are available for Node.js versions 16, 18, 20, and 22. The package supports Node.js 14+ but prebuilt binaries are only provided for actively maintained versions. 77 | 78 | ## Usage 79 | 80 | There are different flavours of images, of which the main ones are palette-based (up to 256 colors) and true color images (millions of colors). GIFs are always palette-based, PNGs can be both palette-based or true color. JPEGs are always true color images. `gd.create()` will create a palette-based base image while `gd.createTrueColor()` will create a true color image. 81 | 82 | ### API 83 | 84 | Full API documentation and more examples can be found in the [docs](https://github.com/y-a-v-a/node-gd/blob/master/docs/index.md) directory or at [the dedicated github page](https://y-a-v-a.github.io/node-gd/). 85 | 86 | ### Examples 87 | 88 | Example of creating a rectangular image with a bright green background and in magenta the text "Hello world!" 89 | 90 | ```javascript 91 | // Import library 92 | import gd from 'node-gd'; 93 | 94 | // Create blank new image in memory 95 | const img = await gd.create(200, 80); 96 | 97 | // Set background color 98 | img.colorAllocate(0, 255, 0); 99 | 100 | // Set text color 101 | const txtColor = img.colorAllocate(255, 0, 255); 102 | 103 | // Set full path to font file 104 | const fontPath = '/full/path/to/font.ttf'; 105 | 106 | // Render string in image 107 | img.stringFT(txtColor, fontPath, 24, 0, 10, 60, 'Hello world!'); 108 | 109 | // Write image buffer to disk 110 | await img.savePng('output.png', 1); 111 | 112 | // Destroy image to clean memory 113 | img.destroy(); 114 | ``` 115 | 116 | Example of drawing a red lined hexagon on a black background: 117 | 118 | ```javascript 119 | import gd from 'node-gd'; 120 | 121 | const img = await gd.createTrueColor(200, 200); 122 | 123 | const points = [ 124 | { x: 100, y: 20 }, 125 | { x: 170, y: 60 }, 126 | { x: 170, y: 140 }, 127 | { x: 100, y: 180 }, 128 | { x: 30, y: 140 }, 129 | { x: 30, y: 60 }, 130 | { x: 100, y: 20 }, 131 | ]; 132 | 133 | img.setThickness(4); 134 | img.polygon(points, 0xff0000); 135 | await img.saveBmp('test1.bmp', 0); 136 | img.destroy(); 137 | ``` 138 | 139 | Another example: 140 | 141 | ```javascript 142 | import gd from 'node-gd'; 143 | 144 | const img = await gd.openFile('/path/to/file.jpg'); 145 | 146 | img.emboss(); 147 | img.brightness(75); 148 | await img.file('/path/to/newFile.bmp'); 149 | img.destroy(); 150 | ``` 151 | 152 | Some output functions are synchronous because they are handled by libgd. An example of this is the creation of animated GIFs. 153 | 154 | ## License & copyright 155 | 156 | Since [December 27th 2012](https://github.com/andris9/node-gd/commit/ad2a80897efc1926ca505b511ffdf0cc1236135a), node-gd is licensed under an MIT license. 157 | 158 | The MIT License (MIT) 159 | Copyright (c) 2010-2020 the contributors. 160 | 161 | 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: 162 | 163 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 164 | 165 | 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. 166 | 167 | ## Contributors 168 | 169 | The current version is based on code created by taggon, [here the original author's repo](https://github.com/taggon/node-gd), and on the additions by [mikesmullin](https://github.com/mikesmullin). Porting node-gd to [node-addon-api](https://github.com/nodejs/node-addon-api) and extending the API is done by [y-a-v-a](https://github.com/y-a-v-a), on Twitter as [@\_y_a_v_a\_](https://twitter.com/_y_a_v_a_). See the `CONTRIBUTORS.md` [file](https://github.com/y-a-v-a/node-gd/blob/master/CONTRIBUTORS.md) for a list of all contributors. 170 | -------------------------------------------------------------------------------- /test/image-creation.test.mjs: -------------------------------------------------------------------------------- 1 | import gd from '../index.js'; 2 | import { assert } from 'chai'; 3 | 4 | /** 5 | * gd.create 6 | * ╦┌┬┐┌─┐┌─┐┌─┐ ┌─┐┬─┐┌─┐┌─┐┌┬┐┬┌─┐┌┐┌ 7 | * ║│││├─┤│ ┬├┤ │ ├┬┘├┤ ├─┤ │ ││ ││││ 8 | * ╩┴ ┴┴ ┴└─┘└─┘ └─┘┴└─└─┘┴ ┴ ┴ ┴└─┘┘└┘ 9 | */ 10 | describe('gd.create - Creating a paletted image', function () { 11 | it('returns a Promise', () => { 12 | const imagePromise = gd.create(100, 100); 13 | assert.strictEqual(imagePromise.constructor, Promise); 14 | 15 | imagePromise.then(image => image.destroy()); 16 | }); 17 | 18 | it('can be done', async () => { 19 | var img = await gd.create(100, 100); 20 | 21 | assert.ok(img instanceof gd.Image); 22 | img.destroy(); 23 | }); 24 | 25 | it('can be done sync', async () => { 26 | var img = gd.createSync(100, 100); 27 | 28 | assert.ok(img instanceof gd.Image); 29 | img.destroy(); 30 | }); 31 | 32 | it('throws Error when accessing instance getter via __proto__', async () => { 33 | var img = gd.createSync(100, 100); 34 | 35 | try { 36 | img.__proto__.width; 37 | } catch (e) { 38 | assert.ok(e instanceof Error); 39 | } 40 | img.destroy(); 41 | }); 42 | 43 | it('throws TypeError when accessing prototype function via __proto__', async () => { 44 | var img = gd.createSync(100, 100); 45 | 46 | try { 47 | img.__proto__.getPixel(1, 1); 48 | } catch (e) { 49 | assert.ok(e instanceof TypeError); 50 | } 51 | img.destroy(); 52 | }); 53 | 54 | it('throws an Error when too few arguments are supplied', async () => { 55 | var img; 56 | try { 57 | img = await gd.create(100); 58 | } catch (e) { 59 | assert.ok(e instanceof Error); 60 | } 61 | }); 62 | 63 | it('throws an Error when argument is not a Number - NaN', async () => { 64 | var img; 65 | try { 66 | img = await gd.create(NaN, 100); 67 | } catch (e) { 68 | assert.ok(e instanceof Error); 69 | } 70 | }); 71 | 72 | it('throws an Error when argument is not a Number - Infinity', async () => { 73 | var img; 74 | try { 75 | img = await gd.create(Infinity, 100); 76 | } catch (e) { 77 | assert.ok(e instanceof Error); 78 | } 79 | }); 80 | 81 | it('throws an TypeError when the first argument if of wrong type', async () => { 82 | var img; 83 | try { 84 | img = await gd.create('bogus', undefined); 85 | } catch (e) { 86 | assert.ok(e instanceof TypeError); 87 | } 88 | }); 89 | 90 | it('throws an TypeError when the second argument if of wrong type', async () => { 91 | var img; 92 | try { 93 | img = await gd.create(100, 'bogus'); 94 | } catch (e) { 95 | assert.ok(e instanceof TypeError); 96 | } 97 | }); 98 | 99 | it('throws a RangeError when the width parameter is 0', async () => { 100 | var img; 101 | try { 102 | img = await gd.create(0, 100); 103 | } catch (e) { 104 | assert.ok(e instanceof RangeError); 105 | } 106 | }); 107 | 108 | it('throws a RangeError when the height parameter is 0', async () => { 109 | var img; 110 | try { 111 | img = await gd.create(100, 0); 112 | } catch (e) { 113 | assert.ok(e instanceof RangeError); 114 | } 115 | }); 116 | 117 | it('throws a RangeError when the height parameter is a negative value', async () => { 118 | var img; 119 | try { 120 | img = await gd.create(100, -10); 121 | } catch (e) { 122 | assert.ok(e instanceof RangeError); 123 | } 124 | }); 125 | 126 | it('throws a RangeError when the height parameter is a fraction value', async () => { 127 | var img; 128 | try { 129 | img = await gd.create(100.5, 101.6); 130 | } catch (e) { 131 | assert.ok(e instanceof RangeError); 132 | } 133 | }); 134 | 135 | it('throws an Error when creating an image without width and height', async () => { 136 | try { 137 | await gd.create(); 138 | } catch (exception) { 139 | assert.ok(exception instanceof Error); 140 | } 141 | }); 142 | 143 | it('throws a Error when the height parameter is 0', async () => { 144 | var img; 145 | try { 146 | await gd.create(100, 0); 147 | } catch (e) { 148 | assert.ok(e instanceof RangeError); 149 | } 150 | }); 151 | 152 | it('returns an object containing basic information about the created image', async () => { 153 | var img = await gd.create(100, 100); 154 | 155 | assert.equal(img.width, 100); 156 | assert.equal(img.height, 100); 157 | assert.equal(img.trueColor, 0); 158 | 159 | img.destroy(); 160 | }); 161 | }); 162 | 163 | /** 164 | * gd.createTrueColor and await gd.createTrueColor 165 | */ 166 | describe('gd.createTrueColor - Create a true color image', function () { 167 | it('returns a Promise', () => { 168 | const imagePromise = gd.createTrueColor(101, 101); 169 | 170 | assert.ok(imagePromise.constructor === Promise); 171 | 172 | imagePromise.then(image => image.destroy()); 173 | }); 174 | 175 | it('returns a Promise that resolves to an Image', async function () { 176 | const imagePromise = gd.createTrueColor(101, 101); 177 | 178 | imagePromise.then(image => { 179 | assert.ok(image.constructor === gd.Image); 180 | }); 181 | }); 182 | 183 | it('can be done', async () => { 184 | var img = await gd.createTrueColor(100, 100); 185 | assert.ok(img instanceof gd.Image); 186 | img.destroy(); 187 | }); 188 | 189 | it('throws an Error when too few arguments are supplied', async () => { 190 | var img; 191 | try { 192 | img = await gd.createTrueColor(100); 193 | } catch (e) { 194 | assert.ok(e instanceof Error); 195 | } 196 | }); 197 | 198 | it('throws an TypeError when the first argument if of wrong type', async () => { 199 | var img; 200 | try { 201 | img = await gd.createTrueColor('bogus', undefined); 202 | } catch (e) { 203 | assert.ok(e instanceof TypeError); 204 | } 205 | }); 206 | 207 | it('throws an TypeError when the second argument if of wrong type', async () => { 208 | var img; 209 | try { 210 | img = await gd.createTrueColor(100, 'bogus'); 211 | } catch (e) { 212 | assert.ok(e instanceof TypeError); 213 | } 214 | }); 215 | 216 | it('throws a RangeError when the width parameter is 0', async () => { 217 | var img; 218 | try { 219 | img = await gd.createTrueColor(0, 100); 220 | } catch (e) { 221 | assert.ok(e instanceof RangeError); 222 | } 223 | }); 224 | 225 | it('throws a RangeError when the height parameter is 0', async () => { 226 | var img; 227 | try { 228 | img = await gd.createTrueColor(100, 0); 229 | } catch (e) { 230 | assert.ok(e instanceof RangeError); 231 | } 232 | }); 233 | 234 | it('returns an object containing basic information about the created image', async () => { 235 | var img = await gd.createTrueColor(100, 100); 236 | assert.ok(img.width === 100 && img.height === 100 && img.trueColor === 1); 237 | img.destroy(); 238 | }); 239 | 240 | it('has 8 enumerable properties', async function () { 241 | const img = await gd.createTrueColor(100, 100); 242 | const props = [ 243 | 'trueColor', 244 | 'width', 245 | 'height', 246 | 'interlace', 247 | 'colorsTotal', 248 | 'toString', 249 | 'interpolationId', 250 | 'resX', 251 | 'resY', 252 | ]; 253 | 254 | let i = 0; 255 | for (let prop in img) { 256 | assert.isTrue(props.includes(prop)); 257 | i++; 258 | } 259 | 260 | assert.equal(i, 9); 261 | 262 | img.destroy(); 263 | }); 264 | }); 265 | -------------------------------------------------------------------------------- /test/main.test.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Note to self: when skipping a test with `this.skip()`, do not use an arrow function, 3 | * since Mocha appears to try to bind `this` to the test function. 4 | * @see https://mochajs.org/#arrow-functions 5 | * 6 | * This file is explicitly run first by mocha using the `--file` directive 7 | * in package.json to let it clean the output directory first. 8 | * 9 | * @author Vincent Bruijn 10 | */ 11 | import fs from 'fs'; 12 | 13 | import gd from '../index.js'; 14 | import { assert } from 'chai'; 15 | 16 | import dirname from './dirname.mjs'; 17 | 18 | const currentDir = dirname(import.meta.url); 19 | 20 | var source = currentDir + '/fixtures/'; 21 | var target = currentDir + '/output/'; 22 | 23 | before(function () { 24 | // declare version 25 | console.log('Built on top of GD version: ' + gd.getGDVersion() + '\n\n'); 26 | 27 | // clear test/output directory 28 | return fs.readdir(target, function (err, files) { 29 | return files.forEach(function (file, idx) { 30 | if (file.substr(0, 6) === 'output') { 31 | return fs.unlink(target + file, function (err) { 32 | if (err) { 33 | throw err; 34 | } 35 | }); 36 | } 37 | }); 38 | }); 39 | }); 40 | 41 | describe('Meta information', function () { 42 | it('gd.getGDVersion() -- will return a version number of format x.y.z', function (done) { 43 | var version = gd.getGDVersion(); 44 | assert.ok(/[0-9]\.[0-9]\.[0-9]+/.test(version)); 45 | return done(); 46 | }); 47 | 48 | it('gd.GD_GIF -- will have built in GIF support', function () { 49 | assert.equal(gd.GD_GIF, 1, 'No GIF support for libgd is impossible!'); 50 | }); 51 | 52 | it('gd.GD_GIF -- is not writeble', function () { 53 | try { 54 | gd.GD_GIF = 99; 55 | } catch (e) { 56 | assert.ok(e instanceof Error); 57 | } 58 | }); 59 | 60 | it('gd.GD_GIFANIM -- will have built in GIF animation support', function () { 61 | assert.equal(gd.GD_GIFANIM, 1, 'No GIF animation support for libgd is impossible!'); 62 | }); 63 | 64 | it('gd.GD_OPENPOLYGON -- will have built in open polygon support', function () { 65 | assert.equal(gd.GD_OPENPOLYGON, 1, 'No open polygon support for libgd is impossible!'); 66 | }); 67 | }); 68 | 69 | describe('GD color functions', function () { 70 | // it('', function(done) {}); 71 | 72 | it('gd.trueColor() -- can return an integer representation of rgb color values', function (done) { 73 | var red = gd.trueColor(255, 0, 0); 74 | assert.ok(16711680 === red); 75 | return done(); 76 | }); 77 | 78 | it('gd.trueColorAlpha() -- can return an integer representation of rgba color values', function (done) { 79 | var transparentRed = gd.trueColorAlpha(255, 0, 0, 63); 80 | assert.ok(1073676288 === transparentRed); 81 | return done(); 82 | }); 83 | }); 84 | 85 | describe('Image query functions', function () { 86 | it('gd.Image#getBoundsSafe() -- getBoundsSafe should return 0 if the coordinate [-10, 1000] is checked against the image bounds', async function () { 87 | var s = source + 'input.png'; 88 | var coord = [-10, 1000]; 89 | const image = await gd.openPng(s); 90 | 91 | assert.ok(image.getBoundsSafe(coord[0], coord[1]) === 0); 92 | image.destroy(); 93 | }); 94 | 95 | it('gd.Image#getBoundsSafe() -- getBoundsSafe should return 1 if the coordinate [10, 10] is checked against the image bounds', async function () { 96 | var s = source + 'input.png'; 97 | var coord = [10, 10]; 98 | const image = await gd.openPng(s); 99 | 100 | assert.ok(image.getBoundsSafe(coord[0], coord[1]) === 1); 101 | image.destroy(); 102 | }); 103 | 104 | it('gd.Image#getTrueColorPixel() -- getTrueColorPixel should return "e6e6e6" when queried for coordinate [10, 10]', async function () { 105 | var s = source + 'input.png'; 106 | var coord = [10, 10]; 107 | const image = await gd.openPng(s); 108 | var color; 109 | color = image.getTrueColorPixel(coord[0], coord[1]); 110 | 111 | assert.ok(color.toString(16) === 'e6e6e6'); 112 | }); 113 | 114 | it('gd.Image#getTrueColorPixel() -- getTrueColorPixel should return 0 when queried for coordinate [101, 101]', async function () { 115 | var s = source + 'input.png'; 116 | var coord = [101, 101]; 117 | const image = await gd.openPng(s); 118 | var color; 119 | color = image.getTrueColorPixel(coord[0], coord[1]); 120 | 121 | assert.ok(color === 0); 122 | }); 123 | 124 | it('gd.Image#imageColorAt() -- imageColorAt should return "be392e" when queried for coordinate [50, 50]', async function () { 125 | var s = source + 'input.png'; 126 | var coord = [50, 50]; 127 | const image = await gd.openPng(s); 128 | var color; 129 | color = image.imageColorAt(coord[0], coord[1]); 130 | 131 | assert.ok(color.toString(16) === 'be392e'); 132 | }); 133 | 134 | it('gd.Image#imageColorAt() -- imageColorAt should throw an error when queried for coordinate [101, 101]', async function () { 135 | const s = source + 'input.png'; 136 | const coord = [101, 101]; 137 | const image = await gd.openPng(s); 138 | let color; 139 | try { 140 | color = image.imageColorAt(coord[0], coord[1]); 141 | } catch (exception) { 142 | assert.ok(exception instanceof Error); 143 | } 144 | }); 145 | }); 146 | 147 | describe('Image filter functions', function () { 148 | it('gd.Image#copyResampled() -- can scale-down (resize) an image', async () => { 149 | var s, t; 150 | s = source + 'input.png'; 151 | t = target + 'output-scale.png'; 152 | const img = await gd.openPng(s); 153 | var canvas, h, scale, w; 154 | 155 | scale = 2; 156 | w = Math.floor(img.width / scale); 157 | h = Math.floor(img.height / scale); 158 | canvas = await gd.createTrueColor(w, h); 159 | img.copyResampled(canvas, 0, 0, 0, 0, w, h, img.width, img.height); 160 | 161 | await canvas.savePng(t, 1); 162 | assert.ok(fs.existsSync(t)); 163 | img.destroy(); 164 | canvas.destroy(); 165 | }); 166 | 167 | it('gd.Image#copyRotated() -- can rotate an image', async function () { 168 | var s, t; 169 | s = source + 'input.png'; 170 | t = target + 'output-rotate.png'; 171 | const img = await gd.openPng(s); 172 | var canvas, h, w; 173 | 174 | w = 100; 175 | h = 100; 176 | canvas = await gd.createTrueColor(w, h); 177 | img.copyRotated(canvas, 50, 50, 0, 0, img.width, img.height, 45); 178 | await canvas.savePng(t, 1); 179 | assert.ok(fs.existsSync(t)); 180 | img.destroy(); 181 | canvas.destroy(); 182 | }); 183 | 184 | it('gd.Image#grayscale() -- can convert to grayscale', async function () { 185 | var s, t; 186 | s = source + 'input.png'; 187 | t = target + 'output-grayscale.png'; 188 | const img = await gd.openPng(s); 189 | img.grayscale(); 190 | await img.savePng(t, -1); 191 | assert.ok(fs.existsSync(t)); 192 | img.destroy(); 193 | }); 194 | 195 | it('gd.Image#gaussianBlur() -- can add gaussian blur to an image', async function () { 196 | var s, t; 197 | s = source + 'input.png'; 198 | t = target + 'output-gaussianblur.png'; 199 | const img = await gd.openPng(s); 200 | var i, j; 201 | for (i = j = 0; j < 10; i = ++j) { 202 | img.gaussianBlur(); 203 | } 204 | 205 | await img.savePng(t, -1); 206 | assert.ok(fs.existsSync(t)); 207 | img.destroy(); 208 | }); 209 | 210 | it('gd.Image#negate() -- can negate an image', async function () { 211 | var s, t; 212 | s = source + 'input.png'; 213 | t = target + 'output-negate.png'; 214 | const img = await gd.openPng(s); 215 | 216 | img.negate(); 217 | await img.savePng(t, -1); 218 | assert.ok(fs.existsSync(t)); 219 | img.destroy(); 220 | }); 221 | 222 | it('gd.Image#brightness() -- can change brightness of an image', async function () { 223 | var s, t; 224 | s = source + 'input.png'; 225 | t = target + 'output-brightness.png'; 226 | const img = await gd.openPng(s); 227 | 228 | const brightness = Math.floor(Math.random() * 100); 229 | img.brightness(brightness); 230 | await img.savePng(t, -1); 231 | assert.ok(fs.existsSync(t)); 232 | img.destroy(); 233 | }); 234 | 235 | it('gd.Image#contrast() -- can change contrast of an image', async function () { 236 | var s, t; 237 | s = source + 'input.png'; 238 | t = target + 'output-contrast.png'; 239 | const img = await gd.openPng(s); 240 | const contrast = Math.floor(Math.random() * 2000) - 900; 241 | img.contrast(contrast); 242 | await img.savePng(t, -1); 243 | assert.ok(fs.existsSync(t)); 244 | img.destroy(); 245 | }); 246 | 247 | it('gd.Image#emboss() -- can emboss an image', async function () { 248 | var s, t; 249 | s = source + 'input.png'; 250 | t = target + 'output-emboss.png'; 251 | const img = await gd.openPng(s); 252 | img.emboss(); 253 | await img.savePng(t, -1); 254 | assert.ok(fs.existsSync(t)); 255 | img.destroy(); 256 | }); 257 | 258 | it('gd.Image#selectiveBlur() -- can apply selective blur to an image', async function () { 259 | var s, t; 260 | s = source + 'input.png'; 261 | t = target + 'output-selectiveBlur.png'; 262 | const img = await gd.openPng(s); 263 | 264 | img.selectiveBlur(); 265 | await img.savePng(t, -1); 266 | assert.ok(fs.existsSync(t)); 267 | img.destroy(); 268 | }); 269 | 270 | it('gd.Image#colorReplace() -- can replace a color to another color', async function () { 271 | var img, s, t; 272 | s = source + 'input.png'; 273 | t = target + 'output-replaced.png'; 274 | const image = await gd.openPng(s); 275 | 276 | var colors = [ 277 | image.getTrueColorPixel(10, 10), 278 | image.getTrueColorPixel(10, 11), 279 | image.getTrueColorPixel(10, 12), 280 | image.getTrueColorPixel(10, 13), 281 | image.getTrueColorPixel(10, 14), 282 | image.getTrueColorPixel(10, 15), 283 | ]; 284 | var colorTo = gd.trueColor(0, 255, 255); 285 | 286 | for (var i = 0; i < colors.length; i++) { 287 | image.colorReplace(colors[i], colorTo); 288 | } 289 | 290 | await image.savePng(t, 0); 291 | 292 | assert.ok(fs.existsSync(t)); 293 | image.destroy(); 294 | }); 295 | 296 | it('gd.Image#stringFT() -- can create a truecolor BMP image with text', async function () { 297 | var f, img, t, txtColor; 298 | f = source + 'FreeSans.ttf'; 299 | t = target + 'output-truecolor-string.bmp'; 300 | img = await gd.createTrueColor(120, 20); 301 | txtColor = img.colorAllocate(255, 255, 0); 302 | img.stringFT(txtColor, f, 16, 0, 8, 18, 'Hello world!'); 303 | await img.saveBmp(t, 0); 304 | assert.ok(fs.existsSync(t)); 305 | }); 306 | }); 307 | -------------------------------------------------------------------------------- /test/heif-avif.test.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import gd from '../index.js'; 3 | import { assert } from 'chai'; 4 | import dirname from './dirname.mjs'; 5 | 6 | const currentDir = dirname(import.meta.url); 7 | const source = currentDir + '/fixtures/'; 8 | const target = currentDir + '/output/'; 9 | 10 | describe('HEIF and AVIF Support', function () { 11 | 12 | describe('HEIF format tests', function () { 13 | it('gd.GD_HEIF -- should be defined correctly', function () { 14 | // GD_HEIF should be defined (can be true, false, 1, or 0) 15 | // In CI environments, it might be undefined if HEIF support wasn't built 16 | if (typeof gd.GD_HEIF === 'undefined') { 17 | console.log('HEIF support not built into this version'); 18 | this.skip(); 19 | return; 20 | } 21 | 22 | assert.ok(typeof gd.GD_HEIF !== 'undefined'); 23 | if (!gd.GD_HEIF) { 24 | console.log('HEIF support not available in this build'); 25 | } 26 | }); 27 | 28 | it('gd.createFromHeif() -- can create image from HEIF file', async function () { 29 | if (!gd.GD_HEIF) { 30 | this.skip(); 31 | return; 32 | } 33 | const s = source + 'input.heif'; 34 | const img = await gd.createFromHeif(s); 35 | 36 | assert.ok(img instanceof gd.Image); 37 | assert.ok(img.width > 0); 38 | assert.ok(img.height > 0); 39 | img.destroy(); 40 | }); 41 | 42 | it('gd.createFromHeifPtr() -- can create image from HEIF buffer', async function () { 43 | if (!gd.GD_HEIF) { 44 | this.skip(); 45 | return; 46 | } 47 | const s = source + 'input.heif'; 48 | const buffer = fs.readFileSync(s); 49 | const img = await gd.createFromHeifPtr(buffer); 50 | 51 | assert.ok(img instanceof gd.Image); 52 | assert.ok(img.width > 0); 53 | assert.ok(img.height > 0); 54 | img.destroy(); 55 | }); 56 | 57 | it('gd.Image#saveHeif() -- can save image as HEIF with quality setting', async function () { 58 | if (!gd.GD_HEIF) { 59 | this.skip(); 60 | return; 61 | } 62 | const s = source + 'input.png'; 63 | const t = target + 'output-heif-quality.heif'; 64 | const img = await gd.openPng(s); 65 | 66 | await img.saveHeif(t, 90); 67 | assert.ok(fs.existsSync(t)); 68 | img.destroy(); 69 | }); 70 | 71 | it('gd.Image#heif() -- returns a Promise', async function () { 72 | if (!gd.GD_HEIF) { 73 | this.skip(); 74 | return; 75 | } 76 | const s = source + 'input.png'; 77 | const t = target + 'output-heif-promise.heif'; 78 | const img = await gd.openPng(s); 79 | const promise = img.heif(t, 80); 80 | 81 | assert.ok(promise instanceof Promise); 82 | await promise; 83 | assert.ok(fs.existsSync(t)); 84 | img.destroy(); 85 | }); 86 | }); 87 | 88 | describe('AVIF format tests', function () { 89 | it('gd.GD_AVIF -- should be defined correctly', function () { 90 | // GD_AVIF should be defined (can be true, false, 1, or 0) 91 | // In CI environments, it might be undefined if AVIF support wasn't built 92 | if (typeof gd.GD_AVIF === 'undefined') { 93 | console.log('AVIF support not built into this version'); 94 | this.skip(); 95 | return; 96 | } 97 | 98 | assert.ok(typeof gd.GD_AVIF !== 'undefined'); 99 | if (!gd.GD_AVIF) { 100 | console.log('AVIF support not available in this build'); 101 | } 102 | }); 103 | 104 | it('gd.createFromAvif() -- can create image from AVIF file', async function () { 105 | if (!gd.GD_AVIF) { 106 | this.skip(); 107 | return; 108 | } 109 | // Skip this test in CI environments where AVIF encoder may not be available 110 | if (process.env.CI || process.env.GITHUB_ACTIONS) { 111 | this.skip(); 112 | return; 113 | } 114 | try { 115 | // First create an AVIF file for testing 116 | const jpegSource = source + 'input.jpg'; 117 | const avifTemp = target + 'temp-for-test.avif'; 118 | const jpegImg = await gd.openJpeg(jpegSource); 119 | await jpegImg.saveAvif(avifTemp, 80); 120 | jpegImg.destroy(); 121 | 122 | // Now test loading the AVIF file 123 | const img = await gd.createFromAvif(avifTemp); 124 | 125 | assert.ok(img instanceof gd.Image); 126 | assert.ok(img.width > 0); 127 | assert.ok(img.height > 0); 128 | img.destroy(); 129 | } catch (error) { 130 | // If AVIF codec is not available at runtime, skip the test 131 | if (error.message && (error.message.includes('codec') || error.message.includes('AVIF'))) { 132 | this.skip(); 133 | return; 134 | } 135 | throw error; 136 | } 137 | }); 138 | 139 | it('gd.createFromAvifPtr() -- can create image from AVIF buffer', async function () { 140 | if (!gd.GD_AVIF) { 141 | this.skip(); 142 | return; 143 | } 144 | // Skip this test in CI environments where AVIF encoder may not be available 145 | if (process.env.CI || process.env.GITHUB_ACTIONS) { 146 | this.skip(); 147 | return; 148 | } 149 | try { 150 | // First create an AVIF file for testing 151 | const jpegSource = source + 'input.jpg'; 152 | const avifTemp = target + 'temp-for-ptr-test.avif'; 153 | const jpegImg = await gd.openJpeg(jpegSource); 154 | await jpegImg.saveAvif(avifTemp, 80); 155 | jpegImg.destroy(); 156 | 157 | // Now test loading from buffer 158 | const buffer = fs.readFileSync(avifTemp); 159 | const img = await gd.createFromAvifPtr(buffer); 160 | 161 | assert.ok(img instanceof gd.Image); 162 | assert.ok(img.width > 0); 163 | assert.ok(img.height > 0); 164 | img.destroy(); 165 | } catch (error) { 166 | // If AVIF codec is not available at runtime, skip the test 167 | if (error.message && (error.message.includes('codec') || error.message.includes('AVIF'))) { 168 | this.skip(); 169 | return; 170 | } 171 | throw error; 172 | } 173 | }); 174 | 175 | it('gd.Image#saveAvif() -- can save image as AVIF with quality setting', async function () { 176 | if (!gd.GD_AVIF) { 177 | this.skip(); 178 | return; 179 | } 180 | // Skip this test in CI environments where AVIF encoder may not be available 181 | if (process.env.CI || process.env.GITHUB_ACTIONS) { 182 | this.skip(); 183 | return; 184 | } 185 | try { 186 | const s = source + 'input.png'; 187 | const t = target + 'output-avif-quality.avif'; 188 | const img = await gd.openPng(s); 189 | 190 | await img.saveAvif(t, 90); 191 | assert.ok(fs.existsSync(t)); 192 | img.destroy(); 193 | } catch (error) { 194 | // If AVIF codec is not available at runtime, skip the test 195 | if (error.message && (error.message.includes('codec') || error.message.includes('AVIF'))) { 196 | this.skip(); 197 | return; 198 | } 199 | throw error; 200 | } 201 | }); 202 | 203 | it('gd.Image#avif() -- returns a Promise', async function () { 204 | if (!gd.GD_AVIF) { 205 | this.skip(); 206 | return; 207 | } 208 | // Skip this test in CI environments where AVIF encoder may not be available 209 | if (process.env.CI || process.env.GITHUB_ACTIONS) { 210 | this.skip(); 211 | return; 212 | } 213 | try { 214 | const s = source + 'input.png'; 215 | const t = target + 'output-avif-promise.avif'; 216 | const img = await gd.openPng(s); 217 | const promise = img.avif(t, 80); 218 | 219 | assert.ok(promise instanceof Promise); 220 | await promise; 221 | assert.ok(fs.existsSync(t)); 222 | img.destroy(); 223 | } catch (error) { 224 | // If AVIF codec is not available at runtime, skip the test 225 | if (error.message && (error.message.includes('codec') || error.message.includes('AVIF'))) { 226 | this.skip(); 227 | return; 228 | } 229 | throw error; 230 | } 231 | }); 232 | }); 233 | 234 | describe('Format interoperability', function () { 235 | it('can convert HEIF to AVIF', async function () { 236 | if (!gd.GD_HEIF || !gd.GD_AVIF) { 237 | this.skip(); 238 | return; 239 | } 240 | // Skip this test in CI environments where AVIF encoder may not be available 241 | if (process.env.CI || process.env.GITHUB_ACTIONS) { 242 | this.skip(); 243 | return; 244 | } 245 | try { 246 | const s = source + 'input.heif'; 247 | const t = target + 'output-heif-to-avif.avif'; 248 | const img = await gd.openHeif(s); 249 | 250 | await img.saveAvif(t, 85); 251 | assert.ok(fs.existsSync(t)); 252 | img.destroy(); 253 | } catch (error) { 254 | // If AVIF codec is not available at runtime, skip the test 255 | if (error.message && (error.message.includes('codec') || error.message.includes('AVIF') || error.message.includes('HEIF'))) { 256 | this.skip(); 257 | return; 258 | } 259 | throw error; 260 | } 261 | }); 262 | 263 | it('can convert AVIF to HEIF', async function () { 264 | if (!gd.GD_HEIF || !gd.GD_AVIF) { 265 | this.skip(); 266 | return; 267 | } 268 | // Skip this test in CI environments where AVIF encoder may not be available 269 | if (process.env.CI || process.env.GITHUB_ACTIONS) { 270 | this.skip(); 271 | return; 272 | } 273 | try { 274 | // First create an AVIF file 275 | const jpegSource = source + 'input.jpg'; 276 | const avifTemp = target + 'temp-avif-to-heif.avif'; 277 | const jpegImg = await gd.openJpeg(jpegSource); 278 | await jpegImg.saveAvif(avifTemp, 80); 279 | jpegImg.destroy(); 280 | 281 | // Now convert AVIF to HEIF 282 | const t = target + 'output-avif-to-heif.heif'; 283 | const img = await gd.openAvif(avifTemp); 284 | await img.saveHeif(t, 85); 285 | assert.ok(fs.existsSync(t)); 286 | img.destroy(); 287 | } catch (error) { 288 | // If AVIF codec is not available at runtime, skip the test 289 | if (error.message && (error.message.includes('codec') || error.message.includes('AVIF') || error.message.includes('HEIF'))) { 290 | this.skip(); 291 | return; 292 | } 293 | throw error; 294 | } 295 | }); 296 | }); 297 | }); -------------------------------------------------------------------------------- /test/fonts-and-images.test.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import gd from '../index.js'; 4 | import { assert } from 'chai'; 5 | 6 | import dirname from './dirname.mjs'; 7 | 8 | const currentDir = dirname(import.meta.url); 9 | 10 | var source = currentDir + '/fixtures/'; 11 | var target = currentDir + '/output/'; 12 | 13 | var fontFile = source + 'FreeSans.ttf'; 14 | 15 | /** 16 | * Text / fonts in images 17 | * ╔═╗┌─┐┌┐┌┌┬┐┌─┐ ┬┌┐┌ ┬┌┬┐┌─┐┌─┐┌─┐┌─┐ 18 | * ╠╣ │ ││││ │ └─┐ ││││ ││││├─┤│ ┬├┤ └─┐ 19 | * ╚ └─┘┘└┘ ┴ └─┘ ┴┘└┘ ┴┴ ┴┴ ┴└─┘└─┘└─┘ 20 | */ 21 | describe('Creating images containing text', function () { 22 | it('gd.Image#stringFT() -- can create an image with text', async () => { 23 | var img, t, txtColor; 24 | t = target + 'output-string.png'; 25 | 26 | img = await gd.create(200, 80); 27 | img.colorAllocate(0, 255, 0); 28 | txtColor = img.colorAllocate(255, 0, 255); 29 | 30 | img.stringFT(txtColor, fontFile, 24, 0, 10, 60, 'Hello world'); 31 | 32 | await img.savePng(t, 1); 33 | 34 | assert.ok(fs.existsSync(t)); 35 | img.destroy(); 36 | }); 37 | 38 | it('gd.Image#stringFT() -- can create a truecolor image with text', async () => { 39 | var img, t, txtColor; 40 | t = target + 'output-truecolor-string.png'; 41 | img = await gd.createTrueColor(120, 20); 42 | txtColor = img.colorAllocate(255, 255, 0); 43 | 44 | img.stringFT(txtColor, fontFile, 16, 0, 8, 18, 'Hello world!'); 45 | 46 | await img.savePng(t, 1); 47 | 48 | assert.ok(fs.existsSync(t)); 49 | img.destroy(); 50 | }); 51 | 52 | it('gd.Image#stringFT() -- can return the coordinates of the bounding box of a string', async () => { 53 | var t = target + 'output-truecolor-string-2.png'; 54 | 55 | var img = await gd.createTrueColor(300, 300); 56 | var txtColor = img.colorAllocate(127, 90, 90); 57 | var boundingBox = img.stringFT( 58 | txtColor, 59 | fontFile, 60 | 16, 61 | 0, 62 | 8, 63 | 18, 64 | 'Hello World2!', 65 | true 66 | ); 67 | 68 | assert.equal(boundingBox.length, 8, 'BoundingBox not eight coordinates?'); 69 | 70 | img.destroy(); 71 | }); 72 | 73 | it('gd.Image#stringFTBBox() -- can return the coordinates of the bounding box of a string using a specific function', async () => { 74 | var t = target + 'output-truecolor-string-2.png'; 75 | 76 | var img = await gd.createTrueColor(300, 300); 77 | var txtColor = img.colorAllocate(127, 90, 90); 78 | var boundingBox = img.stringFTBBox( 79 | txtColor, 80 | fontFile, 81 | 16, 82 | -45, 83 | 20, 84 | 20, 85 | 'Hello World2!', 86 | true 87 | ); 88 | assert.equal(boundingBox.length, 8, 'BoundingBox not eight coordinates?'); 89 | 90 | img.destroy(); 91 | }); 92 | 93 | it('gd.Image#stringFTEx() -- throws an error when gd.Image#stringFTEx() does not receive an object', async () => { 94 | var t = target + 'noob.png'; 95 | var image = await gd.createTrueColor(100, 100); 96 | var txtColor = image.colorAllocate(255, 255, 0); 97 | var extras = ''; 98 | 99 | try { 100 | image.stringFTEx( 101 | txtColor, 102 | fontFile, 103 | 24, 104 | 0, 105 | 10, 106 | 60, 107 | 'Lorem ipsum', 108 | extras 109 | ); 110 | } catch (e) { 111 | assert.ok(e instanceof Error); 112 | image.destroy(); 113 | } 114 | }); 115 | 116 | it('gd.Image#stringFTEx() -- can consume an object with font extras', async () => { 117 | var t = target + 'output-truecolor-string-3.png'; 118 | 119 | var image = await gd.createTrueColor(300, 300); 120 | var extras = { 121 | linespacing: 1.5, 122 | hdpi: 300, 123 | vdpi: 300, 124 | charmap: 'unicode', 125 | disable_kerning: false, 126 | xshow: true, 127 | return_fontpathname: true, 128 | use_fontconfig: false, 129 | fontpath: '', 130 | }; 131 | var txtColor = image.colorAllocate(255, 255, 0); 132 | image.stringFTEx( 133 | txtColor, 134 | fontFile, 135 | 24, 136 | 0, 137 | 10, 138 | 60, 139 | "Hello world\nYes we're here", 140 | extras 141 | ); 142 | 143 | assert.equal( 144 | extras.fontpath, 145 | process.cwd() + '/test/fixtures/FreeSans.ttf' 146 | ); 147 | assert.equal( 148 | extras.xshow, 149 | '72 53 21 21 53 25 72 53 33 21 -424 68 53 49 25 72 53 20 33 53 25 54 53 33 53' 150 | ); 151 | 152 | await image.savePng(t, 0); 153 | image.destroy(); 154 | }); 155 | 156 | it('gd.Image#stringFTEx() -- can set the dpi of an image using a font extras object', async () => { 157 | var t = target + 'output-truecolor-string-300dpi.png'; 158 | 159 | var image = await gd.createTrueColor(300, 300); 160 | var extras = { 161 | hdpi: 300, 162 | vdpi: 150, 163 | }; 164 | var txtColor = image.colorAllocate(255, 0, 255); 165 | image.stringFTEx( 166 | txtColor, 167 | fontFile, 168 | 24, 169 | 0, 170 | 10, 171 | 60, 172 | 'Font extras\ndpi test', 173 | extras 174 | ); 175 | 176 | await image.savePng(t, 0); 177 | image.destroy(); 178 | }); 179 | 180 | it('gd.Image#stringFTEx() -- can set the linespacing of text in an image using a font extras object', async () => { 181 | var t = target + 'output-truecolor-string-linespacing.png'; 182 | 183 | var image = await gd.createTrueColor(300, 300); 184 | var extras = { 185 | linespacing: 2.1, 186 | }; 187 | 188 | var txtColor = image.colorAllocate(0, 255, 255); 189 | image.stringFTEx( 190 | txtColor, 191 | fontFile, 192 | 24, 193 | 0, 194 | 10, 195 | 60, 196 | 'Font extras\nlinespacing', 197 | extras 198 | ); 199 | 200 | await image.savePng(t, 0); 201 | image.destroy(); 202 | }); 203 | 204 | it('gd.Image#stringFTEx() -- can request the kerning table of text in an image using a font extras object', async () => { 205 | var t = target + 'output-truecolor-string-xshow.png'; 206 | 207 | var image = await gd.createTrueColor(300, 300); 208 | var extras = { 209 | xshow: true, 210 | }; 211 | 212 | var txtColor = image.colorAllocate(0, 255, 255); 213 | image.stringFTEx( 214 | txtColor, 215 | fontFile, 216 | 24, 217 | 0, 218 | 10, 219 | 60, 220 | 'Font extras\nxshow', 221 | extras 222 | ); 223 | 224 | assert.equal( 225 | extras.xshow, 226 | '19.2 16.96 17.28 8.96 8 16.96 15.36 8.96 10.56 17.28 -139.52 15.36 15.68 17.28 16.96 23.04' 227 | ); 228 | 229 | await image.savePng(t, 0); 230 | image.destroy(); 231 | }); 232 | 233 | it('gd.Image#stringFTEx() -- can disable the use of kerning of text in an image using a font extras object', async () => { 234 | var t = target + 'output-truecolor-string-disable-kerning.png'; 235 | 236 | var image = await gd.createTrueColor(300, 300); 237 | var extras = { 238 | disable_kerning: true, 239 | }; 240 | 241 | var txtColor = image.colorAllocate(255, 255, 0); 242 | image.stringFTEx( 243 | txtColor, 244 | fontFile, 245 | 24, 246 | 0, 247 | 10, 248 | 60, 249 | 'Font extras\nKerning disabled', 250 | extras 251 | ); 252 | 253 | await image.savePng(t, 0); 254 | image.destroy(); 255 | }); 256 | 257 | it('gd.Image#stringFTEx() -- can return the font path using font extras', async () => { 258 | var t = target + 'output-truecolor-string-3.png'; 259 | 260 | var image = await gd.createTrueColor(300, 300); 261 | var extras = { 262 | return_fontpathname: true, 263 | }; 264 | 265 | var txtColor = image.colorAllocate(127, 255, 0); 266 | image.stringFTEx( 267 | txtColor, 268 | fontFile, 269 | 24, 270 | 0, 271 | 10, 272 | 60, 273 | 'Font extras\nreturn font path', 274 | extras 275 | ); 276 | 277 | assert.equal( 278 | extras.fontpath, 279 | process.cwd() + '/test/fixtures/FreeSans.ttf' 280 | ); 281 | 282 | await image.savePng(t, 0); 283 | image.destroy(); 284 | }); 285 | 286 | it('gd.Image#stringFTEx() -- can use a specified charmap to render a font with font extras', async () => { 287 | var t = target + 'output-truecolor-string-charmap.png'; 288 | 289 | var image = await gd.createTrueColor(300, 300); 290 | var extras = { 291 | charmap: 'unicode', 292 | }; 293 | 294 | var txtColor = image.colorAllocate(255, 255, 0); 295 | image.stringFTEx( 296 | txtColor, 297 | fontFile, 298 | 24, 299 | 0, 300 | 10, 301 | 60, 302 | 'Hello world\nUse unicode!', 303 | extras 304 | ); 305 | 306 | await image.savePng(t, 0); 307 | image.destroy(); 308 | }); 309 | 310 | it('gd.Image#stringFTEx() -- throws an error when an unknown charmap is given with font extras', async () => { 311 | var t = target + 'bogus.png'; 312 | 313 | var image = await gd.createTrueColor(300, 300); 314 | var extras = { 315 | charmap: 'bogus', 316 | }; 317 | 318 | var txtColor = image.colorAllocate(255, 255, 0); 319 | try { 320 | image.stringFTEx( 321 | txtColor, 322 | fontFile, 323 | 24, 324 | 0, 325 | 10, 326 | 60, 327 | 'Hello world\nUse unicode!', 328 | extras 329 | ); 330 | } catch (e) { 331 | assert.ok(e instanceof Error); 332 | image.destroy(); 333 | } 334 | }); 335 | 336 | it('gd.Image#stringFTEx() -- returns an array of coordinates of the bounding box when an 8th boolean parameter is given to', async () => { 337 | var t = target + 'bogus.png'; 338 | var image = await gd.createTrueColor(300, 300); 339 | var extras = { 340 | hdpi: 120, 341 | vdpi: 120, 342 | }; 343 | 344 | var txtColor = image.colorAllocate(255, 255, 0); 345 | 346 | var boundingBox = image.stringFTEx( 347 | txtColor, 348 | fontFile, 349 | 24, 350 | 0, 351 | 10, 352 | 60, 353 | 'Hello world\nxshow string!', 354 | extras, 355 | true 356 | ); 357 | 358 | assert.equal(boundingBox.length, 8); 359 | image.destroy(); 360 | }); 361 | 362 | it('gd.Image#stringFTCircle() -- can put text on a circle', async () => { 363 | var t = target + 'output-truecolor-string-circle.png'; 364 | 365 | var image = await gd.createTrueColor(300, 300); 366 | 367 | var txtColor = image.colorAllocate(255, 255, 0); 368 | image.stringFTCircle( 369 | 150, 370 | 150, 371 | 100, 372 | 32, 373 | 1, 374 | fontFile, 375 | 24, 376 | 'Hello', 377 | 'world!', 378 | txtColor 379 | ); 380 | 381 | await image.savePng(t, 0); 382 | image.destroy(); 383 | }); 384 | }); 385 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace gd { 2 | 3 | type Ptr = Buffer | BufferSource; 4 | 5 | // Creating and opening graphic images 6 | 7 | function create(width: number, height: number): Promise; 8 | 9 | function createTrueColor(width: number, height: number): Promise; 10 | 11 | function createSync(width: number, height: number): gd.Image; 12 | 13 | function createTrueColorSync(width: number, height: number): gd.Image; 14 | 15 | function openJpeg(path: string): Promise; 16 | 17 | function createFromJpeg(path: string): Promise; 18 | 19 | function createFromJpegPtr(data: Ptr): Promise; 20 | 21 | function openPng(path: string): Promise; 22 | 23 | function createFromPng(path: string): Promise; 24 | 25 | function createFromPngPtr(data: Ptr): Promise; 26 | 27 | function openGif(path: string): Promise; 28 | 29 | function createFromGif(path: string): Promise; 30 | 31 | function createFromGifPtr(data: Ptr): Promise; 32 | 33 | function openWBMP(path: string): Promise; 34 | 35 | function createFromWBMP(path: string): Promise; 36 | 37 | function createFromWBMPPtr(data: Ptr): Promise; 38 | 39 | function openBmp(path: string): Promise; 40 | 41 | function createFromBmp(path: string): Promise; 42 | 43 | function createFromBmpPtr(data: Ptr): Promise; 44 | 45 | function openTiff(path: string): Promise; 46 | 47 | function createFromTiff(path: string): Promise; 48 | 49 | function createFromTiffPtr(data: Ptr): Promise; 50 | 51 | function openWebp(path: string): Promise; 52 | 53 | function createFromWebp(path: string): Promise; 54 | 55 | function createFromWebpPtr(data: Ptr): Promise; 56 | 57 | function openHeif(path: string): Promise; 58 | 59 | function createFromHeif(path: string): Promise; 60 | 61 | function createFromHeifPtr(data: Ptr): Promise; 62 | 63 | function openAvif(path: string): Promise; 64 | 65 | function createFromAvif(path: string): Promise; 66 | 67 | function createFromAvifPtr(data: Ptr): Promise; 68 | 69 | function openFile(path: string): Promise; 70 | 71 | function createFromFile(path: string): Promise; 72 | 73 | type Color = number; 74 | 75 | function trueColor(red: number, green: number, blue: number): Color; 76 | 77 | function trueColorAlpha(red: number, green: number, blue: number, alpha: number): Color; 78 | 79 | function getGDVersion(): string; 80 | 81 | type Point = { 82 | x: number; 83 | y: number; 84 | }; 85 | 86 | // Manipulating graphic images 87 | 88 | interface Image { 89 | // Image properties 90 | 91 | readonly width: number; 92 | 93 | readonly height: number; 94 | 95 | readonly trueColor: 0 | 1; 96 | 97 | readonly colorsTotal: number; 98 | 99 | interlace: boolean; 100 | 101 | destroy(): void; 102 | 103 | // Drawing 104 | 105 | setPixel(x: number, y: number, color: Color): gd.Image; 106 | 107 | line(x1: number, y1: number, x2: number, y2: number, color: Color): gd.Image; 108 | 109 | dashedLine(x1: number, y1: number, x2: number, y2: number, color: Color): gd.Image; 110 | 111 | polygon(array: Point[], color: Color): gd.Image; 112 | 113 | openPolygon(array: Point[], color: Color): gd.Image; 114 | 115 | filledPolygon(array: Point[], color: Color): gd.Image; 116 | 117 | rectangle(x1: number, y1: number, x2: number, y2: number, color: Color): gd.Image; 118 | 119 | filledRectangle(x1: number, y1: number, x2: number, y2: number, color: Color): gd.Image; 120 | 121 | arc(cx: number, cy: number, width: number, height: number, begin: number, end: number, color: Color): gd.Image; 122 | 123 | filledArc(cx: number, cy: number, width: number, height: number, begin: number, end: number, color: Color, style?: number): gd.Image; 124 | 125 | ellipse(cx: number, cy: number, width: number, height: number, color: Color): gd.Image; 126 | 127 | filledEllipse(cx: number, cy: number, width: number, height: number, color: Color): gd.Image; 128 | 129 | fillToBorder(x: number, y: number, border: number, color: Color): gd.Image; 130 | 131 | fill(x: number, y: number, color: Color): gd.Image; 132 | 133 | setAntiAliased(color: Color): gd.Image; 134 | 135 | setAntiAliasedDontBlend(color: Color, dontblend: boolean): gd.Image; 136 | 137 | setBrush(image: gd.Image): gd.Image; 138 | 139 | setTile(image: gd.Image): gd.Image; 140 | 141 | setStyle(array: Color[]): gd.Image; 142 | 143 | setThickness(thickness: number): gd.Image; 144 | 145 | alphaBlending(blending: 0 | 1): gd.Image; 146 | 147 | saveAlpha(saveFlag: 0 | 1): gd.Image; 148 | 149 | setClip(x1: number, y1: number, x2: number, y2: number): gd.Image; 150 | 151 | getClip(): { 152 | x1: number; 153 | y1: number; 154 | x2: number; 155 | y2: number; 156 | }; 157 | 158 | setResolution(res_x: number, res_y: number): gd.Image; 159 | 160 | // Query image information 161 | 162 | getPixel(x: number, y: number): Color; 163 | 164 | getTrueColorPixel(x: number, y: number): Color; 165 | 166 | imageColorAt(x: number, y: number): Color; 167 | 168 | getBoundsSafe(x: number, y: number): 0 | 1; 169 | 170 | // Font and text 171 | 172 | stringFTBBox(color: Color, font: string, size: number, angle: number, x: number, y: number, text: string): [number, number, number, number, number, number, number, number]; 173 | 174 | stringFT(color: Color, font: string, size: number, angle: number, x: number, y: number, text: string): void; 175 | stringFT(color: Color, font: string, size: number, angle: number, x: number, y: number, text: string, boundingbox: boolean): [number, number, number, number, number, number, number, number]; 176 | 177 | // Color handling 178 | 179 | colorAllocate(r: number, g: number, b: number): Color; 180 | 181 | colorAllocateAlpha(r: number, g: number, b: number, a: number): Color; 182 | 183 | colorClosest(r: number, g: number, b: number): Color; 184 | 185 | colorClosestAlpha(r: number, g: number, b: number, a: number): Color; 186 | 187 | colorClosestHWB(r: number, g: number, b: number): Color; 188 | 189 | colorExact(r: number, g: number, b: number): Color; 190 | 191 | colorResolve(r: number, g: number, b: number): Color; 192 | 193 | colorResolveAlpha(r: number, g: number, b: number, a: number): Color; 194 | 195 | red(r: number): number; 196 | 197 | green(g: number): number; 198 | 199 | blue(b: number): number; 200 | 201 | alpha(color: Color): number; 202 | 203 | getTransparent(): number; 204 | 205 | colorDeallocate(color: Color): gd.Image; 206 | 207 | // Color Manipulation 208 | 209 | colorTransparent(color: Color): gd.Image; 210 | 211 | colorReplace(fromColor: Color, toColor: Color): number; 212 | 213 | colorReplaceThreshold(fromColor: Color, toColor: Color, threshold: number): number; 214 | 215 | colorReplaceArray(fromColors: Color[], toColors: Color[]): number; 216 | 217 | // Effects 218 | 219 | grayscale(): gd.Image; 220 | 221 | gaussianBlur(): gd.Image; 222 | 223 | negate(): gd.Image; 224 | 225 | brightness(brightness: number): gd.Image; 226 | 227 | contrast(contrast: number): gd.Image; 228 | 229 | selectiveBlur(): gd.Image; 230 | 231 | emboss(): gd.Image; 232 | 233 | flipHorizontal(): gd.Image; 234 | 235 | flipVertical(): gd.Image; 236 | 237 | flipBoth(): gd.Image; 238 | 239 | crop(x: number, y: number, width: number, height: number): gd.Image; 240 | 241 | cropAuto(mode: AutoCrop): gd.Image; 242 | 243 | cropThreshold(color: Color, threshold: number): gd.Image; 244 | 245 | sharpen(pct: number): gd.Image; 246 | 247 | createPaletteFromTrueColor(ditherFlag: 0 | 1, colorsWanted: number): gd.Image; 248 | 249 | trueColorToPalette(ditherFlag: 0 | 1, colorsWanted: number): number; 250 | 251 | paletteToTrueColor(): 0 | 1; 252 | 253 | colorMatch(image: gd.Image): number; 254 | 255 | gifAnimBegin(anim: string, useGlobalColorMap: -1 | 0 | 1, loops: number): Uint8Array; 256 | 257 | gifAnimAdd(anim: string, localColorMap: number, leftOffset: number, topOffset: number, delay: number, disposal: number, prevFrame: gd.Image | null): boolean; 258 | 259 | gifAnimEnd(anim: string): boolean; 260 | 261 | // Copying and resizing 262 | 263 | copy(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, width: number, height: number): gd.Image; 264 | 265 | copyResized(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, dw: number, dh: number, sw: number, sh: number): gd.Image; 266 | 267 | copyResampled(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, dw: number, dh: number, sw: number, sh: number): gd.Image; 268 | 269 | copyRotated(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, sw: number, sh: number, angle: number): gd.Image; 270 | 271 | copyMerge(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, width: number, height: number, pct: number): gd.Image; 272 | 273 | copyMergeGray(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, width: number, height: number, pct: number): gd.Image; 274 | 275 | paletteCopy(dest: gd.Image): gd.Image; 276 | 277 | squareToCircle(radius: number): gd.Image; 278 | 279 | // Misc 280 | 281 | // Returns a bitmask of image comparsion flags 282 | // https://libgd.github.io/manuals/2.3.0/files/gd-c.html#gdImageCompare 283 | compare(image: gd.Image): number; 284 | 285 | // Saving graphic images 286 | 287 | savePng(path: string, level: number): Promise; 288 | saveJpeg(path: string, quality: number): Promise; 289 | saveGif(path: string): Promise; 290 | saveWBMP(path: string, foreground: 0x000000 | 0xffffff | number): Promise; 291 | saveBmp(path: string, compression: 0 | 1): Promise; 292 | saveTiff(path: string): Promise; 293 | saveWebp(path: string, quality?: number): Promise; 294 | saveHeif(path: string, quality?: number): Promise; 295 | saveAvif(path: string, quality?: number): Promise; 296 | 297 | png(path: string, level: number): Promise; 298 | jpeg(path: string, quality: number): Promise; 299 | gif(path: string): Promise; 300 | wbmp(path: string, foreground: 0x000000 | 0xffffff | number): Promise; 301 | bmp(path: string, compression: 0 | 1): Promise; 302 | tiff(path: string): Promise; 303 | webp(path: string, quality?: number): Promise; 304 | heif(path: string, quality?: number): Promise; 305 | avif(path: string, quality?: number): Promise; 306 | 307 | file(path: string): Promise; 308 | } 309 | 310 | export const enum AutoCrop { 311 | DEFAULT = 0, 312 | TRANSPARENT, 313 | BLACK, 314 | WHITE, 315 | SIDES, 316 | THRESHOLD, 317 | } 318 | 319 | export const enum Cmp { 320 | IMAGE = 1, 321 | NUM_COLORS = 2, 322 | COLOR = 4, 323 | SIZE_X = 8, 324 | SIZE_Y = 16, 325 | TRANSPARENT = 32, 326 | BACKGROUND = 64, 327 | INTERLACE = 128, 328 | TRUECOLOR = 256, 329 | } 330 | } 331 | 332 | export = gd; 333 | -------------------------------------------------------------------------------- /src/node_gd.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2009-2011, Taegon Kim 3 | * Copyright (c) 2014-2021, Vincent Bruijn 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | #ifndef NODE_GD_H 19 | #define NODE_GD_H 20 | 21 | #include 22 | #include 23 | 24 | #define SUPPORTS_GD_2_3_3 (GD_MINOR_VERSION == 3 && GD_RELEASE_VERSION >= 3) 25 | 26 | #define SUPPORTS_GD_2_3_0 (SUPPORTS_GD_2_3_3 || GD_MINOR_VERSION == 3 && GD_RELEASE_VERSION >= 0) 27 | 28 | #define HAS_LIBHEIF (HAVE_LIBHEIF && SUPPORTS_GD_2_3_3) 29 | #define HAS_LIBAVIF (HAVE_LIBAVIF && SUPPORTS_GD_2_3_3) 30 | #define HAS_LIBTIFF (HAVE_LIBTIFF) 31 | #define HAS_LIBWEBP (HAVE_LIBWEBP) 32 | 33 | // Since gd 2.0.28, these are always built in 34 | #define GD_GIF 1 35 | #define GD_GIFANIM 1 36 | #define GD_OPENPOLYGON 1 37 | 38 | #define COLOR_ANTIALIASED gdAntiAliased 39 | #define COLOR_BRUSHED gdBrushed 40 | #define COLOR_STYLED gdStyled 41 | #define COLOR_STYLEDBRUSHED gdStyledBrushed 42 | #define COLOR_TITLED gdTiled 43 | #define COLOR_TRANSPARENT gdTransparent 44 | 45 | #define REQ_ARGS(N, MSG) \ 46 | if (info.Length() < (N)) \ 47 | { \ 48 | Napi::Error::New(info.Env(), \ 49 | "Expected " #N " argument(s): " MSG) \ 50 | .ThrowAsJavaScriptException(); \ 51 | return info.Env().Null(); \ 52 | } 53 | 54 | #define REQ_STR_ARG(I, VAR, MSG) \ 55 | if (info.Length() <= (I) || !info[I].IsString()) \ 56 | { \ 57 | Napi::TypeError::New(info.Env(), \ 58 | "Argument " #I " must be a string. " MSG) \ 59 | .ThrowAsJavaScriptException(); \ 60 | return info.Env().Null(); \ 61 | } \ 62 | std::string VAR = info[I].As().Utf8Value().c_str(); 63 | 64 | #define REQ_INT_ARG(I, VAR, MSG) \ 65 | int VAR; \ 66 | if (info.Length() <= (I) || !info[I].IsNumber()) \ 67 | { \ 68 | Napi::TypeError::New(info.Env(), \ 69 | "Argument " #I " must be a Number. " MSG) \ 70 | .ThrowAsJavaScriptException(); \ 71 | return info.Env().Null(); \ 72 | } \ 73 | VAR = info[I].ToNumber(); 74 | 75 | #define INT_ARG_RANGE(I, PROP) \ 76 | if ((I) < 1) \ 77 | { \ 78 | Napi::RangeError::New(info.Env(), \ 79 | "Value for " #PROP " must be greater than 0") \ 80 | .ThrowAsJavaScriptException(); \ 81 | return info.Env().Null(); \ 82 | } 83 | 84 | #define REQ_FN_ARG(I, VAR) \ 85 | if (info.Length() <= (I) || !info[I].IsFunction()) \ 86 | { \ 87 | Napi::TypeError::New(info.Env(), \ 88 | "Argument " #I " must be a Function") \ 89 | .ThrowAsJavaScriptException(); \ 90 | return info.Env().Null(); \ 91 | } \ 92 | Napi::Function VAR = info[I].As(); 93 | 94 | #define REQ_DOUBLE_ARG(I, VAR) \ 95 | double VAR; \ 96 | if (info.Length() <= (I) || !info[I].IsNumber()) \ 97 | { \ 98 | Napi::TypeError::New(info.Env(), \ 99 | "Argument " #I " must be a Number") \ 100 | .ThrowAsJavaScriptException(); \ 101 | return info.Env().Null(); \ 102 | } \ 103 | VAR = info[I].ToNumber(); 104 | 105 | #define REQ_IMG_ARG(I, VAR) \ 106 | if (info.Length() <= (I) || !info[I].IsObject()) \ 107 | { \ 108 | Napi::TypeError::New(info.Env(), \ 109 | "Argument " #I " must be an Image object.") \ 110 | .ThrowAsJavaScriptException(); \ 111 | return info.Env().Null(); \ 112 | } \ 113 | Gd::Image *_obj_ = \ 114 | Napi::ObjectWrap::Unwrap(info[I].As()); \ 115 | gdImagePtr VAR = _obj_->getGdImagePtr(); 116 | 117 | #define OPT_INT_ARG(I, VAR, DEFAULT) \ 118 | int VAR; \ 119 | if (info.Length() <= (I)) \ 120 | { \ 121 | VAR = (DEFAULT); \ 122 | } \ 123 | else if (info[I].IsNumber()) \ 124 | { \ 125 | VAR = info[I].ToNumber(); \ 126 | } \ 127 | else \ 128 | { \ 129 | Napi::TypeError::New(info.Env(), \ 130 | "Optional argument " #I " must be a Number") \ 131 | .ThrowAsJavaScriptException(); \ 132 | return info.Env().Null(); \ 133 | } 134 | 135 | #define OPT_STR_ARG(I, VAR, DEFAULT) \ 136 | std::string VAR; \ 137 | if (info.Length() <= (I)) \ 138 | { \ 139 | VAR = (DEFAULT); \ 140 | } \ 141 | else if (info[I].IsString()) \ 142 | { \ 143 | VAR = info[I].As().Utf8Value().c_str(); \ 144 | } \ 145 | else \ 146 | { \ 147 | Napi::TypeError::New(info.Env(), \ 148 | "Optional argument " #I " must be a String") \ 149 | .ThrowAsJavaScriptException(); \ 150 | return info.Env().Null(); \ 151 | } 152 | 153 | #define OPT_BOOL_ARG(I, VAR, DEFAULT) \ 154 | bool VAR; \ 155 | if (info.Length() <= (I)) \ 156 | { \ 157 | VAR = (DEFAULT); \ 158 | } \ 159 | else if (info[I].IsBoolean()) \ 160 | { \ 161 | VAR = info[I].ToBoolean(); \ 162 | } \ 163 | else \ 164 | { \ 165 | Napi::TypeError::New(info.Env(), \ 166 | "Optional argument " #I " must be a Boolean") \ 167 | .ThrowAsJavaScriptException(); \ 168 | return info.Env().Null(); \ 169 | } 170 | 171 | #define RETURN_IMAGE(IMG) \ 172 | if (!IMG) \ 173 | { \ 174 | return info.Env().Null(); \ 175 | } \ 176 | else \ 177 | { \ 178 | Napi::Value argv = \ 179 | Napi::External::New(info.Env(), &IMG); \ 180 | Napi::Object instance = Gd::Image::constructor.New({argv}); \ 181 | return instance; \ 182 | } 183 | 184 | #define DECLARE_CREATE_FROM(TYPE) \ 185 | Napi::Value Gd::CreateFrom##TYPE(const Napi::CallbackInfo &info) \ 186 | { \ 187 | return CreateFrom##TYPE##Worker::DoWork(info); \ 188 | } \ 189 | Napi::Value Gd::CreateFrom##TYPE##Ptr(const Napi::CallbackInfo &info) \ 190 | { \ 191 | REQ_ARGS(1, "of type Buffer."); \ 192 | ASSERT_IS_BUFFER(info[0]); \ 193 | gdImagePtr im; \ 194 | Napi::Buffer buffer = info[0].As >(); \ 195 | char *buffer_data = buffer.Data(); \ 196 | size_t buffer_length = buffer.Length(); \ 197 | im = gdImageCreateFrom##TYPE##Ptr(buffer_length, buffer_data); \ 198 | RETURN_IMAGE(im) \ 199 | } 200 | 201 | #define ASSERT_IS_BUFFER(val) \ 202 | if (!val.IsBuffer()) \ 203 | { \ 204 | Napi::TypeError::New(info.Env(), "Argument not a Buffer") \ 205 | .ThrowAsJavaScriptException(); \ 206 | return info.Env().Null(); \ 207 | } 208 | 209 | #define RETURN_DATA \ 210 | Napi::Buffer result = \ 211 | Napi::Buffer::Copy(info.Env(), data, size); \ 212 | gdFree(data); \ 213 | return result; 214 | 215 | 216 | #define CHECK_IMAGE_EXISTS \ 217 | if (_isDestroyed) \ 218 | { \ 219 | Napi::Error::New(info.Env(), "Image is already destroyed") \ 220 | .ThrowAsJavaScriptException(); \ 221 | return info.Env().Undefined(); \ 222 | } \ 223 | if (_image == nullptr) \ 224 | { \ 225 | Napi::Error::New(info.Env(), "Image does not exist") \ 226 | .ThrowAsJavaScriptException(); \ 227 | return info.Env().Undefined(); \ 228 | } 229 | 230 | class Gd : public Napi::ObjectWrap 231 | { 232 | public: 233 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 234 | 235 | class Image : public Napi::ObjectWrap 236 | { 237 | public: 238 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 239 | 240 | Image(const Napi::CallbackInfo &info); 241 | ~Image(); 242 | 243 | static Napi::FunctionReference constructor; 244 | 245 | gdImagePtr getGdImagePtr() const { return _image; } 246 | 247 | private: 248 | gdImagePtr _image{nullptr}; 249 | 250 | bool _isDestroyed{true}; 251 | 252 | operator gdImagePtr() const { return _image; } 253 | 254 | /** 255 | * Destruction, Loading and Saving Functions 256 | */ 257 | Napi::Value Destroy(const Napi::CallbackInfo &info); 258 | Napi::Value Jpeg(const Napi::CallbackInfo &info); 259 | Napi::Value JpegPtr(const Napi::CallbackInfo &info); 260 | Napi::Value Gif(const Napi::CallbackInfo &info); 261 | Napi::Value GifPtr(const Napi::CallbackInfo &info); 262 | Napi::Value Png(const Napi::CallbackInfo &info); 263 | Napi::Value PngPtr(const Napi::CallbackInfo &info); 264 | Napi::Value WBMP(const Napi::CallbackInfo &info); 265 | Napi::Value WBMPPtr(const Napi::CallbackInfo &info); 266 | #if HAS_LIBWEBP 267 | Napi::Value Webp(const Napi::CallbackInfo &info); 268 | Napi::Value WebpPtr(const Napi::CallbackInfo &info); 269 | #endif 270 | Napi::Value Bmp(const Napi::CallbackInfo &info); 271 | Napi::Value BmpPtr(const Napi::CallbackInfo &info); 272 | #if HAS_LIBHEIF 273 | Napi::Value Heif(const Napi::CallbackInfo &info); 274 | Napi::Value HeifPtr(const Napi::CallbackInfo &info); 275 | #endif 276 | #if HAS_LIBAVIF 277 | Napi::Value Avif(const Napi::CallbackInfo &info); 278 | Napi::Value AvifPtr(const Napi::CallbackInfo &info); 279 | #endif 280 | 281 | #if HAS_LIBTIFF 282 | Napi::Value Tiff(const Napi::CallbackInfo &info); 283 | Napi::Value TiffPtr(const Napi::CallbackInfo &info); 284 | #endif 285 | Napi::Value File(const Napi::CallbackInfo &info); 286 | 287 | /** 288 | * Drawing Functions 289 | */ 290 | Napi::Value SetPixel(const Napi::CallbackInfo &info); 291 | Napi::Value Line(const Napi::CallbackInfo &info); 292 | Napi::Value DashedLine(const Napi::CallbackInfo &info); 293 | Napi::Value Polygon(const Napi::CallbackInfo &info); 294 | Napi::Value OpenPolygon(const Napi::CallbackInfo &info); 295 | Napi::Value FilledPolygon(const Napi::CallbackInfo &info); 296 | Napi::Value Rectangle(const Napi::CallbackInfo &info); 297 | Napi::Value FilledRectangle(const Napi::CallbackInfo &info); 298 | Napi::Value Arc(const Napi::CallbackInfo &info); 299 | Napi::Value FilledArc(const Napi::CallbackInfo &info); 300 | Napi::Value Ellipse(const Napi::CallbackInfo &info); 301 | Napi::Value FilledEllipse(const Napi::CallbackInfo &info); 302 | Napi::Value FillToBorder(const Napi::CallbackInfo &info); 303 | Napi::Value Fill(const Napi::CallbackInfo &info); 304 | Napi::Value SetAntiAliased(const Napi::CallbackInfo &info); 305 | Napi::Value SetAntiAliasedDontBlend(const Napi::CallbackInfo &info); 306 | Napi::Value SetBrush(const Napi::CallbackInfo &info); 307 | Napi::Value SetTile(const Napi::CallbackInfo &info); 308 | Napi::Value SetStyle(const Napi::CallbackInfo &info); 309 | Napi::Value SetThickness(const Napi::CallbackInfo &info); 310 | Napi::Value AlphaBlending(const Napi::CallbackInfo &info); 311 | Napi::Value SaveAlpha(const Napi::CallbackInfo &info); 312 | Napi::Value SetClip(const Napi::CallbackInfo &info); 313 | Napi::Value GetClip(const Napi::CallbackInfo &info); 314 | Napi::Value SetResolution(const Napi::CallbackInfo &info); 315 | 316 | /** 317 | * Query Functions 318 | */ 319 | Napi::Value GetPixel(const Napi::CallbackInfo &info); 320 | Napi::Value GetTrueColorPixel(const Napi::CallbackInfo &info); 321 | // This is implementation of the PHP-GD specific method imagecolorat 322 | Napi::Value ImageColorAt(const Napi::CallbackInfo &info); 323 | Napi::Value GetBoundsSafe(const Napi::CallbackInfo &info); 324 | Napi::Value WidthGetter(const Napi::CallbackInfo &info); 325 | Napi::Value HeightGetter(const Napi::CallbackInfo &info); 326 | Napi::Value ResolutionXGetter(const Napi::CallbackInfo &info); 327 | Napi::Value ResolutionYGetter(const Napi::CallbackInfo &info); 328 | Napi::Value TrueColorGetter(const Napi::CallbackInfo &info); 329 | Napi::Value InterpolationIdGetter(const Napi::CallbackInfo &info); 330 | void InterpolationIdSetter(const Napi::CallbackInfo &info, const Napi::Value &value); 331 | /** 332 | * Font and Text Handling Funcitons 333 | */ 334 | Napi::Value StringFTBBox(const Napi::CallbackInfo &info); 335 | Napi::Value StringFT(const Napi::CallbackInfo &info); 336 | Napi::Value StringFTEx(const Napi::CallbackInfo &info); 337 | Napi::Value StringFTCircle(const Napi::CallbackInfo &info); 338 | /** 339 | * Color Handling Functions 340 | */ 341 | Napi::Value ColorAllocate(const Napi::CallbackInfo &info); 342 | Napi::Value ColorAllocateAlpha(const Napi::CallbackInfo &info); 343 | Napi::Value ColorClosest(const Napi::CallbackInfo &info); 344 | Napi::Value ColorClosestAlpha(const Napi::CallbackInfo &info); 345 | Napi::Value ColorClosestHWB(const Napi::CallbackInfo &info); 346 | Napi::Value ColorExact(const Napi::CallbackInfo &info); 347 | Napi::Value ColorExactAlpha(const Napi::CallbackInfo &info); 348 | Napi::Value ColorResolve(const Napi::CallbackInfo &info); 349 | Napi::Value ColorResolveAlpha(const Napi::CallbackInfo &info); 350 | Napi::Value ColorsTotalGetter(const Napi::CallbackInfo &info); 351 | Napi::Value Red(const Napi::CallbackInfo &info); 352 | Napi::Value Blue(const Napi::CallbackInfo &info); 353 | Napi::Value Green(const Napi::CallbackInfo &info); 354 | Napi::Value Alpha(const Napi::CallbackInfo &info); 355 | Napi::Value InterlaceGetter(const Napi::CallbackInfo &info); 356 | void InterlaceSetter(const Napi::CallbackInfo &info, const Napi::Value &value); 357 | Napi::Value GetTransparent(const Napi::CallbackInfo &info); 358 | Napi::Value ColorDeallocate(const Napi::CallbackInfo &info); 359 | Napi::Value ColorTransparent(const Napi::CallbackInfo &info); 360 | Napi::Value ColorReplace(const Napi::CallbackInfo &info); 361 | Napi::Value ColorReplaceThreshold(const Napi::CallbackInfo &info); 362 | Napi::Value ColorReplaceArray(const Napi::CallbackInfo &info); 363 | Napi::Value GrayScale(const Napi::CallbackInfo &info); 364 | Napi::Value GaussianBlur(const Napi::CallbackInfo &info); 365 | Napi::Value Negate(const Napi::CallbackInfo &info); 366 | Napi::Value Brightness(const Napi::CallbackInfo &info); 367 | Napi::Value Contrast(const Napi::CallbackInfo &info); 368 | Napi::Value SelectiveBlur(const Napi::CallbackInfo &info); 369 | Napi::Value FlipHorizontal(const Napi::CallbackInfo &info); 370 | Napi::Value FlipVertical(const Napi::CallbackInfo &info); 371 | Napi::Value FlipBoth(const Napi::CallbackInfo &info); 372 | Napi::Value Crop(const Napi::CallbackInfo &info); 373 | Napi::Value CropAuto(const Napi::CallbackInfo &info); 374 | Napi::Value CropThreshold(const Napi::CallbackInfo &info); 375 | Napi::Value Emboss(const Napi::CallbackInfo &info); 376 | Napi::Value Pixelate(const Napi::CallbackInfo &info); 377 | 378 | /** 379 | * Copying and Resizing Functions 380 | */ 381 | Napi::Value Copy(const Napi::CallbackInfo &info); 382 | Napi::Value CopyResized(const Napi::CallbackInfo &info); 383 | Napi::Value CopyResampled(const Napi::CallbackInfo &info); 384 | Napi::Value CopyRotated(const Napi::CallbackInfo &info); 385 | Napi::Value CopyMerge(const Napi::CallbackInfo &info); 386 | Napi::Value CopyMergeGray(const Napi::CallbackInfo &info); 387 | Napi::Value PaletteCopy(const Napi::CallbackInfo &info); 388 | Napi::Value SquareToCircle(const Napi::CallbackInfo &info); 389 | Napi::Value Sharpen(const Napi::CallbackInfo &info); 390 | Napi::Value CreatePaletteFromTrueColor(const Napi::CallbackInfo &info); 391 | Napi::Value TrueColorToPalette(const Napi::CallbackInfo &info); 392 | Napi::Value PaletteToTrueColor(const Napi::CallbackInfo &info); 393 | Napi::Value ColorMatch(const Napi::CallbackInfo &info); 394 | Napi::Value Scale(const Napi::CallbackInfo &info); 395 | Napi::Value RotateInterpolated(const Napi::CallbackInfo &info); 396 | 397 | Napi::Value GifAnimBegin(const Napi::CallbackInfo &info); 398 | Napi::Value GifAnimAdd(const Napi::CallbackInfo &info); 399 | Napi::Value GifAnimEnd(const Napi::CallbackInfo &info); 400 | /** 401 | * Miscellaneous Functions 402 | */ 403 | Napi::Value Compare(const Napi::CallbackInfo &info); 404 | }; 405 | 406 | private: 407 | /** 408 | * Section A - Creation of new image in memory 409 | */ 410 | static Napi::Value ImageCreate(const Napi::CallbackInfo &info); 411 | static Napi::Value ImageCreateTrueColor(const Napi::CallbackInfo &info); 412 | static Napi::Value ImageCreateSync(const Napi::CallbackInfo &info); 413 | static Napi::Value ImageCreateTrueColorSync(const Napi::CallbackInfo &info); 414 | 415 | /** 416 | * Section B - Creation of image in memory from a source (either file or Buffer) 417 | */ 418 | static Napi::Value CreateFromJpeg(const Napi::CallbackInfo &info); 419 | static Napi::Value CreateFromJpegPtr(const Napi::CallbackInfo &info); 420 | static Napi::Value CreateFromPng(const Napi::CallbackInfo &info); 421 | static Napi::Value CreateFromPngPtr(const Napi::CallbackInfo &info); 422 | static Napi::Value CreateFromGif(const Napi::CallbackInfo &info); 423 | static Napi::Value CreateFromGifPtr(const Napi::CallbackInfo &info); 424 | static Napi::Value CreateFromWBMP(const Napi::CallbackInfo &info); 425 | static Napi::Value CreateFromWBMPPtr(const Napi::CallbackInfo &info); 426 | #if HAS_LIBWEBP 427 | static Napi::Value CreateFromWebp(const Napi::CallbackInfo &info); 428 | static Napi::Value CreateFromWebpPtr(const Napi::CallbackInfo &info); 429 | #endif 430 | 431 | static Napi::Value CreateFromBmp(const Napi::CallbackInfo &info); 432 | static Napi::Value CreateFromBmpPtr(const Napi::CallbackInfo &info); 433 | 434 | #if HAS_LIBHEIF 435 | static Napi::Value CreateFromHeif(const Napi::CallbackInfo &info); 436 | static Napi::Value CreateFromHeifPtr(const Napi::CallbackInfo &info); 437 | #endif 438 | #if HAS_LIBAVIF 439 | static Napi::Value CreateFromAvif(const Napi::CallbackInfo &info); 440 | static Napi::Value CreateFromAvifPtr(const Napi::CallbackInfo &info); 441 | #endif 442 | #if HAS_LIBTIFF 443 | static Napi::Value CreateFromTiff(const Napi::CallbackInfo &info); 444 | static Napi::Value CreateFromTiffPtr(const Napi::CallbackInfo &info); 445 | #endif 446 | 447 | /** 448 | * Section C - Creation of image in memory from a file, type based on file extension 449 | */ 450 | static Napi::Value CreateFromFile(const Napi::CallbackInfo &info); 451 | 452 | /** 453 | * Section D - Calculate functions 454 | */ 455 | static Napi::Value TrueColor(const Napi::CallbackInfo &info); 456 | static Napi::Value TrueColorAlpha(const Napi::CallbackInfo &info); 457 | 458 | /** 459 | * Section E - Meta information 460 | */ 461 | static Napi::Value GdVersionGetter(const Napi::CallbackInfo &info); 462 | }; 463 | 464 | #endif 465 | -------------------------------------------------------------------------------- /src/node_gd_workers.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2020, Vincent Bruijn 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | #include 17 | #include 18 | #include "node_gd.h" 19 | 20 | /** 21 | * @see https://github.com/nodejs/node-addon-api 22 | */ 23 | using namespace Napi; 24 | 25 | /** 26 | * CreateFromWorker only to be inherited from 27 | * 28 | * This worker class contains recurring code. The CreateFromJpegWorker and others 29 | * are decendants of this class. Returns a Promise. 30 | */ 31 | class CreateFromWorker : public AsyncWorker 32 | { 33 | public: 34 | CreateFromWorker(napi_env env, const char *resource_name) 35 | : AsyncWorker(env, resource_name), _deferred(Promise::Deferred::New(env)) 36 | { 37 | } 38 | 39 | protected: 40 | virtual void OnOK() override 41 | { 42 | // create new instance of Gd::Image with resulting image 43 | Napi::Value argv = Napi::External::New(Env(), &image); 44 | Napi::Object instance = Gd::Image::constructor.New({argv}); 45 | 46 | // resolve Promise with instance of Gd::Image 47 | _deferred.Resolve(instance); 48 | } 49 | 50 | virtual void OnError(const Napi::Error &e) override 51 | { 52 | // reject Promise with error message 53 | _deferred.Reject(Napi::String::New(Env(), e.Message())); 54 | } 55 | 56 | gdImagePtr image; 57 | 58 | Promise::Deferred _deferred; 59 | 60 | std::string path; 61 | }; 62 | 63 | /** 64 | * CreateFromJpegWorker for Jpeg files 65 | */ 66 | class CreateFromJpegWorker : public CreateFromWorker 67 | { 68 | public: 69 | static Value DoWork(const CallbackInfo &info) 70 | { 71 | REQ_STR_ARG(0, path, "Argument should be a path to the JPEG file to load."); 72 | 73 | CreateFromJpegWorker *worker = new CreateFromJpegWorker(info.Env(), 74 | "CreateFromJpegWorkerResource"); 75 | 76 | worker->path = path; 77 | worker->Queue(); 78 | return worker->_deferred.Promise(); 79 | } 80 | 81 | protected: 82 | void Execute() override 83 | { 84 | // execute the task 85 | FILE *in; 86 | in = fopen(path.c_str(), "rb"); 87 | if (in == nullptr) 88 | { 89 | return SetError("Cannot open JPEG file"); 90 | } 91 | image = gdImageCreateFromJpeg(in); 92 | fclose(in); 93 | } 94 | 95 | private: 96 | CreateFromJpegWorker(napi_env env, const char *resource_name) 97 | : CreateFromWorker(env, resource_name) 98 | { 99 | } 100 | }; 101 | 102 | // PNG 103 | class CreateFromPngWorker : public CreateFromWorker 104 | { 105 | public: 106 | static Value DoWork(const CallbackInfo &info) 107 | { 108 | REQ_STR_ARG(0, path, "Argument should be a path to the PNG file to load."); 109 | 110 | CreateFromPngWorker *worker = new CreateFromPngWorker(info.Env(), 111 | "CreateFromPngWorkerResource"); 112 | 113 | worker->path = path; 114 | worker->Queue(); 115 | return worker->_deferred.Promise(); 116 | } 117 | 118 | protected: 119 | void Execute() override 120 | { 121 | // execute the async task 122 | FILE *in; 123 | in = fopen(path.c_str(), "rb"); 124 | if (in == nullptr) 125 | { 126 | return SetError("Cannot open PNG file"); 127 | } 128 | image = gdImageCreateFromPng(in); 129 | fclose(in); 130 | } 131 | 132 | private: 133 | CreateFromPngWorker(napi_env env, const char *resource_name) 134 | : CreateFromWorker(env, resource_name) 135 | { 136 | } 137 | }; 138 | 139 | // GIF 140 | class CreateFromGifWorker : public CreateFromWorker 141 | { 142 | public: 143 | static Value DoWork(const CallbackInfo &info) 144 | { 145 | REQ_STR_ARG(0, path, "Argument should be a path to the Gif file to load."); 146 | 147 | CreateFromGifWorker *worker = new CreateFromGifWorker(info.Env(), 148 | "CreateFromGifWorkerResource"); 149 | 150 | worker->path = path; 151 | worker->Queue(); 152 | return worker->_deferred.Promise(); 153 | } 154 | 155 | protected: 156 | void Execute() override 157 | { 158 | // execute the async task 159 | FILE *in; 160 | in = fopen(path.c_str(), "rb"); 161 | if (in == nullptr) 162 | { 163 | return SetError("Cannot open GIF file"); 164 | } 165 | image = gdImageCreateFromGif(in); 166 | fclose(in); 167 | } 168 | 169 | private: 170 | CreateFromGifWorker(napi_env env, const char *resource_name) 171 | : CreateFromWorker(env, resource_name) 172 | { 173 | } 174 | }; 175 | 176 | // WBMP 177 | class CreateFromWBMPWorker : public CreateFromWorker 178 | { 179 | public: 180 | static Value DoWork(const CallbackInfo &info) 181 | { 182 | REQ_STR_ARG(0, path, "Argument should be a path to the WBMP file to load."); 183 | 184 | CreateFromWBMPWorker *worker = new CreateFromWBMPWorker(info.Env(), 185 | "CreateFromWBMPWorkerResource"); 186 | 187 | worker->path = path; 188 | worker->Queue(); 189 | return worker->_deferred.Promise(); 190 | } 191 | 192 | protected: 193 | void Execute() override 194 | { 195 | // execute the async task 196 | FILE *in; 197 | in = fopen(path.c_str(), "rb"); 198 | if (in == nullptr) 199 | { 200 | return SetError("Cannot open WBMP file"); 201 | } 202 | image = gdImageCreateFromWBMP(in); 203 | fclose(in); 204 | } 205 | 206 | private: 207 | CreateFromWBMPWorker(napi_env env, const char *resource_name) 208 | : CreateFromWorker(env, resource_name) 209 | { 210 | } 211 | }; 212 | 213 | // Webp 214 | class CreateFromWebpWorker : public CreateFromWorker 215 | { 216 | public: 217 | static Value DoWork(const CallbackInfo &info) 218 | { 219 | REQ_STR_ARG(0, path, "Argument should be a path to the Webp file to load."); 220 | 221 | CreateFromWebpWorker *worker = new CreateFromWebpWorker(info.Env(), 222 | "CreateFromWebpWorkerResource"); 223 | 224 | worker->path = path; 225 | worker->Queue(); 226 | return worker->_deferred.Promise(); 227 | } 228 | 229 | protected: 230 | void Execute() override 231 | { 232 | // execute the async task 233 | FILE *in; 234 | in = fopen(path.c_str(), "rb"); 235 | if (in == nullptr) 236 | { 237 | return SetError("Cannot open WEBP file"); 238 | } 239 | image = gdImageCreateFromWebp(in); 240 | fclose(in); 241 | } 242 | 243 | private: 244 | CreateFromWebpWorker(napi_env env, const char *resource_name) 245 | : CreateFromWorker(env, resource_name) 246 | { 247 | } 248 | }; 249 | 250 | // Bmp 251 | class CreateFromBmpWorker : public CreateFromWorker 252 | { 253 | public: 254 | static Value DoWork(const CallbackInfo &info) 255 | { 256 | REQ_STR_ARG(0, path, "Argument should be a path to the Bmp file to load."); 257 | 258 | CreateFromBmpWorker *worker = new CreateFromBmpWorker(info.Env(), 259 | "CreateFromBmpWorkerResource"); 260 | 261 | worker->path = path; 262 | worker->Queue(); 263 | return worker->_deferred.Promise(); 264 | } 265 | 266 | protected: 267 | void Execute() override 268 | { 269 | // execute the async task 270 | FILE *in; 271 | in = fopen(path.c_str(), "rb"); 272 | if (in == nullptr) 273 | { 274 | return SetError("Cannot open BMP file"); 275 | } 276 | image = gdImageCreateFromBmp(in); 277 | fclose(in); 278 | } 279 | 280 | private: 281 | CreateFromBmpWorker(napi_env env, const char *resource_name) 282 | : CreateFromWorker(env, resource_name) 283 | { 284 | } 285 | }; 286 | 287 | // Tiff 288 | class CreateFromTiffWorker : public CreateFromWorker 289 | { 290 | public: 291 | static Value DoWork(const CallbackInfo &info) 292 | { 293 | REQ_STR_ARG(0, path, "Argument should be a path to the TIFF file to load."); 294 | 295 | CreateFromTiffWorker *worker = new CreateFromTiffWorker(info.Env(), 296 | "CreateFromTiffWorkerResource"); 297 | 298 | worker->path = path; 299 | worker->Queue(); 300 | return worker->_deferred.Promise(); 301 | } 302 | 303 | protected: 304 | void Execute() override 305 | { 306 | // execute the async task 307 | FILE *in; 308 | in = fopen(path.c_str(), "rb"); 309 | if (in == nullptr) 310 | { 311 | return SetError("Cannot open TIFF file"); 312 | } 313 | image = gdImageCreateFromTiff(in); 314 | fclose(in); 315 | } 316 | 317 | private: 318 | CreateFromTiffWorker(napi_env env, const char *resource_name) 319 | : CreateFromWorker(env, resource_name) 320 | { 321 | } 322 | }; 323 | 324 | // Avif 325 | #if HAS_LIBAVIF 326 | class CreateFromAvifWorker : public CreateFromWorker 327 | { 328 | public: 329 | static Value DoWork(const CallbackInfo &info) 330 | { 331 | REQ_STR_ARG(0, path, "Argument should be a path to the Avif file to load."); 332 | 333 | CreateFromAvifWorker *worker = new CreateFromAvifWorker(info.Env(), 334 | "CreateFromAvifWorkerResource"); 335 | 336 | worker->path = path; 337 | worker->Queue(); 338 | return worker->_deferred.Promise(); 339 | } 340 | 341 | protected: 342 | void Execute() override 343 | { 344 | // execute the async task 345 | FILE *in; 346 | in = fopen(path.c_str(), "rb"); 347 | if (in == nullptr) 348 | { 349 | return SetError("Cannot open Avif file"); 350 | } 351 | image = gdImageCreateFromAvif(in); 352 | fclose(in); 353 | } 354 | 355 | private: 356 | CreateFromAvifWorker(napi_env env, const char *resource_name) 357 | : CreateFromWorker(env, resource_name) 358 | { 359 | } 360 | }; 361 | #endif 362 | 363 | #if HAS_LIBHEIF 364 | // Heif 365 | class CreateFromHeifWorker : public CreateFromWorker 366 | { 367 | public: 368 | static Value DoWork(const CallbackInfo &info) 369 | { 370 | REQ_STR_ARG(0, path, "Argument should be a path to the Heif file to load."); 371 | 372 | CreateFromHeifWorker *worker = new CreateFromHeifWorker(info.Env(), 373 | "CreateFromHeifWorkerResource"); 374 | 375 | worker->path = path; 376 | worker->Queue(); 377 | return worker->_deferred.Promise(); 378 | } 379 | 380 | protected: 381 | void Execute() override 382 | { 383 | // execute the async task 384 | FILE *in; 385 | in = fopen(path.c_str(), "rb"); 386 | if (in == nullptr) 387 | { 388 | return SetError("Cannot open Heif file"); 389 | } 390 | image = gdImageCreateFromHeif(in); 391 | fclose(in); 392 | } 393 | 394 | private: 395 | CreateFromHeifWorker(napi_env env, const char *resource_name) 396 | : CreateFromWorker(env, resource_name) 397 | { 398 | } 399 | }; 400 | #endif 401 | 402 | /** 403 | * FileWorker handling gdImageFile via the AsyncWorker 404 | * Returns a Promise 405 | */ 406 | class FileWorker : public AsyncWorker 407 | { 408 | public: 409 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 410 | { 411 | REQ_STR_ARG(0, path, "Argument should be a path to the image file to load."); 412 | 413 | FileWorker *worker = new FileWorker(info.Env(), "FileWorkerResource"); 414 | 415 | worker->path = path; 416 | worker->_gdImage = &gdImage; 417 | worker->Queue(); 418 | return worker->_deferred.Promise(); 419 | } 420 | 421 | protected: 422 | void Execute() override 423 | { 424 | _success = gdImageFile(*_gdImage, path.c_str()); 425 | 426 | if (!_success) 427 | { 428 | return SetError("Cannot save file"); 429 | } 430 | } 431 | 432 | virtual void OnOK() override 433 | { 434 | // resolve Promise with boolean 435 | _deferred.Resolve(Napi::Boolean::New(Env(), _success)); 436 | } 437 | 438 | virtual void OnError(const Napi::Error &e) override 439 | { 440 | // reject Promise with error message 441 | _deferred.Reject(Napi::String::New(Env(), e.Message())); 442 | } 443 | 444 | private: 445 | FileWorker(napi_env env, const char *resource_name) 446 | : AsyncWorker(env, resource_name), _deferred(Promise::Deferred::New(env)) 447 | { 448 | } 449 | 450 | gdImagePtr *_gdImage; 451 | 452 | Promise::Deferred _deferred; 453 | 454 | std::string path; 455 | 456 | bool _success; 457 | }; 458 | 459 | /** 460 | * Async worker class to make the I/O from gdImageCreateFromFile async in JavaScript 461 | * Returns a Promise 462 | */ 463 | class CreateFromFileWorker : public AsyncWorker 464 | { 465 | public: 466 | static Value DoWork(const CallbackInfo &info) 467 | { 468 | REQ_STR_ARG(0, path, "Argument should be a path to the image file to load."); 469 | 470 | CreateFromFileWorker *worker = new CreateFromFileWorker(info.Env(), 471 | "CreateFromFileWorkerResource"); 472 | 473 | worker->path = path; 474 | worker->Queue(); 475 | return worker->_deferred.Promise(); 476 | } 477 | 478 | protected: 479 | void Execute() override 480 | { 481 | // execute the async task 482 | image = gdImageCreateFromFile(path.c_str()); 483 | } 484 | 485 | virtual void OnOK() override 486 | { 487 | // create new instance of Gd::Image with resulting image 488 | Napi::Value argv = Napi::External::New(Env(), &image); 489 | Napi::Object instance = Gd::Image::constructor.New({argv}); 490 | 491 | // resolve Promise with instance of Gd::Image 492 | _deferred.Resolve(instance); 493 | } 494 | 495 | virtual void OnError(const Napi::Error &e) override 496 | { 497 | _deferred.Reject(Napi::String::New(Env(), e.Message())); 498 | } 499 | 500 | gdImagePtr image; 501 | 502 | Promise::Deferred _deferred; 503 | 504 | std::string path; 505 | 506 | private: 507 | CreateFromFileWorker(napi_env env, const char *resource_name) 508 | : AsyncWorker(env, resource_name), _deferred(Promise::Deferred::New(env)) 509 | { 510 | } 511 | }; 512 | 513 | /** 514 | * CreateWorker for async creation of images in memory 515 | */ 516 | class CreateWorker : public AsyncWorker 517 | { 518 | public: 519 | CreateWorker(napi_env env, const char *resource_name, int width, int height, int trueColor) 520 | : AsyncWorker(env, resource_name), _deferred(Promise::Deferred::New(env)), 521 | _width(width), _height(height), _trueColor(trueColor) 522 | { 523 | } 524 | 525 | Promise::Deferred _deferred; 526 | 527 | protected: 528 | void Execute() override 529 | { 530 | if (_trueColor == 0) 531 | { 532 | image = gdImageCreate(_width, _height); 533 | } 534 | else 535 | { 536 | image = gdImageCreateTrueColor(_width, _height); 537 | } 538 | if (!image) 539 | { 540 | return SetError("No image created!"); 541 | } 542 | } 543 | 544 | virtual void OnOK() override 545 | { 546 | Napi::Value _argv = Napi::External::New(Env(), &image); 547 | Napi::Object instance = Gd::Image::constructor.New({_argv}); 548 | 549 | _deferred.Resolve(instance); 550 | } 551 | 552 | virtual void OnError(const Napi::Error &e) override 553 | { 554 | _deferred.Reject(Napi::String::New(Env(), e.Message())); 555 | } 556 | 557 | private: 558 | gdImagePtr image; 559 | 560 | int _width; 561 | 562 | int _height; 563 | 564 | int _trueColor; 565 | }; 566 | 567 | /** 568 | * Save workers 569 | */ 570 | class SaveWorker : public AsyncWorker 571 | { 572 | public: 573 | SaveWorker(napi_env env, const char *resource_name) 574 | : AsyncWorker(env, resource_name), _deferred(Promise::Deferred::New(env)) 575 | { 576 | } 577 | 578 | protected: 579 | virtual void OnOK() override 580 | { 581 | _deferred.Resolve(Napi::Boolean::New(Env(), true)); 582 | } 583 | 584 | virtual void OnError(const Napi::Error &e) override 585 | { 586 | _deferred.Reject(Napi::String::New(Env(), e.Message())); 587 | } 588 | 589 | gdImagePtr *_gdImage; 590 | 591 | int quality; 592 | 593 | int level; 594 | 595 | int foreground; 596 | 597 | Promise::Deferred _deferred; 598 | 599 | std::string path; 600 | }; 601 | 602 | class SaveJpegWorker : public SaveWorker 603 | { 604 | public: 605 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 606 | { 607 | REQ_STR_ARG(0, path, "Argument should be a path and filename to the destination to save the JPEG."); 608 | OPT_INT_ARG(1, quality, -1); 609 | 610 | SaveJpegWorker *worker = new SaveJpegWorker(info.Env(), 611 | "SaveJpegWorkerResource"); 612 | 613 | worker->path = path; 614 | worker->quality = quality; 615 | worker->_gdImage = &gdImage; 616 | worker->Queue(); 617 | return worker->_deferred.Promise(); 618 | } 619 | 620 | protected: 621 | void Execute() override 622 | { 623 | FILE *out = fopen(path.c_str(), "wb"); 624 | if (out == nullptr) 625 | { 626 | return SetError("Cannot save JPEG file"); 627 | } 628 | gdImageJpeg(*_gdImage, out, quality); 629 | fclose(out); 630 | } 631 | 632 | private: 633 | SaveJpegWorker(napi_env env, const char *resource_name) 634 | : SaveWorker(env, resource_name) 635 | { 636 | } 637 | }; 638 | 639 | class SaveGifWorker : public SaveWorker 640 | { 641 | public: 642 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 643 | { 644 | REQ_STR_ARG(0, path, "Argument should be a path and filename to the destination to save the Gif."); 645 | 646 | SaveGifWorker *worker = new SaveGifWorker(info.Env(), 647 | "SaveGifWorkerResource"); 648 | 649 | worker->path = path; 650 | worker->_gdImage = &gdImage; 651 | worker->Queue(); 652 | return worker->_deferred.Promise(); 653 | } 654 | 655 | protected: 656 | void Execute() override 657 | { 658 | FILE *out = fopen(path.c_str(), "wb"); 659 | if (out == nullptr) 660 | { 661 | return SetError("Cannot save GIF file"); 662 | } 663 | gdImageGif(*_gdImage, out); 664 | fclose(out); 665 | } 666 | 667 | private: 668 | SaveGifWorker(napi_env env, const char *resource_name) 669 | : SaveWorker(env, resource_name) 670 | { 671 | } 672 | }; 673 | 674 | class SavePngWorker : public SaveWorker 675 | { 676 | public: 677 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 678 | { 679 | REQ_STR_ARG(0, path, "Argument should be a path and filename to the destination to save the PNG."); 680 | OPT_INT_ARG(1, level, -1); 681 | 682 | SavePngWorker *worker = new SavePngWorker(info.Env(), 683 | "SavePngWorkerResource"); 684 | 685 | worker->path = path; 686 | worker->level = level; 687 | worker->_gdImage = &gdImage; 688 | worker->Queue(); 689 | return worker->_deferred.Promise(); 690 | } 691 | 692 | protected: 693 | void Execute() override 694 | { 695 | FILE *out = fopen(path.c_str(), "wb"); 696 | if (out == nullptr) 697 | { 698 | return SetError("Cannot save PNG file"); 699 | } 700 | gdImagePngEx(*_gdImage, out, level); 701 | fclose(out); 702 | } 703 | 704 | private: 705 | SavePngWorker(napi_env env, const char *resource_name) 706 | : SaveWorker(env, resource_name) 707 | { 708 | } 709 | }; 710 | 711 | class SaveWBMPWorker : public SaveWorker 712 | { 713 | public: 714 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 715 | { 716 | REQ_STR_ARG(0, path, "Argument should be a path and filename to the destination to save the WBMP."); 717 | REQ_INT_ARG(1, foreground, "The index of the foreground color should be supplied."); 718 | 719 | SaveWBMPWorker *worker = new SaveWBMPWorker(info.Env(), 720 | "SaveWBMPWorkerResource"); 721 | 722 | worker->path = path; 723 | worker->foreground = foreground; 724 | worker->_gdImage = &gdImage; 725 | worker->Queue(); 726 | return worker->_deferred.Promise(); 727 | } 728 | 729 | protected: 730 | void Execute() override 731 | { 732 | FILE *out = fopen(path.c_str(), "wb"); 733 | if (out == nullptr) 734 | { 735 | return SetError("Cannot save WBMP file"); 736 | } 737 | gdImageWBMP(*_gdImage, foreground, out); 738 | fclose(out); 739 | } 740 | 741 | private: 742 | SaveWBMPWorker(napi_env env, const char *resource_name) 743 | : SaveWorker(env, resource_name) 744 | { 745 | } 746 | }; 747 | 748 | class SaveWebpWorker : public SaveWorker 749 | { 750 | public: 751 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 752 | { 753 | REQ_STR_ARG(0, path, "Argument should be a path and filename to the destination to save the Webp."); 754 | OPT_INT_ARG(1, level, -1); 755 | 756 | SaveWebpWorker *worker = new SaveWebpWorker(info.Env(), 757 | "SaveWebpWorkerResource"); 758 | 759 | worker->path = path; 760 | worker->level = level; 761 | worker->_gdImage = &gdImage; 762 | worker->Queue(); 763 | return worker->_deferred.Promise(); 764 | } 765 | 766 | protected: 767 | void Execute() override 768 | { 769 | FILE *out = fopen(path.c_str(), "wb"); 770 | if (out == nullptr) 771 | { 772 | return SetError("Cannot save WEBP file"); 773 | } 774 | gdImageWebpEx(*_gdImage, out, level); 775 | fclose(out); 776 | } 777 | 778 | private: 779 | SaveWebpWorker(napi_env env, const char *resource_name) 780 | : SaveWorker(env, resource_name) 781 | { 782 | } 783 | }; 784 | 785 | class SaveBmpWorker : public SaveWorker 786 | { 787 | public: 788 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 789 | { 790 | REQ_ARGS(2, "destination file path and compression flag."); 791 | REQ_STR_ARG(0, path, "Argument should be a path and filename to the destination to save the BMP."); 792 | REQ_INT_ARG(1, compression, "BMP compression flag should be either 0 (no compression) or 1 (compression)."); 793 | 794 | SaveBmpWorker *worker = new SaveBmpWorker(info.Env(), 795 | "SaveBmpWorkerResource"); 796 | 797 | worker->path = path; 798 | worker->compression = compression; 799 | worker->_gdImage = &gdImage; 800 | worker->Queue(); 801 | return worker->_deferred.Promise(); 802 | } 803 | 804 | protected: 805 | void Execute() override 806 | { 807 | FILE *out = fopen(path.c_str(), "wb"); 808 | if (out == nullptr) 809 | { 810 | return SetError("Cannot save BMP file"); 811 | } 812 | gdImageBmp(*_gdImage, out, compression); 813 | fclose(out); 814 | } 815 | 816 | int compression; 817 | 818 | private: 819 | SaveBmpWorker(napi_env env, const char *resource_name) 820 | : SaveWorker(env, resource_name) 821 | { 822 | } 823 | }; 824 | 825 | class SaveTiffWorker : public SaveWorker 826 | { 827 | public: 828 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 829 | { 830 | REQ_STR_ARG(0, path, "Argument should be a path and filename to the destination to save the TIFF."); 831 | 832 | SaveTiffWorker *worker = new SaveTiffWorker(info.Env(), 833 | "SaveTiffWorkerResource"); 834 | 835 | worker->path = path; 836 | worker->_gdImage = &gdImage; 837 | worker->Queue(); 838 | return worker->_deferred.Promise(); 839 | } 840 | 841 | protected: 842 | void Execute() override 843 | { 844 | FILE *out = fopen(path.c_str(), "wb"); 845 | if (out == nullptr) 846 | { 847 | return SetError("Cannot save TIFF file"); 848 | } 849 | gdImageTiff(*_gdImage, out); 850 | fclose(out); 851 | } 852 | 853 | private: 854 | SaveTiffWorker(napi_env env, const char *resource_name) 855 | : SaveWorker(env, resource_name) 856 | { 857 | } 858 | }; 859 | 860 | #if HAS_LIBHEIF 861 | class SaveHeifWorker : public SaveWorker 862 | { 863 | public: 864 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 865 | { 866 | REQ_STR_ARG(0, path, "Argument should be a path and filename to the destination to save the Heif."); 867 | OPT_INT_ARG(1, quality, -1); 868 | OPT_INT_ARG(2, codec_param, 1); 869 | OPT_STR_ARG(3, chroma_param, "444"); 870 | 871 | const char *chroma = chroma_param.c_str(); 872 | 873 | SaveHeifWorker *worker = new SaveHeifWorker(info.Env(), 874 | "SaveHeifWorkerResource"); 875 | 876 | worker->path = path; 877 | worker->_gdImage = &gdImage; 878 | worker->quality = quality; 879 | if (codec_param == 1) 880 | { 881 | worker->codec = GD_HEIF_CODEC_HEVC; 882 | } 883 | else if (codec_param == 4) 884 | { 885 | worker->codec = GD_HEIF_CODEC_AV1; 886 | } 887 | else 888 | { 889 | worker->codec = GD_HEIF_CODEC_UNKNOWN; 890 | } 891 | worker->chroma = chroma; 892 | worker->Queue(); 893 | return worker->_deferred.Promise(); 894 | } 895 | 896 | protected: 897 | void Execute() override 898 | { 899 | FILE *out = fopen(path.c_str(), "wb"); 900 | if (out == nullptr) 901 | { 902 | return SetError("Cannot save Heif file"); 903 | } 904 | gdImageHeifEx(*_gdImage, out, quality, codec, chroma); 905 | fclose(out); 906 | } 907 | 908 | gdHeifCodec codec; 909 | 910 | const char *chroma; 911 | 912 | private: 913 | SaveHeifWorker(napi_env env, const char *resource_name) 914 | : SaveWorker(env, resource_name) 915 | { 916 | } 917 | }; 918 | #endif 919 | 920 | #if HAS_LIBAVIF 921 | class SaveAvifWorker : public SaveWorker 922 | { 923 | public: 924 | static Value DoWork(const CallbackInfo &info, gdImagePtr &gdImage) 925 | { 926 | REQ_STR_ARG(0, path, "Argument should be a path and filename to the destination to save the Avif."); 927 | OPT_INT_ARG(1, quality, -1); 928 | OPT_INT_ARG(2, speed, -1); 929 | 930 | SaveAvifWorker *worker = new SaveAvifWorker(info.Env(), 931 | "SaveAvifWorkerResource"); 932 | 933 | worker->path = path; 934 | worker->_gdImage = &gdImage; 935 | worker->quality = quality; 936 | worker->speed = speed; 937 | worker->Queue(); 938 | return worker->_deferred.Promise(); 939 | } 940 | 941 | protected: 942 | void Execute() override 943 | { 944 | FILE *out = fopen(path.c_str(), "wb"); 945 | if (out == nullptr) 946 | { 947 | return SetError("Cannot save Avif file"); 948 | } 949 | 950 | // Check if image is valid 951 | if (*_gdImage == nullptr) 952 | { 953 | fclose(out); 954 | return SetError("Invalid image for AVIF conversion"); 955 | } 956 | 957 | // Try to save AVIF - this can fail if codec is not available 958 | gdImageAvifEx(*_gdImage, out, quality, speed); 959 | fclose(out); 960 | } 961 | 962 | int speed; 963 | 964 | private: 965 | SaveAvifWorker(napi_env env, const char *resource_name) 966 | : SaveWorker(env, resource_name) 967 | { 968 | } 969 | }; 970 | #endif 971 | --------------------------------------------------------------------------------