├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yaml │ └── prebuild.yaml ├── .gitignore ├── CHANGELOG.md ├── Readme.md ├── benchmarks └── run.js ├── binding.gyp ├── browser.js ├── examples ├── backends.js ├── clock.js ├── crop.js ├── fill-evenodd.js ├── font.js ├── globalAlpha.js ├── gradients.js ├── grayscale-image.js ├── image-caption-overlay.js ├── image-src-svg.js ├── image-src-url.js ├── image-src.js ├── images │ ├── grayscaleImage.jpg │ ├── lime-cat.jpg │ ├── small-svg.svg │ └── squid.png ├── indexed-png-alpha.js ├── indexed-png-image-data.js ├── kraken.js ├── live-clock.js ├── multi-page-pdf.js ├── pango-glyphs.js ├── pdf-images.js ├── pdf-link.js ├── pfennigFont │ ├── FONTLOG.txt │ ├── OFL.txt │ ├── Pfennig.sfd │ ├── Pfennig.ttf │ ├── PfennigBold.sfd │ ├── PfennigBold.ttf │ ├── PfennigBoldItalic.sfd │ ├── PfennigBoldItalic.ttf │ ├── PfennigItalic.sfd │ ├── PfennigItalic.ttf │ └── pfennigMultiByte🚀.ttf ├── ray.js ├── resize.js ├── rhill-voronoi-core-min.js ├── small-pdf.js ├── small-svg.js ├── spark.js ├── state.js ├── text.js └── voronoi.js ├── index.d.ts ├── index.js ├── index.test-d.ts ├── lib ├── DOMMatrix.js ├── bindings.js ├── canvas.js ├── context2d.js ├── image.js ├── jpegstream.js ├── pattern.js ├── pdfstream.js └── pngstream.js ├── package.json ├── src ├── Backends.cc ├── Backends.h ├── Canvas.cc ├── Canvas.h ├── CanvasError.h ├── CanvasGradient.cc ├── CanvasGradient.h ├── CanvasPattern.cc ├── CanvasPattern.h ├── CanvasRenderingContext2d.cc ├── CanvasRenderingContext2d.h ├── CharData.h ├── FontParser.cc ├── FontParser.h ├── Image.cc ├── Image.h ├── ImageData.cc ├── ImageData.h ├── InstanceData.h ├── JPEGStream.h ├── PNG.h ├── Point.h ├── Util.h ├── backend │ ├── Backend.cc │ ├── Backend.h │ ├── ImageBackend.cc │ ├── ImageBackend.h │ ├── PdfBackend.cc │ ├── PdfBackend.h │ ├── SvgBackend.cc │ └── SvgBackend.h ├── bmp │ ├── BMPParser.cc │ ├── BMPParser.h │ └── LICENSE.md ├── closure.cc ├── closure.h ├── color.cc ├── color.h ├── dll_visibility.h ├── init.cc ├── register_font.cc └── register_font.h ├── test ├── canvas.test.js ├── dommatrix.test.js ├── fixtures │ ├── 159-crash1.jpg │ ├── bmp │ │ ├── 1-bit.bmp │ │ ├── 24-bit.bmp │ │ ├── 32-bit.bmp │ │ ├── 4-bit.bmp │ │ ├── bomb.bmp │ │ ├── min.bmp │ │ ├── negative-height.bmp │ │ ├── palette.bmp │ │ └── v3-header.bmp │ ├── checkers.png │ ├── chrome.jpg │ ├── clock.png │ ├── exif-orientation-f1.jpg │ ├── exif-orientation-f2.jpg │ ├── exif-orientation-f3.jpg │ ├── exif-orientation-f4.jpg │ ├── exif-orientation-f5.jpg │ ├── exif-orientation-f6.jpg │ ├── exif-orientation-f7.jpg │ ├── exif-orientation-f8.jpg │ ├── exif-orientation-fi.jpg │ ├── exif-orientation-fm.jpg │ ├── exif-orientation-fn.jpg │ ├── existing.png │ ├── face.jpeg │ ├── grayscale.jpg │ ├── halved-1.jpeg │ ├── halved-2.jpeg │ ├── newcontent.png │ ├── quadrants.png │ ├── star.png │ ├── state.png │ ├── tree.svg │ └── ycck.jpg ├── fontParser.test.js ├── image.test.js ├── imageData.test.js ├── public │ ├── app.html │ ├── app.js │ ├── style.css │ └── tests.js ├── server.js └── wpt │ ├── drawing-text-to-the-canvas.yaml │ ├── fill-and-stroke-styles.yaml │ ├── generate.js │ ├── generated │ ├── drawing-text-to-the-canvas.js │ ├── line-styles.js │ ├── meta.js │ ├── path-objects.js │ ├── pixel-manipulation.js │ ├── shadows.js │ ├── text-styles.js │ ├── the-canvas-element.js │ ├── the-canvas-state.js │ └── transformations.js │ ├── line-styles.yaml │ ├── meta.yaml │ ├── path-objects.yaml │ ├── pixel-manipulation.yaml │ ├── shadows.yaml │ ├── text-styles.yaml │ ├── the-canvas-element.yaml │ ├── the-canvas-state.yaml │ └── transformations.yaml └── util ├── has_lib.js └── win_jpeg_lookup.js /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Issue or Feature 2 | - [ ] If this is an issue with installation, I have read the [troubleshooting guide](https://github.com/Automattic/node-canvas/issues/1511). 3 | 4 | 5 | 6 | ## Steps to Reproduce 7 | 8 | ```js 9 | var Canvas = require('canvas'); 10 | var canvas = Canvas.createCanvas(200, 200); 11 | var ctx = canvas.getContext('2d'); 12 | // etc. 13 | ``` 14 | 15 | ## Your Environment 16 | * Version of node-canvas (output of `npm list canvas` or `yarn list canvas`): 17 | * Environment (e.g. node 20.9.0 on macOS 14.1.1): 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thanks for contributing! 2 | 3 | - [ ] Have you updated CHANGELOG.md? 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | paths-ignore: 5 | - ".github/workflows/prebuild.yaml" 6 | pull_request: 7 | paths-ignore: 8 | - ".github/workflows/prebuild.yaml" 9 | 10 | jobs: 11 | Linux: 12 | name: Test on Linux 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] 17 | steps: 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node }} 21 | - uses: actions/checkout@v4 22 | - name: Install Dependencies 23 | run: | 24 | sudo apt update 25 | sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev 26 | - name: Install 27 | run: npm install --build-from-source 28 | - name: Test 29 | run: npm test 30 | 31 | Windows: 32 | name: Test on Windows 33 | runs-on: windows-2019 34 | strategy: 35 | matrix: 36 | node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] 37 | steps: 38 | - uses: actions/setup-node@v4 39 | with: 40 | node-version: ${{ matrix.node }} 41 | - uses: actions/checkout@v4 42 | - name: Install Dependencies 43 | run: | 44 | Invoke-WebRequest "https://ftp.gnome.org/pub/GNOME/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" 45 | Expand-Archive gtk.zip -DestinationPath "C:\GTK" 46 | Invoke-WebRequest "https://downloads.sourceforge.net/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4-vc64.exe" -OutFile "libjpeg.exe" -UserAgent NativeHost 47 | .\libjpeg.exe /S 48 | npm install -g node-gyp@8 49 | npm prefix -g | % {npm config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"} 50 | - name: Install 51 | run: npm install --build-from-source 52 | - name: Test 53 | run: npm test 54 | 55 | macOS: 56 | name: Test on macOS 57 | runs-on: macos-15 58 | strategy: 59 | matrix: 60 | node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] 61 | steps: 62 | - uses: actions/setup-node@v4 63 | with: 64 | node-version: ${{ matrix.node }} 65 | - uses: actions/checkout@v4 66 | - name: Install Dependencies 67 | run: | 68 | brew update 69 | brew install python-setuptools pkg-config cairo pango libpng jpeg giflib librsvg 70 | - name: Install 71 | run: npm install --build-from-source 72 | - name: Test 73 | run: npm test 74 | 75 | Lint: 76 | name: Lint 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/setup-node@v4 80 | with: 81 | node-version: 20.9.0 82 | - uses: actions/checkout@v4 83 | - name: Install 84 | run: npm install --ignore-scripts 85 | - name: Lint 86 | run: npm run lint 87 | - name: Lint Types 88 | run: npm run tsd 89 | -------------------------------------------------------------------------------- /.github/workflows/prebuild.yaml: -------------------------------------------------------------------------------- 1 | # This is a dummy file so that this workflow shows up in the Actions tab. 2 | # Prebuilds are actually run using the prebuilds branch. 3 | 4 | name: Make Prebuilds 5 | on: workflow_dispatch 6 | 7 | jobs: 8 | Linux: 9 | name: Nothing 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Nothing 13 | run: echo "Nothing to do here" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .DS_Store 3 | .lock-wscript 4 | test/images/*.png 5 | examples/*.png 6 | examples/*.jpg 7 | examples/*.pdf 8 | testing 9 | out.png 10 | out.pdf 11 | out.svg 12 | .pomo 13 | node_modules 14 | package-lock.json 15 | 16 | # Vim cruft 17 | *.swp 18 | *.un~ 19 | npm-debug.log 20 | 21 | .idea 22 | -------------------------------------------------------------------------------- /benchmarks/run.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adaptive benchmarking. Starts with `initialTimes` iterations, increasing by 3 | * a power of two each time until the benchmark takes at least `minDurationMs` 4 | * milliseconds to complete. 5 | */ 6 | 7 | const { createCanvas } = require('../') 8 | const canvas = createCanvas(200, 200) 9 | const largeCanvas = createCanvas(1000, 1000) 10 | const ctx = canvas.getContext('2d') 11 | 12 | const initialTimes = 10 13 | const minDurationMs = 2000 14 | 15 | const queue = [] 16 | let running = false 17 | 18 | function bm (label, fn) { 19 | queue.push({ label: label, fn: fn }) 20 | next() 21 | } 22 | 23 | function next () { 24 | if (queue.length && !running) { 25 | run(queue.pop(), initialTimes, Date.now()) 26 | } 27 | } 28 | 29 | function run (benchmark, n, start) { 30 | running = true 31 | const originalN = n 32 | const fn = benchmark.fn 33 | 34 | if (fn.length) { // async 35 | let pending = n 36 | 37 | while (n--) { 38 | fn(function () { 39 | --pending || done(benchmark, originalN, start, true) 40 | }) 41 | } 42 | } else { 43 | while (n--) fn() 44 | done(benchmark, originalN, start) 45 | } 46 | } 47 | 48 | function done (benchmark, times, start, isAsync) { 49 | const duration = Date.now() - start 50 | 51 | if (duration < minDurationMs) { 52 | run(benchmark, times * 2, Date.now()) 53 | } else { 54 | const opsSec = times / duration * 1000 55 | if (isAsync) { 56 | console.log(' - \x1b[33m%s\x1b[0m %s ops/sec (%s times, async)', benchmark.label, opsSec.toLocaleString(), times) 57 | } else { 58 | console.log(' - \x1b[33m%s\x1b[0m %s ops/sec (%s times)', benchmark.label, opsSec.toLocaleString(), times) 59 | } 60 | running = false 61 | next() 62 | } 63 | } 64 | 65 | // node-canvas 66 | 67 | function fontName () { 68 | return String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) + 69 | String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) + 70 | String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) + 71 | String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) 72 | } 73 | 74 | bm('font setter', function () { 75 | ctx.font = `12px ${fontName()}` 76 | ctx.font = `400 6px ${fontName()}` 77 | ctx.font = `1px ${fontName()}` 78 | ctx.font = `normal normal bold 12cm ${fontName()}` 79 | ctx.font = `italic 9mm ${fontName}, "Times New Roman", "Apple Color Emoji", "Comic Sans"` 80 | ctx.font = `small-caps oblique 44px/44px ${fontName()}, "The Quick Brown", "Fox Jumped", "Over", "The", "Lazy Dog"` 81 | }) 82 | 83 | bm('save/restore', function () { 84 | for (let i = 0; i < 1000; i++) { 85 | const max = i & 15 86 | for (let j = 0; j < max; ++j) { 87 | ctx.save() 88 | } 89 | for (let j = 0; j < max; ++j) { 90 | ctx.restore() 91 | } 92 | } 93 | }) 94 | 95 | bm('fillStyle= name', function () { 96 | for (let i = 0; i < 10000; i++) { 97 | ctx.fillStyle = '#fefefe' 98 | } 99 | }) 100 | 101 | bm('lineTo()', function () { 102 | ctx.lineTo(0, 50) 103 | }) 104 | 105 | bm('arc()', function () { 106 | ctx.arc(75, 75, 50, 0, Math.PI * 2, true) 107 | }) 108 | 109 | bm('fillStyle= hex', function () { 110 | ctx.fillStyle = '#FFCCAA' 111 | }) 112 | 113 | bm('fillStyle= rgba()', function () { 114 | ctx.fillStyle = 'rgba(0,255,80,1)' 115 | }) 116 | 117 | // Apparently there's a bug in cairo by which the fillRect and strokeRect are 118 | // slow only after a ton of arcs have been drawn. 119 | bm('fillRect()', function () { 120 | ctx.fillRect(50, 50, 100, 100) 121 | }) 122 | 123 | bm('strokeRect()', function () { 124 | ctx.strokeRect(50, 50, 100, 100) 125 | }) 126 | 127 | bm('linear gradients', function () { 128 | const lingrad = ctx.createLinearGradient(0, 50, 0, 95) 129 | lingrad.addColorStop(0.5, '#000') 130 | lingrad.addColorStop(1, 'rgba(0,0,0,0)') 131 | ctx.fillStyle = lingrad 132 | ctx.fillRect(10, 10, 130, 130) 133 | }) 134 | 135 | bm('toBuffer() 200x200', function () { 136 | canvas.toBuffer() 137 | }) 138 | 139 | bm('toBuffer() 1000x1000', function () { 140 | largeCanvas.toBuffer() 141 | }) 142 | 143 | bm('toBuffer() async 200x200', function (done) { 144 | canvas.toBuffer(function (err, buf) { 145 | if (err) throw err 146 | 147 | done() 148 | }) 149 | }) 150 | 151 | bm('toBuffer() async 1000x1000', function (done) { 152 | largeCanvas.toBuffer(function (err, buf) { 153 | if (err) throw err 154 | 155 | done() 156 | }) 157 | }) 158 | 159 | bm('toBuffer().toString("base64") 200x200', function () { 160 | canvas.toBuffer().toString('base64') 161 | }) 162 | 163 | bm('toDataURL() 200x200', function () { 164 | canvas.toDataURL() 165 | }) 166 | 167 | bm('moveTo() / arc() / stroke()', function () { 168 | ctx.beginPath() 169 | ctx.arc(75, 75, 50, 0, Math.PI * 2, true) // Outer circle 170 | ctx.moveTo(110, 75) 171 | ctx.arc(75, 75, 35, 0, Math.PI, false) // Mouth 172 | ctx.moveTo(65, 65) 173 | ctx.arc(60, 65, 5, 0, Math.PI * 2, true) // Left eye 174 | ctx.moveTo(95, 65) 175 | ctx.arc(90, 65, 5, 0, Math.PI * 2, true) // Right eye 176 | ctx.stroke() 177 | }) 178 | 179 | bm('createImageData(300,300)', function () { 180 | ctx.createImageData(300, 300) 181 | }) 182 | 183 | bm('getImageData(0,0,100,100)', function () { 184 | ctx.getImageData(0, 0, 100, 100) 185 | }) 186 | 187 | bm('PNGStream 200x200', function (done) { 188 | const stream = canvas.createPNGStream() 189 | stream.on('data', function (chunk) { 190 | // whatever 191 | }) 192 | stream.on('end', function () { 193 | done() 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'conditions': [ 3 | ['OS=="win"', { 4 | 'variables': { 5 | 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle 6 | 'with_jpeg%': 'false', 7 | 'with_gif%': 'false', 8 | 'with_rsvg%': 'false', 9 | 'variables': { # Nest jpeg_root to evaluate it before with_jpeg 10 | 'jpeg_root%': ' Font Information and copy the Family Name 13 | Canvas.registerFont(fontFile('Pfennig.ttf'), { family: 'pfennigFont' }) 14 | Canvas.registerFont(fontFile('PfennigBold.ttf'), { family: 'pfennigFont', weight: 'bold' }) 15 | Canvas.registerFont(fontFile('PfennigItalic.ttf'), { family: 'pfennigFont', style: 'italic' }) 16 | Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), { family: 'pfennigFont', weight: 'bold', style: 'italic' }) 17 | 18 | const canvas = Canvas.createCanvas(320, 320) 19 | const ctx = canvas.getContext('2d') 20 | 21 | ctx.font = 'normal normal 50px Helvetica' 22 | 23 | ctx.fillText('Quo Vaids?', 0, 70) 24 | 25 | ctx.font = 'bold 50px pfennigFont' 26 | ctx.fillText('Quo Vaids?', 0, 140, 100) 27 | 28 | ctx.font = 'italic 50px pfennigFont' 29 | ctx.fillText('Quo Vaids?', 0, 210) 30 | 31 | ctx.font = 'bold italic 50px pfennigFont' 32 | ctx.fillText('Quo Vaids?', 0, 280) 33 | 34 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'font.png'))) 35 | -------------------------------------------------------------------------------- /examples/globalAlpha.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const canvas = Canvas.createCanvas(150, 150) 6 | const ctx = canvas.getContext('2d') 7 | 8 | ctx.fillStyle = '#FD0' 9 | ctx.fillRect(0, 0, 75, 75) 10 | 11 | ctx.fillStyle = '#6C0' 12 | ctx.fillRect(75, 0, 75, 75) 13 | 14 | ctx.fillStyle = '#09F)' 15 | ctx.fillRect(0, 75, 75, 75) 16 | 17 | ctx.fillStyle = '#F30' 18 | ctx.fillRect(75, 75, 150, 150) 19 | 20 | ctx.fillStyle = '#FFF' 21 | 22 | // set transparency value 23 | ctx.globalAlpha = 0.2 24 | 25 | // Draw semi transparent circles 26 | for (let i = 0; i < 7; i++) { 27 | ctx.beginPath() 28 | ctx.arc(75, 75, 10 + 10 * i, 0, Math.PI * 2, true) 29 | ctx.fill() 30 | } 31 | 32 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'globalAlpha.png'))) 33 | -------------------------------------------------------------------------------- /examples/gradients.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const canvas = Canvas.createCanvas(320, 320) 6 | const ctx = canvas.getContext('2d') 7 | 8 | // Create gradients 9 | const lingrad = ctx.createLinearGradient(0, 0, 0, 150) 10 | lingrad.addColorStop(0, '#00ABEB') 11 | lingrad.addColorStop(0.5, '#fff') 12 | lingrad.addColorStop(0.5, '#26C000') 13 | lingrad.addColorStop(1, '#fff') 14 | 15 | const lingrad2 = ctx.createLinearGradient(0, 50, 0, 95) 16 | lingrad2.addColorStop(0.5, '#000') 17 | lingrad2.addColorStop(1, 'rgba(0,0,0,0)') 18 | 19 | // assign gradients to fill and stroke styles 20 | ctx.fillStyle = lingrad 21 | ctx.strokeStyle = lingrad2 22 | 23 | // draw shapes 24 | ctx.fillRect(10, 10, 130, 130) 25 | ctx.strokeRect(50, 50, 50, 50) 26 | 27 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'gradients.png'))) 28 | -------------------------------------------------------------------------------- /examples/grayscale-image.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const Image = Canvas.Image 6 | const canvas = Canvas.createCanvas(288, 288) 7 | const ctx = canvas.getContext('2d') 8 | 9 | const img = new Image() 10 | img.onload = () => { 11 | ctx.drawImage(img, 0, 0) 12 | canvas.createJPEGStream().pipe(fs.createWriteStream(path.join(__dirname, 'passedThroughGrayscale.jpg'))) 13 | } 14 | img.onerror = err => { 15 | throw err 16 | } 17 | 18 | img.src = path.join(__dirname, 'images', 'grayscaleImage.jpg') 19 | -------------------------------------------------------------------------------- /examples/image-caption-overlay.js: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import pify from 'pify' 3 | import imageSizeOf from 'image-size' 4 | import { createCanvas, loadImage, Image } from 'canvas' 5 | 6 | const imageSizeOfP = pify(imageSizeOf) 7 | 8 | function createImageFromBuffer (buffer) { 9 | const image = new Image() 10 | image.src = buffer 11 | 12 | return image 13 | } 14 | 15 | function createCaptionOverlay ({ 16 | text, 17 | width, 18 | height, 19 | font = 'Arial', 20 | fontSize = 48, 21 | captionHeight = 120, 22 | decorateCaptionTextFillStyle = null, 23 | decorateCaptionFillStyle = null, 24 | offsetX = 0, 25 | offsetY = 0 26 | }) { 27 | const canvas = createCanvas(width, height) 28 | const ctx = canvas.getContext('2d') 29 | 30 | const createGradient = (first, second) => { 31 | const grd = ctx.createLinearGradient(width, captionY, width, height) 32 | grd.addColorStop(0, first) 33 | grd.addColorStop(1, second) 34 | 35 | return grd 36 | } 37 | 38 | // Hold computed caption position 39 | const captionX = offsetX 40 | const captionY = offsetY + height - captionHeight 41 | const captionTextX = captionX + (width / 2) 42 | const captionTextY = captionY + (captionHeight / 2) 43 | 44 | // Fill caption rect 45 | ctx.fillStyle = decorateCaptionFillStyle 46 | ? decorateCaptionFillStyle(ctx) 47 | : createGradient('rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0.45)') 48 | ctx.fillRect(captionX, captionY, width, captionHeight) 49 | 50 | // Fill caption text 51 | ctx.textBaseline = 'middle' 52 | ctx.textAlign = 'center' 53 | ctx.font = `${fontSize}px ${font}` 54 | ctx.fillStyle = decorateCaptionTextFillStyle 55 | ? decorateCaptionTextFillStyle(ctx) 56 | : 'white' 57 | ctx.fillText(text, captionTextX, captionTextY) 58 | 59 | return createImageFromBuffer(canvas.toBuffer()) 60 | } 61 | 62 | (async () => { 63 | try { 64 | const source = 'images/lime-cat.jpg' 65 | const { width, height } = await imageSizeOfP(source) 66 | const canvas = createCanvas(width, height) 67 | const ctx = canvas.getContext('2d') 68 | 69 | // Draw base image 70 | const image = await loadImage(source) 71 | ctx.drawImage(image, 0, 0) 72 | 73 | // Draw caption overlay 74 | const overlay = await createCaptionOverlay({ 75 | text: 'Hello!', 76 | width, 77 | height 78 | }) 79 | ctx.drawImage(overlay, 0, 0) 80 | 81 | // Output to `.png` file 82 | canvas.createPNGStream().pipe(createWriteStream('foo.png')) 83 | } catch (err) { 84 | console.log(err) 85 | process.exit(1) 86 | } 87 | })() 88 | -------------------------------------------------------------------------------- /examples/image-src-svg.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const canvas = Canvas.createCanvas(500, 500) 6 | const ctx = canvas.getContext('2d') 7 | ctx.fillStyle = 'white' 8 | ctx.fillRect(0, 0, 500, 500) 9 | 10 | Canvas.loadImage(path.join(__dirname, 'images', 'small-svg.svg')) 11 | .then(image => { 12 | image.width *= 1.5 13 | image.height *= 1.5 14 | ctx.drawImage(image, canvas.width / 2 - image.width / 2, canvas.height / 2 - image.height / 2) 15 | 16 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'image-src-svg.png'))) 17 | }) 18 | .catch(e => console.error(e)) 19 | -------------------------------------------------------------------------------- /examples/image-src-url.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const Image = Canvas.Image 6 | const canvas = Canvas.createCanvas(200, 300) 7 | const ctx = canvas.getContext('2d') 8 | 9 | const img = new Image() 10 | img.onload = () => { 11 | ctx.drawImage(img, 0, 0) 12 | canvas.createPNGStream() 13 | .pipe(fs.createWriteStream(path.join(__dirname, 'image-src-url.png'))) 14 | } 15 | img.onerror = err => { 16 | console.log(err) 17 | } 18 | img.src = 'http://picsum.photos/200/300' 19 | -------------------------------------------------------------------------------- /examples/image-src.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const Image = Canvas.Image 6 | const canvas = Canvas.createCanvas(200, 200) 7 | const ctx = canvas.getContext('2d') 8 | 9 | ctx.fillRect(0, 0, 150, 150) 10 | ctx.save() 11 | 12 | ctx.fillStyle = '#09F' 13 | ctx.fillRect(15, 15, 120, 120) 14 | 15 | ctx.save() 16 | ctx.fillStyle = '#FFF' 17 | ctx.globalAlpha = 0.5 18 | ctx.fillRect(30, 30, 90, 90) 19 | 20 | ctx.restore() 21 | ctx.fillRect(45, 45, 60, 60) 22 | 23 | ctx.restore() 24 | ctx.fillRect(60, 60, 30, 30) 25 | 26 | const img = new Image() 27 | img.onerror = err => { throw err } 28 | img.onload = () => { 29 | img.src = canvas.toBuffer() 30 | ctx.drawImage(img, 0, 0, 50, 50) 31 | ctx.drawImage(img, 50, 0, 50, 50) 32 | ctx.drawImage(img, 100, 0, 50, 50) 33 | 34 | const img2 = new Image() 35 | img2.onload = () => { 36 | ctx.drawImage(img2, 30, 50, img2.width / 4, img2.height / 4) 37 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'image-src.png'))) 38 | } 39 | img2.onerror = err => { throw err } 40 | img2.src = path.join(__dirname, 'images', 'squid.png') 41 | } 42 | -------------------------------------------------------------------------------- /examples/images/grayscaleImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/examples/images/grayscaleImage.jpg -------------------------------------------------------------------------------- /examples/images/lime-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/examples/images/lime-cat.jpg -------------------------------------------------------------------------------- /examples/images/small-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/images/squid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/examples/images/squid.png -------------------------------------------------------------------------------- /examples/indexed-png-alpha.js: -------------------------------------------------------------------------------- 1 | const Canvas = require('..') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const canvas = Canvas.createCanvas(200, 200) 5 | const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) 6 | 7 | // Matches the "fillStyle" browser test, made by using alpha fillStyle value 8 | const palette = new Uint8ClampedArray(37 * 4) 9 | let i, j 10 | let k = 0 11 | // First value is opaque white: 12 | palette[k++] = 255 13 | palette[k++] = 255 14 | palette[k++] = 255 15 | palette[k++] = 255 16 | for (i = 0; i < 6; i++) { 17 | for (j = 0; j < 6; j++) { 18 | palette[k++] = Math.floor(255 - 42.5 * i) 19 | palette[k++] = Math.floor(255 - 42.5 * j) 20 | palette[k++] = 0 21 | palette[k++] = 255 22 | } 23 | } 24 | for (i = 0; i < 6; i++) { 25 | for (j = 0; j < 6; j++) { 26 | const index = i * 6 + j + 1.5 // 0.5 to bias rounding 27 | const fraction = index / 255 28 | ctx.fillStyle = 'rgba(0,0,0,' + fraction + ')' 29 | ctx.fillRect(j * 25, i * 25, 25, 25) 30 | } 31 | } 32 | 33 | canvas.createPNGStream({ palette: palette }) 34 | .pipe(fs.createWriteStream(path.join(__dirname, 'indexed2.png'))) 35 | -------------------------------------------------------------------------------- /examples/indexed-png-image-data.js: -------------------------------------------------------------------------------- 1 | const Canvas = require('..') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const canvas = Canvas.createCanvas(200, 200) 5 | const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) 6 | 7 | // Matches the "fillStyle" browser test, made by manipulating imageData 8 | const palette = new Uint8ClampedArray(37 * 4) 9 | let k = 0 10 | let i, j 11 | // First value is opaque white: 12 | palette[k++] = 255 13 | palette[k++] = 255 14 | palette[k++] = 255 15 | palette[k++] = 255 16 | for (i = 0; i < 6; i++) { 17 | for (j = 0; j < 6; j++) { 18 | palette[k++] = Math.floor(255 - 42.5 * i) 19 | palette[k++] = Math.floor(255 - 42.5 * j) 20 | palette[k++] = 0 21 | palette[k++] = 255 22 | } 23 | } 24 | const idata = ctx.getImageData(0, 0, 200, 200) 25 | for (i = 0; i < 6; i++) { 26 | for (j = 0; j < 6; j++) { 27 | const index = j * 6 + i 28 | // fill rect: 29 | for (let xr = j * 25; xr < j * 25 + 25; xr++) { 30 | for (let yr = i * 25; yr < i * 25 + 25; yr++) { 31 | idata.data[xr * 200 + yr] = index + 1 32 | } 33 | } 34 | } 35 | } 36 | ctx.putImageData(idata, 0, 0) 37 | 38 | canvas.createPNGStream({ palette: palette }) 39 | .pipe(fs.createWriteStream(path.join(__dirname, 'indexed.png'))) 40 | -------------------------------------------------------------------------------- /examples/kraken.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const Image = Canvas.Image 6 | const canvas = Canvas.createCanvas(400, 267) 7 | const ctx = canvas.getContext('2d') 8 | 9 | const img = new Image() 10 | 11 | img.onload = function () { 12 | ctx.drawImage(img, 0, 0) 13 | } 14 | 15 | img.onerror = err => { throw err } 16 | 17 | img.src = path.join(__dirname, 'images', 'squid.png') 18 | 19 | const sigma = 10 // radius 20 | let kernel, kernelSize, kernelSum 21 | 22 | function buildKernel () { 23 | let i, j, g 24 | const ss = sigma * sigma 25 | const factor = 2 * Math.PI * ss 26 | 27 | kernel = [[]] 28 | 29 | i = 0 30 | do { 31 | g = Math.exp(-(i * i) / (2 * ss)) / factor 32 | if (g < 1e-3) break 33 | kernel[0].push(g) 34 | ++i 35 | } while (i < 7) 36 | 37 | kernelSize = i 38 | for (j = 1; j < kernelSize; ++j) { 39 | kernel.push([]) 40 | for (i = 0; i < kernelSize; ++i) { 41 | g = Math.exp(-(i * i + j * j) / (2 * ss)) / factor 42 | kernel[j].push(g) 43 | } 44 | } 45 | 46 | kernelSum = 0 47 | for (j = 1 - kernelSize; j < kernelSize; ++j) { 48 | for (i = 1 - kernelSize; i < kernelSize; ++i) { 49 | kernelSum += kernel[Math.abs(j)][Math.abs(i)] 50 | } 51 | } 52 | } 53 | 54 | function blurTest () { 55 | let x, y, i, j 56 | let r, g, b, a 57 | 58 | console.log('... running') 59 | 60 | const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height) 61 | const data = imgData.data 62 | const width = imgData.width 63 | const height = imgData.height 64 | 65 | const startTime = (new Date()).getTime() 66 | 67 | for (y = 0; y < height; ++y) { 68 | for (x = 0; x < width; ++x) { 69 | r = 0 70 | g = 0 71 | b = 0 72 | a = 0 73 | 74 | for (j = 1 - kernelSize; j < kernelSize; ++j) { 75 | if (y + j < 0 || y + j >= height) continue 76 | 77 | for (i = 1 - kernelSize; i < kernelSize; ++i) { 78 | if (x + i < 0 || x + i >= width) continue 79 | 80 | r += data[4 * ((y + j) * width + (x + i)) + 0] * kernel[Math.abs(j)][Math.abs(i)] 81 | g += data[4 * ((y + j) * width + (x + i)) + 1] * kernel[Math.abs(j)][Math.abs(i)] 82 | b += data[4 * ((y + j) * width + (x + i)) + 2] * kernel[Math.abs(j)][Math.abs(i)] 83 | a += data[4 * ((y + j) * width + (x + i)) + 3] * kernel[Math.abs(j)][Math.abs(i)] 84 | } 85 | } 86 | 87 | data[4 * (y * width + x) + 0] = r / kernelSum 88 | data[4 * (y * width + x) + 1] = g / kernelSum 89 | data[4 * (y * width + x) + 2] = b / kernelSum 90 | data[4 * (y * width + x) + 3] = a / kernelSum 91 | } 92 | } 93 | 94 | const finishTime = Date.now() - startTime 95 | for (i = 0; i < data.length; i++) { 96 | imgData.data[i] = data[i] 97 | } 98 | 99 | ctx.putImageData(imgData, 0, 0) 100 | console.log('... finished in %dms', finishTime) 101 | } 102 | 103 | buildKernel() 104 | blurTest() 105 | 106 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'kraken.png'))) 107 | -------------------------------------------------------------------------------- /examples/live-clock.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const Canvas = require('..') 3 | 4 | const clock = require('./clock') 5 | 6 | const canvas = Canvas.createCanvas(320, 320) 7 | const ctx = canvas.getContext('2d') 8 | 9 | http.createServer(function (req, res) { 10 | clock(ctx) 11 | 12 | res.writeHead(200, { 'Content-Type': 'text/html' }) 13 | res.end( 14 | '' + 15 | '' 16 | ) 17 | }).listen(3000, function () { 18 | console.log('Server started on port 3000') 19 | }) 20 | -------------------------------------------------------------------------------- /examples/multi-page-pdf.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const Canvas = require('..') 3 | 4 | const canvas = Canvas.createCanvas(500, 500, 'pdf') 5 | const ctx = canvas.getContext('2d') 6 | 7 | let x, y 8 | 9 | function reset () { 10 | x = 50 11 | y = 80 12 | } 13 | 14 | function h1 (str) { 15 | ctx.font = '22px Helvetica' 16 | ctx.fillText(str, x, y) 17 | } 18 | 19 | function p (str) { 20 | ctx.font = '10px Arial' 21 | ctx.fillText(str, x, (y += 20)) 22 | } 23 | 24 | reset() 25 | h1('PDF demo') 26 | p('Multi-page PDF demonstration') 27 | ctx.addPage() 28 | 29 | reset() 30 | h1('Page #2') 31 | p('This is the second page') 32 | ctx.addPage(250, 250) // create a page with half the size of the canvas 33 | 34 | reset() 35 | h1('Page #3') 36 | p('This is the third page') 37 | 38 | fs.writeFile('out.pdf', canvas.toBuffer(), function (err) { 39 | if (err) throw err 40 | 41 | console.log('created out.pdf') 42 | }) 43 | -------------------------------------------------------------------------------- /examples/pango-glyphs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const canvas = Canvas.createCanvas(400, 100) 6 | const ctx = canvas.getContext('2d') 7 | 8 | ctx.globalAlpha = 1 9 | ctx.font = 'normal 16px Impact' 10 | 11 | ctx.textBaseline = 'top' 12 | 13 | // Note this demo depends node-canvas being installed with pango support, 14 | // and your system having installed fonts supporting the glyphs. 15 | 16 | ctx.fillStyle = '#000' 17 | ctx.fillText('English: Some text in Impact.', 10, 10) 18 | ctx.fillText('Japanese: 図書館の中では、静かにする。', 10, 30) 19 | ctx.fillText('Arabic: اللغة العربية هي أكثر اللغات تحدثا ضمن', 10, 50) 20 | ctx.fillText('Korean: 모타는사라미 못하는 사람이', 10, 70) 21 | 22 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'pango-glyphs.png'))) 23 | -------------------------------------------------------------------------------- /examples/pdf-images.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { Image, createCanvas } = require('..') 3 | 4 | const canvas = createCanvas(500, 500, 'pdf') 5 | const ctx = canvas.getContext('2d') 6 | 7 | let x, y 8 | 9 | function reset () { 10 | x = 50 11 | y = 80 12 | } 13 | 14 | function h1 (str) { 15 | ctx.font = '22px Helvetica' 16 | ctx.fillText(str, x, y) 17 | } 18 | 19 | function p (str) { 20 | ctx.font = '10px Arial' 21 | ctx.fillText(str, x, (y += 20)) 22 | } 23 | 24 | function img (src) { 25 | const img = new Image() 26 | img.src = src 27 | ctx.drawImage(img, x, (y += 20)) 28 | y += img.height 29 | } 30 | 31 | reset() 32 | h1('PDF image demo') 33 | p('This is an image embedded in a PDF') 34 | img('examples/images/squid.png') 35 | p('Figure 1.0 - Some squid thing') 36 | ctx.addPage() 37 | 38 | reset() 39 | h1('Lime cat') 40 | p('This is a pretty sweet cat') 41 | img('examples/images/lime-cat.jpg') 42 | p('Figure 1.1 - Lime cat is awesome') 43 | ctx.addPage() 44 | 45 | const buff = canvas.toBuffer('application/pdf', { 46 | title: 'Squid and Cat!', 47 | author: 'Octocat', 48 | subject: 'An example PDF made with node-canvas', 49 | keywords: 'node.js squid cat lime', 50 | creator: 'my app', 51 | modDate: new Date() 52 | }) 53 | 54 | fs.writeFile('out.pdf', buff, function (err) { 55 | if (err) throw err 56 | 57 | console.log('created out.pdf') 58 | }) 59 | -------------------------------------------------------------------------------- /examples/pdf-link.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const canvas = Canvas.createCanvas(400, 300, 'pdf') 6 | const ctx = canvas.getContext('2d') 7 | 8 | ctx.beginTag('Link', 'uri=\'https://google.com\'') 9 | ctx.font = '22px Helvetica' 10 | ctx.fillText('Text link to Google', 110, 50) 11 | ctx.endTag('Link') 12 | 13 | ctx.fillText('Rect link to node-canvas below!', 40, 180) 14 | 15 | ctx.beginTag('Link', 'uri=\'https://github.com/Automattic/node-canvas\' rect=[0 200 400 100]') 16 | ctx.endTag('Link') 17 | 18 | fs.writeFile(path.join(__dirname, 'pdf-link.pdf'), canvas.toBuffer(), function (err) { 19 | if (err) throw err 20 | }) 21 | -------------------------------------------------------------------------------- /examples/pfennigFont/FONTLOG.txt: -------------------------------------------------------------------------------- 1 | FONTLOG 2 | Pfennig font family 3 | ========================== 4 | 5 | This file provides detailed information on the Pfennig family of fonts. 6 | This information should be distributed along with the Pfennig fonts and 7 | any derivative works. 8 | 9 | 10 | Basic Font Information 11 | ---------------------- 12 | 13 | Pfennig is a sans-serif font with support for Latin, Cyrillic, Greek and Hebrew 14 | character sets. It contains sufficient characters for Latin-0 through Latin-10, 15 | as well as all modern Cyrillic scripts, the full Vietnamese range, modern Greek, 16 | modern Hebrew, and the Pan-African Alphabet. It supports the standard Roman 17 | ligatures and uses OpenType tables for diacritic placement. 18 | 19 | Pfennig supports the following Unicode ranges: 20 | 21 | Range Description Coverage 22 | .............................................. 23 | U+0020-U+007F Basic Latin Full 24 | U+00A0-U+00FF Latin-1 Supplement Full 25 | U+0100-U+017F Latin Extended-A Full 26 | U+0180-U+024F Latin Extended-B 146/208 27 | U+0250-U+02AF IPA Extensions 32/96 28 | U+02B0-U+02FF Spacing Modifiers 18/80 29 | U+0300-U+036F Combining Diacritics 34/112 30 | U+0370-U+03FF Greek 74/134 31 | U+0400-U+04FF Cyrillic 214/256 32 | U+0500-U+052F Cyrillic Supplement 14/44 33 | U+0590-U+05FF Hebrew 27/87 34 | U+1DC0-U+1DFF Comb. Diacritic Supp. 4/43 35 | U+1E00-U+1EFF Latin Extended Add'l 173/256 36 | U+2000-U+206F General Punctuation 19/107 37 | U+2070-U+209F Super/Subscripts 1/42 38 | U+20A0-U+20CF Currency Symbols 1/26 (Euro sign only) 39 | U+2100-U+214F Letterlike Symbols 2/80 40 | U+2200-U+22FF Mathematical Operators 2/256 41 | U+25A0-U+25FF Geometric Shapes 1/96 (Dotted circle only) 42 | U+2C60-U+2C7F Latin Extended-C 5/32 43 | U+A720-U+A7FF Latin Extended-D 5/129 44 | U+FB00-U+FB06 Latin Ligatures 5/7 (all except archaic ligatures) 45 | 46 | ChangeLog 47 | --------- 48 | 49 | 2012-04-10 Added Cyrillic glyphs for Orok, Khanty, Nenets (in Unicode 50 | pipeline) 51 | 2012-04-07 Improved AE ligature and U+A78D; added a few glyphs with 52 | diacritics. 53 | 2011-09-24 Added a few African Latin glyphs; improved Cyrillic breve; major 54 | spacing improvements in italics; improved TTF hints. 55 | 2010-08-31 Further refinements of Vietnamese range in all faces. 56 | 2010-08-04 Added several obscure African letters. Corrected some stacked 57 | diacritics. Corrected proposed codepoint for H with hook. 58 | 2010-06-23 Added modern Hebrew and Greek ranges 59 | 2010-06-17 Added all anchors needed for diacritic attachment for 60 | Pan-African to upright fonts; italic fonts are by nature unsuitable 61 | for Pan-African due to stylistic clashes (e.g. between a and alpha). 62 | Improved lowercase thorn in all fonts. Added dropped umlaut on A, O 63 | and U in upright fonts, accessible as ss01. 64 | 2010-06-04 Finished up requirements for Pan-African Alphabet; 65 | improved Vietnamese italic & bold-italic 66 | 2010-04-23 Completed support for all Cyrillic codepoints for modern 67 | orthographies. 68 | 2010-04-14 More glyphs: African, modern Pinyin, Amerindian 69 | 2010-04-12 Moved non-Unicode glyphs to PUA; added a few more African 70 | glyphs. 71 | 2010-04-06 Diacritic improvement in Bold Vietnamese. Additional glyphs 72 | for Skolt Sami, the Pan-Nigerian Alphabet, and various other 73 | African languages. 74 | 2010-03-31 Further spacing enhancements in non-italic. Improvements in 75 | Vietnamese range in Medium to prevent excessive diacritic 76 | height. Spacing improvements in italic ligatures. 77 | 2009-09-18 Major overhaul of spacing in non-italic 78 | 2009-08-06 Added Vietnamese range. 79 | 2009-07-30 Kerned Latin ranges. 80 | 2009-07-29 Added Cyrillic range. 81 | 2009-07-24 Initial release 82 | 83 | 84 | Information for Contributors 85 | ---------------------------- 86 | 87 | This font is licensed under the Open Font License (OFL). There is no Reserved 88 | Name clause for the Pfennig font, enabling the free conversion between font 89 | formats. 90 | 91 | You can read more about the OFL here: 92 | http://scripts.sil.org/OFL 93 | 94 | If you'd like to make changes to the original font, you are free to contact 95 | the author of the original font (for contact information, please see the 96 | "Contributors" section below). Glyph changes should be in a FontForge .sfd 97 | file (please make sure your version of FontForge is reasonably up-to-date). 98 | Please send *only* the changed glyphs, not the entire font range. The author 99 | reserves the right to reject or modify any contributions. If your contribution 100 | is accepted, your name will appear in the Contributors section (unless you 101 | specify otherwise). 102 | 103 | 104 | Contributors 105 | ------------ 106 | 107 | Daniel Johnson (font maintainer) 108 | il.basso.buffo at gmail dot com 109 | -------------------------------------------------------------------------------- /examples/pfennigFont/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 - 2012 Daniel Johnson (). 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /examples/pfennigFont/Pfennig.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/examples/pfennigFont/Pfennig.ttf -------------------------------------------------------------------------------- /examples/pfennigFont/PfennigBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/examples/pfennigFont/PfennigBold.ttf -------------------------------------------------------------------------------- /examples/pfennigFont/PfennigBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/examples/pfennigFont/PfennigBoldItalic.ttf -------------------------------------------------------------------------------- /examples/pfennigFont/PfennigItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/examples/pfennigFont/PfennigItalic.ttf -------------------------------------------------------------------------------- /examples/pfennigFont/pfennigMultiByte🚀.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/examples/pfennigFont/pfennigMultiByte🚀.ttf -------------------------------------------------------------------------------- /examples/ray.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const canvas = Canvas.createCanvas(243 * 4, 243) 6 | const ctx = canvas.getContext('2d') 7 | 8 | function render (level) { 9 | ctx.fillStyle = getPointColour(122, 122) 10 | ctx.fillRect(0, 0, 240, 240) 11 | renderLevel(level, 81, 0) 12 | } 13 | 14 | function renderLevel (minimumLevel, level, y) { 15 | let x 16 | 17 | for (x = 0; x < 243 / level; ++x) { 18 | drawBlock(x, y, level) 19 | } 20 | for (x = 0; x < 243 / level; x += 3) { 21 | drawBlock(x, y + 1, level) 22 | drawBlock(x + 2, y + 1, level) 23 | } 24 | for (x = 0; x < 243 / level; ++x) { 25 | drawBlock(x, y + 2, level) 26 | } 27 | if ((y += 3) >= 243 / level) { 28 | y = 0 29 | level /= 3 30 | } 31 | if (level >= minimumLevel) { 32 | renderLevel(minimumLevel, level, y) 33 | } 34 | } 35 | 36 | function drawBlock (x, y, level) { 37 | ctx.fillStyle = getPointColour( 38 | x * level + (level - 1) / 2, 39 | y * level + (level - 1) / 2 40 | ) 41 | 42 | ctx.fillRect( 43 | x * level, 44 | y * level, 45 | level, 46 | level 47 | ) 48 | } 49 | 50 | function getPointColour (x, y) { 51 | x = x / 121.5 - 1 52 | y = -y / 121.5 + 1 53 | 54 | const x2y2 = x * x + y * y 55 | if (x2y2 > 1) { 56 | return '#000' 57 | } 58 | 59 | const root = Math.sqrt(1 - x2y2) 60 | const x3d = x * 0.7071067812 + root / 2 - y / 2 61 | const y3d = x * 0.7071067812 - root / 2 + y / 2 62 | const z3d = 0.7071067812 * root + 0.7071067812 * y 63 | let brightness = -x / 2 + root * 0.7071067812 + y / 2 64 | if (brightness < 0) brightness = 0 65 | 66 | const r = Math.round(brightness * 127.5 * (1 - y3d)) 67 | const g = Math.round(brightness * 127.5 * (x3d + 1)) 68 | const b = Math.round(brightness * 127.5 * (z3d + 1)) 69 | 70 | return 'rgb(' + r + ', ' + g + ', ' + b + ')' 71 | } 72 | 73 | const start = new Date() 74 | 75 | render(10) 76 | ctx.translate(243, 0) 77 | render(6) 78 | ctx.translate(243, 0) 79 | render(3) 80 | ctx.translate(243, 0) 81 | render(1) 82 | 83 | console.log('Rendered in %s seconds', (new Date() - start) / 1000) 84 | 85 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'ray.png'))) 86 | -------------------------------------------------------------------------------- /examples/resize.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const Image = Canvas.Image 6 | 7 | const img = new Image() 8 | const start = new Date() 9 | 10 | img.onerror = function (err) { 11 | throw err 12 | } 13 | 14 | img.onload = function () { 15 | const width = 100 16 | const height = 100 17 | const canvas = Canvas.createCanvas(width, height) 18 | const ctx = canvas.getContext('2d') 19 | const out = fs.createWriteStream(path.join(__dirname, 'resize.png')) 20 | 21 | ctx.imageSmoothingEnabled = true 22 | ctx.drawImage(img, 0, 0, width, height) 23 | 24 | canvas.createPNGStream().pipe(out) 25 | 26 | out.on('finish', function () { 27 | console.log('Resized and saved in %dms', new Date() - start) 28 | }) 29 | } 30 | 31 | img.src = (process.argv[2] || path.join(__dirname, 'images', 'squid.png')) 32 | -------------------------------------------------------------------------------- /examples/small-pdf.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const Canvas = require('..') 3 | 4 | const canvas = Canvas.createCanvas(400, 200, 'pdf') 5 | const ctx = canvas.getContext('2d') 6 | 7 | let y = 80 8 | let x = 50 9 | 10 | ctx.font = '22px Helvetica' 11 | ctx.fillText('node-canvas pdf', x, y) 12 | 13 | ctx.font = '10px Arial' 14 | ctx.fillText('Just a quick example of PDFs with node-canvas', x, (y += 20)) 15 | 16 | ctx.globalAlpha = 0.5 17 | ctx.fillRect(x, (y += 20), 10, 10) 18 | ctx.fillRect((x += 20), y, 10, 10) 19 | ctx.fillRect((x += 20), y, 10, 10) 20 | 21 | fs.writeFile('out.pdf', canvas.toBuffer(), function (err) { 22 | if (err) throw err 23 | 24 | console.log('created out.pdf') 25 | }) 26 | -------------------------------------------------------------------------------- /examples/small-svg.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const Canvas = require('..') 3 | 4 | const canvas = Canvas.createCanvas(400, 200, 'svg') 5 | const ctx = canvas.getContext('2d') 6 | 7 | let y = 80 8 | let x = 50 9 | 10 | ctx.font = '22px Helvetica' 11 | ctx.fillText('node-canvas SVG', x, y) 12 | 13 | ctx.font = '10px Arial' 14 | ctx.fillText('Just a quick example of SVGs with node-canvas', x, (y += 20)) 15 | 16 | ctx.globalAlpha = 0.5 17 | ctx.fillRect(x, (y += 20), 10, 10) 18 | ctx.fillRect((x += 20), y, 10, 10) 19 | ctx.fillRect((x += 20), y, 10, 10) 20 | 21 | fs.writeFile('out.svg', canvas.toBuffer(), function (err) { 22 | if (err) throw err 23 | 24 | console.log('created out.svg') 25 | }) 26 | -------------------------------------------------------------------------------- /examples/spark.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const canvas = Canvas.createCanvas(40, 15) 6 | const ctx = canvas.getContext('2d') 7 | 8 | function spark (ctx, data) { 9 | const len = data.length 10 | const pad = 1 11 | const width = ctx.canvas.width 12 | const height = ctx.canvas.height 13 | const barWidth = width / len 14 | const max = Math.max.apply(null, data) 15 | 16 | ctx.fillStyle = 'rgba(0,0,255,0.5)' 17 | ctx.strokeStyle = 'red' 18 | ctx.lineWidth = 1 19 | 20 | data.forEach(function (n, i) { 21 | const x = i * barWidth + pad 22 | const y = height * (n / max) 23 | 24 | ctx.lineTo(x, height - y) 25 | ctx.fillRect(x, height, barWidth - pad, -y) 26 | }) 27 | 28 | ctx.stroke() 29 | } 30 | 31 | spark(ctx, [1, 2, 4, 5, 10, 4, 2, 5, 4, 3, 3, 2]) 32 | 33 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'spark.png'))) 34 | -------------------------------------------------------------------------------- /examples/state.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const canvas = Canvas.createCanvas(150, 150) 6 | const ctx = canvas.getContext('2d') 7 | 8 | ctx.fillRect(0, 0, 150, 150) // Draw a rectangle with default settings 9 | ctx.save() // Save the default state 10 | 11 | ctx.fillStyle = '#09F' // Make changes to the settings 12 | ctx.fillRect(15, 15, 120, 120) // Draw a rectangle with new settings 13 | 14 | ctx.save() // Save the current state 15 | ctx.fillStyle = '#FFF' // Make changes to the settings 16 | ctx.globalAlpha = 0.5 17 | ctx.fillRect(30, 30, 90, 90) // Draw a rectangle with new settings 18 | 19 | ctx.restore() // Restore previous state 20 | ctx.fillRect(45, 45, 60, 60) // Draw a rectangle with restored settings 21 | 22 | ctx.restore() // Restore original state 23 | ctx.fillRect(60, 60, 30, 30) // Draw a rectangle with restored settings 24 | 25 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'state.png'))) 26 | -------------------------------------------------------------------------------- /examples/text.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Canvas = require('..') 4 | 5 | const canvas = Canvas.createCanvas(200, 200) 6 | const ctx = canvas.getContext('2d') 7 | 8 | ctx.globalAlpha = 0.2 9 | 10 | ctx.strokeRect(0, 0, 200, 200) 11 | ctx.lineTo(0, 100) 12 | ctx.lineTo(200, 100) 13 | ctx.stroke() 14 | 15 | ctx.beginPath() 16 | ctx.lineTo(100, 0) 17 | ctx.lineTo(100, 200) 18 | ctx.stroke() 19 | 20 | ctx.globalAlpha = 1 21 | ctx.font = 'normal 40px Impact, serif' 22 | 23 | ctx.rotate(0.5) 24 | ctx.translate(20, -40) 25 | 26 | ctx.lineWidth = 1 27 | ctx.strokeStyle = '#ddd' 28 | ctx.strokeText('Wahoo', 50, 100) 29 | 30 | ctx.fillStyle = '#000' 31 | ctx.fillText('Wahoo', 49, 99) 32 | 33 | const m = ctx.measureText('Wahoo') 34 | 35 | ctx.strokeStyle = '#f00' 36 | 37 | ctx.strokeRect( 38 | 49 + m.actualBoundingBoxLeft, 39 | 99 - m.actualBoundingBoxAscent, 40 | m.actualBoundingBoxRight - m.actualBoundingBoxLeft, 41 | m.actualBoundingBoxAscent + m.actualBoundingBoxDescent 42 | ) 43 | 44 | canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'text.png'))) 45 | -------------------------------------------------------------------------------- /examples/voronoi.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const Canvas = require('..') 3 | 4 | const canvas = Canvas.createCanvas(1920, 1200) 5 | const ctx = canvas.getContext('2d') 6 | 7 | const voronoiFactory = require('./rhill-voronoi-core-min') 8 | 9 | http.createServer(function (req, res) { 10 | let x, y, v, iHalfedge 11 | 12 | const voronoi = voronoiFactory() 13 | const start = new Date() 14 | const bbox = { xl: 0, xr: canvas.width, yt: 0, yb: canvas.height } 15 | 16 | for (let i = 0; i < 340; i++) { 17 | x = Math.random() * canvas.width 18 | y = Math.random() * canvas.height 19 | voronoi.addSites([{ x: x, y: y }]) 20 | } 21 | 22 | const diagram = voronoi.compute(bbox) 23 | 24 | ctx.beginPath() 25 | ctx.rect(0, 0, canvas.width, canvas.height) 26 | ctx.fillStyle = '#fff' 27 | ctx.fill() 28 | ctx.strokeStyle = 'black' 29 | ctx.stroke() 30 | // voronoi 31 | ctx.strokeStyle = 'rgba(255,255,255,0.5)' 32 | ctx.lineWidth = 4 33 | // edges 34 | const edges = diagram.edges 35 | const nEdges = edges.length 36 | 37 | const sites = diagram.sites 38 | const nSites = sites.length 39 | for (let iSite = nSites - 1; iSite >= 0; iSite -= 1) { 40 | const site = sites[iSite] 41 | ctx.rect(site.x - 0.5, site.y - 0.5, 1, 1) 42 | 43 | const cell = diagram.cells[diagram.sites[iSite].id] 44 | if (cell !== undefined) { 45 | const halfedges = cell.halfedges 46 | const nHalfedges = halfedges.length 47 | if (nHalfedges < 3) return 48 | let minx = canvas.width 49 | let miny = canvas.height 50 | let maxx = 0 51 | let maxy = 0 52 | 53 | v = halfedges[0].getStartpoint() 54 | ctx.beginPath() 55 | ctx.moveTo(v.x, v.y) 56 | 57 | for (iHalfedge = 0; iHalfedge < nHalfedges; iHalfedge++) { 58 | v = halfedges[iHalfedge].getEndpoint() 59 | ctx.lineTo(v.x, v.y) 60 | if (v.x < minx) minx = v.x 61 | if (v.y < miny) miny = v.y 62 | if (v.x > maxx) maxx = v.x 63 | if (v.y > maxy) maxy = v.y 64 | } 65 | 66 | let midx = (maxx + minx) / 2 67 | let midy = (maxy + miny) / 2 68 | let R = 0 69 | 70 | for (iHalfedge = 0; iHalfedge < nHalfedges; iHalfedge++) { 71 | v = halfedges[iHalfedge].getEndpoint() 72 | const dx = v.x - site.x 73 | const dy = v.y - site.y 74 | const newR = Math.sqrt(dx * dx + dy * dy) 75 | if (newR > R) R = newR 76 | } 77 | 78 | midx = site.x 79 | midy = site.y 80 | 81 | const radgrad = ctx.createRadialGradient(midx + R * 0.3, midy - R * 0.3, 0, midx, midy, R) 82 | radgrad.addColorStop(0, '#09760b') 83 | radgrad.addColorStop(1.0, 'black') 84 | ctx.fillStyle = radgrad 85 | ctx.fill() 86 | 87 | const radgrad2 = ctx.createRadialGradient(midx - R * 0.5, midy + R * 0.5, R * 0.1, midx, midy, R) 88 | radgrad2.addColorStop(0, 'rgba(255,255,255,0.5)') 89 | radgrad2.addColorStop(0.04, 'rgba(255,255,255,0.3)') 90 | radgrad2.addColorStop(0.05, 'rgba(255,255,255,0)') 91 | ctx.fillStyle = radgrad2 92 | ctx.fill() 93 | 94 | const lingrad = ctx.createLinearGradient(minx, site.y, minx + 100, site.y - 20) 95 | lingrad.addColorStop(0.0, 'rgba(255,255,255,0.5)') 96 | lingrad.addColorStop(0.2, 'rgba(255,255,255,0.2)') 97 | lingrad.addColorStop(1.0, 'rgba(255,255,255,0)') 98 | ctx.fillStyle = lingrad 99 | ctx.fill() 100 | } 101 | } 102 | 103 | if (nEdges) { 104 | let edge 105 | 106 | ctx.beginPath() 107 | 108 | for (let iEdge = nEdges - 1; iEdge >= 0; iEdge -= 1) { 109 | edge = edges[iEdge] 110 | v = edge.va 111 | ctx.moveTo(v.x, v.y) 112 | v = edge.vb 113 | ctx.lineTo(v.x, v.y) 114 | } 115 | 116 | ctx.stroke() 117 | } 118 | 119 | canvas.toBuffer(function (err, buf) { 120 | if (err) throw err 121 | 122 | const duration = new Date() - start 123 | console.log('Rendered in %dms', duration) 124 | 125 | res.writeHead(200, { 126 | 'Content-Type': 'image/png', 127 | 'Content-Length': buf.length 128 | }) 129 | 130 | res.end(buf) 131 | }) 132 | }).listen(3000, function () { 133 | console.log('Server running on port 3000') 134 | }) 135 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Canvas = require('./lib/canvas') 2 | const Image = require('./lib/image') 3 | const CanvasRenderingContext2D = require('./lib/context2d') 4 | const CanvasPattern = require('./lib/pattern') 5 | const packageJson = require('./package.json') 6 | const bindings = require('./lib/bindings') 7 | const fs = require('fs') 8 | const PNGStream = require('./lib/pngstream') 9 | const PDFStream = require('./lib/pdfstream') 10 | const JPEGStream = require('./lib/jpegstream') 11 | const { DOMPoint, DOMMatrix } = require('./lib/DOMMatrix') 12 | 13 | bindings.setDOMMatrix(DOMMatrix) 14 | 15 | function createCanvas (width, height, type) { 16 | return new Canvas(width, height, type) 17 | } 18 | 19 | function createImageData (array, width, height) { 20 | return new bindings.ImageData(array, width, height) 21 | } 22 | 23 | function loadImage (src) { 24 | return new Promise((resolve, reject) => { 25 | const image = new Image() 26 | 27 | function cleanup () { 28 | image.onload = null 29 | image.onerror = null 30 | } 31 | 32 | image.onload = () => { cleanup(); resolve(image) } 33 | image.onerror = (err) => { cleanup(); reject(err) } 34 | 35 | image.src = src 36 | }) 37 | } 38 | 39 | /** 40 | * Resolve paths for registerFont. Must be called *before* creating a Canvas 41 | * instance. 42 | * @param src {string} Path to font file. 43 | * @param fontFace {{family: string, weight?: string, style?: string}} Object 44 | * specifying font information. `weight` and `style` default to `"normal"`. 45 | */ 46 | function registerFont (src, fontFace) { 47 | // TODO this doesn't need to be on Canvas; it should just be a static method 48 | // of `bindings`. 49 | return Canvas._registerFont(fs.realpathSync(src), fontFace) 50 | } 51 | 52 | /** 53 | * Unload all fonts from pango to free up memory 54 | */ 55 | function deregisterAllFonts () { 56 | return Canvas._deregisterAllFonts() 57 | } 58 | 59 | exports.Canvas = Canvas 60 | exports.Context2d = CanvasRenderingContext2D // Legacy/compat export 61 | exports.CanvasRenderingContext2D = CanvasRenderingContext2D 62 | exports.CanvasGradient = bindings.CanvasGradient 63 | exports.CanvasPattern = CanvasPattern 64 | exports.Image = Image 65 | exports.ImageData = bindings.ImageData 66 | exports.PNGStream = PNGStream 67 | exports.PDFStream = PDFStream 68 | exports.JPEGStream = JPEGStream 69 | exports.DOMMatrix = DOMMatrix 70 | exports.DOMPoint = DOMPoint 71 | 72 | exports.registerFont = registerFont 73 | exports.deregisterAllFonts = deregisterAllFonts 74 | 75 | exports.createCanvas = createCanvas 76 | exports.createImageData = createImageData 77 | exports.loadImage = loadImage 78 | 79 | exports.backends = bindings.Backends 80 | 81 | /** Library version. */ 82 | exports.version = packageJson.version 83 | /** Cairo version. */ 84 | exports.cairoVersion = bindings.cairoVersion 85 | /** jpeglib version. */ 86 | exports.jpegVersion = bindings.jpegVersion 87 | /** gif_lib version. */ 88 | exports.gifVersion = bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined 89 | /** freetype version. */ 90 | exports.freetypeVersion = bindings.freetypeVersion 91 | /** rsvg version. */ 92 | exports.rsvgVersion = bindings.rsvgVersion 93 | /** pango version. */ 94 | exports.pangoVersion = bindings.pangoVersion 95 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectType } from 'tsd' 2 | import * as path from 'path' 3 | import { Readable } from 'stream' 4 | 5 | import * as Canvas from './index' 6 | 7 | Canvas.registerFont(path.join(__dirname, '../pfennigFont/Pfennig.ttf'), {family: 'pfennigFont'}) 8 | 9 | Canvas.createCanvas(5, 10) 10 | Canvas.createCanvas(200, 200, 'pdf') 11 | Canvas.createCanvas(150, 150, 'svg') 12 | 13 | const canv = Canvas.createCanvas(10, 10) 14 | const ctx = canv.getContext('2d') 15 | canv.getContext('2d', {alpha: false}) 16 | 17 | // LHS is ImageData, not Canvas.ImageData 18 | const id = ctx.getImageData(0, 0, 10, 10) 19 | expectType(id.height) 20 | expectType(id.width) 21 | 22 | ctx.currentTransform = ctx.getTransform() 23 | 24 | ctx.quality = 'best' 25 | ctx.textDrawingMode = 'glyph' 26 | 27 | const grad = ctx.createLinearGradient(0, 1, 2, 3) 28 | expectType(grad) 29 | grad.addColorStop(0.1, 'red') 30 | 31 | const dm = new Canvas.DOMMatrix([1, 2, 3, 4, 5, 6]) 32 | expectType(dm.a) 33 | 34 | expectType(canv.toBuffer()) 35 | expectType(canv.toBuffer('application/pdf')) 36 | canv.toBuffer((err, data) => {}, 'image/png', {filters: Canvas.Canvas.PNG_ALL_FILTERS}) 37 | expectAssignable(canv.createJPEGStream({ quality: 0.5 })) 38 | expectAssignable(canv.createPDFStream({ author: 'octocat' })) 39 | canv.toDataURL() 40 | 41 | const img = new Canvas.Image() 42 | img.src = Buffer.alloc(0) 43 | img.dataMode = Canvas.Image.MODE_IMAGE | Canvas.Image.MODE_MIME 44 | img.onload = () => {} 45 | img.onload = null 46 | 47 | const id2 = Canvas.createImageData(new Uint16Array(4), 1) 48 | expectType(id2) 49 | ctx.putImageData(id2, 0, 0) 50 | 51 | ctx.drawImage(canv, 0, 0) 52 | 53 | Canvas.deregisterAllFonts() 54 | -------------------------------------------------------------------------------- /lib/bindings.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const bindings = require('../build/Release/canvas.node') 4 | 5 | module.exports = bindings 6 | 7 | Object.defineProperty(bindings.Canvas.prototype, Symbol.toStringTag, { 8 | value: 'HTMLCanvasElement', 9 | configurable: true 10 | }) 11 | 12 | Object.defineProperty(bindings.Image.prototype, Symbol.toStringTag, { 13 | value: 'HTMLImageElement', 14 | configurable: true 15 | }) 16 | 17 | bindings.ImageData.prototype.toString = function () { 18 | return '[object ImageData]' 19 | } 20 | 21 | Object.defineProperty(bindings.ImageData.prototype, Symbol.toStringTag, { 22 | value: 'ImageData', 23 | configurable: true 24 | }) 25 | 26 | bindings.CanvasGradient.prototype.toString = function () { 27 | return '[object CanvasGradient]' 28 | } 29 | 30 | Object.defineProperty(bindings.CanvasGradient.prototype, Symbol.toStringTag, { 31 | value: 'CanvasGradient', 32 | configurable: true 33 | }) 34 | 35 | Object.defineProperty(bindings.CanvasPattern.prototype, Symbol.toStringTag, { 36 | value: 'CanvasPattern', 37 | configurable: true 38 | }) 39 | 40 | Object.defineProperty(bindings.CanvasRenderingContext2d.prototype, Symbol.toStringTag, { 41 | value: 'CanvasRenderingContext2d', 42 | configurable: true 43 | }) 44 | -------------------------------------------------------------------------------- /lib/canvas.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * Canvas 5 | * Copyright (c) 2010 LearnBoost 6 | * MIT Licensed 7 | */ 8 | 9 | const bindings = require('./bindings') 10 | const Canvas = module.exports = bindings.Canvas 11 | const Context2d = require('./context2d') 12 | const PNGStream = require('./pngstream') 13 | const PDFStream = require('./pdfstream') 14 | const JPEGStream = require('./jpegstream') 15 | const FORMATS = ['image/png', 'image/jpeg'] 16 | const util = require('util') 17 | 18 | // TODO || is for Node.js pre-v6.6.0 19 | Canvas.prototype[util.inspect.custom || 'inspect'] = function () { 20 | return `[Canvas ${this.width}x${this.height}]` 21 | } 22 | 23 | Canvas.prototype.getContext = function (contextType, contextAttributes) { 24 | if (contextType == '2d') { 25 | const ctx = this._context2d || (this._context2d = new Context2d(this, contextAttributes)) 26 | this.context = ctx 27 | ctx.canvas = this 28 | return ctx 29 | } 30 | } 31 | 32 | Canvas.prototype.pngStream = 33 | Canvas.prototype.createPNGStream = function (options) { 34 | return new PNGStream(this, options) 35 | } 36 | 37 | Canvas.prototype.pdfStream = 38 | Canvas.prototype.createPDFStream = function (options) { 39 | return new PDFStream(this, options) 40 | } 41 | 42 | Canvas.prototype.jpegStream = 43 | Canvas.prototype.createJPEGStream = function (options) { 44 | return new JPEGStream(this, options) 45 | } 46 | 47 | Canvas.prototype.toDataURL = function (a1, a2, a3) { 48 | // valid arg patterns (args -> [type, opts, fn]): 49 | // [] -> ['image/png', null, null] 50 | // [qual] -> ['image/png', null, null] 51 | // [undefined] -> ['image/png', null, null] 52 | // ['image/png'] -> ['image/png', null, null] 53 | // ['image/png', qual] -> ['image/png', null, null] 54 | // [fn] -> ['image/png', null, fn] 55 | // [type, fn] -> [type, null, fn] 56 | // [undefined, fn] -> ['image/png', null, fn] 57 | // ['image/png', qual, fn] -> ['image/png', null, fn] 58 | // ['image/jpeg', fn] -> ['image/jpeg', null, fn] 59 | // ['image/jpeg', opts, fn] -> ['image/jpeg', opts, fn] 60 | // ['image/jpeg', qual, fn] -> ['image/jpeg', {quality: qual}, fn] 61 | // ['image/jpeg', undefined, fn] -> ['image/jpeg', null, fn] 62 | // ['image/jpeg'] -> ['image/jpeg', null, fn] 63 | // ['image/jpeg', opts] -> ['image/jpeg', opts, fn] 64 | // ['image/jpeg', qual] -> ['image/jpeg', {quality: qual}, fn] 65 | 66 | let type = 'image/png' 67 | let opts = {} 68 | let fn 69 | 70 | if (typeof a1 === 'function') { 71 | fn = a1 72 | } else { 73 | if (typeof a1 === 'string' && FORMATS.includes(a1.toLowerCase())) { 74 | type = a1.toLowerCase() 75 | } 76 | 77 | if (typeof a2 === 'function') { 78 | fn = a2 79 | } else { 80 | if (typeof a2 === 'object') { 81 | opts = a2 82 | } else if (typeof a2 === 'number') { 83 | opts = { quality: Math.max(0, Math.min(1, a2)) } 84 | } 85 | 86 | if (typeof a3 === 'function') { 87 | fn = a3 88 | } else if (undefined !== a3) { 89 | throw new TypeError(`${typeof a3} is not a function`) 90 | } 91 | } 92 | } 93 | 94 | if (this.width === 0 || this.height === 0) { 95 | // Per spec, if the bitmap has no pixels, return this string: 96 | const str = 'data:,' 97 | if (fn) { 98 | setTimeout(() => fn(null, str)) 99 | return 100 | } else { 101 | return str 102 | } 103 | } 104 | 105 | if (fn) { 106 | this.toBuffer((err, buf) => { 107 | if (err) return fn(err) 108 | fn(null, `data:${type};base64,${buf.toString('base64')}`) 109 | }, type, opts) 110 | } else { 111 | return `data:${type};base64,${this.toBuffer(type, opts).toString('base64')}` 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/context2d.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * Canvas - Context2d 5 | * Copyright (c) 2010 LearnBoost 6 | * MIT Licensed 7 | */ 8 | 9 | const bindings = require('./bindings') 10 | 11 | module.exports = bindings.CanvasRenderingContext2d 12 | -------------------------------------------------------------------------------- /lib/image.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * Canvas - Image 5 | * Copyright (c) 2010 LearnBoost 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | const bindings = require('./bindings') 14 | const Image = module.exports = bindings.Image 15 | const util = require('util') 16 | 17 | const { GetSource, SetSource } = bindings 18 | 19 | Object.defineProperty(Image.prototype, 'src', { 20 | /** 21 | * src setter. Valid values: 22 | * * `data:` URI 23 | * * Local file path 24 | * * HTTP or HTTPS URL 25 | * * Buffer containing image data (i.e. not a `data:` URI stored in a Buffer) 26 | * 27 | * @param {String|Buffer} val filename, buffer, data URI, URL 28 | * @api public 29 | */ 30 | set (val) { 31 | if (typeof val === 'string') { 32 | if (/^\s*data:/.test(val)) { // data: URI 33 | const commaI = val.indexOf(',') 34 | // 'base64' must come before the comma 35 | const isBase64 = val.lastIndexOf('base64', commaI) !== -1 36 | const content = val.slice(commaI + 1) 37 | setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val) 38 | } else if (/^\s*https?:\/\//.test(val)) { // remote URL 39 | const onerror = err => { 40 | if (typeof this.onerror === 'function') { 41 | this.onerror(err) 42 | } else { 43 | throw err 44 | } 45 | } 46 | 47 | fetch(val, { 48 | method: 'GET', 49 | headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36' } 50 | }) 51 | .then(res => { 52 | if (!res.ok) { 53 | throw new Error(`Server responded with ${res.statusCode}`) 54 | } 55 | return res.arrayBuffer() 56 | }) 57 | .then(data => { 58 | setSource(this, Buffer.from(data)) 59 | }) 60 | .catch(onerror) 61 | } else { // local file path assumed 62 | setSource(this, val) 63 | } 64 | } else if (Buffer.isBuffer(val)) { 65 | setSource(this, val) 66 | } 67 | }, 68 | 69 | get () { 70 | // TODO https://github.com/Automattic/node-canvas/issues/118 71 | return getSource(this) 72 | }, 73 | 74 | configurable: true 75 | }) 76 | 77 | // TODO || is for Node.js pre-v6.6.0 78 | Image.prototype[util.inspect.custom || 'inspect'] = function () { 79 | return '[Image' + 80 | (this.complete ? ':' + this.width + 'x' + this.height : '') + 81 | (this.src ? ' ' + this.src : '') + 82 | (this.complete ? ' complete' : '') + 83 | ']' 84 | } 85 | 86 | function getSource (img) { 87 | return img._originalSource || GetSource.call(img) 88 | } 89 | 90 | function setSource (img, src, origSrc) { 91 | SetSource.call(img, src) 92 | img._originalSource = origSrc 93 | } 94 | -------------------------------------------------------------------------------- /lib/jpegstream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * Canvas - JPEGStream 5 | * Copyright (c) 2010 LearnBoost 6 | * MIT Licensed 7 | */ 8 | 9 | const { Readable } = require('stream') 10 | function noop () {} 11 | 12 | class JPEGStream extends Readable { 13 | constructor (canvas, options) { 14 | super() 15 | 16 | if (canvas.streamJPEGSync === undefined) { 17 | throw new Error('node-canvas was built without JPEG support.') 18 | } 19 | 20 | this.options = options 21 | this.canvas = canvas 22 | } 23 | 24 | _read () { 25 | // For now we're not controlling the c++ code's data emission, so we only 26 | // call canvas.streamJPEGSync once and let it emit data at will. 27 | this._read = noop 28 | 29 | this.canvas.streamJPEGSync(this.options, (err, chunk) => { 30 | if (err) { 31 | this.emit('error', err) 32 | } else if (chunk) { 33 | this.push(chunk) 34 | } else { 35 | this.push(null) 36 | } 37 | }) 38 | } 39 | }; 40 | 41 | module.exports = JPEGStream 42 | -------------------------------------------------------------------------------- /lib/pattern.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * Canvas - CanvasPattern 5 | * Copyright (c) 2010 LearnBoost 6 | * MIT Licensed 7 | */ 8 | 9 | const bindings = require('./bindings') 10 | 11 | module.exports = bindings.CanvasPattern 12 | 13 | bindings.CanvasPattern.prototype.toString = function () { 14 | return '[object CanvasPattern]' 15 | } 16 | -------------------------------------------------------------------------------- /lib/pdfstream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * Canvas - PDFStream 5 | */ 6 | 7 | const { Readable } = require('stream') 8 | function noop () {} 9 | 10 | class PDFStream extends Readable { 11 | constructor (canvas, options) { 12 | super() 13 | 14 | this.canvas = canvas 15 | this.options = options 16 | } 17 | 18 | _read () { 19 | // For now we're not controlling the c++ code's data emission, so we only 20 | // call canvas.streamPDFSync once and let it emit data at will. 21 | this._read = noop 22 | 23 | this.canvas.streamPDFSync((err, chunk, len) => { 24 | if (err) { 25 | this.emit('error', err) 26 | } else if (len) { 27 | this.push(chunk) 28 | } else { 29 | this.push(null) 30 | } 31 | }, this.options) 32 | } 33 | } 34 | 35 | module.exports = PDFStream 36 | -------------------------------------------------------------------------------- /lib/pngstream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * Canvas - PNGStream 5 | * Copyright (c) 2010 LearnBoost 6 | * MIT Licensed 7 | */ 8 | 9 | const { Readable } = require('stream') 10 | function noop () {} 11 | 12 | class PNGStream extends Readable { 13 | constructor (canvas, options) { 14 | super() 15 | 16 | if (options && 17 | options.palette instanceof Uint8ClampedArray && 18 | options.palette.length % 4 !== 0) { 19 | throw new Error('Palette length must be a multiple of 4.') 20 | } 21 | this.canvas = canvas 22 | this.options = options || {} 23 | } 24 | 25 | _read () { 26 | // For now we're not controlling the c++ code's data emission, so we only 27 | // call canvas.streamPNGSync once and let it emit data at will. 28 | this._read = noop 29 | 30 | this.canvas.streamPNGSync((err, chunk, len) => { 31 | if (err) { 32 | this.emit('error', err) 33 | } else if (len) { 34 | this.push(chunk) 35 | } else { 36 | this.push(null) 37 | } 38 | }, this.options) 39 | } 40 | } 41 | 42 | module.exports = PNGStream 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canvas", 3 | "description": "Canvas graphics API backed by Cairo", 4 | "version": "3.1.0", 5 | "author": "TJ Holowaychuk ", 6 | "main": "index.js", 7 | "browser": "browser.js", 8 | "types": "index.d.ts", 9 | "contributors": [ 10 | "Nathan Rajlich ", 11 | "Rod Vagg ", 12 | "Juriy Zaytsev " 13 | ], 14 | "keywords": [ 15 | "canvas", 16 | "graphic", 17 | "graphics", 18 | "pixman", 19 | "cairo", 20 | "image", 21 | "images", 22 | "pdf" 23 | ], 24 | "homepage": "https://github.com/Automattic/node-canvas", 25 | "repository": "git://github.com/Automattic/node-canvas.git", 26 | "scripts": { 27 | "prebenchmark": "node-gyp build", 28 | "benchmark": "node benchmarks/run.js", 29 | "lint": "standard examples/*.js test/server.js test/public/*.js benchmarks/run.js lib/context2d.js util/has_lib.js browser.js index.js", 30 | "test": "mocha test/*.test.js", 31 | "pretest-server": "node-gyp build", 32 | "test-server": "node test/server.js", 33 | "generate-wpt": "node ./test/wpt/generate.js", 34 | "test-wpt": "mocha test/wpt/generated/*.js", 35 | "install": "prebuild-install -r napi || node-gyp rebuild", 36 | "tsd": "tsd" 37 | }, 38 | "files": [ 39 | "binding.gyp", 40 | "browser.js", 41 | "index.d.ts", 42 | "index.js", 43 | "lib/", 44 | "src/", 45 | "util/" 46 | ], 47 | "dependencies": { 48 | "node-addon-api": "^7.0.0", 49 | "prebuild-install": "^7.1.3" 50 | }, 51 | "devDependencies": { 52 | "@types/node": "^10.12.18", 53 | "assert-rejects": "^1.0.0", 54 | "express": "^4.16.3", 55 | "js-yaml": "^4.1.0", 56 | "mocha": "^5.2.0", 57 | "pixelmatch": "^4.0.2", 58 | "standard": "^12.0.1", 59 | "tsd": "^0.29.0", 60 | "typescript": "^4.2.2" 61 | }, 62 | "engines": { 63 | "node": "^18.12.0 || >= 20.9.0" 64 | }, 65 | "binary": { 66 | "napi_versions": [7] 67 | }, 68 | "license": "MIT" 69 | } 70 | -------------------------------------------------------------------------------- /src/Backends.cc: -------------------------------------------------------------------------------- 1 | #include "Backends.h" 2 | 3 | #include "backend/ImageBackend.h" 4 | #include "backend/PdfBackend.h" 5 | #include "backend/SvgBackend.h" 6 | 7 | using namespace Napi; 8 | 9 | void 10 | Backends::Initialize(Napi::Env env, Napi::Object exports) { 11 | Napi::Object obj = Napi::Object::New(env); 12 | 13 | ImageBackend::Initialize(obj); 14 | PdfBackend::Initialize(obj); 15 | SvgBackend::Initialize(obj); 16 | 17 | exports.Set("Backends", obj); 18 | } 19 | -------------------------------------------------------------------------------- /src/Backends.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "backend/Backend.h" 4 | #include 5 | 6 | class Backends : public Napi::ObjectWrap { 7 | public: 8 | static void Initialize(Napi::Env env, Napi::Object exports); 9 | }; 10 | -------------------------------------------------------------------------------- /src/Canvas.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #pragma once 4 | 5 | struct Closure; 6 | 7 | #include "backend/Backend.h" 8 | #include "closure.h" 9 | #include 10 | #include "dll_visibility.h" 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | /* 17 | * FontFace describes a font file in terms of one PangoFontDescription that 18 | * will resolve to it and one that the user describes it as (like @font-face) 19 | */ 20 | class FontFace { 21 | public: 22 | PangoFontDescription *sys_desc = nullptr; 23 | PangoFontDescription *user_desc = nullptr; 24 | unsigned char file_path[1024]; 25 | }; 26 | 27 | enum text_baseline_t : uint8_t { 28 | TEXT_BASELINE_ALPHABETIC = 0, 29 | TEXT_BASELINE_TOP = 1, 30 | TEXT_BASELINE_BOTTOM = 2, 31 | TEXT_BASELINE_MIDDLE = 3, 32 | TEXT_BASELINE_IDEOGRAPHIC = 4, 33 | TEXT_BASELINE_HANGING = 5 34 | }; 35 | 36 | enum text_align_t : int8_t { 37 | TEXT_ALIGNMENT_LEFT = -1, 38 | TEXT_ALIGNMENT_CENTER = 0, 39 | TEXT_ALIGNMENT_RIGHT = 1, 40 | TEXT_ALIGNMENT_START = -2, 41 | TEXT_ALIGNMENT_END = 2 42 | }; 43 | 44 | enum canvas_draw_mode_t : uint8_t { 45 | TEXT_DRAW_PATHS, 46 | TEXT_DRAW_GLYPHS 47 | }; 48 | 49 | /* 50 | * Canvas. 51 | */ 52 | 53 | class Canvas : public Napi::ObjectWrap { 54 | public: 55 | Canvas(const Napi::CallbackInfo& info); 56 | static void Initialize(Napi::Env& env, Napi::Object& target); 57 | 58 | Napi::Value ToBuffer(const Napi::CallbackInfo& info); 59 | Napi::Value GetType(const Napi::CallbackInfo& info); 60 | Napi::Value GetStride(const Napi::CallbackInfo& info); 61 | Napi::Value GetWidth(const Napi::CallbackInfo& info); 62 | Napi::Value GetHeight(const Napi::CallbackInfo& info); 63 | void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value); 64 | void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value); 65 | void StreamPNGSync(const Napi::CallbackInfo& info); 66 | void StreamPDFSync(const Napi::CallbackInfo& info); 67 | void StreamJPEGSync(const Napi::CallbackInfo& info); 68 | static void RegisterFont(const Napi::CallbackInfo& info); 69 | static void DeregisterAllFonts(const Napi::CallbackInfo& info); 70 | static Napi::Value ParseFont(const Napi::CallbackInfo& info); 71 | Napi::Error CairoError(cairo_status_t status); 72 | static void ToPngBufferAsync(Closure* closure); 73 | static void ToJpegBufferAsync(Closure* closure); 74 | static PangoWeight GetWeightFromCSSString(const char *weight); 75 | static PangoStyle GetStyleFromCSSString(const char *style); 76 | static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); 77 | 78 | DLL_PUBLIC inline Backend* backend() { return _backend; } 79 | DLL_PUBLIC inline cairo_surface_t* surface(){ return backend()->getSurface(); } 80 | cairo_t* createCairoContext(); 81 | 82 | DLL_PUBLIC inline uint8_t *data(){ return cairo_image_surface_get_data(surface()); } 83 | DLL_PUBLIC inline int stride(){ return cairo_image_surface_get_stride(surface()); } 84 | DLL_PUBLIC inline std::size_t nBytes(){ 85 | return static_cast(backend()->getHeight()) * stride(); 86 | } 87 | 88 | DLL_PUBLIC inline int getWidth() { return backend()->getWidth(); } 89 | DLL_PUBLIC inline int getHeight() { return backend()->getHeight(); } 90 | 91 | void resurface(Napi::Object This); 92 | 93 | Napi::Env env; 94 | static int fontSerial; 95 | 96 | private: 97 | Backend* _backend; 98 | Napi::ObjectReference _jsBackend; 99 | Napi::FunctionReference ctor; 100 | static std::vector font_face_list; 101 | }; 102 | -------------------------------------------------------------------------------- /src/CanvasError.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class CanvasError { 7 | public: 8 | std::string message; 9 | std::string syscall; 10 | std::string path; 11 | int cerrno = 0; 12 | void set(const char* iMessage = NULL, const char* iSyscall = NULL, int iErrno = 0, const char* iPath = NULL) { 13 | if (iMessage) message.assign(iMessage); 14 | if (iSyscall) syscall.assign(iSyscall); 15 | cerrno = iErrno; 16 | if (iPath) path.assign(iPath); 17 | } 18 | void reset() { 19 | message.clear(); 20 | syscall.clear(); 21 | path.clear(); 22 | cerrno = 0; 23 | } 24 | bool empty() { 25 | return cerrno == 0 && message.empty(); 26 | } 27 | Napi::Error toError(Napi::Env env) { 28 | if (cerrno) { 29 | Napi::Error err = Napi::Error::New(env, strerror(cerrno)); 30 | if (!syscall.empty()) err.Value().Set("syscall", syscall); 31 | if (!path.empty()) err.Value().Set("path", path); 32 | return err; 33 | } else { 34 | return Napi::Error::New(env, message); 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/CanvasGradient.cc: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #include "CanvasGradient.h" 4 | #include "InstanceData.h" 5 | 6 | #include "Canvas.h" 7 | #include "color.h" 8 | 9 | using namespace Napi; 10 | 11 | /* 12 | * Initialize CanvasGradient. 13 | */ 14 | 15 | void 16 | Gradient::Initialize(Napi::Env& env, Napi::Object& exports) { 17 | Napi::HandleScope scope(env); 18 | InstanceData* data = env.GetInstanceData(); 19 | 20 | Napi::Function ctor = DefineClass(env, "CanvasGradient", { 21 | InstanceMethod<&Gradient::AddColorStop>("addColorStop", napi_default_method) 22 | }); 23 | 24 | exports.Set("CanvasGradient", ctor); 25 | data->CanvasGradientCtor = Napi::Persistent(ctor); 26 | } 27 | 28 | /* 29 | * Initialize a new CanvasGradient. 30 | */ 31 | 32 | Gradient::Gradient(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { 33 | // Linear 34 | if ( 35 | 4 == info.Length() && 36 | info[0].IsNumber() && 37 | info[1].IsNumber() && 38 | info[2].IsNumber() && 39 | info[3].IsNumber() 40 | ) { 41 | double x0 = info[0].As().DoubleValue(); 42 | double y0 = info[1].As().DoubleValue(); 43 | double x1 = info[2].As().DoubleValue(); 44 | double y1 = info[3].As().DoubleValue(); 45 | _pattern = cairo_pattern_create_linear(x0, y0, x1, y1); 46 | return; 47 | } 48 | 49 | // Radial 50 | if ( 51 | 6 == info.Length() && 52 | info[0].IsNumber() && 53 | info[1].IsNumber() && 54 | info[2].IsNumber() && 55 | info[3].IsNumber() && 56 | info[4].IsNumber() && 57 | info[5].IsNumber() 58 | ) { 59 | double x0 = info[0].As().DoubleValue(); 60 | double y0 = info[1].As().DoubleValue(); 61 | double r0 = info[2].As().DoubleValue(); 62 | double x1 = info[3].As().DoubleValue(); 63 | double y1 = info[4].As().DoubleValue(); 64 | double r1 = info[5].As().DoubleValue(); 65 | _pattern = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); 66 | return; 67 | } 68 | 69 | Napi::TypeError::New(env, "invalid arguments").ThrowAsJavaScriptException(); 70 | } 71 | 72 | /* 73 | * Add color stop. 74 | */ 75 | 76 | void 77 | Gradient::AddColorStop(const Napi::CallbackInfo& info) { 78 | if (!info[0].IsNumber()) { 79 | Napi::TypeError::New(env, "offset required").ThrowAsJavaScriptException(); 80 | return; 81 | } 82 | 83 | if (!info[1].IsString()) { 84 | Napi::TypeError::New(env, "color string required").ThrowAsJavaScriptException(); 85 | return; 86 | } 87 | 88 | short ok; 89 | std::string str = info[1].As(); 90 | uint32_t rgba = rgba_from_string(str.c_str(), &ok); 91 | 92 | if (ok) { 93 | rgba_t color = rgba_create(rgba); 94 | cairo_pattern_add_color_stop_rgba( 95 | _pattern 96 | , info[0].As().DoubleValue() 97 | , color.r 98 | , color.g 99 | , color.b 100 | , color.a); 101 | } else { 102 | Napi::TypeError::New(env, "parse color failed").ThrowAsJavaScriptException(); 103 | } 104 | } 105 | 106 | 107 | /* 108 | * Destroy the pattern. 109 | */ 110 | 111 | Gradient::~Gradient() { 112 | if (_pattern) cairo_pattern_destroy(_pattern); 113 | } 114 | -------------------------------------------------------------------------------- /src/CanvasGradient.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | 8 | class Gradient : public Napi::ObjectWrap { 9 | public: 10 | static void Initialize(Napi::Env& env, Napi::Object& target); 11 | Gradient(const Napi::CallbackInfo& info); 12 | void AddColorStop(const Napi::CallbackInfo& info); 13 | inline cairo_pattern_t *pattern(){ return _pattern; } 14 | ~Gradient(); 15 | 16 | Napi::Env env; 17 | 18 | private: 19 | cairo_pattern_t *_pattern; 20 | }; 21 | -------------------------------------------------------------------------------- /src/CanvasPattern.cc: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #include "CanvasPattern.h" 4 | 5 | #include "Canvas.h" 6 | #include "Image.h" 7 | #include "InstanceData.h" 8 | 9 | using namespace Napi; 10 | 11 | const cairo_user_data_key_t *pattern_repeat_key; 12 | 13 | /* 14 | * Initialize CanvasPattern. 15 | */ 16 | 17 | void 18 | Pattern::Initialize(Napi::Env& env, Napi::Object& exports) { 19 | Napi::HandleScope scope(env); 20 | InstanceData* data = env.GetInstanceData(); 21 | 22 | // Constructor 23 | Napi::Function ctor = DefineClass(env, "CanvasPattern", { 24 | InstanceMethod<&Pattern::setTransform>("setTransform", napi_default_method) 25 | }); 26 | 27 | // Prototype 28 | exports.Set("CanvasPattern", ctor); 29 | data->CanvasPatternCtor = Napi::Persistent(ctor); 30 | } 31 | 32 | /* 33 | * Initialize a new CanvasPattern. 34 | */ 35 | 36 | Pattern::Pattern(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info.Env()) { 37 | if (!info[0].IsObject()) { 38 | Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); 39 | return; 40 | } 41 | 42 | Napi::Object obj = info[0].As(); 43 | InstanceData* data = env.GetInstanceData(); 44 | cairo_surface_t *surface; 45 | 46 | // Image 47 | if (obj.InstanceOf(data->ImageCtor.Value()).UnwrapOr(false)) { 48 | Image *img = Image::Unwrap(obj); 49 | if (!img->isComplete()) { 50 | Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException(); 51 | return; 52 | } 53 | surface = img->surface(); 54 | 55 | // Canvas 56 | } else if (obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) { 57 | Canvas *canvas = Canvas::Unwrap(obj); 58 | surface = canvas->surface(); 59 | // Invalid 60 | } else { 61 | if (!env.IsExceptionPending()) { 62 | Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); 63 | } 64 | return; 65 | } 66 | _pattern = cairo_pattern_create_for_surface(surface); 67 | 68 | if (info[1].IsString()) { 69 | if ("no-repeat" == info[1].As().Utf8Value()) { 70 | _repeat = NO_REPEAT; 71 | } else if ("repeat-x" == info[1].As().Utf8Value()) { 72 | _repeat = REPEAT_X; 73 | } else if ("repeat-y" == info[1].As().Utf8Value()) { 74 | _repeat = REPEAT_Y; 75 | } 76 | } 77 | 78 | cairo_pattern_set_user_data(_pattern, pattern_repeat_key, &_repeat, NULL); 79 | } 80 | 81 | /* 82 | * Set the pattern-space to user-space transform. 83 | */ 84 | void 85 | Pattern::setTransform(const Napi::CallbackInfo& info) { 86 | if (!info[0].IsObject()) { 87 | Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); 88 | return; 89 | } 90 | 91 | Napi::Object mat = info[0].As(); 92 | 93 | InstanceData* data = env.GetInstanceData(); 94 | if (!mat.InstanceOf(data->DOMMatrixCtor.Value()).UnwrapOr(false)) { 95 | if (!env.IsExceptionPending()) { 96 | Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); 97 | } 98 | return; 99 | } 100 | 101 | Napi::Value one = Napi::Number::New(env, 1); 102 | Napi::Value zero = Napi::Number::New(env, 0); 103 | 104 | cairo_matrix_t matrix; 105 | cairo_matrix_init(&matrix, 106 | mat.Get("a").UnwrapOr(one).As().DoubleValue(), 107 | mat.Get("b").UnwrapOr(zero).As().DoubleValue(), 108 | mat.Get("c").UnwrapOr(zero).As().DoubleValue(), 109 | mat.Get("d").UnwrapOr(one).As().DoubleValue(), 110 | mat.Get("e").UnwrapOr(zero).As().DoubleValue(), 111 | mat.Get("f").UnwrapOr(zero).As().DoubleValue() 112 | ); 113 | 114 | cairo_matrix_invert(&matrix); 115 | cairo_pattern_set_matrix(_pattern, &matrix); 116 | } 117 | 118 | repeat_type_t Pattern::get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern) { 119 | void *ud = cairo_pattern_get_user_data(pattern, pattern_repeat_key); 120 | return *reinterpret_cast(ud); 121 | } 122 | 123 | /* 124 | * Destroy the pattern. 125 | */ 126 | 127 | Pattern::~Pattern() { 128 | if (_pattern) cairo_pattern_destroy(_pattern); 129 | } 130 | -------------------------------------------------------------------------------- /src/CanvasPattern.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011 LearnBoost 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | 8 | /* 9 | * Canvas types. 10 | */ 11 | 12 | typedef enum { 13 | NO_REPEAT, // match CAIRO_EXTEND_NONE 14 | REPEAT, // match CAIRO_EXTEND_REPEAT 15 | REPEAT_X, // needs custom processing 16 | REPEAT_Y // needs custom processing 17 | } repeat_type_t; 18 | 19 | extern const cairo_user_data_key_t *pattern_repeat_key; 20 | 21 | class Pattern : public Napi::ObjectWrap { 22 | public: 23 | Pattern(const Napi::CallbackInfo& info); 24 | static void Initialize(Napi::Env& env, Napi::Object& target); 25 | void setTransform(const Napi::CallbackInfo& info); 26 | static repeat_type_t get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern); 27 | inline cairo_pattern_t *pattern(){ return _pattern; } 28 | ~Pattern(); 29 | Napi::Env env; 30 | private: 31 | cairo_pattern_t *_pattern; 32 | repeat_type_t _repeat = REPEAT; 33 | }; 34 | -------------------------------------------------------------------------------- /src/FontParser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "CharData.h" 10 | 11 | enum class FontStyle { 12 | Normal, 13 | Italic, 14 | Oblique 15 | }; 16 | 17 | enum class FontVariant { 18 | Normal, 19 | SmallCaps 20 | }; 21 | 22 | struct FontProperties { 23 | double fontSize{16.0f}; 24 | std::vector fontFamily; 25 | uint16_t fontWeight{400}; 26 | FontVariant fontVariant{FontVariant::Normal}; 27 | FontStyle fontStyle{FontStyle::Normal}; 28 | }; 29 | 30 | class Token { 31 | public: 32 | enum class Type { 33 | Invalid, 34 | Number, 35 | Percent, 36 | Identifier, 37 | Slash, 38 | Comma, 39 | QuotedString, 40 | Whitespace, 41 | EndOfInput 42 | }; 43 | 44 | Token(Type type, std::string value); 45 | Token(Type type, double value); 46 | Token(Type type); 47 | 48 | Type type() const { return type_; } 49 | 50 | const std::string& getString() const; 51 | double getNumber() const; 52 | 53 | private: 54 | Type type_; 55 | std::variant value_; 56 | }; 57 | 58 | class Tokenizer { 59 | public: 60 | Tokenizer(std::string_view input); 61 | Token nextToken(); 62 | 63 | private: 64 | std::string_view input_; 65 | size_t position_{0}; 66 | 67 | // Util 68 | std::string utf8Encode(uint32_t codepoint); 69 | inline bool isWhitespace(char c) const { 70 | return charData[static_cast(c)] & CharData::Whitespace; 71 | } 72 | inline bool isNewline(char c) const { 73 | return charData[static_cast(c)] & CharData::Newline; 74 | } 75 | 76 | // Moving through the string 77 | char peek() const; 78 | char advance(); 79 | 80 | // Tokenize 81 | Token parseNumber(); 82 | Token parseIdentifier(); 83 | uint32_t parseUnicode(); 84 | bool parseEscape(std::string& str); 85 | Token parseString(char quote); 86 | }; 87 | 88 | class FontParser { 89 | public: 90 | static FontProperties parse(const std::string& fontString, bool* success = nullptr); 91 | 92 | private: 93 | static const std::unordered_map weightMap; 94 | static const std::unordered_map unitMap; 95 | 96 | FontParser(std::string_view input); 97 | 98 | void advance(); 99 | void skipWs(); 100 | bool check(Token::Type type) const; 101 | bool checkWs() const; 102 | 103 | bool parseFontStyle(FontProperties& props); 104 | bool parseFontVariant(FontProperties& props); 105 | bool parseFontWeight(FontProperties& props); 106 | bool parseFontSize(FontProperties& props); 107 | bool parseLineHeight(FontProperties& props); 108 | bool parseFontFamily(FontProperties& props); 109 | FontProperties parseFont(); 110 | 111 | Tokenizer tokenizer_; 112 | Token currentToken_; 113 | Token nextToken_; 114 | bool hasError_{false}; 115 | }; 116 | -------------------------------------------------------------------------------- /src/Image.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #pragma once 4 | 5 | #include 6 | #include "CanvasError.h" 7 | #include 8 | #include 9 | #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 10 | 11 | #ifdef HAVE_JPEG 12 | #include 13 | #include 14 | #endif 15 | 16 | #ifdef HAVE_GIF 17 | #include 18 | 19 | #if GIFLIB_MAJOR > 5 || GIFLIB_MAJOR == 5 && GIFLIB_MINOR >= 1 20 | #define GIF_CLOSE_FILE(gif) DGifCloseFile(gif, NULL) 21 | #else 22 | #define GIF_CLOSE_FILE(gif) DGifCloseFile(gif) 23 | #endif 24 | #endif 25 | 26 | #ifdef HAVE_RSVG 27 | #include 28 | // librsvg <= 2.36.1, identified by undefined macro, needs an extra include 29 | #ifndef LIBRSVG_CHECK_VERSION 30 | #include 31 | #endif 32 | #endif 33 | 34 | using JPEGDecodeL = std::function; 35 | 36 | class Image : public Napi::ObjectWrap { 37 | public: 38 | char *filename; 39 | int width, height; 40 | int naturalWidth, naturalHeight; 41 | Napi::Env env; 42 | static Napi::FunctionReference constructor; 43 | static void Initialize(Napi::Env& env, Napi::Object& target); 44 | Image(const Napi::CallbackInfo& info); 45 | Napi::Value GetComplete(const Napi::CallbackInfo& info); 46 | Napi::Value GetWidth(const Napi::CallbackInfo& info); 47 | Napi::Value GetHeight(const Napi::CallbackInfo& info); 48 | Napi::Value GetNaturalWidth(const Napi::CallbackInfo& info); 49 | Napi::Value GetNaturalHeight(const Napi::CallbackInfo& info); 50 | Napi::Value GetDataMode(const Napi::CallbackInfo& info); 51 | void SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value); 52 | void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value); 53 | void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value); 54 | static Napi::Value GetSource(const Napi::CallbackInfo& info); 55 | static void SetSource(const Napi::CallbackInfo& info); 56 | inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } 57 | inline int stride(){ return cairo_image_surface_get_stride(_surface); } 58 | static int isPNG(uint8_t *data); 59 | static int isJPEG(uint8_t *data); 60 | static int isGIF(uint8_t *data); 61 | static int isSVG(uint8_t *data, unsigned len); 62 | static int isBMP(uint8_t *data, unsigned len); 63 | static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len); 64 | inline int isComplete(){ return COMPLETE == state; } 65 | cairo_surface_t *surface(); 66 | cairo_status_t loadSurface(); 67 | cairo_status_t loadFromBuffer(uint8_t *buf, unsigned len); 68 | cairo_status_t loadPNGFromBuffer(uint8_t *buf); 69 | cairo_status_t loadPNG(); 70 | void clearData(); 71 | #ifdef HAVE_RSVG 72 | cairo_status_t loadSVGFromBuffer(uint8_t *buf, unsigned len); 73 | cairo_status_t loadSVG(FILE *stream); 74 | cairo_status_t renderSVGToSurface(); 75 | #endif 76 | #ifdef HAVE_GIF 77 | cairo_status_t loadGIFFromBuffer(uint8_t *buf, unsigned len); 78 | cairo_status_t loadGIF(FILE *stream); 79 | #endif 80 | #ifdef HAVE_JPEG 81 | enum Orientation { 82 | NORMAL, 83 | MIRROR_HORIZ, 84 | MIRROR_VERT, 85 | ROTATE_180, 86 | ROTATE_90_CW, 87 | ROTATE_270_CW, 88 | MIRROR_HORIZ_AND_ROTATE_90_CW, 89 | MIRROR_HORIZ_AND_ROTATE_270_CW 90 | }; 91 | cairo_status_t loadJPEGFromBuffer(uint8_t *buf, unsigned len); 92 | cairo_status_t loadJPEG(FILE *stream); 93 | void jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode); 94 | cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info, Orientation orientation); 95 | cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len); 96 | cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type); 97 | 98 | class Reader { 99 | public: 100 | virtual bool hasBytes(unsigned n) const = 0; 101 | virtual uint8_t getNext() = 0; 102 | virtual void skipBytes(unsigned n) = 0; 103 | }; 104 | Orientation getExifOrientation(Reader& jpeg); 105 | void updateDimensionsForOrientation(Orientation orientation); 106 | void rotatePixels(uint8_t* pixels, int width, int height, int channels, Orientation orientation); 107 | #endif 108 | cairo_status_t loadBMPFromBuffer(uint8_t *buf, unsigned len); 109 | cairo_status_t loadBMP(FILE *stream); 110 | CanvasError errorInfo; 111 | void loaded(); 112 | cairo_status_t load(); 113 | ~Image(); 114 | 115 | enum { 116 | DEFAULT 117 | , LOADING 118 | , COMPLETE 119 | } state; 120 | 121 | enum data_mode_t { 122 | DATA_IMAGE = 1 123 | , DATA_MIME = 2 124 | } data_mode; 125 | 126 | typedef enum { 127 | UNKNOWN 128 | , GIF 129 | , JPEG 130 | , PNG 131 | , SVG 132 | } type; 133 | 134 | static type extension(const char *filename); 135 | 136 | private: 137 | cairo_surface_t *_surface; 138 | uint8_t *_data = nullptr; 139 | int _data_len; 140 | #ifdef HAVE_RSVG 141 | RsvgHandle *_rsvg; 142 | bool _is_svg; 143 | int _svg_last_width; 144 | int _svg_last_height; 145 | #endif 146 | }; 147 | -------------------------------------------------------------------------------- /src/ImageData.cc: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #include "ImageData.h" 4 | #include "InstanceData.h" 5 | 6 | /* 7 | * Initialize ImageData. 8 | */ 9 | 10 | void 11 | ImageData::Initialize(Napi::Env& env, Napi::Object& exports) { 12 | Napi::HandleScope scope(env); 13 | 14 | InstanceData *data = env.GetInstanceData(); 15 | 16 | Napi::Function ctor = DefineClass(env, "ImageData", { 17 | InstanceAccessor<&ImageData::GetWidth>("width", napi_default_jsproperty), 18 | InstanceAccessor<&ImageData::GetHeight>("height", napi_default_jsproperty) 19 | }); 20 | 21 | exports.Set("ImageData", ctor); 22 | data->ImageDataCtor = Napi::Persistent(ctor); 23 | } 24 | 25 | /* 26 | * Initialize a new ImageData object. 27 | */ 28 | 29 | ImageData::ImageData(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { 30 | Napi::TypedArray dataArray; 31 | uint32_t width; 32 | uint32_t height; 33 | int length; 34 | 35 | if (info[0].IsNumber() && info[1].IsNumber()) { 36 | width = info[0].As().Uint32Value(); 37 | if (width == 0) { 38 | Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException(); 39 | return; 40 | } 41 | height = info[1].As().Uint32Value(); 42 | if (height == 0) { 43 | Napi::RangeError::New(env, "The source height is zero.").ThrowAsJavaScriptException(); 44 | return; 45 | } 46 | length = width * height * 4; // ImageData(w, h) constructor assumes 4 BPP; documented. 47 | 48 | dataArray = Napi::Uint8Array::New(env, length, napi_uint8_clamped_array); 49 | } else if ( 50 | info[0].IsTypedArray() && 51 | info[0].As().TypedArrayType() == napi_uint8_clamped_array && 52 | info[1].IsNumber() 53 | ) { 54 | dataArray = info[0].As(); 55 | 56 | length = dataArray.ElementLength(); 57 | if (length == 0) { 58 | Napi::RangeError::New(env, "The input data has a zero byte length.").ThrowAsJavaScriptException(); 59 | return; 60 | } 61 | 62 | // Don't assert that the ImageData length is a multiple of four because some 63 | // data formats are not 4 BPP. 64 | 65 | width = info[1].As().Uint32Value(); 66 | if (width == 0) { 67 | Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException(); 68 | return; 69 | } 70 | 71 | // Don't assert that the byte length is a multiple of 4 * width, ditto. 72 | 73 | if (info[2].IsNumber()) { // Explicit height given 74 | height = info[2].As().Uint32Value(); 75 | } else { // Calculate height assuming 4 BPP 76 | int size = length / 4; 77 | height = size / width; 78 | } 79 | } else if ( 80 | info[0].IsTypedArray() && 81 | info[0].As().TypedArrayType() == napi_uint16_array && 82 | info[1].IsNumber() 83 | ) { // Intended for RGB16_565 format 84 | dataArray = info[0].As(); 85 | 86 | length = dataArray.ElementLength(); 87 | if (length == 0) { 88 | Napi::RangeError::New(env, "The input data has a zero byte length.").ThrowAsJavaScriptException(); 89 | return; 90 | } 91 | 92 | width = info[1].As().Uint32Value(); 93 | if (width == 0) { 94 | Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException(); 95 | return; 96 | } 97 | 98 | if (info[2].IsNumber()) { // Explicit height given 99 | height = info[2].As().Uint32Value(); 100 | } else { // Calculate height assuming 2 BPP 101 | int size = length / 2; 102 | height = size / width; 103 | } 104 | } else { 105 | Napi::TypeError::New(env, "Expected (Uint8ClampedArray, width[, height]), (Uint16Array, width[, height]) or (width, height)").ThrowAsJavaScriptException(); 106 | return; 107 | } 108 | 109 | _width = width; 110 | _height = height; 111 | _data = dataArray.As().Data(); 112 | 113 | info.This().As().Set("data", dataArray); 114 | } 115 | 116 | /* 117 | * Get width. 118 | */ 119 | 120 | Napi::Value 121 | ImageData::GetWidth(const Napi::CallbackInfo& info) { 122 | return Napi::Number::New(env, width()); 123 | } 124 | 125 | /* 126 | * Get height. 127 | */ 128 | 129 | Napi::Value 130 | ImageData::GetHeight(const Napi::CallbackInfo& info) { 131 | return Napi::Number::New(env, height()); 132 | } 133 | -------------------------------------------------------------------------------- /src/ImageData.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #pragma once 4 | 5 | #include 6 | #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 7 | 8 | class ImageData : public Napi::ObjectWrap { 9 | public: 10 | static void Initialize(Napi::Env& env, Napi::Object& exports); 11 | ImageData(const Napi::CallbackInfo& info); 12 | Napi::Value GetWidth(const Napi::CallbackInfo& info); 13 | Napi::Value GetHeight(const Napi::CallbackInfo& info); 14 | 15 | inline int width() { return _width; } 16 | inline int height() { return _height; } 17 | inline uint8_t *data() { return _data; } 18 | 19 | Napi::Env env; 20 | 21 | private: 22 | int _width; 23 | int _height; 24 | uint8_t *_data; 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /src/InstanceData.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct InstanceData { 4 | Napi::FunctionReference ImageBackendCtor; 5 | Napi::FunctionReference PdfBackendCtor; 6 | Napi::FunctionReference SvgBackendCtor; 7 | Napi::FunctionReference CanvasCtor; 8 | Napi::FunctionReference CanvasGradientCtor; 9 | Napi::FunctionReference DOMMatrixCtor; 10 | Napi::FunctionReference ImageCtor; 11 | Napi::FunctionReference parseFont; 12 | Napi::FunctionReference Context2dCtor; 13 | Napi::FunctionReference ImageDataCtor; 14 | Napi::FunctionReference CanvasPatternCtor; 15 | }; 16 | -------------------------------------------------------------------------------- /src/JPEGStream.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "closure.h" 4 | #include 5 | #include 6 | 7 | /* 8 | * Expanded data destination object for closure output, 9 | * inspired by IJG's jdatadst.c 10 | */ 11 | 12 | struct closure_destination_mgr { 13 | jpeg_destination_mgr pub; 14 | JpegClosure* closure; 15 | JOCTET *buffer; 16 | int bufsize; 17 | }; 18 | 19 | void 20 | init_closure_destination(j_compress_ptr cinfo){ 21 | // we really don't have to do anything here 22 | } 23 | 24 | boolean 25 | empty_closure_output_buffer(j_compress_ptr cinfo){ 26 | closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest; 27 | Napi::Env env = dest->closure->canvas->Env(); 28 | Napi::HandleScope scope(env); 29 | Napi::AsyncContext async(env, "canvas:empty_closure_output_buffer"); 30 | 31 | Napi::Object buf = Napi::Buffer::New(env, (char *)dest->buffer, dest->bufsize); 32 | 33 | // emit "data" 34 | dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async); 35 | 36 | dest->buffer = (JOCTET *)malloc(dest->bufsize); 37 | cinfo->dest->next_output_byte = dest->buffer; 38 | cinfo->dest->free_in_buffer = dest->bufsize; 39 | return true; 40 | } 41 | 42 | void 43 | term_closure_destination(j_compress_ptr cinfo){ 44 | closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest; 45 | Napi::Env env = dest->closure->canvas->Env(); 46 | Napi::HandleScope scope(env); 47 | Napi::AsyncContext async(env, "canvas:term_closure_destination"); 48 | 49 | /* emit remaining data */ 50 | Napi::Object buf = Napi::Buffer::New(env, (char *)dest->buffer, dest->bufsize - dest->pub.free_in_buffer); 51 | 52 | dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async); 53 | 54 | // emit "end" 55 | dest->closure->cb.MakeCallback(env.Global(), {env.Null(), env.Null()}, async); 56 | } 57 | 58 | void 59 | jpeg_closure_dest(j_compress_ptr cinfo, JpegClosure* closure, int bufsize){ 60 | closure_destination_mgr * dest; 61 | 62 | /* The destination object is made permanent so that multiple JPEG images 63 | * can be written to the same buffer without re-executing jpeg_mem_dest. 64 | */ 65 | if (cinfo->dest == NULL) { /* first time for this JPEG object? */ 66 | cinfo->dest = (struct jpeg_destination_mgr *) 67 | (*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_PERMANENT, 68 | sizeof(closure_destination_mgr)); 69 | } 70 | 71 | dest = (closure_destination_mgr *) cinfo->dest; 72 | 73 | cinfo->dest->init_destination = &init_closure_destination; 74 | cinfo->dest->empty_output_buffer = &empty_closure_output_buffer; 75 | cinfo->dest->term_destination = &term_closure_destination; 76 | 77 | dest->closure = closure; 78 | dest->bufsize = bufsize; 79 | dest->buffer = (JOCTET *)malloc(bufsize); 80 | 81 | cinfo->dest->next_output_byte = dest->buffer; 82 | cinfo->dest->free_in_buffer = dest->bufsize; 83 | } 84 | 85 | void encode_jpeg(jpeg_compress_struct cinfo, cairo_surface_t *surface, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor) { 86 | int w = cairo_image_surface_get_width(surface); 87 | int h = cairo_image_surface_get_height(surface); 88 | 89 | cinfo.in_color_space = JCS_RGB; 90 | cinfo.input_components = 3; 91 | cinfo.image_width = w; 92 | cinfo.image_height = h; 93 | jpeg_set_defaults(&cinfo); 94 | if (progressive) 95 | jpeg_simple_progression(&cinfo); 96 | jpeg_set_quality(&cinfo, quality, (quality < 25) ? 0 : 1); 97 | cinfo.comp_info[0].h_samp_factor = chromaHSampFactor; 98 | cinfo.comp_info[0].v_samp_factor = chromaVSampFactor; 99 | 100 | JSAMPROW slr; 101 | jpeg_start_compress(&cinfo, TRUE); 102 | unsigned char *dst; 103 | unsigned int *src = (unsigned int *)cairo_image_surface_get_data(surface); 104 | int sl = 0; 105 | dst = (unsigned char *)malloc(w * 3); 106 | while (sl < h) { 107 | unsigned char *dp = dst; 108 | int x = 0; 109 | while (x < w) { 110 | dp[0] = (*src >> 16) & 255; 111 | dp[1] = (*src >> 8) & 255; 112 | dp[2] = *src & 255; 113 | src++; 114 | dp += 3; 115 | x++; 116 | } 117 | slr = dst; 118 | jpeg_write_scanlines(&cinfo, &slr, 1); 119 | sl++; 120 | } 121 | free(dst); 122 | jpeg_finish_compress(&cinfo); 123 | jpeg_destroy_compress(&cinfo); 124 | } 125 | 126 | void 127 | write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, JpegClosure* closure) { 128 | jpeg_compress_struct cinfo; 129 | jpeg_error_mgr jerr; 130 | cinfo.err = jpeg_std_error(&jerr); 131 | jpeg_create_compress(&cinfo); 132 | jpeg_closure_dest(&cinfo, closure, bufsize); 133 | encode_jpeg( 134 | cinfo, 135 | surface, 136 | closure->quality, 137 | closure->progressive, 138 | closure->chromaSubsampling, 139 | closure->chromaSubsampling); 140 | } 141 | 142 | void 143 | write_to_jpeg_buffer(cairo_surface_t* surface, JpegClosure* closure) { 144 | jpeg_compress_struct cinfo; 145 | jpeg_error_mgr jerr; 146 | cinfo.err = jpeg_std_error(&jerr); 147 | jpeg_create_compress(&cinfo); 148 | cinfo.client_data = closure; 149 | cinfo.dest = closure->jpeg_dest_mgr; 150 | encode_jpeg( 151 | cinfo, 152 | surface, 153 | closure->quality, 154 | closure->progressive, 155 | closure->chromaSubsampling, 156 | closure->chromaSubsampling); 157 | } 158 | -------------------------------------------------------------------------------- /src/PNG.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "closure.h" 5 | #include // round 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #if defined(__GNUC__) && (__GNUC__ > 2) && defined(__OPTIMIZE__) 12 | #define likely(expr) (__builtin_expect (!!(expr), 1)) 13 | #define unlikely(expr) (__builtin_expect (!!(expr), 0)) 14 | #else 15 | #define likely(expr) (expr) 16 | #define unlikely(expr) (expr) 17 | #endif 18 | 19 | static void canvas_png_flush(png_structp png_ptr) { 20 | /* Do nothing; fflush() is said to be just a waste of energy. */ 21 | (void) png_ptr; /* Stifle compiler warning */ 22 | } 23 | 24 | /* Converts native endian xRGB => RGBx bytes */ 25 | static void canvas_convert_data_to_bytes(png_structp png, png_row_infop row_info, png_bytep data) { 26 | unsigned int i; 27 | 28 | for (i = 0; i < row_info->rowbytes; i += 4) { 29 | uint8_t *b = &data[i]; 30 | uint32_t pixel; 31 | 32 | memcpy(&pixel, b, sizeof (uint32_t)); 33 | 34 | b[0] = (pixel & 0xff0000) >> 16; 35 | b[1] = (pixel & 0x00ff00) >> 8; 36 | b[2] = (pixel & 0x0000ff) >> 0; 37 | b[3] = 0; 38 | } 39 | } 40 | 41 | /* Unpremultiplies data and converts native endian ARGB => RGBA bytes */ 42 | static void canvas_unpremultiply_data(png_structp png, png_row_infop row_info, png_bytep data) { 43 | unsigned int i; 44 | 45 | for (i = 0; i < row_info->rowbytes; i += 4) { 46 | uint8_t *b = &data[i]; 47 | uint32_t pixel; 48 | uint8_t alpha; 49 | 50 | memcpy(&pixel, b, sizeof (uint32_t)); 51 | alpha = (pixel & 0xff000000) >> 24; 52 | if (alpha == 0) { 53 | b[0] = b[1] = b[2] = b[3] = 0; 54 | } else { 55 | b[0] = (((pixel & 0xff0000) >> 16) * 255 + alpha / 2) / alpha; 56 | b[1] = (((pixel & 0x00ff00) >> 8) * 255 + alpha / 2) / alpha; 57 | b[2] = (((pixel & 0x0000ff) >> 0) * 255 + alpha / 2) / alpha; 58 | b[3] = alpha; 59 | } 60 | } 61 | } 62 | 63 | /* Converts RGB16_565 format data to RGBA32 */ 64 | static void canvas_convert_565_to_888(png_structp png, png_row_infop row_info, png_bytep data) { 65 | // Loop in reverse to unpack in-place. 66 | for (ptrdiff_t col = row_info->width - 1; col >= 0; col--) { 67 | uint8_t* src = &data[col * sizeof(uint16_t)]; 68 | uint8_t* dst = &data[col * 3]; 69 | uint16_t pixel; 70 | 71 | memcpy(&pixel, src, sizeof(uint16_t)); 72 | 73 | // Convert and rescale to the full 0-255 range 74 | // See http://stackoverflow.com/a/29326693 75 | const uint8_t red5 = (pixel & 0xF800) >> 11; 76 | const uint8_t green6 = (pixel & 0x7E0) >> 5; 77 | const uint8_t blue5 = (pixel & 0x001F); 78 | 79 | dst[0] = ((red5 * 255 + 15) / 31); 80 | dst[1] = ((green6 * 255 + 31) / 63); 81 | dst[2] = ((blue5 * 255 + 15) / 31); 82 | } 83 | } 84 | 85 | struct canvas_png_write_closure_t { 86 | cairo_write_func_t write_func; 87 | PngClosure* closure; 88 | }; 89 | 90 | #ifdef PNG_SETJMP_SUPPORTED 91 | bool setjmp_wrapper(png_structp png) { 92 | return setjmp(png_jmpbuf(png)); 93 | } 94 | #endif 95 | 96 | static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr write_func, canvas_png_write_closure_t *closure) { 97 | unsigned int i; 98 | cairo_status_t status = CAIRO_STATUS_SUCCESS; 99 | uint8_t *data; 100 | png_structp png; 101 | png_infop info; 102 | png_bytep *volatile rows = NULL; 103 | png_color_16 white; 104 | int png_color_type; 105 | int bpc; 106 | unsigned int width = cairo_image_surface_get_width(surface); 107 | unsigned int height = cairo_image_surface_get_height(surface); 108 | 109 | data = cairo_image_surface_get_data(surface); 110 | if (data == NULL) { 111 | status = CAIRO_STATUS_SURFACE_TYPE_MISMATCH; 112 | return status; 113 | } 114 | cairo_surface_flush(surface); 115 | 116 | if (width == 0 || height == 0) { 117 | status = CAIRO_STATUS_WRITE_ERROR; 118 | return status; 119 | } 120 | 121 | rows = (png_bytep *) malloc(height * sizeof (png_byte*)); 122 | if (unlikely(rows == NULL)) { 123 | status = CAIRO_STATUS_NO_MEMORY; 124 | return status; 125 | } 126 | 127 | int stride = cairo_image_surface_get_stride(surface); 128 | for (i = 0; i < height; i++) { 129 | rows[i] = (png_byte *) data + i * stride; 130 | } 131 | 132 | #ifdef PNG_USER_MEM_SUPPORTED 133 | png = png_create_write_struct_2(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL, NULL, NULL, NULL); 134 | #else 135 | png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 136 | #endif 137 | 138 | if (unlikely(png == NULL)) { 139 | status = CAIRO_STATUS_NO_MEMORY; 140 | free(rows); 141 | return status; 142 | } 143 | 144 | info = png_create_info_struct (png); 145 | if (unlikely(info == NULL)) { 146 | status = CAIRO_STATUS_NO_MEMORY; 147 | png_destroy_write_struct(&png, &info); 148 | free(rows); 149 | return status; 150 | 151 | } 152 | 153 | #ifdef PNG_SETJMP_SUPPORTED 154 | if (setjmp_wrapper(png)) { 155 | png_destroy_write_struct(&png, &info); 156 | free(rows); 157 | return status; 158 | } 159 | #endif 160 | 161 | png_set_write_fn(png, closure, write_func, canvas_png_flush); 162 | png_set_compression_level(png, closure->closure->compressionLevel); 163 | png_set_filter(png, 0, closure->closure->filters); 164 | if (closure->closure->resolution != 0) { 165 | uint32_t res = static_cast(round(static_cast(closure->closure->resolution) * 39.3701)); 166 | png_set_pHYs(png, info, res, res, PNG_RESOLUTION_METER); 167 | } 168 | 169 | cairo_format_t format = cairo_image_surface_get_format(surface); 170 | 171 | switch (format) { 172 | case CAIRO_FORMAT_ARGB32: 173 | bpc = 8; 174 | png_color_type = PNG_COLOR_TYPE_RGB_ALPHA; 175 | break; 176 | #ifdef CAIRO_FORMAT_RGB30 177 | case CAIRO_FORMAT_RGB30: 178 | bpc = 10; 179 | png_color_type = PNG_COLOR_TYPE_RGB; 180 | break; 181 | #endif 182 | case CAIRO_FORMAT_RGB24: 183 | bpc = 8; 184 | png_color_type = PNG_COLOR_TYPE_RGB; 185 | break; 186 | case CAIRO_FORMAT_A8: 187 | bpc = 8; 188 | png_color_type = PNG_COLOR_TYPE_GRAY; 189 | break; 190 | case CAIRO_FORMAT_A1: 191 | bpc = 1; 192 | png_color_type = PNG_COLOR_TYPE_GRAY; 193 | #ifndef WORDS_BIGENDIAN 194 | png_set_packswap(png); 195 | #endif 196 | break; 197 | case CAIRO_FORMAT_RGB16_565: 198 | bpc = 8; // 565 gets upconverted to 888 199 | png_color_type = PNG_COLOR_TYPE_RGB; 200 | break; 201 | case CAIRO_FORMAT_INVALID: 202 | default: 203 | status = CAIRO_STATUS_INVALID_FORMAT; 204 | png_destroy_write_struct(&png, &info); 205 | free(rows); 206 | return status; 207 | } 208 | 209 | if ((format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) && 210 | closure->closure->palette != NULL) { 211 | png_color_type = PNG_COLOR_TYPE_PALETTE; 212 | } 213 | 214 | png_set_IHDR(png, info, width, height, bpc, png_color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); 215 | 216 | if (png_color_type == PNG_COLOR_TYPE_PALETTE) { 217 | size_t nColors = closure->closure->nPaletteColors; 218 | uint8_t* colors = closure->closure->palette; 219 | uint8_t backgroundIndex = closure->closure->backgroundIndex; 220 | png_colorp pngPalette = (png_colorp)png_malloc(png, nColors * sizeof(png_colorp)); 221 | png_bytep transparency = (png_bytep)png_malloc(png, nColors * sizeof(png_bytep)); 222 | for (i = 0; i < nColors; i++) { 223 | pngPalette[i].red = colors[4 * i]; 224 | pngPalette[i].green = colors[4 * i + 1]; 225 | pngPalette[i].blue = colors[4 * i + 2]; 226 | transparency[i] = colors[4 * i + 3]; 227 | } 228 | png_set_PLTE(png, info, pngPalette, nColors); 229 | png_set_tRNS(png, info, transparency, nColors, NULL); 230 | png_set_packing(png); // pack pixels 231 | // have libpng free palette and trans: 232 | png_data_freer(png, info, PNG_DESTROY_WILL_FREE_DATA, PNG_FREE_PLTE | PNG_FREE_TRNS); 233 | png_color_16 bkg; 234 | bkg.index = backgroundIndex; 235 | png_set_bKGD(png, info, &bkg); 236 | } 237 | 238 | if (png_color_type != PNG_COLOR_TYPE_PALETTE) { 239 | white.gray = (1 << bpc) - 1; 240 | white.red = white.blue = white.green = white.gray; 241 | png_set_bKGD(png, info, &white); 242 | } 243 | 244 | /* We have to call png_write_info() before setting up the write 245 | * transformation, since it stores data internally in 'png' 246 | * that is needed for the write transformation functions to work. 247 | */ 248 | png_write_info(png, info); 249 | if (png_color_type == PNG_COLOR_TYPE_RGB_ALPHA) { 250 | png_set_write_user_transform_fn(png, canvas_unpremultiply_data); 251 | } else if (format == CAIRO_FORMAT_RGB16_565) { 252 | png_set_write_user_transform_fn(png, canvas_convert_565_to_888); 253 | } else if (png_color_type == PNG_COLOR_TYPE_RGB) { 254 | png_set_write_user_transform_fn(png, canvas_convert_data_to_bytes); 255 | png_set_filler(png, 0, PNG_FILLER_AFTER); 256 | } 257 | 258 | png_write_image(png, rows); 259 | png_write_end(png, info); 260 | 261 | png_destroy_write_struct(&png, &info); 262 | free(rows); 263 | return status; 264 | } 265 | 266 | static void canvas_stream_write_func(png_structp png, png_bytep data, png_size_t size) { 267 | cairo_status_t status; 268 | struct canvas_png_write_closure_t *png_closure; 269 | 270 | png_closure = (struct canvas_png_write_closure_t *) png_get_io_ptr(png); 271 | status = png_closure->write_func(png_closure->closure, data, size); 272 | if (unlikely(status)) { 273 | cairo_status_t *error = (cairo_status_t *) png_get_error_ptr(png); 274 | if (*error == CAIRO_STATUS_SUCCESS) { 275 | *error = status; 276 | } 277 | png_error(png, NULL); 278 | } 279 | } 280 | 281 | static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PngClosure* closure) { 282 | struct canvas_png_write_closure_t png_closure; 283 | 284 | if (cairo_surface_status(surface)) { 285 | return cairo_surface_status(surface); 286 | } 287 | 288 | png_closure.write_func = write_func; 289 | png_closure.closure = closure; 290 | 291 | return canvas_write_png(surface, canvas_stream_write_func, &png_closure); 292 | } 293 | -------------------------------------------------------------------------------- /src/Point.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | #pragma once 3 | 4 | template 5 | class Point { 6 | public: 7 | T x, y; 8 | Point(T x=0, T y=0): x(x), y(y) {} 9 | Point(const Point&) = default; 10 | Point& operator=(const Point&) = default; 11 | }; 12 | -------------------------------------------------------------------------------- /src/Util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | inline bool streq_casein(std::string& str1, std::string& str2) { 6 | return str1.size() == str2.size() && std::equal(str1.begin(), str1.end(), str2.begin(), [](char& c1, char& c2) { 7 | return c1 == c2 || std::toupper(c1) == std::toupper(c2); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/backend/Backend.cc: -------------------------------------------------------------------------------- 1 | #include "Backend.h" 2 | #include 3 | #include 4 | 5 | Backend::Backend(std::string name, Napi::CallbackInfo& info) : name(name), env(info.Env()) { 6 | int width = 0; 7 | int height = 0; 8 | if (info[0].IsNumber()) width = info[0].As().Int32Value(); 9 | if (info[1].IsNumber()) height = info[1].As().Int32Value(); 10 | this->width = width; 11 | this->height = height; 12 | } 13 | 14 | Backend::~Backend() 15 | { 16 | Backend::destroySurface(); 17 | } 18 | 19 | void Backend::setCanvas(Canvas* _canvas) 20 | { 21 | this->canvas = _canvas; 22 | } 23 | 24 | 25 | cairo_surface_t* Backend::recreateSurface() 26 | { 27 | this->destroySurface(); 28 | 29 | return this->createSurface(); 30 | } 31 | 32 | DLL_PUBLIC cairo_surface_t* Backend::getSurface() { 33 | if (!surface) createSurface(); 34 | return surface; 35 | } 36 | 37 | void Backend::destroySurface() 38 | { 39 | if(this->surface) 40 | { 41 | cairo_surface_destroy(this->surface); 42 | this->surface = NULL; 43 | } 44 | } 45 | 46 | 47 | std::string Backend::getName() 48 | { 49 | return name; 50 | } 51 | 52 | int Backend::getWidth() 53 | { 54 | return this->width; 55 | } 56 | void Backend::setWidth(int width_) 57 | { 58 | this->width = width_; 59 | this->recreateSurface(); 60 | } 61 | 62 | int Backend::getHeight() 63 | { 64 | return this->height; 65 | } 66 | void Backend::setHeight(int height_) 67 | { 68 | this->height = height_; 69 | this->recreateSurface(); 70 | } 71 | 72 | bool Backend::isSurfaceValid(){ 73 | bool hadSurface = surface != NULL; 74 | bool isValid = true; 75 | 76 | cairo_status_t status = cairo_surface_status(getSurface()); 77 | 78 | if (status != CAIRO_STATUS_SUCCESS) { 79 | error = cairo_status_to_string(status); 80 | isValid = false; 81 | } 82 | 83 | if (!hadSurface) 84 | destroySurface(); 85 | 86 | return isValid; 87 | } 88 | 89 | 90 | BackendOperationNotAvailable::BackendOperationNotAvailable(Backend* backend, 91 | std::string operation_name) 92 | : operation_name(operation_name) 93 | { 94 | msg = "operation " + operation_name + 95 | " not supported by backend " + backend->getName(); 96 | }; 97 | 98 | BackendOperationNotAvailable::~BackendOperationNotAvailable() throw() {}; 99 | 100 | const char* BackendOperationNotAvailable::what() const throw() 101 | { 102 | return msg.c_str(); 103 | }; 104 | -------------------------------------------------------------------------------- /src/backend/Backend.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../dll_visibility.h" 5 | #include 6 | #include 7 | #include 8 | 9 | class Canvas; 10 | 11 | class Backend 12 | { 13 | private: 14 | const std::string name; 15 | const char* error = NULL; 16 | 17 | protected: 18 | int width; 19 | int height; 20 | cairo_surface_t* surface = nullptr; 21 | Canvas* canvas = nullptr; 22 | 23 | Backend(std::string name, Napi::CallbackInfo& info); 24 | 25 | public: 26 | Napi::Env env; 27 | 28 | virtual ~Backend(); 29 | 30 | void setCanvas(Canvas* canvas); 31 | 32 | virtual cairo_surface_t* createSurface() = 0; 33 | virtual cairo_surface_t* recreateSurface(); 34 | 35 | DLL_PUBLIC cairo_surface_t* getSurface(); 36 | virtual void destroySurface(); 37 | 38 | DLL_PUBLIC std::string getName(); 39 | 40 | DLL_PUBLIC int getWidth(); 41 | virtual void setWidth(int width); 42 | 43 | DLL_PUBLIC int getHeight(); 44 | virtual void setHeight(int height); 45 | 46 | // Overridden by ImageBackend. SVG and PDF thus always return INVALID. 47 | virtual cairo_format_t getFormat() { 48 | return CAIRO_FORMAT_INVALID; 49 | } 50 | 51 | bool isSurfaceValid(); 52 | inline const char* getError(){ return error; } 53 | }; 54 | 55 | 56 | class BackendOperationNotAvailable: public std::exception 57 | { 58 | private: 59 | std::string operation_name; 60 | std::string msg; 61 | 62 | public: 63 | BackendOperationNotAvailable(Backend* backend, std::string operation_name); 64 | ~BackendOperationNotAvailable() throw(); 65 | 66 | const char* what() const throw(); 67 | }; 68 | -------------------------------------------------------------------------------- /src/backend/ImageBackend.cc: -------------------------------------------------------------------------------- 1 | #include "ImageBackend.h" 2 | #include "../InstanceData.h" 3 | #include 4 | #include 5 | 6 | ImageBackend::ImageBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("image", info) 7 | { 8 | } 9 | 10 | // This returns an approximate value only, suitable for 11 | // Napi::MemoryManagement:: AdjustExternalMemory. 12 | // The formats that don't map to intrinsic types (RGB30, A1) round up. 13 | int32_t ImageBackend::approxBytesPerPixel() { 14 | switch (format) { 15 | case CAIRO_FORMAT_ARGB32: 16 | case CAIRO_FORMAT_RGB24: 17 | return 4; 18 | #ifdef CAIRO_FORMAT_RGB30 19 | case CAIRO_FORMAT_RGB30: 20 | return 3; 21 | #endif 22 | case CAIRO_FORMAT_RGB16_565: 23 | return 2; 24 | case CAIRO_FORMAT_A8: 25 | case CAIRO_FORMAT_A1: 26 | return 1; 27 | default: 28 | return 0; 29 | } 30 | } 31 | 32 | cairo_surface_t* ImageBackend::createSurface() { 33 | assert(!surface); 34 | surface = cairo_image_surface_create(format, width, height); 35 | assert(surface); 36 | Napi::MemoryManagement::AdjustExternalMemory(env, approxBytesPerPixel() * width * height); 37 | return surface; 38 | } 39 | 40 | void ImageBackend::destroySurface() { 41 | if (surface) { 42 | cairo_surface_destroy(surface); 43 | surface = nullptr; 44 | Napi::MemoryManagement::AdjustExternalMemory(env, -approxBytesPerPixel() * width * height); 45 | } 46 | } 47 | 48 | cairo_format_t ImageBackend::getFormat() { 49 | return format; 50 | } 51 | 52 | void ImageBackend::setFormat(cairo_format_t _format) { 53 | this->format = _format; 54 | } 55 | 56 | Napi::FunctionReference ImageBackend::constructor; 57 | 58 | void ImageBackend::Initialize(Napi::Object target) { 59 | Napi::Env env = target.Env(); 60 | Napi::Function ctor = DefineClass(env, "ImageBackend", {}); 61 | InstanceData* data = env.GetInstanceData(); 62 | data->ImageBackendCtor = Napi::Persistent(ctor); 63 | } 64 | -------------------------------------------------------------------------------- /src/backend/ImageBackend.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Backend.h" 4 | #include 5 | 6 | class ImageBackend : public Napi::ObjectWrap, public Backend 7 | { 8 | private: 9 | cairo_surface_t* createSurface(); 10 | void destroySurface(); 11 | cairo_format_t format = DEFAULT_FORMAT; 12 | 13 | public: 14 | ImageBackend(Napi::CallbackInfo& info); 15 | 16 | cairo_format_t getFormat(); 17 | void setFormat(cairo_format_t format); 18 | 19 | int32_t approxBytesPerPixel(); 20 | 21 | static Napi::FunctionReference constructor; 22 | static void Initialize(Napi::Object target); 23 | const static cairo_format_t DEFAULT_FORMAT = CAIRO_FORMAT_ARGB32; 24 | }; 25 | -------------------------------------------------------------------------------- /src/backend/PdfBackend.cc: -------------------------------------------------------------------------------- 1 | #include "PdfBackend.h" 2 | 3 | #include 4 | #include "../InstanceData.h" 5 | #include "../Canvas.h" 6 | #include "../closure.h" 7 | 8 | PdfBackend::PdfBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("pdf", info) { 9 | PdfBackend::createSurface(); 10 | } 11 | 12 | PdfBackend::~PdfBackend() { 13 | cairo_surface_finish(surface); 14 | if (_closure) delete _closure; 15 | destroySurface(); 16 | } 17 | 18 | cairo_surface_t* PdfBackend::createSurface() { 19 | if (!_closure) _closure = new PdfSvgClosure(canvas); 20 | surface = cairo_pdf_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); 21 | return surface; 22 | } 23 | 24 | cairo_surface_t* PdfBackend::recreateSurface() { 25 | cairo_pdf_surface_set_size(surface, width, height); 26 | 27 | return surface; 28 | } 29 | 30 | void 31 | PdfBackend::Initialize(Napi::Object target) { 32 | Napi::Env env = target.Env(); 33 | InstanceData* data = env.GetInstanceData(); 34 | Napi::Function ctor = DefineClass(env, "PdfBackend", {}); 35 | data->PdfBackendCtor = Napi::Persistent(ctor); 36 | } 37 | -------------------------------------------------------------------------------- /src/backend/PdfBackend.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Backend.h" 4 | #include "../closure.h" 5 | #include 6 | 7 | class PdfBackend : public Napi::ObjectWrap, public Backend 8 | { 9 | private: 10 | cairo_surface_t* createSurface(); 11 | cairo_surface_t* recreateSurface(); 12 | 13 | public: 14 | PdfSvgClosure* _closure = NULL; 15 | inline PdfSvgClosure* closure() { return _closure; } 16 | 17 | PdfBackend(Napi::CallbackInfo& info); 18 | ~PdfBackend(); 19 | 20 | static Napi::FunctionReference constructor; 21 | static void Initialize(Napi::Object target); 22 | static Napi::Value New(const Napi::CallbackInfo& info); 23 | }; 24 | -------------------------------------------------------------------------------- /src/backend/SvgBackend.cc: -------------------------------------------------------------------------------- 1 | #include "SvgBackend.h" 2 | 3 | #include 4 | #include 5 | #include "../Canvas.h" 6 | #include "../closure.h" 7 | #include "../InstanceData.h" 8 | #include 9 | 10 | using namespace Napi; 11 | 12 | SvgBackend::SvgBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("svg", info) { 13 | SvgBackend::createSurface(); 14 | } 15 | 16 | SvgBackend::~SvgBackend() { 17 | cairo_surface_finish(surface); 18 | if (_closure) { 19 | delete _closure; 20 | _closure = nullptr; 21 | } 22 | destroySurface(); 23 | } 24 | 25 | cairo_surface_t* SvgBackend::createSurface() { 26 | assert(!_closure); 27 | _closure = new PdfSvgClosure(canvas); 28 | surface = cairo_svg_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); 29 | return surface; 30 | } 31 | 32 | cairo_surface_t* SvgBackend::recreateSurface() { 33 | cairo_surface_finish(surface); 34 | delete _closure; 35 | _closure = nullptr; 36 | cairo_surface_destroy(surface); 37 | 38 | return createSurface(); 39 | } 40 | 41 | 42 | void 43 | SvgBackend::Initialize(Napi::Object target) { 44 | Napi::Env env = target.Env(); 45 | Napi::Function ctor = DefineClass(env, "SvgBackend", {}); 46 | InstanceData* data = env.GetInstanceData(); 47 | data->SvgBackendCtor = Napi::Persistent(ctor); 48 | } 49 | -------------------------------------------------------------------------------- /src/backend/SvgBackend.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Backend.h" 4 | #include "../closure.h" 5 | #include 6 | 7 | class SvgBackend : public Napi::ObjectWrap, public Backend 8 | { 9 | private: 10 | cairo_surface_t* createSurface(); 11 | cairo_surface_t* recreateSurface(); 12 | 13 | public: 14 | PdfSvgClosure* _closure = NULL; 15 | inline PdfSvgClosure* closure() { return _closure; } 16 | 17 | SvgBackend(Napi::CallbackInfo& info); 18 | ~SvgBackend(); 19 | 20 | static void Initialize(Napi::Object target); 21 | }; 22 | -------------------------------------------------------------------------------- /src/bmp/BMPParser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef ERROR 4 | #define ERROR_ ERROR 5 | #undef ERROR 6 | #endif 7 | 8 | #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 9 | #include 10 | 11 | namespace BMPParser{ 12 | enum Status{ 13 | EMPTY, 14 | OK, 15 | ERROR, 16 | }; 17 | 18 | class Parser{ 19 | public: 20 | Parser()=default; 21 | ~Parser(); 22 | void parse(uint8_t *buf, int bufSize, uint8_t *format=nullptr); 23 | void clearImgd(); 24 | int32_t getWidth() const; 25 | int32_t getHeight() const; 26 | uint8_t *getImgd() const; 27 | Status getStatus() const; 28 | std::string getErrMsg() const; 29 | 30 | private: 31 | Status status = Status::EMPTY; 32 | uint8_t *data = nullptr; 33 | uint8_t *ptr = nullptr; 34 | int len = 0; 35 | int32_t w = 0; 36 | int32_t h = 0; 37 | uint8_t *imgd = nullptr; 38 | std::string err = ""; 39 | std::string op = ""; 40 | 41 | template inline T get(); 42 | template inline T get(uint8_t* pointer); 43 | std::string getStr(int len, bool reverse=false); 44 | inline void skip(int len); 45 | void calcMaskShift(uint32_t& shift, uint32_t& mask, double& multp); 46 | 47 | void setOp(std::string val); 48 | std::string getOp() const; 49 | 50 | void setErrUnsupported(std::string msg); 51 | void setErrUnknown(std::string msg); 52 | void setErr(std::string msg); 53 | std::string getErr() const; 54 | }; 55 | } 56 | 57 | #ifdef ERROR_ 58 | #define ERROR ERROR_ 59 | #undef ERROR_ 60 | #endif 61 | -------------------------------------------------------------------------------- /src/bmp/LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /src/closure.cc: -------------------------------------------------------------------------------- 1 | #include "closure.h" 2 | #include "Canvas.h" 3 | 4 | #ifdef HAVE_JPEG 5 | void JpegClosure::init_destination(j_compress_ptr cinfo) { 6 | JpegClosure* closure = (JpegClosure*)cinfo->client_data; 7 | closure->vec.resize(PAGE_SIZE); 8 | closure->jpeg_dest_mgr->next_output_byte = &closure->vec[0]; 9 | closure->jpeg_dest_mgr->free_in_buffer = closure->vec.size(); 10 | } 11 | 12 | boolean JpegClosure::empty_output_buffer(j_compress_ptr cinfo) { 13 | JpegClosure* closure = (JpegClosure*)cinfo->client_data; 14 | size_t currentSize = closure->vec.size(); 15 | closure->vec.resize(currentSize * 1.5); 16 | closure->jpeg_dest_mgr->next_output_byte = &closure->vec[currentSize]; 17 | closure->jpeg_dest_mgr->free_in_buffer = closure->vec.size() - currentSize; 18 | return true; 19 | } 20 | 21 | void JpegClosure::term_destination(j_compress_ptr cinfo) { 22 | JpegClosure* closure = (JpegClosure*)cinfo->client_data; 23 | size_t finalSize = closure->vec.size() - closure->jpeg_dest_mgr->free_in_buffer; 24 | closure->vec.resize(finalSize); 25 | } 26 | #endif 27 | 28 | void 29 | EncodingWorker::Init(void (*work_fn)(Closure*), Closure* closure) { 30 | this->work_fn = work_fn; 31 | this->closure = closure; 32 | } 33 | 34 | void 35 | EncodingWorker::Execute() { 36 | this->work_fn(this->closure); 37 | } 38 | 39 | void 40 | EncodingWorker::OnWorkComplete(Napi::Env env, napi_status status) { 41 | Napi::HandleScope scope(env); 42 | 43 | if (closure->status) { 44 | closure->cb.Call({ closure->canvas->CairoError(closure->status).Value() }); 45 | } else { 46 | Napi::Object buf = Napi::Buffer::Copy(env, &closure->vec[0], closure->vec.size()); 47 | closure->cb.Call({ env.Null(), buf }); 48 | } 49 | 50 | closure->canvas->Unref(); 51 | delete closure; 52 | } 53 | -------------------------------------------------------------------------------- /src/closure.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #pragma once 4 | 5 | #include "Canvas.h" 6 | 7 | #ifdef HAVE_JPEG 8 | #include 9 | #endif 10 | 11 | #include 12 | #include 13 | #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 14 | #include 15 | 16 | #ifndef PAGE_SIZE 17 | #define PAGE_SIZE 4096 18 | #endif 19 | 20 | /* 21 | * Image encoding closures. 22 | */ 23 | 24 | struct Closure { 25 | std::vector vec; 26 | Napi::FunctionReference cb; 27 | Canvas* canvas = nullptr; 28 | cairo_status_t status = CAIRO_STATUS_SUCCESS; 29 | 30 | static cairo_status_t writeVec(void *c, const uint8_t *odata, unsigned len) { 31 | Closure* closure = static_cast(c); 32 | try { 33 | closure->vec.insert(closure->vec.end(), odata, odata + len); 34 | } catch (const std::bad_alloc &) { 35 | return CAIRO_STATUS_NO_MEMORY; 36 | } 37 | return CAIRO_STATUS_SUCCESS; 38 | } 39 | 40 | Closure(Canvas* canvas) : canvas(canvas) {}; 41 | }; 42 | 43 | struct PdfSvgClosure : Closure { 44 | PdfSvgClosure(Canvas* canvas) : Closure(canvas) {}; 45 | }; 46 | 47 | struct PngClosure : Closure { 48 | uint32_t compressionLevel = 6; 49 | uint32_t filters = PNG_ALL_FILTERS; 50 | uint32_t resolution = 0; // 0 = unspecified 51 | // Indexed PNGs: 52 | uint32_t nPaletteColors = 0; 53 | uint8_t* palette = nullptr; 54 | uint8_t backgroundIndex = 0; 55 | 56 | PngClosure(Canvas* canvas) : Closure(canvas) {}; 57 | }; 58 | 59 | #ifdef HAVE_JPEG 60 | struct JpegClosure : Closure { 61 | uint32_t quality = 75; 62 | uint32_t chromaSubsampling = 2; 63 | bool progressive = false; 64 | jpeg_destination_mgr* jpeg_dest_mgr = nullptr; 65 | 66 | static void init_destination(j_compress_ptr cinfo); 67 | static boolean empty_output_buffer(j_compress_ptr cinfo); 68 | static void term_destination(j_compress_ptr cinfo); 69 | 70 | JpegClosure(Canvas* canvas) : Closure(canvas) { 71 | jpeg_dest_mgr = new jpeg_destination_mgr; 72 | jpeg_dest_mgr->init_destination = init_destination; 73 | jpeg_dest_mgr->empty_output_buffer = empty_output_buffer; 74 | jpeg_dest_mgr->term_destination = term_destination; 75 | }; 76 | 77 | ~JpegClosure() { 78 | delete jpeg_dest_mgr; 79 | } 80 | }; 81 | #endif 82 | 83 | class EncodingWorker : public Napi::AsyncWorker { 84 | public: 85 | EncodingWorker(Napi::Env env): Napi::AsyncWorker(env) {}; 86 | void Init(void (*work_fn)(Closure*), Closure* closure); 87 | void Execute() override; 88 | void OnWorkComplete(Napi::Env env, napi_status status) override; 89 | 90 | private: 91 | void (*work_fn)(Closure*) = nullptr; 92 | Closure* closure = nullptr; 93 | }; 94 | -------------------------------------------------------------------------------- /src/color.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #pragma once 4 | 5 | #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 6 | #include 7 | 8 | /* 9 | * RGBA struct. 10 | */ 11 | 12 | typedef struct { 13 | double r, g, b, a; 14 | } rgba_t; 15 | 16 | /* 17 | * Prototypes. 18 | */ 19 | 20 | rgba_t 21 | rgba_create(uint32_t rgba); 22 | 23 | int32_t 24 | rgba_from_string(const char *str, short *ok); 25 | 26 | void 27 | rgba_to_string(rgba_t rgba, char *buf, size_t len); 28 | 29 | void 30 | rgba_inspect(int32_t rgba); 31 | -------------------------------------------------------------------------------- /src/dll_visibility.h: -------------------------------------------------------------------------------- 1 | #ifndef DLL_PUBLIC 2 | 3 | #if defined _WIN32 4 | #ifdef __GNUC__ 5 | #define DLL_PUBLIC __attribute__ ((dllexport)) 6 | #else 7 | #define DLL_PUBLIC __declspec(dllexport) 8 | #endif 9 | #define DLL_LOCAL 10 | #else 11 | #if __GNUC__ >= 4 12 | #define DLL_PUBLIC __attribute__ ((visibility ("default"))) 13 | #define DLL_LOCAL __attribute__ ((visibility ("hidden"))) 14 | #else 15 | #define DLL_PUBLIC 16 | #define DLL_LOCAL 17 | #endif 18 | #endif 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/init.cc: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 LearnBoost 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1, 10, 0) 8 | // CAIRO_FORMAT_RGB16_565: undeprecated in v1.10.0 9 | // CAIRO_STATUS_INVALID_SIZE: v1.10.0 10 | // CAIRO_FORMAT_INVALID: v1.10.0 11 | // Lots of the compositing operators: v1.10.0 12 | // JPEG MIME tracking: v1.10.0 13 | // Note: CAIRO_FORMAT_RGB30 is v1.12.0 and still optional 14 | #error("cairo v1.10.0 or later is required") 15 | #endif 16 | 17 | #include "Backends.h" 18 | #include "Canvas.h" 19 | #include "CanvasGradient.h" 20 | #include "CanvasPattern.h" 21 | #include "CanvasRenderingContext2d.h" 22 | #include "Image.h" 23 | #include "ImageData.h" 24 | #include "InstanceData.h" 25 | 26 | #include 27 | #include FT_FREETYPE_H 28 | 29 | /* 30 | * Save some external modules as private references. 31 | */ 32 | 33 | static void 34 | setDOMMatrix(const Napi::CallbackInfo& info) { 35 | InstanceData* data = info.Env().GetInstanceData(); 36 | data->DOMMatrixCtor = Napi::Persistent(info[0].As()); 37 | } 38 | 39 | static void 40 | setParseFont(const Napi::CallbackInfo& info) { 41 | InstanceData* data = info.Env().GetInstanceData(); 42 | data->parseFont = Napi::Persistent(info[0].As()); 43 | } 44 | 45 | // Compatibility with Visual Studio versions prior to VS2015 46 | #if defined(_MSC_VER) && _MSC_VER < 1900 47 | #define snprintf _snprintf 48 | #endif 49 | 50 | Napi::Object init(Napi::Env env, Napi::Object exports) { 51 | env.SetInstanceData(new InstanceData()); 52 | 53 | Backends::Initialize(env, exports); 54 | Canvas::Initialize(env, exports); 55 | Image::Initialize(env, exports); 56 | ImageData::Initialize(env, exports); 57 | Context2d::Initialize(env, exports); 58 | Gradient::Initialize(env, exports); 59 | Pattern::Initialize(env, exports); 60 | 61 | exports.Set("setDOMMatrix", Napi::Function::New(env, &setDOMMatrix)); 62 | exports.Set("setParseFont", Napi::Function::New(env, &setParseFont)); 63 | 64 | exports.Set("cairoVersion", Napi::String::New(env, cairo_version_string())); 65 | #ifdef HAVE_JPEG 66 | 67 | #ifndef JPEG_LIB_VERSION_MAJOR 68 | #ifdef JPEG_LIB_VERSION 69 | #define JPEG_LIB_VERSION_MAJOR (JPEG_LIB_VERSION / 10) 70 | #else 71 | #define JPEG_LIB_VERSION_MAJOR 0 72 | #endif 73 | #endif 74 | 75 | #ifndef JPEG_LIB_VERSION_MINOR 76 | #ifdef JPEG_LIB_VERSION 77 | #define JPEG_LIB_VERSION_MINOR (JPEG_LIB_VERSION % 10) 78 | #else 79 | #define JPEG_LIB_VERSION_MINOR 0 80 | #endif 81 | #endif 82 | 83 | char jpeg_version[10]; 84 | static bool minor_gt_0 = JPEG_LIB_VERSION_MINOR > 0; 85 | if (minor_gt_0) { 86 | snprintf(jpeg_version, 10, "%d%c", JPEG_LIB_VERSION_MAJOR, JPEG_LIB_VERSION_MINOR + 'a' - 1); 87 | } else { 88 | snprintf(jpeg_version, 10, "%d", JPEG_LIB_VERSION_MAJOR); 89 | } 90 | exports.Set("jpegVersion", Napi::String::New(env, jpeg_version)); 91 | #endif 92 | 93 | #ifdef HAVE_GIF 94 | #ifndef GIF_LIB_VERSION 95 | char gif_version[10]; 96 | snprintf(gif_version, 10, "%d.%d.%d", GIFLIB_MAJOR, GIFLIB_MINOR, GIFLIB_RELEASE); 97 | exports.Set("gifVersion", Napi::String::New(env, gif_version)); 98 | #else 99 | exports.Set("gifVersion", Napi::String::New(env, GIF_LIB_VERSION)); 100 | #endif 101 | #endif 102 | 103 | #ifdef HAVE_RSVG 104 | exports.Set("rsvgVersion", Napi::String::New(env, LIBRSVG_VERSION)); 105 | #endif 106 | 107 | exports.Set("pangoVersion", Napi::String::New(env, PANGO_VERSION_STRING)); 108 | 109 | char freetype_version[10]; 110 | snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); 111 | exports.Set("freetypeVersion", Napi::String::New(env, freetype_version)); 112 | 113 | return exports; 114 | } 115 | 116 | NODE_API_MODULE(canvas, init); 117 | -------------------------------------------------------------------------------- /src/register_font.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | PangoFontDescription *get_pango_font_description(unsigned char *filepath); 6 | bool register_font(unsigned char *filepath); 7 | bool deregister_font(unsigned char *filepath); 8 | -------------------------------------------------------------------------------- /test/fixtures/159-crash1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/159-crash1.jpg -------------------------------------------------------------------------------- /test/fixtures/bmp/1-bit.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/bmp/1-bit.bmp -------------------------------------------------------------------------------- /test/fixtures/bmp/24-bit.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/bmp/24-bit.bmp -------------------------------------------------------------------------------- /test/fixtures/bmp/32-bit.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/bmp/32-bit.bmp -------------------------------------------------------------------------------- /test/fixtures/bmp/4-bit.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/bmp/4-bit.bmp -------------------------------------------------------------------------------- /test/fixtures/bmp/bomb.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/bmp/bomb.bmp -------------------------------------------------------------------------------- /test/fixtures/bmp/min.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/bmp/min.bmp -------------------------------------------------------------------------------- /test/fixtures/bmp/negative-height.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/bmp/negative-height.bmp -------------------------------------------------------------------------------- /test/fixtures/bmp/palette.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/bmp/palette.bmp -------------------------------------------------------------------------------- /test/fixtures/bmp/v3-header.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/bmp/v3-header.bmp -------------------------------------------------------------------------------- /test/fixtures/checkers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/checkers.png -------------------------------------------------------------------------------- /test/fixtures/chrome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/chrome.jpg -------------------------------------------------------------------------------- /test/fixtures/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/clock.png -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-f1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-f1.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-f2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-f2.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-f3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-f3.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-f4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-f4.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-f5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-f5.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-f6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-f6.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-f7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-f7.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-f8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-f8.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-fi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-fi.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-fm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-fm.jpg -------------------------------------------------------------------------------- /test/fixtures/exif-orientation-fn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/exif-orientation-fn.jpg -------------------------------------------------------------------------------- /test/fixtures/existing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/existing.png -------------------------------------------------------------------------------- /test/fixtures/face.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/face.jpeg -------------------------------------------------------------------------------- /test/fixtures/grayscale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/grayscale.jpg -------------------------------------------------------------------------------- /test/fixtures/halved-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/halved-1.jpeg -------------------------------------------------------------------------------- /test/fixtures/halved-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/halved-2.jpeg -------------------------------------------------------------------------------- /test/fixtures/newcontent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/newcontent.png -------------------------------------------------------------------------------- /test/fixtures/quadrants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/quadrants.png -------------------------------------------------------------------------------- /test/fixtures/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/star.png -------------------------------------------------------------------------------- /test/fixtures/state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/state.png -------------------------------------------------------------------------------- /test/fixtures/tree.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/ycck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/node-canvas/1234a86f65b2318906334ce0790cd12401c67a25/test/fixtures/ycck.jpg -------------------------------------------------------------------------------- /test/fontParser.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | 'use strict' 4 | 5 | /** 6 | * Module dependencies. 7 | */ 8 | const assert = require('assert') 9 | const {Canvas} = require('..'); 10 | 11 | const tests = [ 12 | '20px Arial', 13 | { size: 20, families: ['arial'] }, 14 | '20pt Arial', 15 | { size: 26.666667461395264, families: ['arial'] }, 16 | '20.5pt Arial', 17 | { size: 27.333334147930145, families: ['arial'] }, 18 | '20% Arial', 19 | { size: 3.1999999284744263, families: ['arial'] }, 20 | '20mm Arial', 21 | { size: 75.59999942779541, families: ['arial'] }, 22 | '20px serif', 23 | { size: 20, families: ['serif'] }, 24 | '20px sans-serif', 25 | { size: 20, families: ['sans-serif'] }, 26 | '20px monospace', 27 | { size: 20, families: ['monospace'] }, 28 | '50px Arial, sans-serif', 29 | { size: 50, families: ['arial', 'sans-serif'] }, 30 | 'bold italic 50px Arial, sans-serif', 31 | { style: 1, weight: 700, size: 50, families: ['arial', 'sans-serif'] }, 32 | '50px Helvetica , Arial, sans-serif', 33 | { size: 50, families: ['helvetica', 'arial', 'sans-serif'] }, 34 | '50px "Helvetica Neue", sans-serif', 35 | { size: 50, families: ['Helvetica Neue', 'sans-serif'] }, 36 | '50px "Helvetica Neue", "foo bar baz" , sans-serif', 37 | { size: 50, families: ['Helvetica Neue', 'foo bar baz', 'sans-serif'] }, 38 | "50px 'Helvetica Neue'", 39 | { size: 50, families: ['Helvetica Neue'] }, 40 | 'italic 20px Arial', 41 | { size: 20, style: 1, families: ['arial'] }, 42 | 'oblique 20px Arial', 43 | { size: 20, style: 2, families: ['arial'] }, 44 | 'normal 20px Arial', 45 | { size: 20, families: ['arial'] }, 46 | '300 20px Arial', 47 | { size: 20, weight: 300, families: ['arial'] }, 48 | '800 20px Arial', 49 | { size: 20, weight: 800, families: ['arial'] }, 50 | 'bolder 20px Arial', 51 | { size: 20, weight: 700, families: ['arial'] }, 52 | 'lighter 20px Arial', 53 | { size: 20, weight: 100, families: ['arial'] }, 54 | 'normal normal normal 16px Impact', 55 | { size: 16, families: ['impact'] }, 56 | 'italic small-caps bolder 16px cursive', 57 | { size: 16, style: 1, variant: 1, weight: 700, families: ['cursive'] }, 58 | '20px "new century schoolbook", serif', 59 | { size: 20, families: ['new century schoolbook', 'serif'] }, 60 | '20px "Arial bold 300"', // synthetic case with weight keyword inside family 61 | { size: 20, families: ['Arial bold 300'] }, 62 | `50px "Helvetica 'Neue'", "foo \\"bar\\" baz" , "Someone's weird \\'edge\\' case", sans-serif`, 63 | { size: 50, families: [`Helvetica 'Neue'`, 'foo "bar" baz', `Someone's weird 'edge' case`, 'sans-serif'] }, 64 | 'Helvetica, sans', 65 | undefined, 66 | '123px thefont/123abc', 67 | undefined, 68 | '123px /\tnormal thefont', 69 | {size: 123, families: ['thefont']}, 70 | '12px/1.2whoops arial', 71 | undefined, 72 | 'bold bold 12px thefont', 73 | undefined, 74 | 'italic italic 12px Arial', 75 | undefined, 76 | 'small-caps bold italic small-caps 12px Arial', 77 | undefined, 78 | 'small-caps bold oblique 12px \'A\'ri\\61l', 79 | {size: 12, style: 2, weight: 700, variant: 1, families: ['Arial']}, 80 | '12px/34% "The\\\n Word"', 81 | {size: 12, families: ['The Word']}, 82 | '', 83 | undefined, 84 | 'normal normal normal 1%/normal a , \'b\'', 85 | {size: 0.1599999964237213, families: ['a', 'b']}, 86 | 'normalnormalnormal 1px/normal a', 87 | undefined, 88 | '12px _the_font', 89 | {size: 12, families: ['_the_font']}, 90 | '9px 7 birds', 91 | undefined, 92 | '2em "Courier', 93 | undefined, 94 | `2em \\'Courier\\"`, 95 | {size: 32, families: ['\'courier"']}, 96 | '1px \\10abcde', 97 | {size: 1, families: [String.fromCodePoint(parseInt('10abcd', 16)) + 'e']}, 98 | '3E+2 1e-1px yay', 99 | {weight: 300, size: 0.1, families: ['yay']} 100 | ]; 101 | 102 | describe('Font parser', function () { 103 | for (let i = 0; i < tests.length; i++) { 104 | const str = tests[i++] 105 | it(str, function () { 106 | const expected = tests[i] 107 | const actual = Canvas.parseFont(str) 108 | 109 | if (expected) { 110 | if (expected.style == null) expected.style = 0 111 | if (expected.weight == null) expected.weight = 400 112 | if (expected.variant == null) expected.variant = 0 113 | } 114 | 115 | assert.deepEqual(actual, expected) 116 | }) 117 | } 118 | }) 119 | -------------------------------------------------------------------------------- /test/imageData.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | 'use strict' 4 | 5 | const {createImageData} = require('../') 6 | const {ImageData} = require('../') 7 | 8 | const assert = require('assert') 9 | 10 | describe('ImageData', function () { 11 | it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { 12 | assert.throws(function () { ImageData.prototype.width }, /invalid argument/i) 13 | }) 14 | 15 | it('stringifies as [object ImageData]', function () { 16 | const imageData = createImageData(2, 3) 17 | assert.strictEqual(imageData.toString(), '[object ImageData]') 18 | }) 19 | 20 | it('gives class string as `ImageData`', function () { 21 | const imageData = createImageData(2, 3) 22 | assert.strictEqual(Object.prototype.toString.call(imageData), '[object ImageData]') 23 | }) 24 | 25 | it('should throw with invalid numeric arguments', function () { 26 | assert.throws(() => { createImageData(0, 0) }, /width is zero/) 27 | assert.throws(() => { createImageData(1, 0) }, /height is zero/) 28 | assert.throws(() => { createImageData(0) }, TypeError) 29 | }) 30 | 31 | it('should construct with width and height', function () { 32 | const imageData = createImageData(2, 3) 33 | 34 | assert.strictEqual(imageData.width, 2) 35 | assert.strictEqual(imageData.height, 3) 36 | 37 | assert.ok(imageData.data instanceof Uint8ClampedArray) 38 | assert.strictEqual(imageData.data.length, 24) 39 | }) 40 | 41 | it('should throw with invalid typed array', function () { 42 | assert.throws(() => { createImageData(new Uint8ClampedArray(0), 0) }, /input data has a zero byte length/) 43 | assert.throws(() => { createImageData(new Uint8ClampedArray(3), 0) }, /source width is zero/) 44 | // Note: Some errors thrown by browsers are not thrown by node-canvas 45 | // because our ImageData can support different BPPs. 46 | }) 47 | 48 | it('should construct with Uint8ClampedArray', function () { 49 | let data, imageData 50 | 51 | data = new Uint8ClampedArray(2 * 3 * 4) 52 | imageData = createImageData(data, 2) 53 | assert.strictEqual(imageData.width, 2) 54 | assert.strictEqual(imageData.height, 3) 55 | assert(imageData.data instanceof Uint8ClampedArray) 56 | assert.strictEqual(imageData.data.length, 24) 57 | 58 | data = new Uint8ClampedArray(3 * 4 * 4) 59 | imageData = createImageData(data, 3, 4) 60 | assert.strictEqual(imageData.width, 3) 61 | assert.strictEqual(imageData.height, 4) 62 | assert(imageData.data instanceof Uint8ClampedArray) 63 | assert.strictEqual(imageData.data.length, 48) 64 | }) 65 | 66 | it('should construct with Uint16Array', function () { 67 | let data = new Uint16Array(2 * 3 * 2) 68 | let imagedata = createImageData(data, 2) 69 | assert.strictEqual(imagedata.width, 2) 70 | assert.strictEqual(imagedata.height, 3) 71 | assert(imagedata.data instanceof Uint16Array) 72 | assert.strictEqual(imagedata.data.length, 12) 73 | 74 | data = new Uint16Array(3 * 4 * 2) 75 | imagedata = createImageData(data, 3, 4) 76 | assert.strictEqual(imagedata.width, 3) 77 | assert.strictEqual(imagedata.height, 4) 78 | assert(imagedata.data instanceof Uint16Array) 79 | assert.strictEqual(imagedata.data.length, 24) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /test/public/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | node-canvas 5 | 6 | 7 | 8 | 9 |

node-canvas

10 | 11 |

12 | The tests below assert visual and api integrity by running the exact same code utilizing the client canvas api, as well as node-canvas. 13 |

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/public/app.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', runTests) 2 | 3 | function create (type, attrs, children) { 4 | const element = Object.assign(document.createElement(type), attrs) 5 | 6 | if (children) { 7 | children.forEach(function (child) { element.appendChild(child) }) 8 | } 9 | 10 | return element 11 | } 12 | 13 | function pdfLink (name) { 14 | return create('a', { 15 | href: '/pdf?name=' + encodeURIComponent(name), 16 | target: '_blank', 17 | textContent: 'PDF' 18 | }) 19 | } 20 | 21 | function localRendering (name, callback) { 22 | const canvas = create('canvas', { width: 200, height: 200, title: name }) 23 | const tests = window.tests 24 | const ctx = canvas.getContext('2d', { alpha: true }) 25 | const initialFillStyle = ctx.fillStyle 26 | ctx.fillStyle = 'white' 27 | ctx.fillRect(0, 0, 200, 200) 28 | ctx.fillStyle = initialFillStyle 29 | if (tests[name].length === 2) { 30 | tests[name](ctx, callback) 31 | } else { 32 | tests[name](ctx) 33 | callback(null) 34 | } 35 | return canvas 36 | } 37 | 38 | function getDifference (canvas, image, outputCanvas) { 39 | const imgCanvas = create('canvas', { width: 200, height: 200 }) 40 | const ctx = imgCanvas.getContext('2d', { alpha: true }) 41 | const output = outputCanvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200) 42 | ctx.drawImage(image, 0, 0, 200, 200) 43 | const imageDataCanvas = ctx.getImageData(0, 0, 200, 200).data 44 | const imageDataGolden = canvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200).data 45 | window.pixelmatch(imageDataCanvas, imageDataGolden, output.data, 200, 200, { 46 | includeAA: false, 47 | threshold: 0.15 48 | }) 49 | outputCanvas.getContext('2d', { alpha: true }).putImageData(output, 0, 0) 50 | return outputCanvas 51 | } 52 | 53 | function clearTests () { 54 | const table = document.getElementById('tests') 55 | if (table) document.body.removeChild(table) 56 | } 57 | 58 | function runTests () { 59 | clearTests() 60 | 61 | const testNames = Object.keys(window.tests) 62 | 63 | const table = create('table', { id: 'tests' }, [ 64 | create('thead', {}, [ 65 | create('th', { textContent: 'node-canvas' }), 66 | create('th', { textContent: 'browser canvas' }), 67 | create('th', { textContent: 'visual diffs' }), 68 | create('th', { textContent: '' }) 69 | ]), 70 | create('tbody', {}, testNames.map(function (name) { 71 | const img = create('img') 72 | const canvasOuput = create('canvas', { width: 200, height: 200, title: name }) 73 | const canvas = localRendering(name, function () { 74 | img.onload = function () { 75 | getDifference(canvas, img, canvasOuput) 76 | } 77 | img.src = '/render?name=' + encodeURIComponent(name) 78 | }) 79 | return create('tr', {}, [ 80 | create('td', {}, [img]), 81 | create('td', {}, [canvas]), 82 | create('td', {}, [canvasOuput]), 83 | create('td', {}, [create('h3', { textContent: name }), pdfLink(name)]) 84 | ]) 85 | })) 86 | ]) 87 | 88 | document.body.appendChild(table) 89 | } 90 | -------------------------------------------------------------------------------- /test/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 40px 50px; 3 | font: 13px/1.4 "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | p { 7 | margin: 15px 5px; 8 | } 9 | 10 | a { 11 | color: #00C5F7; 12 | } 13 | 14 | canvas, img { 15 | padding: 5px; 16 | border: 1px solid #eee; 17 | } 18 | 19 | p.msg { 20 | width: 400px; 21 | } 22 | 23 | #tests { 24 | width: 100%; 25 | margin-top: 35px; 26 | } 27 | 28 | table tr td:nth-child(1), 29 | table tr td:nth-child(2), 30 | table tr td:nth-child(3) { 31 | width: 200px; 32 | } 33 | 34 | table tr td:nth-child(4) { 35 | padding: 0 45px; 36 | } 37 | 38 | table tr td p { 39 | margin: 5px 0; 40 | } 41 | 42 | table th { 43 | background: white; 44 | position: -webkit-sticky; 45 | position: sticky; 46 | top: 0; 47 | } 48 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | 4 | const Canvas = require('../') 5 | const tests = require('./public/tests') 6 | 7 | const app = express() 8 | const port = parseInt(process.argv[2] || '4000', 10) 9 | 10 | function renderTest (canvas, name, cb) { 11 | if (!tests[name]) { 12 | throw new Error('Unknown test: ' + name) 13 | } 14 | 15 | const ctx = canvas.getContext('2d', { pixelFormat: 'RGBA32' }) 16 | const initialFillStyle = ctx.fillStyle 17 | ctx.fillStyle = 'white' 18 | ctx.fillRect(0, 0, 200, 200) 19 | ctx.fillStyle = initialFillStyle 20 | if (tests[name].length === 2) { 21 | tests[name](ctx, cb) 22 | } else { 23 | tests[name](ctx) 24 | cb(null) 25 | } 26 | } 27 | 28 | app.use(express.static(path.join(__dirname, 'fixtures'))) 29 | app.use(express.static(path.join(__dirname, 'public'))) 30 | 31 | app.get('/', function (req, res) { 32 | res.sendFile(path.join(__dirname, 'public', 'app.html')) 33 | }) 34 | 35 | app.get('/pixelmatch.js', function (req, res) { 36 | res.sendFile(path.join(__dirname, '../node_modules/pixelmatch/', 'index.js')) 37 | }) 38 | 39 | app.get('/render', function (req, res, next) { 40 | const canvas = Canvas.createCanvas(200, 200) 41 | 42 | renderTest(canvas, req.query.name, function (err) { 43 | if (err) return next(err) 44 | 45 | res.writeHead(200, { 'Content-Type': 'image/png' }) 46 | canvas.pngStream().pipe(res) 47 | }) 48 | }) 49 | 50 | app.get('/pdf', function (req, res, next) { 51 | const canvas = Canvas.createCanvas(200, 200, 'pdf') 52 | 53 | renderTest(canvas, req.query.name, function (err) { 54 | if (err) return next(err) 55 | 56 | res.writeHead(200, { 'Content-Type': 'application/pdf' }) 57 | canvas.pdfStream().pipe(res) 58 | }) 59 | }) 60 | 61 | app.listen(port, function () { 62 | console.log('👉 http://localhost:%d/', port) 63 | }) 64 | -------------------------------------------------------------------------------- /test/wpt/generate.js: -------------------------------------------------------------------------------- 1 | // This file is a port of gentestutils.py from 2 | // https://github.com/web-platform-tests/wpt/tree/master/html/canvas/tools 3 | 4 | const yaml = require("js-yaml"); 5 | const fs = require("fs"); 6 | 7 | const yamlFiles = fs.readdirSync(__dirname).filter(f => f.endsWith(".yaml")); 8 | // Files that should be skipped: 9 | const SKIP_FILES = new Set("meta.yaml"); 10 | // Tests that should be skipped (e.g. because they cause hangs or V8 crashes): 11 | const SKIP_TESTS = new Set([ 12 | "2d.imageData.create2.negative", 13 | "2d.imageData.create2.zero", 14 | "2d.imageData.create2.nonfinite", 15 | "2d.imageData.create1.zero", 16 | "2d.imageData.create2.double", 17 | "2d.imageData.get.source.outside", 18 | "2d.imageData.get.source.negative", 19 | "2d.imageData.get.double", 20 | "2d.imageData.get.large.crash", // expected 21 | ]); 22 | 23 | function expandNonfinite(method, argstr, tail) { 24 | // argstr is ", ..." (where usually 25 | // 'invalid' is Infinity/-Infinity/NaN) 26 | const args = []; 27 | for (const arg of argstr.split(', ')) { 28 | const [, a] = arg.match(/<(.*)>/); 29 | args.push(a.split(' ')); 30 | } 31 | const calls = []; 32 | // Start with the valid argument list 33 | const call = []; 34 | for (let i = 0; i < args.length; i++) { 35 | call.push(args[i][0]); 36 | } 37 | // For each argument alone, try setting it to all its invalid values: 38 | for (let i = 0; i < args.length; i++) { 39 | for (let j = 1; j < args[i].length; j++) { 40 | const c2 = [...call] 41 | c2[i] = args[i][j]; 42 | calls.push(c2); 43 | } 44 | } 45 | // For all combinations of >= 2 arguments, try setting them to their first 46 | // invalid values. (Don't do all invalid values, because the number of 47 | // combinations explodes.) 48 | const f = (c, start, depth) => { 49 | for (let i = start; i < args.length; i++) { 50 | if (args[i].length > 1) { 51 | const a = args[i][1] 52 | const c2 = [...c] 53 | c2[i] = a 54 | if (depth > 0) 55 | calls.push(c2) 56 | f(c2, i+1, depth+1) 57 | } 58 | } 59 | }; 60 | f(call, 0, 0); 61 | 62 | return calls.map(c => `${method}(${c.join(", ")})${tail}`).join("\n\t\t"); 63 | } 64 | 65 | function simpleEscapeJS(str) { 66 | return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') 67 | } 68 | 69 | function escapeJS(str) { 70 | str = simpleEscapeJS(str) 71 | str = str.replace(/\[(\w+)\]/g, '[\\""+($1)+"\\"]') // kind of an ugly hack, for nicer failure-message output 72 | return str 73 | } 74 | 75 | /** @type {string} test */ 76 | function convert(test) { 77 | let code = test.code; 78 | if (!code) return ""; 79 | // Indent it 80 | code = code.trim().replace(/^/gm, "\t\t"); 81 | 82 | code = code.replace(/@nonfinite ([^(]+)\(([^)]+)\)(.*)/g, (match, g1, g2, g3) => { 83 | return expandNonfinite(g1, g2, g3); 84 | }); 85 | 86 | code = code.replace(/@assert pixel (\d+,\d+) == (\d+,\d+,\d+,\d+);/g, 87 | "_assertPixel(canvas, $1, $2);"); 88 | 89 | code = code.replace(/@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+);/g, 90 | "_assertPixelApprox(canvas, $1, $2);"); 91 | 92 | code = code.replace(/@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+) \+\/- (\d+);/g, 93 | "_assertPixelApprox(canvas, $1, $2, $3);"); 94 | 95 | code = code.replace(/@assert throws (\S+_ERR) (.*);/g, 96 | 'assert.throws(function() { $2; }, /$1/);'); 97 | 98 | code = code.replace(/@assert throws (\S+Error) (.*);/g, 99 | 'assert.throws(function() { $2; }, $1);'); 100 | 101 | code = code.replace(/@assert (.*) === (.*);/g, (match, g1, g2) => { 102 | return `assert.strictEqual(${g1}, ${g2}, "${escapeJS(g1)}", "${escapeJS(g2)}")`; 103 | }); 104 | 105 | code = code.replace(/@assert (.*) !== (.*);/g, (match, g1, g2) => { 106 | return `assert.notStrictEqual(${g1}, ${g2}, "${escapeJS(g1)}", "${escapeJS(g2)}");`; 107 | }); 108 | 109 | code = code.replace(/@assert (.*) =~ (.*);/g, (match, g1, g2) => { 110 | return `assert.match(${g1}, ${g2});`; 111 | }); 112 | 113 | code = code.replace(/@assert (.*);/g, (match, g1) => { 114 | return `assert(${g1}, "${escapeJS(g1)}");`; 115 | }); 116 | 117 | code = code.replace(/ @moz-todo/g, ""); 118 | 119 | code = code.replace(/@moz-UniversalBrowserRead;/g, ""); 120 | 121 | if (code.includes("@")) 122 | throw new Error("@ found in code; generation failed"); 123 | 124 | const name = test.name.replace(/"/g, /\"/); 125 | 126 | const skip = SKIP_TESTS.has(name) ? ".skip" : ""; 127 | 128 | return ` 129 | it${skip}("${name}", function () {${test.desc ? `\n\t\t// ${test.desc}` : ""} 130 | const canvas = createCanvas(100, 50); 131 | const ctx = canvas.getContext("2d"); 132 | const t = new Test(); 133 | 134 | ${code} 135 | }); 136 | ` 137 | } 138 | 139 | 140 | for (const filename of yamlFiles) { 141 | if (SKIP_FILES.has(filename)) 142 | continue; 143 | 144 | let tests; 145 | try { 146 | const content = fs.readFileSync(`${__dirname}/${filename}`, "utf8"); 147 | tests = yaml.load(content, { 148 | filename, 149 | // schema: yaml.DEFAULT_SCHEMA 150 | }); 151 | } catch (ex) { 152 | console.error(ex.toString()); 153 | continue; 154 | } 155 | 156 | const out = fs.createWriteStream(`${__dirname}/generated/${filename.replace(".yaml", ".js")}`); 157 | 158 | out.write(`// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. 159 | 160 | const assert = require('assert'); 161 | const path = require('path'); 162 | 163 | const { 164 | createCanvas, 165 | CanvasRenderingContext2D, 166 | ImageData, 167 | Image, 168 | DOMMatrix, 169 | DOMPoint, 170 | CanvasPattern, 171 | CanvasGradient 172 | } = require('../../..'); 173 | 174 | const window = { 175 | CanvasRenderingContext2D, 176 | ImageData, 177 | Image, 178 | DOMMatrix, 179 | DOMPoint, 180 | Uint8ClampedArray, 181 | CanvasPattern, 182 | CanvasGradient 183 | }; 184 | 185 | const document = { 186 | createElement(type, ...args) { 187 | if (type !== "canvas") 188 | throw new Error(\`createElement(\${type}) not supported\`); 189 | return createCanvas(...args); 190 | } 191 | }; 192 | 193 | function _getPixel(canvas, x, y) { 194 | const ctx = canvas.getContext('2d'); 195 | const imgdata = ctx.getImageData(x, y, 1, 1); 196 | return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; 197 | } 198 | 199 | function _assertApprox(actual, expected, epsilon=0, msg="") { 200 | assert(typeof actual === "number", "actual should be a number but got a \${typeof type_actual}"); 201 | 202 | // The epsilon math below does not place nice with NaN and Infinity 203 | // But in this case Infinity = Infinity and NaN = NaN 204 | if (isFinite(actual) || isFinite(expected)) { 205 | assert(Math.abs(actual - expected) <= epsilon, 206 | \`expected \${actual} to equal \${expected} +/- \${epsilon}. \${msg}\`); 207 | } else { 208 | assert.strictEqual(actual, expected); 209 | } 210 | } 211 | 212 | function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { 213 | const c = _getPixel(canvas, x,y); 214 | assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); 215 | assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); 216 | assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); 217 | assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); 218 | } 219 | 220 | function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { 221 | const c = _getPixel(canvas, x,y); 222 | _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); 223 | _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); 224 | _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); 225 | _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); 226 | } 227 | 228 | function assert_throws_js(Type, fn) { 229 | assert.throws(fn, Type); 230 | } 231 | 232 | // Used by font tests to allow fonts to load. 233 | function deferTest() {} 234 | 235 | class Test { 236 | // Two cases of this in the tests, look unnecessary. 237 | done() {} 238 | // Used by font tests to allow fonts to load. 239 | step_func_done(func) { func(); } 240 | // Used for image onload callback. 241 | step_func(func) { func(); } 242 | } 243 | 244 | function step_timeout(result, time) { 245 | // Nothing; code needs to be converted for this to work. 246 | } 247 | 248 | describe("WPT: ${filename.replace(".yaml", "")}", function () { 249 | `); 250 | 251 | for (const test of tests) { 252 | out.write(convert(test)); 253 | } 254 | 255 | out.write(`}); 256 | `) 257 | 258 | out.end(); 259 | } 260 | -------------------------------------------------------------------------------- /test/wpt/generated/meta.js: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. 2 | 3 | const assert = require('assert'); 4 | const path = require('path'); 5 | 6 | const { 7 | createCanvas, 8 | CanvasRenderingContext2D, 9 | ImageData, 10 | Image, 11 | DOMMatrix, 12 | DOMPoint, 13 | CanvasPattern, 14 | CanvasGradient 15 | } = require('../../..'); 16 | 17 | const window = { 18 | CanvasRenderingContext2D, 19 | ImageData, 20 | Image, 21 | DOMMatrix, 22 | DOMPoint, 23 | Uint8ClampedArray, 24 | CanvasPattern, 25 | CanvasGradient 26 | }; 27 | 28 | const document = { 29 | createElement(type, ...args) { 30 | if (type !== "canvas") 31 | throw new Error(`createElement(${type}) not supported`); 32 | return createCanvas(...args); 33 | } 34 | }; 35 | 36 | function _getPixel(canvas, x, y) { 37 | const ctx = canvas.getContext('2d'); 38 | const imgdata = ctx.getImageData(x, y, 1, 1); 39 | return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; 40 | } 41 | 42 | function _assertApprox(actual, expected, epsilon=0, msg="") { 43 | assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); 44 | 45 | // The epsilon math below does not place nice with NaN and Infinity 46 | // But in this case Infinity = Infinity and NaN = NaN 47 | if (isFinite(actual) || isFinite(expected)) { 48 | assert(Math.abs(actual - expected) <= epsilon, 49 | `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); 50 | } else { 51 | assert.strictEqual(actual, expected); 52 | } 53 | } 54 | 55 | function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { 56 | const c = _getPixel(canvas, x,y); 57 | assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); 58 | assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); 59 | assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); 60 | assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); 61 | } 62 | 63 | function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { 64 | const c = _getPixel(canvas, x,y); 65 | _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); 66 | _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); 67 | _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); 68 | _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); 69 | } 70 | 71 | function assert_throws_js(Type, fn) { 72 | assert.throws(fn, Type); 73 | } 74 | 75 | // Used by font tests to allow fonts to load. 76 | function deferTest() {} 77 | 78 | class Test { 79 | // Two cases of this in the tests, look unnecessary. 80 | done() {} 81 | // Used by font tests to allow fonts to load. 82 | step_func_done(func) { func(); } 83 | // Used for image onload callback. 84 | step_func(func) { func(); } 85 | } 86 | 87 | function step_timeout(result, time) { 88 | // Nothing; code needs to be converted for this to work. 89 | } 90 | 91 | describe("WPT: meta", function () { 92 | }); 93 | -------------------------------------------------------------------------------- /test/wpt/generated/the-canvas-state.js: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. 2 | 3 | const assert = require('assert'); 4 | const path = require('path'); 5 | 6 | const { 7 | createCanvas, 8 | CanvasRenderingContext2D, 9 | ImageData, 10 | Image, 11 | DOMMatrix, 12 | DOMPoint, 13 | CanvasPattern, 14 | CanvasGradient 15 | } = require('../../..'); 16 | 17 | const window = { 18 | CanvasRenderingContext2D, 19 | ImageData, 20 | Image, 21 | DOMMatrix, 22 | DOMPoint, 23 | Uint8ClampedArray, 24 | CanvasPattern, 25 | CanvasGradient 26 | }; 27 | 28 | const document = { 29 | createElement(type, ...args) { 30 | if (type !== "canvas") 31 | throw new Error(`createElement(${type}) not supported`); 32 | return createCanvas(...args); 33 | } 34 | }; 35 | 36 | function _getPixel(canvas, x, y) { 37 | const ctx = canvas.getContext('2d'); 38 | const imgdata = ctx.getImageData(x, y, 1, 1); 39 | return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; 40 | } 41 | 42 | function _assertApprox(actual, expected, epsilon=0, msg="") { 43 | assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); 44 | 45 | // The epsilon math below does not place nice with NaN and Infinity 46 | // But in this case Infinity = Infinity and NaN = NaN 47 | if (isFinite(actual) || isFinite(expected)) { 48 | assert(Math.abs(actual - expected) <= epsilon, 49 | `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); 50 | } else { 51 | assert.strictEqual(actual, expected); 52 | } 53 | } 54 | 55 | function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { 56 | const c = _getPixel(canvas, x,y); 57 | assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); 58 | assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); 59 | assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); 60 | assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); 61 | } 62 | 63 | function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { 64 | const c = _getPixel(canvas, x,y); 65 | _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); 66 | _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); 67 | _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); 68 | _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); 69 | } 70 | 71 | function assert_throws_js(Type, fn) { 72 | assert.throws(fn, Type); 73 | } 74 | 75 | // Used by font tests to allow fonts to load. 76 | function deferTest() {} 77 | 78 | class Test { 79 | // Two cases of this in the tests, look unnecessary. 80 | done() {} 81 | // Used by font tests to allow fonts to load. 82 | step_func_done(func) { func(); } 83 | // Used for image onload callback. 84 | step_func(func) { func(); } 85 | } 86 | 87 | function step_timeout(result, time) { 88 | // Nothing; code needs to be converted for this to work. 89 | } 90 | 91 | describe("WPT: the-canvas-state", function () { 92 | 93 | it("2d.state.saverestore.transformation", function () { 94 | // save()/restore() affects the current transformation matrix 95 | const canvas = createCanvas(100, 50); 96 | const ctx = canvas.getContext("2d"); 97 | const t = new Test(); 98 | 99 | ctx.fillStyle = '#0f0'; 100 | ctx.fillRect(0, 0, 100, 50); 101 | ctx.save(); 102 | ctx.translate(200, 0); 103 | ctx.restore(); 104 | ctx.fillStyle = '#f00'; 105 | ctx.fillRect(-200, 0, 100, 50); 106 | _assertPixel(canvas, 50,25, 0,255,0,255); 107 | }); 108 | 109 | it("2d.state.saverestore.clip", function () { 110 | // save()/restore() affects the clipping path 111 | const canvas = createCanvas(100, 50); 112 | const ctx = canvas.getContext("2d"); 113 | const t = new Test(); 114 | 115 | ctx.fillStyle = '#f00'; 116 | ctx.fillRect(0, 0, 100, 50); 117 | ctx.save(); 118 | ctx.rect(0, 0, 1, 1); 119 | ctx.clip(); 120 | ctx.restore(); 121 | ctx.fillStyle = '#0f0'; 122 | ctx.fillRect(0, 0, 100, 50); 123 | _assertPixel(canvas, 50,25, 0,255,0,255); 124 | }); 125 | 126 | it("2d.state.saverestore.path", function () { 127 | // save()/restore() does not affect the current path 128 | const canvas = createCanvas(100, 50); 129 | const ctx = canvas.getContext("2d"); 130 | const t = new Test(); 131 | 132 | ctx.fillStyle = '#f00'; 133 | ctx.fillRect(0, 0, 100, 50); 134 | ctx.save(); 135 | ctx.rect(0, 0, 100, 50); 136 | ctx.restore(); 137 | ctx.fillStyle = '#0f0'; 138 | ctx.fill(); 139 | _assertPixel(canvas, 50,25, 0,255,0,255); 140 | }); 141 | 142 | it("2d.state.saverestore.bitmap", function () { 143 | // save()/restore() does not affect the current bitmap 144 | const canvas = createCanvas(100, 50); 145 | const ctx = canvas.getContext("2d"); 146 | const t = new Test(); 147 | 148 | ctx.fillStyle = '#f00'; 149 | ctx.fillRect(0, 0, 100, 50); 150 | ctx.save(); 151 | ctx.fillStyle = '#0f0'; 152 | ctx.fillRect(0, 0, 100, 50); 153 | ctx.restore(); 154 | _assertPixel(canvas, 50,25, 0,255,0,255); 155 | }); 156 | 157 | it("2d.state.saverestore.stack", function () { 158 | // save()/restore() can be nested as a stack 159 | const canvas = createCanvas(100, 50); 160 | const ctx = canvas.getContext("2d"); 161 | const t = new Test(); 162 | 163 | ctx.lineWidth = 1; 164 | ctx.save(); 165 | ctx.lineWidth = 2; 166 | ctx.save(); 167 | ctx.lineWidth = 3; 168 | assert.strictEqual(ctx.lineWidth, 3, "ctx.lineWidth", "3") 169 | ctx.restore(); 170 | assert.strictEqual(ctx.lineWidth, 2, "ctx.lineWidth", "2") 171 | ctx.restore(); 172 | assert.strictEqual(ctx.lineWidth, 1, "ctx.lineWidth", "1") 173 | }); 174 | 175 | it("2d.state.saverestore.stackdepth", function () { 176 | // save()/restore() stack depth is not unreasonably limited 177 | const canvas = createCanvas(100, 50); 178 | const ctx = canvas.getContext("2d"); 179 | const t = new Test(); 180 | 181 | var limit = 512; 182 | for (var i = 1; i < limit; ++i) 183 | { 184 | ctx.save(); 185 | ctx.lineWidth = i; 186 | } 187 | for (var i = limit-1; i > 0; --i) 188 | { 189 | assert.strictEqual(ctx.lineWidth, i, "ctx.lineWidth", "i") 190 | ctx.restore(); 191 | } 192 | }); 193 | 194 | it("2d.state.saverestore.underflow", function () { 195 | // restore() with an empty stack has no effect 196 | const canvas = createCanvas(100, 50); 197 | const ctx = canvas.getContext("2d"); 198 | const t = new Test(); 199 | 200 | for (var i = 0; i < 16; ++i) 201 | ctx.restore(); 202 | ctx.lineWidth = 0.5; 203 | ctx.restore(); 204 | assert.strictEqual(ctx.lineWidth, 0.5, "ctx.lineWidth", "0.5") 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /test/wpt/the-canvas-element.yaml: -------------------------------------------------------------------------------- 1 | - name: 2d.getcontext.exists 2 | desc: The 2D context is implemented 3 | testing: 4 | - context.2d 5 | code: | 6 | @assert canvas.getContext('2d') !== null; 7 | 8 | - name: 2d.getcontext.invalid.args 9 | desc: Calling getContext with invalid arguments. 10 | testing: 11 | - context.2d 12 | code: | 13 | @assert canvas.getContext('') === null; 14 | @assert canvas.getContext('2d#') === null; 15 | @assert canvas.getContext('This is clearly not a valid context name.') === null; 16 | @assert canvas.getContext('2d\0') === null; 17 | @assert canvas.getContext('2\uFF44') === null; 18 | @assert canvas.getContext('2D') === null; 19 | @assert throws TypeError canvas.getContext(); 20 | @assert canvas.getContext('null') === null; 21 | @assert canvas.getContext('undefined') === null; 22 | 23 | - name: 2d.getcontext.extraargs.create 24 | desc: The 2D context doesn't throw with extra getContext arguments (new context) 25 | testing: 26 | - context.2d.extraargs 27 | code: | 28 | @assert document.createElement("canvas").getContext('2d', false, {}, [], 1, "2") !== null; 29 | @assert document.createElement("canvas").getContext('2d', 123) !== null; 30 | @assert document.createElement("canvas").getContext('2d', "test") !== null; 31 | @assert document.createElement("canvas").getContext('2d', undefined) !== null; 32 | @assert document.createElement("canvas").getContext('2d', null) !== null; 33 | @assert document.createElement("canvas").getContext('2d', Symbol.hasInstance) !== null; 34 | 35 | - name: 2d.getcontext.extraargs.cache 36 | desc: The 2D context doesn't throw with extra getContext arguments (cached) 37 | testing: 38 | - context.2d.extraargs 39 | code: | 40 | @assert canvas.getContext('2d', false, {}, [], 1, "2") !== null; 41 | @assert canvas.getContext('2d', 123) !== null; 42 | @assert canvas.getContext('2d', "test") !== null; 43 | @assert canvas.getContext('2d', undefined) !== null; 44 | @assert canvas.getContext('2d', null) !== null; 45 | @assert canvas.getContext('2d', Symbol.hasInstance) !== null; 46 | 47 | - name: 2d.type.exists 48 | desc: The 2D context interface is a property of 'window' 49 | notes: &bindings Defined in "Web IDL" (draft) 50 | testing: 51 | - context.2d.type 52 | code: | 53 | @assert window.CanvasRenderingContext2D; 54 | 55 | - name: 2d.type.prototype 56 | desc: window.CanvasRenderingContext2D.prototype are not [[Writable]] and not [[Configurable]], 57 | and its methods are [[Configurable]]. 58 | notes: *bindings 59 | testing: 60 | - context.2d.type 61 | code: | 62 | @assert window.CanvasRenderingContext2D.prototype; 63 | @assert window.CanvasRenderingContext2D.prototype.fill; 64 | window.CanvasRenderingContext2D.prototype = null; 65 | @assert window.CanvasRenderingContext2D.prototype; 66 | delete window.CanvasRenderingContext2D.prototype; 67 | @assert window.CanvasRenderingContext2D.prototype; 68 | window.CanvasRenderingContext2D.prototype.fill = 1; 69 | @assert window.CanvasRenderingContext2D.prototype.fill === 1; 70 | delete window.CanvasRenderingContext2D.prototype.fill; 71 | @assert window.CanvasRenderingContext2D.prototype.fill === undefined; 72 | 73 | - name: 2d.type.replace 74 | desc: Interface methods can be overridden 75 | notes: *bindings 76 | testing: 77 | - context.2d.type 78 | code: | 79 | var fillRect = window.CanvasRenderingContext2D.prototype.fillRect; 80 | window.CanvasRenderingContext2D.prototype.fillRect = function (x, y, w, h) 81 | { 82 | this.fillStyle = '#0f0'; 83 | fillRect.call(this, x, y, w, h); 84 | }; 85 | ctx.fillStyle = '#f00'; 86 | ctx.fillRect(0, 0, 100, 50); 87 | @assert pixel 50,25 == 0,255,0,255; 88 | expected: green 89 | 90 | - name: 2d.type.extend 91 | desc: Interface methods can be added 92 | notes: *bindings 93 | testing: 94 | - context.2d.type 95 | code: | 96 | window.CanvasRenderingContext2D.prototype.fillRectGreen = function (x, y, w, h) 97 | { 98 | this.fillStyle = '#0f0'; 99 | this.fillRect(x, y, w, h); 100 | }; 101 | ctx.fillStyle = '#f00'; 102 | ctx.fillRectGreen(0, 0, 100, 50); 103 | @assert pixel 50,25 == 0,255,0,255; 104 | expected: green 105 | 106 | - name: 2d.getcontext.unique 107 | desc: getContext('2d') returns the same object 108 | testing: 109 | - context.unique 110 | code: | 111 | @assert canvas.getContext('2d') === canvas.getContext('2d'); 112 | 113 | - name: 2d.getcontext.shared 114 | desc: getContext('2d') returns objects which share canvas state 115 | testing: 116 | - context.unique 117 | code: | 118 | var ctx2 = canvas.getContext('2d'); 119 | ctx.fillStyle = '#f00'; 120 | ctx2.fillStyle = '#0f0'; 121 | ctx.fillRect(0, 0, 100, 50); 122 | @assert pixel 50,25 == 0,255,0,255; 123 | expected: green 124 | 125 | - name: 2d.scaled 126 | desc: CSS-scaled canvases get drawn correctly 127 | canvas: 'width="50" height="25" style="width: 100px; height: 50px"' 128 | manual: 129 | code: | 130 | ctx.fillStyle = '#00f'; 131 | ctx.fillRect(0, 0, 50, 25); 132 | ctx.fillStyle = '#0ff'; 133 | ctx.fillRect(0, 0, 25, 10); 134 | expected: | 135 | size 100 50 136 | cr.set_source_rgb(0, 0, 1) 137 | cr.rectangle(0, 0, 100, 50) 138 | cr.fill() 139 | cr.set_source_rgb(0, 1, 1) 140 | cr.rectangle(0, 0, 50, 20) 141 | cr.fill() 142 | 143 | - name: 2d.canvas.reference 144 | desc: CanvasRenderingContext2D.canvas refers back to its canvas 145 | testing: 146 | - 2d.canvas 147 | code: | 148 | @assert ctx.canvas === canvas; 149 | 150 | - name: 2d.canvas.readonly 151 | desc: CanvasRenderingContext2D.canvas is readonly 152 | testing: 153 | - 2d.canvas.attribute 154 | code: | 155 | var c = document.createElement('canvas'); 156 | var d = ctx.canvas; 157 | @assert c !== d; 158 | ctx.canvas = c; 159 | @assert ctx.canvas === d; 160 | 161 | - name: 2d.canvas.context 162 | desc: checks CanvasRenderingContext2D prototype 163 | testing: 164 | - 2d.path.contexttypexxx.basic 165 | code: | 166 | @assert Object.getPrototypeOf(CanvasRenderingContext2D.prototype) === Object.prototype; 167 | @assert Object.getPrototypeOf(ctx) === CanvasRenderingContext2D.prototype; 168 | t.done(); 169 | 170 | -------------------------------------------------------------------------------- /test/wpt/the-canvas-state.yaml: -------------------------------------------------------------------------------- 1 | - name: 2d.state.saverestore.transformation 2 | desc: save()/restore() affects the current transformation matrix 3 | testing: 4 | - 2d.state.transformation 5 | code: | 6 | ctx.fillStyle = '#0f0'; 7 | ctx.fillRect(0, 0, 100, 50); 8 | ctx.save(); 9 | ctx.translate(200, 0); 10 | ctx.restore(); 11 | ctx.fillStyle = '#f00'; 12 | ctx.fillRect(-200, 0, 100, 50); 13 | @assert pixel 50,25 == 0,255,0,255; 14 | expected: green 15 | 16 | - name: 2d.state.saverestore.clip 17 | desc: save()/restore() affects the clipping path 18 | testing: 19 | - 2d.state.clip 20 | code: | 21 | ctx.fillStyle = '#f00'; 22 | ctx.fillRect(0, 0, 100, 50); 23 | ctx.save(); 24 | ctx.rect(0, 0, 1, 1); 25 | ctx.clip(); 26 | ctx.restore(); 27 | ctx.fillStyle = '#0f0'; 28 | ctx.fillRect(0, 0, 100, 50); 29 | @assert pixel 50,25 == 0,255,0,255; 30 | expected: green 31 | 32 | - name: 2d.state.saverestore.path 33 | desc: save()/restore() does not affect the current path 34 | testing: 35 | - 2d.state.path 36 | code: | 37 | ctx.fillStyle = '#f00'; 38 | ctx.fillRect(0, 0, 100, 50); 39 | ctx.save(); 40 | ctx.rect(0, 0, 100, 50); 41 | ctx.restore(); 42 | ctx.fillStyle = '#0f0'; 43 | ctx.fill(); 44 | @assert pixel 50,25 == 0,255,0,255; 45 | expected: green 46 | 47 | - name: 2d.state.saverestore.bitmap 48 | desc: save()/restore() does not affect the current bitmap 49 | testing: 50 | - 2d.state.bitmap 51 | code: | 52 | ctx.fillStyle = '#f00'; 53 | ctx.fillRect(0, 0, 100, 50); 54 | ctx.save(); 55 | ctx.fillStyle = '#0f0'; 56 | ctx.fillRect(0, 0, 100, 50); 57 | ctx.restore(); 58 | @assert pixel 50,25 == 0,255,0,255; 59 | expected: green 60 | 61 | - name: 2d.state.saverestore.stack 62 | desc: save()/restore() can be nested as a stack 63 | testing: 64 | - 2d.state.save 65 | - 2d.state.restore 66 | code: | 67 | ctx.lineWidth = 1; 68 | ctx.save(); 69 | ctx.lineWidth = 2; 70 | ctx.save(); 71 | ctx.lineWidth = 3; 72 | @assert ctx.lineWidth === 3; 73 | ctx.restore(); 74 | @assert ctx.lineWidth === 2; 75 | ctx.restore(); 76 | @assert ctx.lineWidth === 1; 77 | 78 | - name: 2d.state.saverestore.stackdepth 79 | desc: save()/restore() stack depth is not unreasonably limited 80 | testing: 81 | - 2d.state.save 82 | - 2d.state.restore 83 | code: | 84 | var limit = 512; 85 | for (var i = 1; i < limit; ++i) 86 | { 87 | ctx.save(); 88 | ctx.lineWidth = i; 89 | } 90 | for (var i = limit-1; i > 0; --i) 91 | { 92 | @assert ctx.lineWidth === i; 93 | ctx.restore(); 94 | } 95 | 96 | - name: 2d.state.saverestore.underflow 97 | desc: restore() with an empty stack has no effect 98 | testing: 99 | - 2d.state.restore.underflow 100 | code: | 101 | for (var i = 0; i < 16; ++i) 102 | ctx.restore(); 103 | ctx.lineWidth = 0.5; 104 | ctx.restore(); 105 | @assert ctx.lineWidth === 0.5; 106 | 107 | 108 | -------------------------------------------------------------------------------- /util/has_lib.js: -------------------------------------------------------------------------------- 1 | const query = process.argv[2] 2 | const fs = require('fs') 3 | const childProcess = require('child_process') 4 | 5 | const SYSTEM_PATHS = [ 6 | '/lib', 7 | '/usr/lib', 8 | '/usr/lib64', 9 | '/usr/local/lib', 10 | '/opt/local/lib', 11 | '/opt/homebrew/lib', 12 | '/usr/lib/x86_64-linux-gnu', 13 | '/usr/lib/i386-linux-gnu', 14 | '/usr/lib/arm-linux-gnueabihf', 15 | '/usr/lib/arm-linux-gnueabi', 16 | '/usr/lib/aarch64-linux-gnu' 17 | ] 18 | 19 | /** 20 | * Checks for lib using ldconfig if present, or searching SYSTEM_PATHS 21 | * otherwise. 22 | * @param {string} lib - library name, e.g. 'jpeg' in 'libjpeg64.so' (see first line) 23 | * @return {boolean} exists 24 | */ 25 | function hasSystemLib (lib) { 26 | const libName = 'lib' + lib + '.+(so|dylib)' 27 | const libNameRegex = new RegExp(libName) 28 | 29 | // Try using ldconfig on linux systems 30 | if (hasLdconfig()) { 31 | try { 32 | if (childProcess.execSync('ldconfig -p 2>/dev/null | grep -E "' + libName + '"').length) { 33 | return true 34 | } 35 | } catch (err) { 36 | // noop -- proceed to other search methods 37 | } 38 | } 39 | 40 | // Try checking common library locations 41 | return SYSTEM_PATHS.some(function (systemPath) { 42 | try { 43 | const dirListing = fs.readdirSync(systemPath) 44 | return dirListing.some(function (file) { 45 | return libNameRegex.test(file) 46 | }) 47 | } catch (err) { 48 | return false 49 | } 50 | }) 51 | } 52 | 53 | /** 54 | * Checks for ldconfig on the path and /sbin 55 | * @return {boolean} exists 56 | */ 57 | function hasLdconfig () { 58 | try { 59 | // Add /sbin to path as ldconfig is located there on some systems -- e.g. 60 | // Debian (and it can still be used by unprivileged users): 61 | childProcess.execSync('export PATH="$PATH:/sbin"') 62 | process.env.PATH = '...' 63 | // execSync throws on nonzero exit 64 | childProcess.execSync('hash ldconfig 2>/dev/null') 65 | return true 66 | } catch (err) { 67 | return false 68 | } 69 | } 70 | 71 | /** 72 | * Checks for freetype2 with --cflags-only-I 73 | * @return Boolean exists 74 | */ 75 | function hasFreetype () { 76 | try { 77 | if (childProcess.execSync('pkg-config cairo --cflags-only-I 2>/dev/null | grep freetype2').length) { 78 | return true 79 | } 80 | } catch (err) { 81 | // noop 82 | } 83 | return false 84 | } 85 | 86 | /** 87 | * Checks for lib using pkg-config. 88 | * @param {string} lib - library name 89 | * @return {boolean} exists 90 | */ 91 | function hasPkgconfigLib (lib) { 92 | try { 93 | // execSync throws on nonzero exit 94 | childProcess.execSync('pkg-config --exists "' + lib + '" 2>/dev/null') 95 | return true 96 | } catch (err) { 97 | return false 98 | } 99 | } 100 | 101 | function main (query) { 102 | switch (query) { 103 | case 'gif': 104 | case 'cairo': 105 | return hasSystemLib(query) 106 | case 'pango': 107 | return hasPkgconfigLib(query) 108 | case 'freetype': 109 | return hasFreetype() 110 | case 'jpeg': 111 | return hasPkgconfigLib('libjpeg') 112 | case 'rsvg': 113 | return hasPkgconfigLib('librsvg-2.0') 114 | default: 115 | throw new Error('Unknown library: ' + query) 116 | } 117 | } 118 | 119 | process.stdout.write(main(query).toString()) 120 | -------------------------------------------------------------------------------- /util/win_jpeg_lookup.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const paths = ['C:/libjpeg-turbo'] 3 | 4 | if (process.arch === 'x64') { 5 | paths.unshift('C:/libjpeg-turbo64') 6 | } 7 | 8 | paths.forEach(function (path) { 9 | if (exists(path)) { 10 | process.stdout.write(path) 11 | process.exit() 12 | } 13 | }) 14 | 15 | function exists (path) { 16 | try { 17 | return fs.lstatSync(path).isDirectory() 18 | } catch (e) { 19 | return false 20 | } 21 | } 22 | --------------------------------------------------------------------------------