├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── Dockerfile ├── LICENSE ├── README.md ├── binding.gyp ├── docs ├── _config.yml └── index.md ├── index.d.ts ├── index.js ├── lib ├── GifAnim.js ├── bindings.js └── node-gd.js ├── package-lock.json ├── package.json ├── src ├── addon.cc ├── node_gd.cc ├── node_gd.h └── node_gd_workers.cc ├── test ├── colormatch.test.mjs ├── destroy.test.mjs ├── dirname.mjs ├── file-types.test.mjs ├── fixtures │ ├── FreeSans.ttf │ ├── input-transparent.png │ ├── input.bmp │ ├── input.jpg │ ├── input.png │ ├── input.tif │ └── node-gd.gif ├── fonts-and-images.test.mjs ├── gifanim.test.mjs ├── image-creation.test.mjs ├── image-pointer.test.mjs ├── main.test.mjs ├── openfile.test.mjs ├── output │ └── .gitignore ├── query-image-info.test.mjs └── tiff.test.mjs └── util.sh /.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: [16, 18, 20] 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: sudo apt-get -y install libgd-dev 24 | - name: Run npm tasks 25 | run: npm install && npm run test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .DS_Store 4 | test/fixtures/.DS_Store 5 | DS_Store 6 | test/dev/* 7 | .cache 8 | .npm 9 | .node_repl_history 10 | .bash_history 11 | .python_history 12 | .vscode -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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:18 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 -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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ### Preconditions 10 | 11 | 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). 12 | 13 | ### On Debian/Ubuntu 14 | 15 | ```bash 16 | $ sudo apt-get install libgd-dev # libgd 17 | $ npm install node-gd 18 | ``` 19 | 20 | ### On RHEL/CentOS 21 | 22 | ```bash 23 | $ sudo yum install gd-devel 24 | $ npm install node-gd 25 | ``` 26 | 27 | ### On Mac OS/X 28 | 29 | Using Homebrew 30 | 31 | ```bash 32 | $ brew install pkg-config gd 33 | $ npm install node-gd 34 | ``` 35 | 36 | ...or using MacPorts 37 | 38 | ```bash 39 | $ sudo port install pkgconfig gd2 40 | $ npm install node-gd 41 | ``` 42 | 43 | ### Will not build on Windows! 44 | 45 | Sorry, will not build on Windows. I have no Windows machine to make it work. It could work, but I just don't have the stuff at hand. 46 | 47 | ## Usage 48 | 49 | 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. 50 | 51 | ### API 52 | 53 | 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/). 54 | 55 | ### Examples 56 | 57 | Example of creating a rectangular image with a bright green background and in magenta the text "Hello world!" 58 | 59 | ```javascript 60 | // Import library 61 | import gd from 'node-gd'; 62 | 63 | // Create blank new image in memory 64 | const img = await gd.create(200, 80); 65 | 66 | // Set background color 67 | img.colorAllocate(0, 255, 0); 68 | 69 | // Set text color 70 | const txtColor = img.colorAllocate(255, 0, 255); 71 | 72 | // Set full path to font file 73 | const fontPath = '/full/path/to/font.ttf'; 74 | 75 | // Render string in image 76 | img.stringFT(txtColor, fontPath, 24, 0, 10, 60, 'Hello world!'); 77 | 78 | // Write image buffer to disk 79 | await img.savePng('output.png', 1); 80 | 81 | // Destroy image to clean memory 82 | img.destroy(); 83 | ``` 84 | 85 | Example of drawing a red lined hexagon on a black background: 86 | 87 | ```javascript 88 | import gd from 'node-gd'; 89 | 90 | const img = await gd.createTrueColor(200, 200); 91 | 92 | const points = [ 93 | { x: 100, y: 20 }, 94 | { x: 170, y: 60 }, 95 | { x: 170, y: 140 }, 96 | { x: 100, y: 180 }, 97 | { x: 30, y: 140 }, 98 | { x: 30, y: 60 }, 99 | { x: 100, y: 20 }, 100 | ]; 101 | 102 | img.setThickness(4); 103 | img.polygon(points, 0xff0000); 104 | await img.saveBmp('test1.bmp', 0); 105 | img.destroy(); 106 | ``` 107 | 108 | Another example: 109 | 110 | ```javascript 111 | import gd from 'node-gd'; 112 | 113 | const img = await gd.openFile('/path/to/file.jpg'); 114 | 115 | img.emboss(); 116 | img.brightness(75); 117 | await img.file('/path/to/newFile.bmp'); 118 | img.destroy(); 119 | ``` 120 | 121 | Some output functions are synchronous because they are handled by libgd. An example of this is the creation of animated GIFs. 122 | 123 | ## License & copyright 124 | 125 | Since [December 27th 2012](https://github.com/andris9/node-gd/commit/ad2a80897efc1926ca505b511ffdf0cc1236135a), node-gd is licensed under an MIT license. 126 | 127 | The MIT License (MIT) 128 | Copyright (c) 2010-2020 the contributors. 129 | 130 | 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: 131 | 132 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 133 | 134 | 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. 135 | 136 | ## Contributors 137 | 138 | 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. 139 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "conditions": [ 3 | ['OS!="win"', { 4 | 'variables': { 5 | 'with_avif%': ' { 858 | var frame = await gd.create(200, 200); 859 | arr[idx] = frame; 860 | frame.ellipse(100, i * 10 - 40, 100, 100, pink); 861 | var lastFrame = i === 0 ? firstFrame : arr[i - 1]; 862 | frame.gifAnimAdd(anim, 0, 0, 0, 5, 1, lastFrame); 863 | frame.destroy(); 864 | }); 865 | 866 | firstFrame.gifAnimEnd(anim); 867 | firstFrame.destroy(); 868 | ``` 869 | 870 | # Copying and resizing 871 | 872 | ### gd.Image#copy(dest, dx, dy, sx, sy, width, height) 873 | 874 | Copy an image onto a destination image: `dest`. You'll have to save the destination image to see the resulting image. This method returns _the image on which it is called_ in order to be able to chain methods. 875 | 876 | ```javascript 877 | const gd = require('node-gd'); 878 | var output = '../output/image-watermark.png'; 879 | 880 | var watermark = await gd.createFromPng('../input/watermark.png'); 881 | var input = await gd.createFromPng('../input/input.png'); 882 | 883 | watermark.alphaBlending(1); 884 | watermark.saveAlpha(1); 885 | 886 | // copy watermark onto input, i.e. onto the destination 887 | watermark.copy(input, 0, 0, 0, 0, 100, 100); 888 | 889 | // save the destination 890 | await input.savePng(output, 0); 891 | input.destroy(); 892 | watermark.destroy(); 893 | ``` 894 | 895 | Example using chaining: 896 | 897 | ```javascript 898 | const gd = require('node-gd'); 899 | var output = '../output/image-watermark.png'; 900 | var output2 = '../output/image-watermark2.png'; 901 | 902 | var watermark = await gd.createFromPng('../input/watermark.png'); 903 | var input = await gd.createFromPng('../input/input.png'); 904 | var input2 = await gd.createFromPng('../input/input2.png'); 905 | 906 | watermark 907 | .alphaBlending(1) 908 | .saveAlpha(1) 909 | .copy(input, 0, 0, 0, 0, 100, 100) 910 | .contrast(-900) 911 | .copy(input2, 0, 0, 0, 0, 100, 100); 912 | 913 | await input.savePng(output); 914 | await input2.savePng(output2, 0); 915 | ``` 916 | 917 | ### gd.Image#copyResized(dest, dx, dy, sx, sy, dw, dh, sw, sh) 918 | 919 | ### gd.Image#copyResampled(dest, dx, dy, sx, sy, dw, dh, sw, sh) 920 | 921 | ### gd.Image#copyRotated(dest, dx, dy, sx, sy, sw, sh, angle) 922 | 923 | ### gd.Image#copyMerge(dest, dx, dy, sx, sy, width, height, pct) 924 | 925 | ### gd.Image#copyMergeGray(dest, dx, dy, sx, sy, width, height pct) 926 | 927 | ### gd.Image#paletteCopy(dest) 928 | 929 | ### gd.Image#squareToCircle(radius) 930 | 931 | # Misc 932 | 933 | ### gd.Image#compare(image) 934 | 935 | Returns a bitmask of Image Comparison flags where each set flag signals which attributes of the images are different. 936 | 937 | - `1`: Actual image IS different; 938 | - `2`: Number of colors in pallette differ; 939 | - `4`: Image colors differ; 940 | - `8`: Image width differs; 941 | - `16`: Image heights differ; 942 | - `32`: Transparent color differs; 943 | - `64`: Background color differs; 944 | - `128`: Interlaced setting differs; 945 | - `256`: Truecolor vs palette differs. 946 | 947 | ### Saving graphic images 948 | 949 | The functions `gd.Image#savePng`, `gd.Image#saveJpeg`, `gd.Image#saveGif`, etc. are convenience functions which will be processed asynchronously when a callback is supplied. All of the following have a counterpart like `gd.Image#png` and `gd.Image#pngPtr` which write to disk synchronously or store the image data in a memory pointer respectively. `gd.Image#jpeg` will return the instance of `gd.Image`, `gd.Image#jpgPtr` will return the newly created image data. 950 | 951 | ```javascript 952 | const gd = require('node-gd'); 953 | gd.openPng('/path/to/input.png', async function (err, img) { 954 | if (err) { 955 | throw err; 956 | } 957 | 958 | // create jpg pointer from png 959 | var jpgImageData = img.jpegPtr(0); // jpeg quality 0 960 | // create gif pointer from png 961 | var gifImageData = img.gifPtr(); 962 | 963 | // create instance of gd.Image() for jpg file 964 | var jpgImage = gd.createFromJpegPtr(jpgImageData); 965 | await jpgImage.file('./test01.jpg'); 966 | 967 | // create instance of gd.Image() for gif file 968 | var gifImage = gd.createFromGifPtr(gifImageData); 969 | await gifImage.file('./test01.gif'); 970 | img.destroy(); 971 | }); 972 | ``` 973 | 974 | The above example shows how to create a JPEG and GIF file from a PNG file. 975 | 976 | ### gd.Image#savePng(path, level) 977 | 978 | Save image data as a PNG file. The callback will receive an error object as a parameter, only if an error occurred. When a callback is supplied, the image will be written asynchronously by `fs.writeFile()`, using `gd.Image#pngPtr()` to first write it to memory in the given format. `level` can be value between `0` and `9` and refers to a zlib compression level. A level of `-1` will let libpng12 decide what the default is. 979 | 980 | ### gd.Image#saveJpeg(path, quality) 981 | 982 | Save image data as a JPEG file. Returns Promise which resolves with true. Quality can be a `Number` between `0` and `100`. 983 | 984 | ### gd.Image#saveGif(path) 985 | 986 | Save image data as a GIF file. 987 | 988 | ### gd.Image#saveWBMP(path, foreground) 989 | 990 | Save image as a 2 color WBMP file. `foreground` is an integer value or `0x000000` value which defines the dark color of the image. All other colors will be changed into white. 991 | 992 | ### gd.Image#saveBmp(path, compression) 993 | 994 | Only available from GD version 2.1.1. The compression parameter is eiterh `0` for no compression and `1` for compression. This value only affects paletted images. 995 | 996 | ### gd.Image#saveTiff(path) 997 | 998 | As per libgd 2.2.4, opening TIFF files appears to be fixed, and saving image data as TIFF worked already fine. Therefore, `gd.openTiff()` is available again. Only available from GD version 2.1.1. 999 | 1000 | ### gd.Image#file(path) 1001 | 1002 | Lets GD decide in which format the image should be stored to disk, based on the supplied file name extension. Only available from GD version 2.1.1. Returns a Promise. 1003 | 1004 | ### Image properties 1005 | 1006 | Any instance of `gd.Image()` has a basic set of instance properties accessible as read only values. 1007 | 1008 | ```javascript 1009 | const gd = require('node-gd'); 1010 | var img = gd.creatTrueColor(100, 100); 1011 | console.log(img); 1012 | img.destroy(); 1013 | ``` 1014 | 1015 | Will yield to something like: 1016 | 1017 | ```javascript 1018 | { 1019 | colorsTotal: 0, 1020 | interlace: true, 1021 | height: 100, 1022 | width: 100, 1023 | trueColor: 1 1024 | } 1025 | ``` 1026 | 1027 | ### gd.Image#colorsTotal 1028 | 1029 | For paletted images, returns the amount of colors in the palette. 1030 | 1031 | ### gd.Image#interlace 1032 | 1033 | `Boolean` value for if the image is interlaced or not. This property can also be set. When set to `true` for Jpeg images, GD will save it as a progressive Jpeg image. 1034 | 1035 | ```javascript 1036 | const gd = require('node-gd'); 1037 | var img = await gd.createTrueColor(100, 100); 1038 | img.interlace = true; // set interlace to true 1039 | 1040 | // will save jpeg as progressive jpeg image. 1041 | await img.saveJpeg('/path/to/output.jpg', 100); 1042 | img.destroy(); 1043 | ``` 1044 | 1045 | ### gd.Image#height 1046 | 1047 | Returns the height of the image as `Number`. 1048 | 1049 | ### gd.Image#width 1050 | 1051 | Returns the width of the image as `Number`. 1052 | 1053 | ### gd.Image#trueColor 1054 | 1055 | Returns nonzero if the image is a truecolor image, zero for a palette image. 1056 | 1057 | # libgd2 version information 1058 | 1059 | Be aware that since `node-gd` version 0.3.x libgd2 version 2.1.x is mostly supported. `node-gd` version 0.2.x is backed at best with libgd2 version 2.0.x. Run `gdlib-config --version` to check the version of libgd2 on your system. `node-gd` should build successfully for both libgd2 version 2.0.x as wel as for 2.1.x. The main difference is that some functions will not be available. These include: 1060 | 1061 | - `toGrayscale()` 1062 | - `emboss()` 1063 | - `gaussianBlur()` 1064 | - `negate()` 1065 | - `brightness()` 1066 | - `contrast()` 1067 | - `selectiveBlur()` 1068 | - `openBmp()` 1069 | - `openFile()` 1070 | - `createFromBmp()` 1071 | - `createFromBmpPtr()` 1072 | - `createFromFile()` 1073 | - `saveBmp()` 1074 | - `saveTiff()` 1075 | - `saveImage()` 1076 | 1077 | Another way to check the installed GD version on your system: 1078 | 1079 | ```javascript 1080 | import gd from 'node-gd'; 1081 | 1082 | const version = gd.getGDVersion(); 1083 | console.log(version); // 2.3.1 or the like 1084 | ``` 1085 | 1086 | # Test 1087 | 1088 | ```bash 1089 | $ npm test 1090 | ``` 1091 | 1092 | The `test/output/` directory contains the resulting images of the test script. The tests use, in some cases, a GNU Freefont font, which is licensed under the GNU General Public License v3. 1093 | 1094 | # Related 1095 | 1096 | - [Original author's repo](https://github.com/taggon/node-gd) 1097 | - [node-canvas](https://github.com/LearnBoost/node-canvas) uses libcairo to emulate browser HTML5 Canvas' image manipulation abilities within Node.js 1098 | -------------------------------------------------------------------------------- /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 openFile(path: string): Promise; 52 | 53 | function createFromFile(path: string): Promise; 54 | 55 | type Color = number; 56 | 57 | function trueColor(red: number, green: number, blue: number): Color; 58 | 59 | function trueColorAlpha(red: number, green: number, blue: number, alpha: number): Color; 60 | 61 | function getGDVersion(): string; 62 | 63 | type Point = { 64 | x: number; 65 | y: number; 66 | }; 67 | 68 | // Manipulating graphic images 69 | 70 | interface Image { 71 | // Image properties 72 | 73 | readonly width: number; 74 | 75 | readonly height: number; 76 | 77 | readonly trueColor: 0 | 1; 78 | 79 | readonly colorsTotal: number; 80 | 81 | interlace: boolean; 82 | 83 | destroy(): void; 84 | 85 | // Drawing 86 | 87 | setPixel(x: number, y: number, color: Color): gd.Image; 88 | 89 | line(x1: number, y1: number, x2: number, y2: number, color: Color): gd.Image; 90 | 91 | dashedLine(x1: number, y1: number, x2: number, y2: number, color: Color): gd.Image; 92 | 93 | polygon(array: Point[], color: Color): gd.Image; 94 | 95 | openPolygon(array: Point[], color: Color): gd.Image; 96 | 97 | filledPolygon(array: Point[], color: Color): gd.Image; 98 | 99 | rectangle(x1: number, y1: number, x2: number, y2: number, color: Color): gd.Image; 100 | 101 | filledRectangle(x1: number, y1: number, x2: number, y2: number, color: Color): gd.Image; 102 | 103 | arc(cx: number, cy: number, width: number, height: number, begin: number, end: number, color: Color): gd.Image; 104 | 105 | filledArc(cx: number, cy: number, width: number, height: number, begin: number, end: number, color: Color, style?: number): gd.Image; 106 | 107 | ellipse(cx: number, cy: number, width: number, height: number, color: Color): gd.Image; 108 | 109 | filledEllipse(cx: number, cy: number, width: number, height: number, color: Color): gd.Image; 110 | 111 | fillToBorder(x: number, y: number, border: number, color: Color): gd.Image; 112 | 113 | fill(x: number, y: number, color: Color): gd.Image; 114 | 115 | setAntiAliased(color: Color): gd.Image; 116 | 117 | setAntiAliasedDontBlend(color: Color, dontblend: boolean): gd.Image; 118 | 119 | setBrush(image): gd.Image; 120 | 121 | setTile(image): gd.Image; 122 | 123 | setStyle(array): gd.Image; 124 | 125 | setThickness(thickness: number): gd.Image; 126 | 127 | alphaBlending(blending: 0 | 1): gd.Image; 128 | 129 | saveAlpha(saveFlag: 0 | 1): gd.Image; 130 | 131 | setClip(x1: number, y1: number, x2: number, y2: number): gd.Image; 132 | 133 | getClip(): { 134 | x1: number; 135 | y1: number; 136 | x2: number; 137 | y2: number; 138 | }; 139 | 140 | setResolution(res_x: number, res_y: number): gd.Image; 141 | 142 | // Query image information 143 | 144 | getPixel(x: number, y: number): Color; 145 | 146 | getTrueColorPixel(x: number, y: number): Color; 147 | 148 | imageColorAt(x: number, y: number): Color; 149 | 150 | getBoundsSafe(x: number, y: number): 0 | 1; 151 | 152 | // Font and text 153 | 154 | stringFTBBox(color: Color, font: string, size: number, angle: number, x: number, y: number, text: string): [number, number, number, number, number, number, number, number]; 155 | 156 | stringFT(color: Color, font: string, size: number, angle: number, x: number, y: number, text: string): void; 157 | 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]; 158 | 159 | // Color handling 160 | 161 | colorAllocate(r: number, g: number, b: number): Color; 162 | 163 | colorAllocateAlpha(r: number, g: number, b: number, a: number): Color; 164 | 165 | colorClosest(r: number, g: number, b: number): Color; 166 | 167 | colorClosestAlpha(r: number, g: number, b: number, a: number): Color; 168 | 169 | colorClosestHWB(r: number, g: number, b: number): Color; 170 | 171 | colorExact(r: number, g: number, b: number): Color; 172 | 173 | colorResolve(r: number, g: number, b: number): Color; 174 | 175 | colorResolveAlpha(r: number, g: number, b: number, a: number): Color; 176 | 177 | red(r: number): number; 178 | 179 | green(g: number): number; 180 | 181 | blue(b: number): number; 182 | 183 | alpha(color: Color): number; 184 | 185 | getTransparent(): number; 186 | 187 | colorDeallocate(color: Color): gd.Image; 188 | 189 | // Color Manipulation 190 | 191 | colorTransparent(color: Color): gd.Image; 192 | 193 | colorReplace(fromColor: Color, toColor: Color): number; 194 | 195 | colorReplaceThreshold(fromColor: Color, toColor: Color, threshold: number): number; 196 | 197 | colorReplaceArray(fromColors: Color[], toColors: Color[]): number; 198 | 199 | // Effects 200 | 201 | grayscale(): gd.Image; 202 | 203 | gaussianBlur(): gd.Image; 204 | 205 | negate(): gd.Image; 206 | 207 | brightness(brightness: number): gd.Image; 208 | 209 | contrast(contrast: number): gd.Image; 210 | 211 | selectiveBlur(): gd.Image; 212 | 213 | emboss(): gd.Image; 214 | 215 | flipHorizontal(): gd.Image; 216 | 217 | flipVertical(): gd.Image; 218 | 219 | flipBoth(): gd.Image; 220 | 221 | crop(x: number, y: number, width: number, height: number): gd.Image; 222 | 223 | cropAuto(mode: AutoCrop): gd.Image; 224 | 225 | cropThreshold(color: Color, threshold: number): gd.Image; 226 | 227 | sharpen(pct: number): gd.Image; 228 | 229 | createPaletteFromTrueColor(ditherFlag: 0 | 1, colorsWanted: number): gd.Image; 230 | 231 | trueColorToPalette(ditherFlag: 0 | 1, colorsWanted: number): number; 232 | 233 | paletteToTrueColor(): 0 | 1; 234 | 235 | colorMatch(image: gd.Image): number; 236 | 237 | gifAnimBegin(anim: string, useGlobalColorMap: -1 | 0 | 1, loops: number): Uint8Array; 238 | 239 | gifAnimAdd(anim: string, localColorMap: number, leftOffset: number, topOffset: number, delay: number, disposal: number, prevFrame: gd.Image | null): boolean; 240 | 241 | gifAnimEnd(anim: string): boolean; 242 | 243 | // Copying and resizing 244 | 245 | copy(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, width: number, height: number): gd.Image; 246 | 247 | copyResized(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, dw: number, dh: number, sw: number, sh: number): gd.Image; 248 | 249 | copyResampled(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, dw: number, dh: number, sw: number, sh: number): gd.Image; 250 | 251 | copyRotated(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, sw: number, sh: number, angle: number): gd.Image; 252 | 253 | copyMerge(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, width: number, height: number, pct: number): gd.Image; 254 | 255 | copyMergeGray(dest: gd.Image, dx: number, dy: number, sx: number, sy: number, width: number, height: number, pct: number): gd.Image; 256 | 257 | paletteCopy(dest: gd.Image): gd.Image; 258 | 259 | squareToCircle(radius: number): gd.Image; 260 | 261 | // Misc 262 | 263 | // Returns a bitmask of image comparsion flags 264 | // https://libgd.github.io/manuals/2.3.0/files/gd-c.html#gdImageCompare 265 | compare(image: gd.Image): number; 266 | 267 | // Saving graphic images 268 | 269 | savePng(path: string, level: number): Promise; 270 | saveJpeg(path: string, quality: number): Promise; 271 | saveGif(path: string): Promise; 272 | saveWBMP(path: string, foreground: 0x000000 | 0xffffff | number): Promise; 273 | saveBmp(path: string, compression: 0 | 1): Promise; 274 | saveTiff(path: string): Promise; 275 | 276 | png(path: string, level: number): Promise; 277 | jpeg(path: string, quality: number): Promise; 278 | gif(path: string): Promise; 279 | wbmp(path: string, foreground: 0x000000 | 0xffffff | number): Promise; 280 | bmp(path: string, compression: 0 | 1): Promise; 281 | tiff(path: string): Promise; 282 | 283 | file(path: string): Promise; 284 | } 285 | 286 | export const enum AutoCrop { 287 | DEFAULT = 0, 288 | TRANSPARENT, 289 | BLACK, 290 | WHITE, 291 | SIDES, 292 | THRESHOLD, 293 | } 294 | 295 | export const enum Cmp { 296 | IMAGE = 1, 297 | NUM_COLORS = 2, 298 | COLOR = 4, 299 | SIZE_X = 8, 300 | SIZE_Y = 16, 301 | TRANSPARENT = 32, 302 | BACKGROUND = 64, 303 | INTERLACE = 128, 304 | TRUECOLOR = 256, 305 | } 306 | } 307 | 308 | export = gd; 309 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/GifAnim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapper class around GD's Gif animation functions 3 | * Copyright (c) 2020 Vincent Bruijn 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 | -------------------------------------------------------------------------------- /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 | const libPaths = [ 17 | path.normalize(`${dirname}/../build/Release/node_gd.node`), 18 | path.normalize(`${dirname}/../build/default/node_gd.node`), 19 | ]; 20 | 21 | let bindings; 22 | 23 | try { 24 | bindings = require(libPaths.shift()); 25 | } catch (e) { 26 | console.log(e.message); 27 | try { 28 | bindings = require(libPaths.shift()); 29 | } catch (e) { 30 | console.log(e.message); 31 | console.log('Unable to find addon node_gd.node in build directory.'); 32 | process.exit(1); 33 | } 34 | } 35 | 36 | export default bindings; 37 | -------------------------------------------------------------------------------- /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 | Object.defineProperty(gd.Image.prototype, `save${format}`, { 79 | value: saveFormatFn(format), 80 | }); 81 | }); 82 | 83 | /** 84 | * Wrapper around gdImageCreateFromFile 85 | * With safety check for file existence to mitigate 86 | * uninformative segmentation faults from libgd 87 | * 88 | * @param {string} file Path of file to open 89 | * @returns {Promise} 90 | */ 91 | function openFile(file) { 92 | return new Promise((resolve, reject) => { 93 | const filePath = path.normalize(file); 94 | 95 | fs.access(filePath, fs.constants.F_OK, error => { 96 | if (error) { 97 | return reject(error); 98 | } 99 | 100 | resolve(gd.createFromFile(filePath)); 101 | }); 102 | }); 103 | } 104 | 105 | gd.openFile = openFile; 106 | 107 | gd.toString = function toString() { 108 | return '[object Gd]'; 109 | }; 110 | 111 | gd.Image.prototype.toString = function toString() { 112 | return '[object Image]'; 113 | }; 114 | 115 | export default gd; 116 | -------------------------------------------------------------------------------- /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 | ], 22 | "os": [ 23 | "!win32" 24 | ], 25 | "engines": { 26 | "node": ">=14" 27 | }, 28 | "homepage": "https://github.com/y-a-v-a/node-gd", 29 | "bugs": "https://github.com/y-a-v-a/node-gd/issues", 30 | "scripts": { 31 | "clean": "rm -rf test/output/* build/", 32 | "rebuild": "node-gyp rebuild -j max", 33 | "pretest": "node-gyp build -j max", 34 | "test": "./node_modules/.bin/mocha --reporter spec --bail --ui bdd --colors --file ./test/main.test.mjs", 35 | "install": "node-gyp rebuild -j max", 36 | "update-contributors": "git shortlog -sen | sed 's/^ /*/' > CONTRIBUTORS.md", 37 | "docker-build": "docker build --progress=plain -t yava:node-gd .", 38 | "docker-run": "docker run -it -v $PWD:/usr/src yava:node-gd" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git://github.com/y-a-v-a/node-gd.git" 43 | }, 44 | "keywords": [ 45 | "libgd", 46 | "libgd2", 47 | "gd", 48 | "image", 49 | "png", 50 | "jpg", 51 | "jpeg", 52 | "gif", 53 | "graphics", 54 | "library" 55 | ], 56 | "author": "Taegon Kim ", 57 | "license": "MIT", 58 | "contributors": [ 59 | { 60 | "name": "Dudochkin Victor", 61 | "email": "blacksmith@gogoo.ru" 62 | }, 63 | { 64 | "name": "Andris Reinman", 65 | "email": "andris@node.ee" 66 | }, 67 | { 68 | "name": "Peter Magnusson" 69 | }, 70 | { 71 | "name": "Damian Senn", 72 | "email": "damian.senn@adfinis-sygroup.ch" 73 | }, 74 | { 75 | "name": "Farrin Reid" 76 | }, 77 | { 78 | "name": "Josh (zer0x304)" 79 | }, 80 | { 81 | "name": "Mike Smullin", 82 | "email": "mike@smullindesign.com" 83 | }, 84 | { 85 | "name": "Vincent Bruijn (y_a_v_a)", 86 | "email": "vebruijn@gmail.com" 87 | } 88 | ], 89 | "gypfile": true, 90 | "readmeFilename": "README.md", 91 | "devDependencies": { 92 | "chai": "4.3.7", 93 | "mocha": "^10.2.0" 94 | }, 95 | "dependencies": { 96 | "node-addon-api": "^6.1.0", 97 | "node-gyp": "9.3.1" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | #define CHECK_IMAGE_EXISTS \ 216 | if (_isDestroyed) \ 217 | { \ 218 | Napi::Error::New(info.Env(), "Image is already destroyed") \ 219 | .ThrowAsJavaScriptException(); \ 220 | return info.Env().Undefined(); \ 221 | } \ 222 | if (_image == nullptr) \ 223 | { \ 224 | Napi::Error::New(info.Env(), "Image does not exist") \ 225 | .ThrowAsJavaScriptException(); \ 226 | return info.Env().Undefined(); \ 227 | } 228 | 229 | class Gd : public Napi::ObjectWrap 230 | { 231 | public: 232 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 233 | 234 | class Image : public Napi::ObjectWrap 235 | { 236 | public: 237 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 238 | 239 | Image(const Napi::CallbackInfo &info); 240 | ~Image(); 241 | 242 | static Napi::FunctionReference constructor; 243 | 244 | gdImagePtr getGdImagePtr() const { return _image; } 245 | 246 | private: 247 | gdImagePtr _image{nullptr}; 248 | 249 | bool _isDestroyed{true}; 250 | 251 | operator gdImagePtr() const { return _image; } 252 | 253 | /** 254 | * Destruction, Loading and Saving Functions 255 | */ 256 | Napi::Value Destroy(const Napi::CallbackInfo &info); 257 | Napi::Value Jpeg(const Napi::CallbackInfo &info); 258 | Napi::Value JpegPtr(const Napi::CallbackInfo &info); 259 | Napi::Value Gif(const Napi::CallbackInfo &info); 260 | Napi::Value GifPtr(const Napi::CallbackInfo &info); 261 | Napi::Value Png(const Napi::CallbackInfo &info); 262 | Napi::Value PngPtr(const Napi::CallbackInfo &info); 263 | Napi::Value WBMP(const Napi::CallbackInfo &info); 264 | Napi::Value WBMPPtr(const Napi::CallbackInfo &info); 265 | #if HAS_LIBWEBP 266 | Napi::Value Webp(const Napi::CallbackInfo &info); 267 | Napi::Value WebpPtr(const Napi::CallbackInfo &info); 268 | #endif 269 | Napi::Value Bmp(const Napi::CallbackInfo &info); 270 | Napi::Value BmpPtr(const Napi::CallbackInfo &info); 271 | #if HAS_LIBHEIF 272 | Napi::Value Heif(const Napi::CallbackInfo &info); 273 | Napi::Value HeifPtr(const Napi::CallbackInfo &info); 274 | #endif 275 | #if HAS_LIBAVIF 276 | Napi::Value Avif(const Napi::CallbackInfo &info); 277 | Napi::Value AvifPtr(const Napi::CallbackInfo &info); 278 | #endif 279 | 280 | #if HAS_LIBTIFF 281 | Napi::Value Tiff(const Napi::CallbackInfo &info); 282 | Napi::Value TiffPtr(const Napi::CallbackInfo &info); 283 | #endif 284 | Napi::Value File(const Napi::CallbackInfo &info); 285 | 286 | /** 287 | * Drawing Functions 288 | */ 289 | Napi::Value SetPixel(const Napi::CallbackInfo &info); 290 | Napi::Value Line(const Napi::CallbackInfo &info); 291 | Napi::Value DashedLine(const Napi::CallbackInfo &info); 292 | Napi::Value Polygon(const Napi::CallbackInfo &info); 293 | Napi::Value OpenPolygon(const Napi::CallbackInfo &info); 294 | Napi::Value FilledPolygon(const Napi::CallbackInfo &info); 295 | Napi::Value Rectangle(const Napi::CallbackInfo &info); 296 | Napi::Value FilledRectangle(const Napi::CallbackInfo &info); 297 | Napi::Value Arc(const Napi::CallbackInfo &info); 298 | Napi::Value FilledArc(const Napi::CallbackInfo &info); 299 | Napi::Value Ellipse(const Napi::CallbackInfo &info); 300 | Napi::Value FilledEllipse(const Napi::CallbackInfo &info); 301 | Napi::Value FillToBorder(const Napi::CallbackInfo &info); 302 | Napi::Value Fill(const Napi::CallbackInfo &info); 303 | Napi::Value SetAntiAliased(const Napi::CallbackInfo &info); 304 | Napi::Value SetAntiAliasedDontBlend(const Napi::CallbackInfo &info); 305 | Napi::Value SetBrush(const Napi::CallbackInfo &info); 306 | Napi::Value SetTile(const Napi::CallbackInfo &info); 307 | Napi::Value SetStyle(const Napi::CallbackInfo &info); 308 | Napi::Value SetThickness(const Napi::CallbackInfo &info); 309 | Napi::Value AlphaBlending(const Napi::CallbackInfo &info); 310 | Napi::Value SaveAlpha(const Napi::CallbackInfo &info); 311 | Napi::Value SetClip(const Napi::CallbackInfo &info); 312 | Napi::Value GetClip(const Napi::CallbackInfo &info); 313 | Napi::Value SetResolution(const Napi::CallbackInfo &info); 314 | 315 | /** 316 | * Query Functions 317 | */ 318 | Napi::Value GetPixel(const Napi::CallbackInfo &info); 319 | Napi::Value GetTrueColorPixel(const Napi::CallbackInfo &info); 320 | // This is implementation of the PHP-GD specific method imagecolorat 321 | Napi::Value ImageColorAt(const Napi::CallbackInfo &info); 322 | Napi::Value GetBoundsSafe(const Napi::CallbackInfo &info); 323 | Napi::Value WidthGetter(const Napi::CallbackInfo &info); 324 | Napi::Value HeightGetter(const Napi::CallbackInfo &info); 325 | Napi::Value ResolutionXGetter(const Napi::CallbackInfo &info); 326 | Napi::Value ResolutionYGetter(const Napi::CallbackInfo &info); 327 | Napi::Value TrueColorGetter(const Napi::CallbackInfo &info); 328 | Napi::Value InterpolationIdGetter(const Napi::CallbackInfo &info); 329 | void InterpolationIdSetter(const Napi::CallbackInfo &info, const Napi::Value &value); 330 | /** 331 | * Font and Text Handling Funcitons 332 | */ 333 | Napi::Value StringFTBBox(const Napi::CallbackInfo &info); 334 | Napi::Value StringFT(const Napi::CallbackInfo &info); 335 | Napi::Value StringFTEx(const Napi::CallbackInfo &info); 336 | Napi::Value StringFTCircle(const Napi::CallbackInfo &info); 337 | /** 338 | * Color Handling Functions 339 | */ 340 | Napi::Value ColorAllocate(const Napi::CallbackInfo &info); 341 | Napi::Value ColorAllocateAlpha(const Napi::CallbackInfo &info); 342 | Napi::Value ColorClosest(const Napi::CallbackInfo &info); 343 | Napi::Value ColorClosestAlpha(const Napi::CallbackInfo &info); 344 | Napi::Value ColorClosestHWB(const Napi::CallbackInfo &info); 345 | Napi::Value ColorExact(const Napi::CallbackInfo &info); 346 | Napi::Value ColorExactAlpha(const Napi::CallbackInfo &info); 347 | Napi::Value ColorResolve(const Napi::CallbackInfo &info); 348 | Napi::Value ColorResolveAlpha(const Napi::CallbackInfo &info); 349 | Napi::Value ColorsTotalGetter(const Napi::CallbackInfo &info); 350 | Napi::Value Red(const Napi::CallbackInfo &info); 351 | Napi::Value Blue(const Napi::CallbackInfo &info); 352 | Napi::Value Green(const Napi::CallbackInfo &info); 353 | Napi::Value Alpha(const Napi::CallbackInfo &info); 354 | Napi::Value InterlaceGetter(const Napi::CallbackInfo &info); 355 | void InterlaceSetter(const Napi::CallbackInfo &info, const Napi::Value &value); 356 | Napi::Value GetTransparent(const Napi::CallbackInfo &info); 357 | Napi::Value ColorDeallocate(const Napi::CallbackInfo &info); 358 | Napi::Value ColorTransparent(const Napi::CallbackInfo &info); 359 | Napi::Value ColorReplace(const Napi::CallbackInfo &info); 360 | Napi::Value ColorReplaceThreshold(const Napi::CallbackInfo &info); 361 | Napi::Value ColorReplaceArray(const Napi::CallbackInfo &info); 362 | Napi::Value GrayScale(const Napi::CallbackInfo &info); 363 | Napi::Value GaussianBlur(const Napi::CallbackInfo &info); 364 | Napi::Value Negate(const Napi::CallbackInfo &info); 365 | Napi::Value Brightness(const Napi::CallbackInfo &info); 366 | Napi::Value Contrast(const Napi::CallbackInfo &info); 367 | Napi::Value SelectiveBlur(const Napi::CallbackInfo &info); 368 | Napi::Value FlipHorizontal(const Napi::CallbackInfo &info); 369 | Napi::Value FlipVertical(const Napi::CallbackInfo &info); 370 | Napi::Value FlipBoth(const Napi::CallbackInfo &info); 371 | Napi::Value Crop(const Napi::CallbackInfo &info); 372 | Napi::Value CropAuto(const Napi::CallbackInfo &info); 373 | Napi::Value CropThreshold(const Napi::CallbackInfo &info); 374 | Napi::Value Emboss(const Napi::CallbackInfo &info); 375 | Napi::Value Pixelate(const Napi::CallbackInfo &info); 376 | 377 | /** 378 | * Copying and Resizing Functions 379 | */ 380 | Napi::Value Copy(const Napi::CallbackInfo &info); 381 | Napi::Value CopyResized(const Napi::CallbackInfo &info); 382 | Napi::Value CopyResampled(const Napi::CallbackInfo &info); 383 | Napi::Value CopyRotated(const Napi::CallbackInfo &info); 384 | Napi::Value CopyMerge(const Napi::CallbackInfo &info); 385 | Napi::Value CopyMergeGray(const Napi::CallbackInfo &info); 386 | Napi::Value PaletteCopy(const Napi::CallbackInfo &info); 387 | Napi::Value SquareToCircle(const Napi::CallbackInfo &info); 388 | Napi::Value Sharpen(const Napi::CallbackInfo &info); 389 | Napi::Value CreatePaletteFromTrueColor(const Napi::CallbackInfo &info); 390 | Napi::Value TrueColorToPalette(const Napi::CallbackInfo &info); 391 | Napi::Value PaletteToTrueColor(const Napi::CallbackInfo &info); 392 | Napi::Value ColorMatch(const Napi::CallbackInfo &info); 393 | Napi::Value Scale(const Napi::CallbackInfo &info); 394 | Napi::Value RotateInterpolated(const Napi::CallbackInfo &info); 395 | 396 | Napi::Value GifAnimBegin(const Napi::CallbackInfo &info); 397 | Napi::Value GifAnimAdd(const Napi::CallbackInfo &info); 398 | Napi::Value GifAnimEnd(const Napi::CallbackInfo &info); 399 | /** 400 | * Miscellaneous Functions 401 | */ 402 | Napi::Value Compare(const Napi::CallbackInfo &info); 403 | }; 404 | 405 | private: 406 | /** 407 | * Section A - Creation of new image in memory 408 | */ 409 | static Napi::Value ImageCreate(const Napi::CallbackInfo &info); 410 | static Napi::Value ImageCreateTrueColor(const Napi::CallbackInfo &info); 411 | static Napi::Value ImageCreateSync(const Napi::CallbackInfo &info); 412 | static Napi::Value ImageCreateTrueColorSync(const Napi::CallbackInfo &info); 413 | 414 | /** 415 | * Section B - Creation of image in memory from a source (either file or Buffer) 416 | */ 417 | static Napi::Value CreateFromJpeg(const Napi::CallbackInfo &info); 418 | static Napi::Value CreateFromJpegPtr(const Napi::CallbackInfo &info); 419 | static Napi::Value CreateFromPng(const Napi::CallbackInfo &info); 420 | static Napi::Value CreateFromPngPtr(const Napi::CallbackInfo &info); 421 | static Napi::Value CreateFromGif(const Napi::CallbackInfo &info); 422 | static Napi::Value CreateFromGifPtr(const Napi::CallbackInfo &info); 423 | static Napi::Value CreateFromWBMP(const Napi::CallbackInfo &info); 424 | static Napi::Value CreateFromWBMPPtr(const Napi::CallbackInfo &info); 425 | #if HAS_LIBWEBP 426 | static Napi::Value CreateFromWebp(const Napi::CallbackInfo &info); 427 | static Napi::Value CreateFromWebpPtr(const Napi::CallbackInfo &info); 428 | #endif 429 | 430 | static Napi::Value CreateFromBmp(const Napi::CallbackInfo &info); 431 | static Napi::Value CreateFromBmpPtr(const Napi::CallbackInfo &info); 432 | 433 | #if HAS_LIBHEIF 434 | static Napi::Value CreateFromHeif(const Napi::CallbackInfo &info); 435 | static Napi::Value CreateFromHeifPtr(const Napi::CallbackInfo &info); 436 | #endif 437 | #if HAS_LIBAVIF 438 | static Napi::Value CreateFromAvif(const Napi::CallbackInfo &info); 439 | static Napi::Value CreateFromAvifPtr(const Napi::CallbackInfo &info); 440 | #endif 441 | #if HAS_LIBTIFF 442 | static Napi::Value CreateFromTiff(const Napi::CallbackInfo &info); 443 | static Napi::Value CreateFromTiffPtr(const Napi::CallbackInfo &info); 444 | #endif 445 | 446 | /** 447 | * Section C - Creation of image in memory from a file, type based on file extension 448 | */ 449 | static Napi::Value CreateFromFile(const Napi::CallbackInfo &info); 450 | 451 | /** 452 | * Section D - Calculate functions 453 | */ 454 | static Napi::Value TrueColor(const Napi::CallbackInfo &info); 455 | static Napi::Value TrueColorAlpha(const Napi::CallbackInfo &info); 456 | 457 | /** 458 | * Section E - Meta information 459 | */ 460 | static Napi::Value GdVersionGetter(const Napi::CallbackInfo &info); 461 | }; 462 | 463 | #endif 464 | -------------------------------------------------------------------------------- /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_LIBAVIF 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 | gdImageAvifEx(*_gdImage, out, quality, speed); 950 | fclose(out); 951 | } 952 | 953 | int speed; 954 | 955 | private: 956 | SaveAvifWorker(napi_env env, const char *resource_name) 957 | : SaveWorker(env, resource_name) 958 | { 959 | } 960 | }; 961 | #endif 962 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | var s, t; 105 | s = source + 'input.jpg'; 106 | t = target + 'output-from-jpeg.avif'; 107 | const img = await gd.openJpeg(s); 108 | 109 | await img.saveAvif(t, -1); 110 | assert.ok(fs.existsSync(t)); 111 | img.destroy(); 112 | }); 113 | 114 | it('gd.Image#savePng() -- can open a bmp and save it as png', async function () { 115 | var s; 116 | var t; 117 | s = source + 'input.bmp'; 118 | t = target + 'output-from-bmp.png'; 119 | const img = await gd.openBmp(s); 120 | await img.savePng(t, -1); 121 | assert.ok(fs.existsSync(t)); 122 | img.destroy(); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /test/fixtures/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/4d6efa3f0694cf9e2da103440a93d1800581723b/test/fixtures/FreeSans.ttf -------------------------------------------------------------------------------- /test/fixtures/input-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/4d6efa3f0694cf9e2da103440a93d1800581723b/test/fixtures/input-transparent.png -------------------------------------------------------------------------------- /test/fixtures/input.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/4d6efa3f0694cf9e2da103440a93d1800581723b/test/fixtures/input.bmp -------------------------------------------------------------------------------- /test/fixtures/input.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/4d6efa3f0694cf9e2da103440a93d1800581723b/test/fixtures/input.jpg -------------------------------------------------------------------------------- /test/fixtures/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/4d6efa3f0694cf9e2da103440a93d1800581723b/test/fixtures/input.png -------------------------------------------------------------------------------- /test/fixtures/input.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/4d6efa3f0694cf9e2da103440a93d1800581723b/test/fixtures/input.tif -------------------------------------------------------------------------------- /test/fixtures/node-gd.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-a-v-a/node-gd/4d6efa3f0694cf9e2da103440a93d1800581723b/test/fixtures/node-gd.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | ~.gitignore 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------