feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+657
View File
@@ -0,0 +1,657 @@
# node-canvas
![Test](https://github.com/Automattic/node-canvas/workflows/Test/badge.svg)
[![NPM version](https://badge.fury.io/js/canvas.svg)](http://badge.fury.io/js/canvas)
node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation for [Node.js](http://nodejs.org).
## Installation
```bash
$ npm install canvas
```
By default, pre-built binaries will be downloaded if you're on one of the following platforms:
- macOS x86/64
- macOS aarch64 (aka Apple silicon)
- Linux x86/64 (glibc only)
- Windows x86/64
If you want to build from source, use `npm install --build-from-source` and see the **Compiling** section below.
The minimum version of Node.js required is **18.12.0**.
### Compiling
If you don't have a supported OS or processor architecture, or you use `--build-from-source`, the module will be compiled on your system. This requires several dependencies, including Cairo and Pango.
For detailed installation information, see the [wiki](https://github.com/Automattic/node-canvas/wiki/_pages). One-line installation instructions for common OSes are below. Note that libgif/giflib, librsvg and libjpeg are optional and only required if you need GIF, SVG and JPEG support, respectively. Cairo v1.10.0 or later is required.
OS | Command
----- | -----
macOS | Using [Homebrew](https://brew.sh/):<br/>`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman python-setuptools`
Ubuntu | `sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev`
Fedora | `sudo yum install gcc-c++ cairo-devel pango-devel libjpeg-turbo-devel giflib-devel`
Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto`
OpenBSD | `doas pkg_add cairo pango png jpeg giflib`
Windows | See the [wiki](https://github.com/Automattic/node-canvas/wiki/Installation:-Windows)
Others | See the [wiki](https://github.com/Automattic/node-canvas/wiki)
**Mac OS X v10.11+:** If you have recently updated to Mac OS X v10.11+ and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072).
If you have xcode 10.0 or higher installed, in order to build from source you need NPM 6.4.1 or higher.
## Quick Example
```javascript
const { createCanvas, loadImage } = require('canvas')
const canvas = createCanvas(200, 200)
const ctx = canvas.getContext('2d')
// Write "Awesome!"
ctx.font = '30px Impact'
ctx.rotate(0.1)
ctx.fillText('Awesome!', 50, 100)
// Draw line under text
var text = ctx.measureText('Awesome!')
ctx.strokeStyle = 'rgba(0,0,0,0.5)'
ctx.beginPath()
ctx.lineTo(50, 102)
ctx.lineTo(50 + text.width, 102)
ctx.stroke()
// Draw cat with lime helmet
loadImage('examples/images/lime-cat.jpg').then((image) => {
ctx.drawImage(image, 50, 0, 70, 70)
console.log('<img src="' + canvas.toDataURL() + '" />')
})
```
## Upgrading from 1.x to 2.x
See the [changelog](https://github.com/Automattic/node-canvas/blob/master/CHANGELOG.md) for a guide to upgrading from 1.x to 2.x.
For version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x).
## Documentation
This project is an implementation of the Web Canvas API and implements that API as closely as possible. For API documentation, please visit [Mozilla Web Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API). (See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status) for the current API compliance.) All utility methods and non-standard APIs are documented below.
### Utility methods
* [createCanvas()](#createcanvas)
* [createImageData()](#createimagedata)
* [loadImage()](#loadimage)
* [registerFont()](#registerfont)
* [deregisterAllFonts()](#deregisterAllFonts)
### Non-standard APIs
* [Image#src](#imagesrc)
* [Image#dataMode](#imagedatamode)
* [Canvas#toBuffer()](#canvastobuffer)
* [Canvas#createPNGStream()](#canvascreatepngstream)
* [Canvas#createJPEGStream()](#canvascreatejpegstream)
* [Canvas#createPDFStream()](#canvascreatepdfstream)
* [Canvas#toDataURL()](#canvastodataurl)
* [CanvasRenderingContext2D#patternQuality](#canvasrenderingcontext2dpatternquality)
* [CanvasRenderingContext2D#quality](#canvasrenderingcontext2dquality)
* [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode)
* [CanvasRenderingContext2D#globalCompositeOperation = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperation--saturate)
* [CanvasRenderingContext2D#antialias](#canvasrenderingcontext2dantialias)
### createCanvas()
> ```ts
> createCanvas(width: number, height: number, type?: 'PDF'|'SVG') => Canvas
> ```
Creates a Canvas instance. This method works in both Node.js and Web browsers, where there is no Canvas constructor. (See `browser.js` for the implementation that runs in browsers.)
```js
const { createCanvas } = require('canvas')
const mycanvas = createCanvas(200, 200)
const myPDFcanvas = createCanvas(600, 800, 'pdf') // see "PDF Support" section
```
### createImageData()
> ```ts
> createImageData(width: number, height: number) => ImageData
> createImageData(data: Uint8ClampedArray, width: number, height?: number) => ImageData
> // for alternative pixel formats:
> createImageData(data: Uint16Array, width: number, height?: number) => ImageData
> ```
Creates an ImageData instance. This method works in both Node.js and Web browsers.
```js
const { createImageData } = require('canvas')
const width = 20, height = 20
const arraySize = width * height * 4
const mydata = createImageData(new Uint8ClampedArray(arraySize), width)
```
### loadImage()
> ```ts
> loadImage() => Promise<Image>
> ```
Convenience method for loading images. This method works in both Node.js and Web browsers.
```js
const { loadImage } = require('canvas')
const myimg = loadImage('http://server.com/image.png')
myimg.then(() => {
// do something with image
}).catch(err => {
console.log('oh no!', err)
})
// or with async/await:
const myimg = await loadImage('http://server.com/image.png')
// do something with image
```
### registerFont()
> ```ts
> registerFont(path: string, { family: string, weight?: string, style?: string }) => void
> ```
To use a font file that is not installed as a system font, use `registerFont()` to register the font with Canvas.
```js
const { registerFont, createCanvas } = require('canvas')
registerFont('comicsans.ttf', { family: 'Comic Sans' })
const canvas = createCanvas(500, 500)
const ctx = canvas.getContext('2d')
ctx.font = '12px "Comic Sans"'
ctx.fillText('Everyone hates this font :(', 250, 10)
```
The second argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight`, and `style` are optional and default to `'normal'`.
### deregisterAllFonts()
> ```ts
> deregisterAllFonts() => void
> ```
Use `deregisterAllFonts` to unregister all fonts that have been previously registered. This method is useful when you want to remove all registered fonts, such as when using the canvas in tests
```ts
const { registerFont, createCanvas, deregisterAllFonts } = require('canvas')
describe('text rendering', () => {
afterEach(() => {
deregisterAllFonts();
})
it('should render text with Comic Sans', () => {
registerFont('comicsans.ttf', { family: 'Comic Sans' })
const canvas = createCanvas(500, 500)
const ctx = canvas.getContext('2d')
ctx.font = '12px "Comic Sans"'
ctx.fillText('Everyone loves this font :)', 250, 10)
// assertScreenshot()
})
})
```
### Image#src
> ```ts
> img.src: string|Buffer
> ```
As in browsers, `img.src` can be set to a `data:` URI or a remote URL. In addition, node-canvas allows setting `src` to a local file path or `Buffer` instance.
```javascript
const { Image } = require('canvas')
// From a buffer:
fs.readFile('images/squid.png', (err, squid) => {
if (err) throw err
const img = new Image()
img.onload = () => ctx.drawImage(img, 0, 0)
img.onerror = err => { throw err }
img.src = squid
})
// From a local file path:
const img = new Image()
img.onload = () => ctx.drawImage(img, 0, 0)
img.onerror = err => { throw err }
img.src = 'images/squid.png'
// From a remote URL:
img.src = 'http://picsum.photos/200/300'
// ... as above
// From a `data:` URI:
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
// ... as above
```
*Note: In some cases, `img.src=` is currently synchronous. However, you should always use `img.onload` and `img.onerror`, as we intend to make `img.src=` always asynchronous as it is in browsers. See https://github.com/Automattic/node-canvas/issues/1007.*
### Image#dataMode
> ```ts
> img.dataMode: number
> ```
Applies to JPEG images drawn to PDF canvases only.
Setting `img.dataMode = Image.MODE_MIME` or `Image.MODE_MIME|Image.MODE_IMAGE` enables MIME data tracking of images. When MIME data is tracked, PDF canvases can embed JPEGs directly into the output, rather than re-encoding into PNG. This can drastically reduce filesize and speed up rendering.
```javascript
const { Image, createCanvas } = require('canvas')
const canvas = createCanvas(w, h, 'pdf')
const img = new Image()
img.dataMode = Image.MODE_IMAGE // Only image data tracked
img.dataMode = Image.MODE_MIME // Only mime data tracked
img.dataMode = Image.MODE_MIME | Image.MODE_IMAGE // Both are tracked
```
If working with a non-PDF canvas, image data *must* be tracked; otherwise the output will be junk.
Enabling mime data tracking has no benefits (only a slow down) unless you are generating a PDF.
### Canvas#toBuffer()
> ```ts
> canvas.toBuffer((err: Error|null, result: Buffer) => void, mimeType?: string, config?: any) => void
> canvas.toBuffer(mimeType?: string, config?: any) => Buffer
> ```
Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the image contained in the canvas.
* **callback** If provided, the buffer will be provided in the callback instead of being returned by the function. Invoked with an error as the first argument if encoding failed, or the resulting buffer as the second argument if it succeeded. Not supported for mimeType `raw` or for PDF or SVG canvases.
* **mimeType** A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support), `raw` (unencoded data in BGRA order on little-endian (most) systems, ARGB on big-endian systems; top-to-bottom), `application/pdf` (for PDF canvases) and `image/svg+xml` (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
* **config**
* For `image/jpeg`, an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional.
* For `image/png`, an object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only), the the background palette index (indexed PNGs only) and/or the resolution (ppi): `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional.
Note that the PNG format encodes the resolution in pixels per meter, so if you specify `96`, the file will encode 3780 ppm (~96.01 ppi). The resolution is undefined by default to match common browser behavior.
* For `application/pdf`, an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. All properties are optional and default to `undefined`, except for `creationDate`, which defaults to the current date. *Adding metadata requires Cairo 1.16.0 or later.*
For a description of these properties, see page 550 of [PDF 32000-1:2008](https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf).
Note that there is no standard separator for `keywords`. A space is recommended because it is in common use by other applications, and Cairo will enclose the list of keywords in quotes if a comma or semicolon is used.
**Return value**
If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html). If a callback is provided, none.
#### Examples
```js
// Default: buf contains a PNG-encoded image
const buf = canvas.toBuffer()
// PNG-encoded, zlib compression level 3 for faster compression but bigger files, no filtering
const buf2 = canvas.toBuffer('image/png', { compressionLevel: 3, filters: canvas.PNG_FILTER_NONE })
// JPEG-encoded, 50% quality
const buf3 = canvas.toBuffer('image/jpeg', { quality: 0.5 })
// Asynchronous PNG
canvas.toBuffer((err, buf) => {
if (err) throw err // encoding failed
// buf is PNG-encoded image
})
canvas.toBuffer((err, buf) => {
if (err) throw err // encoding failed
// buf is JPEG-encoded image at 95% quality
}, 'image/jpeg', { quality: 0.95 })
// BGRA pixel values, native-endian
const buf4 = canvas.toBuffer('raw')
const { stride, width } = canvas
// In memory, this is `canvas.height * canvas.stride` bytes long.
// The top row of pixels, in BGRA order on little-endian hardware,
// left-to-right, is:
const topPixelsBGRALeftToRight = buf4.slice(0, width * 4)
// And the third row is:
const row3 = buf4.slice(2 * stride, 2 * stride + width * 4)
// SVG and PDF canvases
const myCanvas = createCanvas(w, h, 'pdf')
myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas
// With optional metadata:
myCanvas.toBuffer('application/pdf', {
title: 'my picture',
keywords: 'node.js demo cairo',
creationDate: new Date()
})
```
### Canvas#createPNGStream()
> ```ts
> canvas.createPNGStream(config?: any) => ReadableStream
> ```
Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits PNG-encoded data.
* `config` An object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only) and/or the background palette index (indexed PNGs only): `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional.
#### Examples
```javascript
const fs = require('fs')
const out = fs.createWriteStream(__dirname + '/test.png')
const stream = canvas.createPNGStream()
stream.pipe(out)
out.on('finish', () => console.log('The PNG file was created.'))
```
To encode indexed PNGs from canvases with `pixelFormat: 'A8'` or `'A1'`, provide an options object:
```js
const palette = new Uint8ClampedArray([
//r g b a
0, 50, 50, 255, // index 1
10, 90, 90, 255, // index 2
127, 127, 255, 255
// ...
])
canvas.createPNGStream({
palette: palette,
backgroundIndex: 0 // optional, defaults to 0
})
```
### Canvas#createJPEGStream()
> ```ts
> canvas.createJPEGStream(config?: any) => ReadableStream
> ```
Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits JPEG-encoded data.
*Note: At the moment, `createJPEGStream()` is synchronous under the hood. That is, it runs in the main thread, not in the libuv threadpool.*
* `config` an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional.
#### Examples
```javascript
const fs = require('fs')
const out = fs.createWriteStream(__dirname + '/test.jpeg')
const stream = canvas.createJPEGStream()
stream.pipe(out)
out.on('finish', () => console.log('The JPEG file was created.'))
// Disable 2x2 chromaSubsampling for deeper colors and use a higher quality
const stream = canvas.createJPEGStream({
quality: 0.95,
chromaSubsampling: false
})
```
### Canvas#createPDFStream()
> ```ts
> canvas.createPDFStream(config?: any) => ReadableStream
> ```
* `config` an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. See `toBuffer()` for more information. *Adding metadata requires Cairo 1.16.0 or later.*
Applies to PDF canvases only. Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits the encoded PDF. `canvas.toBuffer()` also produces an encoded PDF, but `createPDFStream()` can be used to reduce memory usage.
### Canvas#toDataURL()
This is a standard API, but several non-standard calls are supported. The full list of supported calls is:
```js
dataUrl = canvas.toDataURL() // defaults to PNG
dataUrl = canvas.toDataURL('image/png')
dataUrl = canvas.toDataURL('image/jpeg')
dataUrl = canvas.toDataURL('image/jpeg', quality) // quality from 0 to 1
canvas.toDataURL((err, png) => { }) // defaults to PNG
canvas.toDataURL('image/png', (err, png) => { })
canvas.toDataURL('image/jpeg', (err, jpeg) => { }) // sync JPEG is not supported
canvas.toDataURL('image/jpeg', {...opts}, (err, jpeg) => { }) // see Canvas#createJPEGStream for valid options
canvas.toDataURL('image/jpeg', quality, (err, jpeg) => { }) // spec-following; quality from 0 to 1
```
### CanvasRenderingContext2D#patternQuality
> ```ts
> context.patternQuality: 'fast'|'good'|'best'|'nearest'|'bilinear'
> ```
Defaults to `'good'`. Affects pattern (gradient, image, etc.) rendering quality.
### CanvasRenderingContext2D#quality
> ```ts
> context.quality: 'fast'|'good'|'best'|'nearest'|'bilinear'
> ```
Defaults to `'good'`. Like `patternQuality`, but applies to transformations affecting more than just patterns.
### CanvasRenderingContext2D#textDrawingMode
> ```ts
> context.textDrawingMode: 'path'|'glyph'
> ```
Defaults to `'path'`. The effect depends on the canvas type:
* **Standard (image)** `glyph` and `path` both result in rasterized text. Glyph mode is faster than `path`, but may result in lower-quality text, especially when rotated or translated.
* **PDF** `glyph` will embed text instead of paths into the PDF. This is faster to encode, faster to open with PDF viewers, yields a smaller file size and makes the text selectable. The subset of the font needed to render the glyphs will be embedded in the PDF. This is usually the mode you want to use with PDF canvases.
* **SVG** `glyph` does *not* cause `<text>` elements to be produced as one might expect ([cairo bug](https://gitlab.freedesktop.org/cairo/cairo/issues/253)). Rather, `glyph` will create a `<defs>` section with a `<symbol>` for each glyph, then those glyphs be reused via `<use>` elements. `path` mode creates a `<path>` element for each text string. `glyph` mode is faster and yields a smaller file size.
In `glyph` mode, `ctx.strokeText()` and `ctx.fillText()` behave the same (aside from using the stroke and fill style, respectively).
This property is tracked as part of the canvas state in save/restore.
### CanvasRenderingContext2D#globalCompositeOperation = 'saturate'
In addition to all of the standard global composite operations defined by the Canvas specification, the ['saturate'](https://www.cairographics.org/operators/#saturate) operation is also available.
### CanvasRenderingContext2D#antialias
> ```ts
> context.antialias: 'default'|'none'|'gray'|'subpixel'
> ```
Sets the anti-aliasing mode.
## PDF Output Support
node-canvas can create PDF documents instead of images. The canvas type must be set when creating the canvas as follows:
```js
const canvas = createCanvas(200, 500, 'pdf')
```
An additional method `.addPage()` is then available to create multiple page PDFs:
```js
// On first page
ctx.font = '22px Helvetica'
ctx.fillText('Hello World', 50, 80)
ctx.addPage()
// Now on second page
ctx.font = '22px Helvetica'
ctx.fillText('Hello World 2', 50, 80)
canvas.toBuffer() // returns a PDF file
canvas.createPDFStream() // returns a ReadableStream that emits a PDF
// With optional document metadata (requires Cairo 1.16.0):
canvas.toBuffer('application/pdf', {
title: 'my picture',
keywords: 'node.js demo cairo',
creationDate: new Date()
})
```
It is also possible to create pages with different sizes by passing `width` and `height` to the `.addPage()` method:
```js
ctx.font = '22px Helvetica'
ctx.fillText('Hello World', 50, 80)
ctx.addPage(400, 800)
ctx.fillText('Hello World 2', 50, 80)
```
It is possible to add hyperlinks using `.beginTag()` and `.endTag()`:
```js
ctx.beginTag('Link', "uri='https://google.com'")
ctx.font = '22px Helvetica'
ctx.fillText('Hello World', 50, 80)
ctx.endTag('Link')
```
Or with a defined rectangle:
```js
ctx.beginTag('Link', "uri='https://google.com' rect=[50 80 100 20]")
ctx.endTag('Link')
```
Note that the syntax for attributes is unique to Cairo. See [cairo_tag_begin](https://www.cairographics.org/manual/cairo-Tags-and-Links.html#cairo-tag-begin) for the full documentation.
You can create areas on the canvas using the "cairo.dest" tag, and then link to them using the "Link" tag with the `dest=` attribute. You can also define PDF structure for accessibility by using tag names like "P", "H1", and "TABLE". The standard tags are defined in §14.8.4 of the [PDF 1.7](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf) specification.
See also:
* [Image#dataMode](#imagedatamode) for embedding JPEGs in PDFs
* [Canvas#createPDFStream()](#canvascreatepdfstream) for creating PDF streams
* [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode)
for embedding text instead of paths
## SVG Output Support
node-canvas can create SVG documents instead of images. The canvas type must be set when creating the canvas as follows:
```js
const canvas = createCanvas(200, 500, 'svg')
// Use the normal primitives.
fs.writeFileSync('out.svg', canvas.toBuffer())
```
## SVG Image Support
If librsvg is available when node-canvas is installed, node-canvas can render SVG images to your canvas context. This currently works by rasterizing the SVG image (i.e. drawing an SVG image to an SVG canvas will not preserve the SVG data).
```js
const img = new Image()
img.onload = () => ctx.drawImage(img, 0, 0)
img.onerror = err => { throw err }
img.src = './example.svg'
```
## Image pixel formats (experimental)
node-canvas has experimental support for additional pixel formats, roughly following the [Canvas color space proposal](https://github.com/WICG/canvas-color-space/blob/master/CanvasColorSpaceProposal.md).
```js
const canvas = createCanvas(200, 200)
const ctx = canvas.getContext('2d', { pixelFormat: 'A8' })
```
By default, canvases are created in the `RGBA32` format, which corresponds to the native HTML Canvas behavior. Each pixel is 32 bits. The JavaScript APIs that involve pixel data (`getImageData`, `putImageData`) store the colors in the order {red, green, blue, alpha} without alpha pre-multiplication. (The C++ API stores the colors in the order {alpha, red, green, blue} in native-[endian](https://en.wikipedia.org/wiki/Endianness) ordering, with alpha pre-multiplication.)
These additional pixel formats have experimental support:
* `RGB24` Like `RGBA32`, but the 8 alpha bits are always opaque. This format is always used if the `alpha` context attribute is set to false (i.e. `canvas.getContext('2d', {alpha: false})`). This format can be faster than `RGBA32` because transparency does not need to be calculated.
* `A8` Each pixel is 8 bits. This format can either be used for creating grayscale images (treating each byte as an alpha value), or for creating indexed PNGs (treating each byte as a palette index) (see [the example using alpha values with `fillStyle`](examples/indexed-png-alpha.js) and [the example using `imageData`](examples/indexed-png-image-data.js)).
* `RGB16_565` Each pixel is 16 bits, with red in the upper 5 bits, green in the middle 6 bits, and blue in the lower 5 bits, in native platform endianness. Some hardware devices and frame buffers use this format. Note that PNG does not support this format; when creating a PNG, the image will be converted to 24-bit RGB. This format is thus suboptimal for generating PNGs. `ImageData` instances for this mode use a `Uint16Array` instead of a `Uint8ClampedArray`.
* `A1` Each pixel is 1 bit, and pixels are packed together into 32-bit quantities. The ordering of the bits matches the endianness of the
platform: on a little-endian machine, the first pixel is the least-significant bit. This format can be used for creating single-color images. *Support for this format is incomplete, see note below.*
* `RGB30` Each pixel is 30 bits, with red in the upper 10, green in the middle 10, and blue in the lower 10. (Requires Cairo 1.12 or later.) *Support for this format is incomplete, see note below.*
Notes and caveats:
* Using a non-default format can affect the behavior of APIs that involve pixel data:
* `context2d.createImageData` The size of the array returned depends on the number of bit per pixel for the underlying image data format, per the above descriptions.
* `context2d.getImageData` The format of the array returned depends on the underlying image mode, per the above descriptions. Be aware of platform endianness, which can be determined using node.js's [`os.endianness()`](https://nodejs.org/api/os.html#os_os_endianness)
function.
* `context2d.putImageData` As above.
* `A1` and `RGB30` do not yet support `getImageData` or `putImageData`. Have a use case and/or opinion on working with these formats? Open an issue and let us know! (See #935.)
* `A1`, `A8`, `RGB30` and `RGB16_565` with shadow blurs may crash or not render properly.
* The `ImageData(width, height)` and `ImageData(Uint8ClampedArray, width)` constructors assume 4 bytes per pixel. To create an `ImageData` instance with a different number of bytes per pixel, use `new ImageData(new Uint8ClampedArray(size), width, height)` or `new ImageData(new Uint16ClampedArray(size), width, height)`.
## Testing
First make sure you've built the latest version. Get all the deps you need (see [compiling](#compiling) above), and run:
```
npm install --build-from-source
```
For visual tests: `npm run test-server` and point your browser to http://localhost:4000.
For unit tests: `npm run test`.
## Benchmarks
Benchmarks live in the `benchmarks` directory.
## Examples
Examples line in the `examples` directory. Most produce a png image of the same name, and others such as *live-clock.js* launch an HTTP server to be viewed in the browser.
## Original Authors
- TJ Holowaychuk ([tj](http://github.com/tj))
- Nathan Rajlich ([TooTallNate](http://github.com/TooTallNate))
- Rod Vagg ([rvagg](http://github.com/rvagg))
- Juriy Zaytsev ([kangax](http://github.com/kangax))
## License
### node-canvas
(The MIT License)
Copyright (c) 2010 LearnBoost, and contributors &lt;dev@learnboost.com&gt;
Copyright (c) 2014 Automattic, Inc and contributors &lt;dev@automattic.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the 'Software'), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### BMP parser
See [license](src/bmp/LICENSE.md)
+229
View File
@@ -0,0 +1,229 @@
{
'conditions': [
['OS=="win"', {
'variables': {
'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle
'with_jpeg%': 'false',
'with_gif%': 'false',
'with_rsvg%': 'false',
'variables': { # Nest jpeg_root to evaluate it before with_jpeg
'jpeg_root%': '<!(node ./util/win_jpeg_lookup)'
},
'jpeg_root%': '<(jpeg_root)', # Take value of nested variable
'conditions': [
['jpeg_root==""', {
'with_jpeg%': 'false'
}, {
'with_jpeg%': 'true'
}]
]
}
}, { # 'OS!="win"'
'variables': {
'with_jpeg%': '<!(node ./util/has_lib.js jpeg)',
'with_gif%': '<!(node ./util/has_lib.js gif)',
'with_rsvg%': '<!(node ./util/has_lib.js rsvg)'
}
}]
],
'targets': [
{
'target_name': 'canvas-postbuild',
'dependencies': ['canvas'],
'conditions': [
['OS=="win"', {
'copies': [{
'destination': '<(PRODUCT_DIR)',
'files': [
'<(GTK_Root)/bin/zlib1.dll',
'<(GTK_Root)/bin/libintl-8.dll',
'<(GTK_Root)/bin/libpng14-14.dll',
'<(GTK_Root)/bin/libpangocairo-1.0-0.dll',
'<(GTK_Root)/bin/libpango-1.0-0.dll',
'<(GTK_Root)/bin/libpangoft2-1.0-0.dll',
'<(GTK_Root)/bin/libpangowin32-1.0-0.dll',
'<(GTK_Root)/bin/libcairo-2.dll',
'<(GTK_Root)/bin/libfontconfig-1.dll',
'<(GTK_Root)/bin/libfreetype-6.dll',
'<(GTK_Root)/bin/libglib-2.0-0.dll',
'<(GTK_Root)/bin/libgobject-2.0-0.dll',
'<(GTK_Root)/bin/libgmodule-2.0-0.dll',
'<(GTK_Root)/bin/libgthread-2.0-0.dll',
'<(GTK_Root)/bin/libexpat-1.dll'
]
}]
}]
]
},
{
'target_name': 'canvas',
'include_dirs': ["<!(node -p \"require('node-addon-api').include_dir\")"],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS', 'NODE_ADDON_API_ENABLE_MAYBE' ],
'sources': [
'src/bmp/BMPParser.cc',
'src/Canvas.cc',
'src/CanvasGradient.cc',
'src/CanvasPattern.cc',
'src/CanvasRenderingContext2d.cc',
'src/closure.cc',
'src/color.cc',
'src/Image.cc',
'src/ImageData.cc',
'src/init.cc',
'src/register_font.cc',
'src/FontParser.cc'
],
'conditions': [
['OS=="win"', {
'libraries': [
'-l<(GTK_Root)/lib/cairo.lib',
'-l<(GTK_Root)/lib/libpng.lib',
'-l<(GTK_Root)/lib/pangocairo-1.0.lib',
'-l<(GTK_Root)/lib/pango-1.0.lib',
'-l<(GTK_Root)/lib/freetype.lib',
'-l<(GTK_Root)/lib/glib-2.0.lib',
'-l<(GTK_Root)/lib/gobject-2.0.lib'
],
'include_dirs': [
'<(GTK_Root)/include',
'<(GTK_Root)/include/cairo',
'<(GTK_Root)/include/pango-1.0',
'<(GTK_Root)/include/glib-2.0',
'<(GTK_Root)/include/freetype2',
'<(GTK_Root)/lib/glib-2.0/include'
],
'defines': [
'_USE_MATH_DEFINES', # for M_PI
'NOMINMAX' # allow std::min/max to work
],
'configurations': {
'Debug': {
'msvs_settings': {
'VCCLCompilerTool': {
'WarningLevel': 4,
'ExceptionHandling': 1,
'DisableSpecificWarnings': [
4100, 4611
]
}
}
},
'Release': {
'msvs_settings': {
'VCCLCompilerTool': {
'WarningLevel': 4,
'ExceptionHandling': 1,
'DisableSpecificWarnings': [
4100, 4611
]
}
}
}
}
}, { # 'OS!="win"'
'libraries': [
'<!@(pkg-config pixman-1 --libs)',
'<!@(pkg-config cairo --libs)',
'<!@(pkg-config libpng --libs)',
'<!@(pkg-config pangocairo --libs)',
'<!@(pkg-config freetype2 --libs)'
],
'include_dirs': [
'<!@(pkg-config cairo --cflags-only-I | sed s/-I//g)',
'<!@(pkg-config libpng --cflags-only-I | sed s/-I//g)',
'<!@(pkg-config pangocairo --cflags-only-I | sed s/-I//g)',
'<!@(pkg-config freetype2 --cflags-only-I | sed s/-I//g)'
],
'cflags': ['-Wno-cast-function-type'],
'cflags!': ['-fno-exceptions'],
'cflags_cc!': ['-fno-exceptions']
}],
['OS=="mac"', {
'cflags+': ['-fvisibility=hidden'],
'xcode_settings': {
'GCC_SYMBOLS_PRIVATE_EXTERN': 'YES', # -fvisibility=hidden
'GCC_ENABLE_CPP_EXCEPTIONS': 'YES'
}
}],
['with_jpeg=="true"', {
'defines': [
'HAVE_JPEG'
],
'conditions': [
['OS=="win"', {
'copies': [{
'destination': '<(PRODUCT_DIR)',
'files': [
'<(jpeg_root)/bin/jpeg62.dll',
]
}],
'include_dirs': [
'<(jpeg_root)/include'
],
'libraries': [
'-l<(jpeg_root)/lib/jpeg.lib',
]
}, {
'include_dirs': [
'<!@(pkg-config libjpeg --cflags-only-I | sed s/-I//g)'
],
'libraries': [
'<!@(pkg-config libjpeg --libs)'
]
}]
]
}],
['with_gif=="true"', {
'defines': [
'HAVE_GIF'
],
'conditions': [
['OS=="win"', {
'libraries': [
'-l<(GTK_Root)/lib/gif.lib'
]
}, {
'include_dirs': [
'/opt/homebrew/include'
],
'libraries': [
'-L/opt/homebrew/lib',
'-lgif'
]
}]
]
}],
['with_rsvg=="true"', {
'defines': [
'HAVE_RSVG'
],
'conditions': [
['OS=="win"', {
'copies': [{
'destination': '<(PRODUCT_DIR)',
'files': [
'<(GTK_Root)/bin/librsvg-2-2.dll',
'<(GTK_Root)/bin/libgdk_pixbuf-2.0-0.dll',
'<(GTK_Root)/bin/libgio-2.0-0.dll',
'<(GTK_Root)/bin/libcroco-0.6-3.dll',
'<(GTK_Root)/bin/libgsf-1-114.dll',
'<(GTK_Root)/bin/libxml2-2.dll'
]
}],
'libraries': [
'-l<(GTK_Root)/lib/librsvg-2-2.lib'
]
}, {
'include_dirs': [
'<!@(pkg-config librsvg-2.0 --cflags-only-I | sed s/-I//g)'
],
'libraries': [
'<!@(pkg-config librsvg-2.0 --libs)'
]
}]
]
}]
]
}
]
}
+31
View File
@@ -0,0 +1,31 @@
/* globals document, ImageData */
exports.createCanvas = function (width, height) {
return Object.assign(document.createElement('canvas'), { width: width, height: height })
}
exports.createImageData = function (array, width, height) {
// Browser implementation of ImageData looks at the number of arguments passed
switch (arguments.length) {
case 0: return new ImageData()
case 1: return new ImageData(array)
case 2: return new ImageData(array, width)
default: return new ImageData(array, width, height)
}
}
exports.loadImage = function (src, options) {
return new Promise(function (resolve, reject) {
const image = Object.assign(document.createElement('img'), options)
function cleanup () {
image.onload = null
image.onerror = null
}
image.onload = function () { cleanup(); resolve(image) }
image.onerror = function () { cleanup(); reject(new Error('Failed to load the image "' + src + '"')) }
image.src = src
})
}
+354
View File
@@ -0,0 +1,354 @@
# We borrow heavily from the kernel build setup, though we are simpler since
# we don't have Kconfig tweaking settings on us.
# The implicit make rules have it looking for RCS files, among other things.
# We instead explicitly write all the rules we care about.
# It's even quicker (saves ~200ms) to pass -r on the command line.
MAKEFLAGS=-r
# The source directory tree.
srcdir := ..
abs_srcdir := $(abspath $(srcdir))
# The name of the builddir.
builddir_name ?= .
# The V=1 flag on command line makes us verbosely print command lines.
ifdef V
quiet=
else
quiet=quiet_
endif
# Specify BUILDTYPE=Release on the command line for a release build.
BUILDTYPE ?= Release
# Directory all our build output goes into.
# Note that this must be two directories beneath src/ for unit tests to pass,
# as they reach into the src/ directory for data with relative paths.
builddir ?= $(builddir_name)/$(BUILDTYPE)
abs_builddir := $(abspath $(builddir))
depsdir := $(builddir)/.deps
# Object output directory.
obj := $(builddir)/obj
abs_obj := $(abspath $(obj))
# We build up a list of every single one of the targets so we can slurp in the
# generated dependency rule Makefiles in one pass.
all_deps :=
CC.target ?= $(CC)
CFLAGS.target ?= $(CPPFLAGS) $(CFLAGS)
CXX.target ?= $(CXX)
CXXFLAGS.target ?= $(CPPFLAGS) $(CXXFLAGS)
LINK.target ?= $(LINK)
LDFLAGS.target ?= $(LDFLAGS)
AR.target ?= $(AR)
PLI.target ?= pli
# C++ apps need to be linked with g++.
LINK ?= $(CXX.target)
# TODO(evan): move all cross-compilation logic to gyp-time so we don't need
# to replicate this environment fallback in make as well.
CC.host ?= gcc
CFLAGS.host ?= $(CPPFLAGS_host) $(CFLAGS_host)
CXX.host ?= g++
CXXFLAGS.host ?= $(CPPFLAGS_host) $(CXXFLAGS_host)
LINK.host ?= $(CXX.host)
LDFLAGS.host ?= $(LDFLAGS_host)
AR.host ?= ar
PLI.host ?= pli
# Define a dir function that can handle spaces.
# http://www.gnu.org/software/make/manual/make.html#Syntax-of-Functions
# "leading spaces cannot appear in the text of the first argument as written.
# These characters can be put into the argument value by variable substitution."
empty :=
space := $(empty) $(empty)
# http://stackoverflow.com/questions/1189781/using-make-dir-or-notdir-on-a-path-with-spaces
replace_spaces = $(subst $(space),?,$1)
unreplace_spaces = $(subst ?,$(space),$1)
dirx = $(call unreplace_spaces,$(dir $(call replace_spaces,$1)))
# Flags to make gcc output dependency info. Note that you need to be
# careful here to use the flags that ccache and distcc can understand.
# We write to a dep file on the side first and then rename at the end
# so we can't end up with a broken dep file.
depfile = $(depsdir)/$(call replace_spaces,$@).d
DEPFLAGS = -MMD -MF $(depfile).raw
# We have to fixup the deps output in a few ways.
# (1) the file output should mention the proper .o file.
# ccache or distcc lose the path to the target, so we convert a rule of
# the form:
# foobar.o: DEP1 DEP2
# into
# path/to/foobar.o: DEP1 DEP2
# (2) we want missing files not to cause us to fail to build.
# We want to rewrite
# foobar.o: DEP1 DEP2 \
# DEP3
# to
# DEP1:
# DEP2:
# DEP3:
# so if the files are missing, they're just considered phony rules.
# We have to do some pretty insane escaping to get those backslashes
# and dollar signs past make, the shell, and sed at the same time.
# Doesn't work with spaces, but that's fine: .d files have spaces in
# their names replaced with other characters.
define fixup_dep
# The depfile may not exist if the input file didn't have any #includes.
touch $(depfile).raw
# Fixup path as in (1).
sed -e "s|^$(notdir $@)|$@|" $(depfile).raw >> $(depfile)
# Add extra rules as in (2).
# We remove slashes and replace spaces with new lines;
# remove blank lines;
# delete the first line and append a colon to the remaining lines.
sed -e 's|\\||' -e 'y| |\n|' $(depfile).raw |\
grep -v '^$$' |\
sed -e 1d -e 's|$$|:|' \
>> $(depfile)
rm $(depfile).raw
endef
# Command definitions:
# - cmd_foo is the actual command to run;
# - quiet_cmd_foo is the brief-output summary of the command.
quiet_cmd_cc = CC($(TOOLSET)) $@
cmd_cc = $(CC.$(TOOLSET)) -o $@ $< $(GYP_CFLAGS) $(DEPFLAGS) $(CFLAGS.$(TOOLSET)) -c
quiet_cmd_cxx = CXX($(TOOLSET)) $@
cmd_cxx = $(CXX.$(TOOLSET)) -o $@ $< $(GYP_CXXFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c
quiet_cmd_touch = TOUCH $@
cmd_touch = touch $@
quiet_cmd_copy = COPY $@
# send stderr to /dev/null to ignore messages when linking directories.
cmd_copy = ln -f "$<" "$@" 2>/dev/null || (rm -rf "$@" && cp -af "$<" "$@")
quiet_cmd_symlink = SYMLINK $@
cmd_symlink = ln -sf "$<" "$@"
quiet_cmd_alink = AR($(TOOLSET)) $@
cmd_alink = rm -f $@ && $(AR.$(TOOLSET)) crs $@ $(filter %.o,$^)
quiet_cmd_alink_thin = AR($(TOOLSET)) $@
cmd_alink_thin = rm -f $@ && $(AR.$(TOOLSET)) crsT $@ $(filter %.o,$^)
# Due to circular dependencies between libraries :(, we wrap the
# special "figure out circular dependencies" flags around the entire
# input list during linking.
quiet_cmd_link = LINK($(TOOLSET)) $@
cmd_link = $(LINK.$(TOOLSET)) -o $@ $(GYP_LDFLAGS) $(LDFLAGS.$(TOOLSET)) -Wl,--start-group $(LD_INPUTS) $(LIBS) -Wl,--end-group
# Note: this does not handle spaces in paths
define xargs
$(1) $(word 1,$(2))
$(if $(word 2,$(2)),$(call xargs,$(1),$(wordlist 2,$(words $(2)),$(2))))
endef
define write-to-file
@: >$(1)
$(call xargs,@printf "%s\n" >>$(1),$(2))
endef
OBJ_FILE_LIST := ar-file-list
define create_archive
rm -f $(1) $(1).$(OBJ_FILE_LIST); mkdir -p `dirname $(1)`
$(call write-to-file,$(1).$(OBJ_FILE_LIST),$(filter %.o,$(2)))
$(AR.$(TOOLSET)) crs $(1) @$(1).$(OBJ_FILE_LIST)
endef
define create_thin_archive
rm -f $(1) $(OBJ_FILE_LIST); mkdir -p `dirname $(1)`
$(call write-to-file,$(1).$(OBJ_FILE_LIST),$(filter %.o,$(2)))
$(AR.$(TOOLSET)) crsT $(1) @$(1).$(OBJ_FILE_LIST)
endef
# We support two kinds of shared objects (.so):
# 1) shared_library, which is just bundling together many dependent libraries
# into a link line.
# 2) loadable_module, which is generating a module intended for dlopen().
#
# They differ only slightly:
# In the former case, we want to package all dependent code into the .so.
# In the latter case, we want to package just the API exposed by the
# outermost module.
# This means shared_library uses --whole-archive, while loadable_module doesn't.
# (Note that --whole-archive is incompatible with the --start-group used in
# normal linking.)
# Other shared-object link notes:
# - Set SONAME to the library filename so our binaries don't reference
# the local, absolute paths used on the link command-line.
quiet_cmd_solink = SOLINK($(TOOLSET)) $@
cmd_solink = $(LINK.$(TOOLSET)) -o $@ -shared $(GYP_LDFLAGS) $(LDFLAGS.$(TOOLSET)) -Wl,-soname=$(@F) -Wl,--whole-archive $(LD_INPUTS) -Wl,--no-whole-archive $(LIBS)
quiet_cmd_solink_module = SOLINK_MODULE($(TOOLSET)) $@
cmd_solink_module = $(LINK.$(TOOLSET)) -o $@ -shared $(GYP_LDFLAGS) $(LDFLAGS.$(TOOLSET)) -Wl,-soname=$(@F) -Wl,--start-group $(filter-out FORCE_DO_CMD, $^) -Wl,--end-group $(LIBS)
# Define an escape_quotes function to escape single quotes.
# This allows us to handle quotes properly as long as we always use
# use single quotes and escape_quotes.
escape_quotes = $(subst ','\'',$(1))
# This comment is here just to include a ' to unconfuse syntax highlighting.
# Define an escape_vars function to escape '$' variable syntax.
# This allows us to read/write command lines with shell variables (e.g.
# $LD_LIBRARY_PATH), without triggering make substitution.
escape_vars = $(subst $$,$$$$,$(1))
# Helper that expands to a shell command to echo a string exactly as it is in
# make. This uses printf instead of echo because printf's behaviour with respect
# to escape sequences is more portable than echo's across different shells
# (e.g., dash, bash).
exact_echo = printf '%s\n' '$(call escape_quotes,$(1))'
# Helper to compare the command we're about to run against the command
# we logged the last time we ran the command. Produces an empty
# string (false) when the commands match.
# Tricky point: Make has no string-equality test function.
# The kernel uses the following, but it seems like it would have false
# positives, where one string reordered its arguments.
# arg_check = $(strip $(filter-out $(cmd_$(1)), $(cmd_$@)) \
# $(filter-out $(cmd_$@), $(cmd_$(1))))
# We instead substitute each for the empty string into the other, and
# say they're equal if both substitutions produce the empty string.
# .d files contain ? instead of spaces, take that into account.
command_changed = $(or $(subst $(cmd_$(1)),,$(cmd_$(call replace_spaces,$@))),\
$(subst $(cmd_$(call replace_spaces,$@)),,$(cmd_$(1))))
# Helper that is non-empty when a prerequisite changes.
# Normally make does this implicitly, but we force rules to always run
# so we can check their command lines.
# $? -- new prerequisites
# $| -- order-only dependencies
prereq_changed = $(filter-out FORCE_DO_CMD,$(filter-out $|,$?))
# Helper that executes all postbuilds until one fails.
define do_postbuilds
@E=0;\
for p in $(POSTBUILDS); do\
eval $$p;\
E=$$?;\
if [ $$E -ne 0 ]; then\
break;\
fi;\
done;\
if [ $$E -ne 0 ]; then\
rm -rf "$@";\
exit $$E;\
fi
endef
# do_cmd: run a command via the above cmd_foo names, if necessary.
# Should always run for a given target to handle command-line changes.
# Second argument, if non-zero, makes it do asm/C/C++ dependency munging.
# Third argument, if non-zero, makes it do POSTBUILDS processing.
# Note: We intentionally do NOT call dirx for depfile, since it contains ? for
# spaces already and dirx strips the ? characters.
define do_cmd
$(if $(or $(command_changed),$(prereq_changed)),
@$(call exact_echo, $($(quiet)cmd_$(1)))
@mkdir -p "$(call dirx,$@)" "$(dir $(depfile))"
$(if $(findstring flock,$(word 1,$(cmd_$1))),
@$(cmd_$(1))
@echo " $(quiet_cmd_$(1)): Finished",
@$(cmd_$(1))
)
@$(call exact_echo,$(call escape_vars,cmd_$(call replace_spaces,$@) := $(cmd_$(1)))) > $(depfile)
@$(if $(2),$(fixup_dep))
$(if $(and $(3), $(POSTBUILDS)),
$(call do_postbuilds)
)
)
endef
# Declare the "all" target first so it is the default,
# even though we don't have the deps yet.
.PHONY: all
all:
# make looks for ways to re-generate included makefiles, but in our case, we
# don't have a direct way. Explicitly telling make that it has nothing to do
# for them makes it go faster.
%.d: ;
# Use FORCE_DO_CMD to force a target to run. Should be coupled with
# do_cmd.
.PHONY: FORCE_DO_CMD
FORCE_DO_CMD:
TOOLSET := target
# Suffix rules, putting all outputs into $(obj).
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.c FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cc FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cpp FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cxx FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.s FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.S FORCE_DO_CMD
@$(call do_cmd,cc,1)
# Try building from generated source, too.
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.c FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.cc FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.cpp FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.cxx FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.s FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.S FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.c FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.cc FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.cpp FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.cxx FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.s FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.S FORCE_DO_CMD
@$(call do_cmd,cc,1)
ifeq ($(strip $(foreach prefix,$(NO_LOAD),\
$(findstring $(join ^,$(prefix)),\
$(join ^,canvas.target.mk)))),)
include canvas.target.mk
endif
quiet_cmd_regen_makefile = ACTION Regenerating $@
cmd_regen_makefile = cd $(srcdir); /__t/node/21.7.3/arm64/lib/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/github/home/.cache/node-gyp/21.7.3" "-Dnode_gyp_dir=/__t/node/21.7.3/arm64/lib/node_modules/node-gyp" "-Dnode_lib_file=/github/home/.cache/node-gyp/21.7.3/<(target_arch)/node.lib" "-Dmodule_root_dir=/__w/node-canvas/node-canvas" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/__w/node-canvas/node-canvas/build/config.gypi -I/__t/node/21.7.3/arm64/lib/node_modules/node-gyp/addon.gypi -I/github/home/.cache/node-gyp/21.7.3/include/node/common.gypi "--toplevel-dir=." binding.gyp
Makefile: $(srcdir)/../../../__t/node/21.7.3/arm64/lib/node_modules/node-gyp/addon.gypi $(srcdir)/../../../github/home/.cache/node-gyp/21.7.3/include/node/common.gypi $(srcdir)/binding.gyp $(srcdir)/build/config.gypi
$(call do_cmd,regen_makefile)
# "all" is a concatenation of the "all" targets from all the included
# sub-makefiles. This is just here to clarify.
all:
# Add in dependency-tracking rules. $(all_deps) is the list of every single
# target in our tree. Only consider the ones with .d (dependency) info:
d_files := $(wildcard $(foreach f,$(all_deps),$(depsdir)/$(f).d))
ifneq ($(d_files),)
include $(d_files)
endif
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
# This file is generated by gyp; do not edit.
export builddir_name ?= ./build/.
.PHONY: all
all:
$(MAKE) canvas
+214
View File
@@ -0,0 +1,214 @@
# This file is generated by gyp; do not edit.
TOOLSET := target
TARGET := canvas
DEFS_Debug := \
'-DNODE_GYP_MODULE_NAME=canvas' \
'-DUSING_UV_SHARED=1' \
'-DUSING_V8_SHARED=1' \
'-DV8_DEPRECATION_WARNINGS=1' \
'-D_GLIBCXX_USE_CXX11_ABI=1' \
'-D_LARGEFILE_SOURCE' \
'-D_FILE_OFFSET_BITS=64' \
'-D__STDC_FORMAT_MACROS' \
'-DOPENSSL_NO_PINSHARED' \
'-DOPENSSL_THREADS' \
'-DHAVE_GIF' \
'-DHAVE_JPEG' \
'-DHAVE_RSVG' \
'-DNAPI_DISABLE_CPP_EXCEPTIONS' \
'-DNODE_ADDON_API_ENABLE_MAYBE' \
'-DBUILDING_NODE_EXTENSION' \
'-DDEBUG' \
'-D_DEBUG'
# Flags passed to all source files.
CFLAGS_Debug := \
-fPIC \
-pthread \
-Wall \
-Wextra \
-Wno-unused-parameter \
-g \
-O0
# Flags passed to only C files.
CFLAGS_C_Debug :=
# Flags passed to only C++ files.
CFLAGS_CC_Debug := \
-fno-rtti \
-std=gnu++17
INCS_Debug := \
-I/github/home/.cache/node-gyp/21.7.3/include/node \
-I/github/home/.cache/node-gyp/21.7.3/src \
-I/github/home/.cache/node-gyp/21.7.3/deps/openssl/config \
-I/github/home/.cache/node-gyp/21.7.3/deps/openssl/openssl/include \
-I/github/home/.cache/node-gyp/21.7.3/deps/uv/include \
-I/github/home/.cache/node-gyp/21.7.3/deps/zlib \
-I/github/home/.cache/node-gyp/21.7.3/deps/v8/include \
-I$(srcdir)/node_modules/node-addon-api \
-I/usr/local/include/cairo \
-I/usr/local/include \
-I/usr/local/include/glib-2.0 \
-I/usr/local/lib/glib-2.0/include \
-I/usr/local/include/pixman-1 \
-I/usr/local/include/freetype2 \
-I/usr/local/include/libpng16 \
-I/usr/local/include/pango-1.0 \
-I/usr/local/include/fribidi \
-I/usr/local/include/harfbuzz \
-I/usr/local/include/librsvg-2.0 \
-I/usr/local/include/gdk-pixbuf-2.0
DEFS_Release := \
'-DNODE_GYP_MODULE_NAME=canvas' \
'-DUSING_UV_SHARED=1' \
'-DUSING_V8_SHARED=1' \
'-DV8_DEPRECATION_WARNINGS=1' \
'-D_GLIBCXX_USE_CXX11_ABI=1' \
'-D_LARGEFILE_SOURCE' \
'-D_FILE_OFFSET_BITS=64' \
'-D__STDC_FORMAT_MACROS' \
'-DOPENSSL_NO_PINSHARED' \
'-DOPENSSL_THREADS' \
'-DHAVE_GIF' \
'-DHAVE_JPEG' \
'-DHAVE_RSVG' \
'-DNAPI_DISABLE_CPP_EXCEPTIONS' \
'-DNODE_ADDON_API_ENABLE_MAYBE' \
'-DBUILDING_NODE_EXTENSION'
# Flags passed to all source files.
CFLAGS_Release := \
-fPIC \
-pthread \
-Wall \
-Wextra \
-Wno-unused-parameter \
-O3 \
-fno-omit-frame-pointer
# Flags passed to only C files.
CFLAGS_C_Release :=
# Flags passed to only C++ files.
CFLAGS_CC_Release := \
-fno-rtti \
-std=gnu++17
INCS_Release := \
-I/github/home/.cache/node-gyp/21.7.3/include/node \
-I/github/home/.cache/node-gyp/21.7.3/src \
-I/github/home/.cache/node-gyp/21.7.3/deps/openssl/config \
-I/github/home/.cache/node-gyp/21.7.3/deps/openssl/openssl/include \
-I/github/home/.cache/node-gyp/21.7.3/deps/uv/include \
-I/github/home/.cache/node-gyp/21.7.3/deps/zlib \
-I/github/home/.cache/node-gyp/21.7.3/deps/v8/include \
-I$(srcdir)/node_modules/node-addon-api \
-I/usr/local/include/cairo \
-I/usr/local/include \
-I/usr/local/include/glib-2.0 \
-I/usr/local/lib/glib-2.0/include \
-I/usr/local/include/pixman-1 \
-I/usr/local/include/freetype2 \
-I/usr/local/include/libpng16 \
-I/usr/local/include/pango-1.0 \
-I/usr/local/include/fribidi \
-I/usr/local/include/harfbuzz \
-I/usr/local/include/librsvg-2.0 \
-I/usr/local/include/gdk-pixbuf-2.0
OBJS := \
$(obj).target/$(TARGET)/src/bmp/BMPParser.o \
$(obj).target/$(TARGET)/src/Canvas.o \
$(obj).target/$(TARGET)/src/CanvasGradient.o \
$(obj).target/$(TARGET)/src/CanvasPattern.o \
$(obj).target/$(TARGET)/src/CanvasRenderingContext2d.o \
$(obj).target/$(TARGET)/src/closure.o \
$(obj).target/$(TARGET)/src/color.o \
$(obj).target/$(TARGET)/src/Image.o \
$(obj).target/$(TARGET)/src/ImageData.o \
$(obj).target/$(TARGET)/src/init.o \
$(obj).target/$(TARGET)/src/register_font.o \
$(obj).target/$(TARGET)/src/FontParser.o
# Add to the list of files we specially track dependencies for.
all_deps += $(OBJS)
# CFLAGS et al overrides must be target-local.
# See "Target-specific Variable Values" in the GNU Make manual.
$(OBJS): TOOLSET := $(TOOLSET)
$(OBJS): GYP_CFLAGS := $(DEFS_$(BUILDTYPE)) $(INCS_$(BUILDTYPE)) $(CFLAGS_$(BUILDTYPE)) $(CFLAGS_C_$(BUILDTYPE))
$(OBJS): GYP_CXXFLAGS := $(DEFS_$(BUILDTYPE)) $(INCS_$(BUILDTYPE)) $(CFLAGS_$(BUILDTYPE)) $(CFLAGS_CC_$(BUILDTYPE))
# Suffix rules, putting all outputs into $(obj).
$(obj).$(TOOLSET)/$(TARGET)/%.o: $(srcdir)/%.cc FORCE_DO_CMD
@$(call do_cmd,cxx,1)
# Try building from generated source, too.
$(obj).$(TOOLSET)/$(TARGET)/%.o: $(obj).$(TOOLSET)/%.cc FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/$(TARGET)/%.o: $(obj)/%.cc FORCE_DO_CMD
@$(call do_cmd,cxx,1)
# End of this set of suffix rules
### Rules for final target.
LDFLAGS_Debug := \
-pthread \
-rdynamic \
-Wl,-rpath '-Wl,$$ORIGIN'
LDFLAGS_Release := \
-pthread \
-rdynamic \
-Wl,-rpath '-Wl,$$ORIGIN'
LIBS := \
-L/usr/local/lib \
-lpixman-1 \
-lcairo \
-lpng16 \
-lz \
-lpangocairo-1.0 \
-lpango-1.0 \
-lgobject-2.0 \
-lglib-2.0 \
-lharfbuzz \
-lfreetype \
-lrsvg-2 \
-lm \
-lgio-2.0 \
-lgdk_pixbuf-2.0 \
-ljpeg \
-lgif
$(obj).target/canvas.node: GYP_LDFLAGS := $(LDFLAGS_$(BUILDTYPE))
$(obj).target/canvas.node: LIBS := $(LIBS)
$(obj).target/canvas.node: TOOLSET := $(TOOLSET)
$(obj).target/canvas.node: $(OBJS) FORCE_DO_CMD
$(call do_cmd,solink_module)
all_deps += $(obj).target/canvas.node
# Add target alias
.PHONY: canvas
canvas: $(builddir)/canvas.node
# Copy this to the executable output path.
$(builddir)/canvas.node: TOOLSET := $(TOOLSET)
$(builddir)/canvas.node: $(obj).target/canvas.node FORCE_DO_CMD
$(call do_cmd,copy)
all_deps += $(builddir)/canvas.node
# Short alias for building this executable.
.PHONY: canvas.node
canvas.node: $(obj).target/canvas.node $(builddir)/canvas.node
# Add executable to "all" target.
.PHONY: all
all: $(builddir)/canvas.node
+422
View File
@@ -0,0 +1,422 @@
# Do not edit. File was generated by node-gyp's "configure" step
{
"target_defaults": {
"cflags": [],
"default_configuration": "Release",
"defines": [],
"include_dirs": [],
"libraries": [],
"msvs_configuration_platform": "ARM64",
"xcode_configuration_platform": "arm64"
},
"variables": {
"arm_fpu": "neon",
"asan": 0,
"coverage": "false",
"dcheck_always_on": 0,
"debug_nghttp2": "false",
"debug_node": "false",
"enable_lto": "false",
"enable_pgo_generate": "false",
"enable_pgo_use": "false",
"error_on_warn": "false",
"force_dynamic_crt": 0,
"gas_version": "2.35",
"host_arch": "arm64",
"icu_data_in": "../../deps/icu-tmp/icudt74l.dat",
"icu_endianness": "l",
"icu_gyp_path": "tools/icu/icu-generic.gyp",
"icu_path": "deps/icu-small",
"icu_small": "false",
"icu_ver_major": "74",
"is_debug": 0,
"libdir": "lib",
"llvm_version": "0.0",
"napi_build_version": "9",
"node_builtin_shareable_builtins": [
"deps/cjs-module-lexer/lexer.js",
"deps/cjs-module-lexer/dist/lexer.js",
"deps/undici/undici.js"
],
"node_byteorder": "little",
"node_debug_lib": "false",
"node_enable_d8": "false",
"node_enable_v8_vtunejit": "false",
"node_fipsinstall": "false",
"node_install_corepack": "true",
"node_install_npm": "true",
"node_library_files": [
"lib/_http_agent.js",
"lib/_http_client.js",
"lib/_http_common.js",
"lib/_http_incoming.js",
"lib/_http_outgoing.js",
"lib/_http_server.js",
"lib/_stream_duplex.js",
"lib/_stream_passthrough.js",
"lib/_stream_readable.js",
"lib/_stream_transform.js",
"lib/_stream_wrap.js",
"lib/_stream_writable.js",
"lib/_tls_common.js",
"lib/_tls_wrap.js",
"lib/assert.js",
"lib/assert/strict.js",
"lib/async_hooks.js",
"lib/buffer.js",
"lib/child_process.js",
"lib/cluster.js",
"lib/console.js",
"lib/constants.js",
"lib/crypto.js",
"lib/dgram.js",
"lib/diagnostics_channel.js",
"lib/dns.js",
"lib/dns/promises.js",
"lib/domain.js",
"lib/events.js",
"lib/fs.js",
"lib/fs/promises.js",
"lib/http.js",
"lib/http2.js",
"lib/https.js",
"lib/inspector.js",
"lib/inspector/promises.js",
"lib/internal/abort_controller.js",
"lib/internal/assert.js",
"lib/internal/assert/assertion_error.js",
"lib/internal/assert/calltracker.js",
"lib/internal/async_hooks.js",
"lib/internal/blob.js",
"lib/internal/blocklist.js",
"lib/internal/bootstrap/node.js",
"lib/internal/bootstrap/realm.js",
"lib/internal/bootstrap/shadow_realm.js",
"lib/internal/bootstrap/switches/does_not_own_process_state.js",
"lib/internal/bootstrap/switches/does_own_process_state.js",
"lib/internal/bootstrap/switches/is_main_thread.js",
"lib/internal/bootstrap/switches/is_not_main_thread.js",
"lib/internal/bootstrap/web/exposed-wildcard.js",
"lib/internal/bootstrap/web/exposed-window-or-worker.js",
"lib/internal/buffer.js",
"lib/internal/child_process.js",
"lib/internal/child_process/serialization.js",
"lib/internal/cli_table.js",
"lib/internal/cluster/child.js",
"lib/internal/cluster/primary.js",
"lib/internal/cluster/round_robin_handle.js",
"lib/internal/cluster/shared_handle.js",
"lib/internal/cluster/utils.js",
"lib/internal/cluster/worker.js",
"lib/internal/console/constructor.js",
"lib/internal/console/global.js",
"lib/internal/constants.js",
"lib/internal/crypto/aes.js",
"lib/internal/crypto/certificate.js",
"lib/internal/crypto/cfrg.js",
"lib/internal/crypto/cipher.js",
"lib/internal/crypto/diffiehellman.js",
"lib/internal/crypto/ec.js",
"lib/internal/crypto/hash.js",
"lib/internal/crypto/hashnames.js",
"lib/internal/crypto/hkdf.js",
"lib/internal/crypto/keygen.js",
"lib/internal/crypto/keys.js",
"lib/internal/crypto/mac.js",
"lib/internal/crypto/pbkdf2.js",
"lib/internal/crypto/random.js",
"lib/internal/crypto/rsa.js",
"lib/internal/crypto/scrypt.js",
"lib/internal/crypto/sig.js",
"lib/internal/crypto/util.js",
"lib/internal/crypto/webcrypto.js",
"lib/internal/crypto/webidl.js",
"lib/internal/crypto/x509.js",
"lib/internal/debugger/inspect.js",
"lib/internal/debugger/inspect_client.js",
"lib/internal/debugger/inspect_repl.js",
"lib/internal/dgram.js",
"lib/internal/dns/callback_resolver.js",
"lib/internal/dns/promises.js",
"lib/internal/dns/utils.js",
"lib/internal/encoding.js",
"lib/internal/error_serdes.js",
"lib/internal/errors.js",
"lib/internal/event_target.js",
"lib/internal/events/symbols.js",
"lib/internal/file.js",
"lib/internal/fixed_queue.js",
"lib/internal/freelist.js",
"lib/internal/freeze_intrinsics.js",
"lib/internal/fs/cp/cp-sync.js",
"lib/internal/fs/cp/cp.js",
"lib/internal/fs/dir.js",
"lib/internal/fs/glob.js",
"lib/internal/fs/promises.js",
"lib/internal/fs/read/context.js",
"lib/internal/fs/recursive_watch.js",
"lib/internal/fs/rimraf.js",
"lib/internal/fs/streams.js",
"lib/internal/fs/sync_write_stream.js",
"lib/internal/fs/utils.js",
"lib/internal/fs/watchers.js",
"lib/internal/heap_utils.js",
"lib/internal/histogram.js",
"lib/internal/http.js",
"lib/internal/http2/compat.js",
"lib/internal/http2/core.js",
"lib/internal/http2/util.js",
"lib/internal/idna.js",
"lib/internal/inspector_async_hook.js",
"lib/internal/js_stream_socket.js",
"lib/internal/legacy/processbinding.js",
"lib/internal/linkedlist.js",
"lib/internal/main/check_syntax.js",
"lib/internal/main/embedding.js",
"lib/internal/main/eval_stdin.js",
"lib/internal/main/eval_string.js",
"lib/internal/main/inspect.js",
"lib/internal/main/mksnapshot.js",
"lib/internal/main/print_help.js",
"lib/internal/main/prof_process.js",
"lib/internal/main/repl.js",
"lib/internal/main/run_main_module.js",
"lib/internal/main/test_runner.js",
"lib/internal/main/watch_mode.js",
"lib/internal/main/worker_thread.js",
"lib/internal/mime.js",
"lib/internal/modules/cjs/loader.js",
"lib/internal/modules/esm/assert.js",
"lib/internal/modules/esm/create_dynamic_module.js",
"lib/internal/modules/esm/fetch_module.js",
"lib/internal/modules/esm/formats.js",
"lib/internal/modules/esm/get_format.js",
"lib/internal/modules/esm/handle_process_exit.js",
"lib/internal/modules/esm/hooks.js",
"lib/internal/modules/esm/initialize_import_meta.js",
"lib/internal/modules/esm/load.js",
"lib/internal/modules/esm/loader.js",
"lib/internal/modules/esm/module_job.js",
"lib/internal/modules/esm/module_map.js",
"lib/internal/modules/esm/resolve.js",
"lib/internal/modules/esm/shared_constants.js",
"lib/internal/modules/esm/translators.js",
"lib/internal/modules/esm/utils.js",
"lib/internal/modules/esm/worker.js",
"lib/internal/modules/helpers.js",
"lib/internal/modules/package_json_reader.js",
"lib/internal/modules/run_main.js",
"lib/internal/navigator.js",
"lib/internal/net.js",
"lib/internal/options.js",
"lib/internal/per_context/domexception.js",
"lib/internal/per_context/messageport.js",
"lib/internal/per_context/primordials.js",
"lib/internal/perf/event_loop_delay.js",
"lib/internal/perf/event_loop_utilization.js",
"lib/internal/perf/nodetiming.js",
"lib/internal/perf/observe.js",
"lib/internal/perf/performance.js",
"lib/internal/perf/performance_entry.js",
"lib/internal/perf/resource_timing.js",
"lib/internal/perf/timerify.js",
"lib/internal/perf/usertiming.js",
"lib/internal/perf/utils.js",
"lib/internal/policy/manifest.js",
"lib/internal/policy/sri.js",
"lib/internal/priority_queue.js",
"lib/internal/process/esm_loader.js",
"lib/internal/process/execution.js",
"lib/internal/process/per_thread.js",
"lib/internal/process/permission.js",
"lib/internal/process/policy.js",
"lib/internal/process/pre_execution.js",
"lib/internal/process/promises.js",
"lib/internal/process/report.js",
"lib/internal/process/signal.js",
"lib/internal/process/task_queues.js",
"lib/internal/process/warning.js",
"lib/internal/process/worker_thread_only.js",
"lib/internal/promise_hooks.js",
"lib/internal/querystring.js",
"lib/internal/readline/callbacks.js",
"lib/internal/readline/emitKeypressEvents.js",
"lib/internal/readline/interface.js",
"lib/internal/readline/promises.js",
"lib/internal/readline/utils.js",
"lib/internal/repl.js",
"lib/internal/repl/await.js",
"lib/internal/repl/history.js",
"lib/internal/repl/utils.js",
"lib/internal/socket_list.js",
"lib/internal/socketaddress.js",
"lib/internal/source_map/prepare_stack_trace.js",
"lib/internal/source_map/source_map.js",
"lib/internal/source_map/source_map_cache.js",
"lib/internal/stream_base_commons.js",
"lib/internal/streams/add-abort-signal.js",
"lib/internal/streams/compose.js",
"lib/internal/streams/destroy.js",
"lib/internal/streams/duplex.js",
"lib/internal/streams/duplexify.js",
"lib/internal/streams/end-of-stream.js",
"lib/internal/streams/from.js",
"lib/internal/streams/lazy_transform.js",
"lib/internal/streams/legacy.js",
"lib/internal/streams/operators.js",
"lib/internal/streams/passthrough.js",
"lib/internal/streams/pipeline.js",
"lib/internal/streams/readable.js",
"lib/internal/streams/state.js",
"lib/internal/streams/transform.js",
"lib/internal/streams/utils.js",
"lib/internal/streams/writable.js",
"lib/internal/test/binding.js",
"lib/internal/test/transfer.js",
"lib/internal/test_runner/coverage.js",
"lib/internal/test_runner/harness.js",
"lib/internal/test_runner/mock/mock.js",
"lib/internal/test_runner/mock/mock_timers.js",
"lib/internal/test_runner/reporter/dot.js",
"lib/internal/test_runner/reporter/junit.js",
"lib/internal/test_runner/reporter/lcov.js",
"lib/internal/test_runner/reporter/spec.js",
"lib/internal/test_runner/reporter/tap.js",
"lib/internal/test_runner/reporter/v8-serializer.js",
"lib/internal/test_runner/runner.js",
"lib/internal/test_runner/test.js",
"lib/internal/test_runner/tests_stream.js",
"lib/internal/test_runner/utils.js",
"lib/internal/timers.js",
"lib/internal/tls/secure-context.js",
"lib/internal/tls/secure-pair.js",
"lib/internal/trace_events_async_hooks.js",
"lib/internal/tty.js",
"lib/internal/url.js",
"lib/internal/util.js",
"lib/internal/util/colors.js",
"lib/internal/util/comparisons.js",
"lib/internal/util/debuglog.js",
"lib/internal/util/embedding.js",
"lib/internal/util/inspect.js",
"lib/internal/util/inspector.js",
"lib/internal/util/iterable_weak_map.js",
"lib/internal/util/parse_args/parse_args.js",
"lib/internal/util/parse_args/utils.js",
"lib/internal/util/types.js",
"lib/internal/v8/startup_snapshot.js",
"lib/internal/v8_prof_polyfill.js",
"lib/internal/v8_prof_processor.js",
"lib/internal/validators.js",
"lib/internal/vm.js",
"lib/internal/vm/module.js",
"lib/internal/wasm_web_api.js",
"lib/internal/watch_mode/files_watcher.js",
"lib/internal/watchdog.js",
"lib/internal/webidl.js",
"lib/internal/webstreams/adapters.js",
"lib/internal/webstreams/compression.js",
"lib/internal/webstreams/encoding.js",
"lib/internal/webstreams/queuingstrategies.js",
"lib/internal/webstreams/readablestream.js",
"lib/internal/webstreams/transfer.js",
"lib/internal/webstreams/transformstream.js",
"lib/internal/webstreams/util.js",
"lib/internal/webstreams/writablestream.js",
"lib/internal/worker.js",
"lib/internal/worker/io.js",
"lib/internal/worker/js_transferable.js",
"lib/module.js",
"lib/net.js",
"lib/os.js",
"lib/path.js",
"lib/path/posix.js",
"lib/path/win32.js",
"lib/perf_hooks.js",
"lib/process.js",
"lib/punycode.js",
"lib/querystring.js",
"lib/readline.js",
"lib/readline/promises.js",
"lib/repl.js",
"lib/sea.js",
"lib/stream.js",
"lib/stream/consumers.js",
"lib/stream/promises.js",
"lib/stream/web.js",
"lib/string_decoder.js",
"lib/sys.js",
"lib/test.js",
"lib/test/reporters.js",
"lib/timers.js",
"lib/timers/promises.js",
"lib/tls.js",
"lib/trace_events.js",
"lib/tty.js",
"lib/url.js",
"lib/util.js",
"lib/util/types.js",
"lib/v8.js",
"lib/vm.js",
"lib/wasi.js",
"lib/worker_threads.js",
"lib/zlib.js"
],
"node_module_version": 120,
"node_no_browser_globals": "false",
"node_prefix": "/",
"node_release_urlbase": "https://nodejs.org/download/release/",
"node_section_ordering_info": "",
"node_shared": "false",
"node_shared_brotli": "false",
"node_shared_cares": "false",
"node_shared_http_parser": "false",
"node_shared_libuv": "false",
"node_shared_nghttp2": "false",
"node_shared_nghttp3": "false",
"node_shared_ngtcp2": "false",
"node_shared_openssl": "false",
"node_shared_zlib": "false",
"node_tag": "",
"node_target_type": "executable",
"node_use_bundled_v8": "true",
"node_use_node_code_cache": "true",
"node_use_node_snapshot": "true",
"node_use_openssl": "true",
"node_use_v8_platform": "true",
"node_with_ltcg": "false",
"node_without_node_options": "false",
"node_write_snapshot_as_array_literals": "false",
"openssl_is_fips": "false",
"openssl_quic": "true",
"ossfuzz": "false",
"shlib_suffix": "so.120",
"single_executable_application": "true",
"target_arch": "arm64",
"use_prefix_to_find_headers": "false",
"v8_enable_31bit_smis_on_64bit_arch": 0,
"v8_enable_extensible_ro_snapshot": 0,
"v8_enable_gdbjit": 0,
"v8_enable_hugepage": 0,
"v8_enable_i18n_support": 1,
"v8_enable_inspector": 1,
"v8_enable_javascript_promise_hooks": 1,
"v8_enable_lite_mode": 0,
"v8_enable_maglev": 0,
"v8_enable_object_print": 1,
"v8_enable_pointer_compression": 0,
"v8_enable_shared_ro_heap": 1,
"v8_enable_v8_checks": 0,
"v8_enable_webassembly": 1,
"v8_no_strict_aliasing": 1,
"v8_optimized_debug": 1,
"v8_promise_internal_field_count": 1,
"v8_random_seed": 0,
"v8_trace_maps": 0,
"v8_use_siphash": 1,
"want_separate_host_toolset": 0,
"nodedir": "/github/home/.cache/node-gyp/21.7.3",
"python": "/usr/local/bin/python3",
"standalone_static_library": 1
}
}
+507
View File
@@ -0,0 +1,507 @@
// TypeScript Version: 3.0
import { Readable } from 'stream'
export interface PngConfig {
/** Specifies the ZLIB compression level. Defaults to 6. */
compressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
/**
* Any bitwise combination of `PNG_FILTER_NONE`, `PNG_FILTER_SUB`,
* `PNG_FILTER_UP`, `PNG_FILTER_AVG` and `PNG_FILTER_PATETH`; or one of
* `PNG_ALL_FILTERS` or `PNG_NO_FILTERS` (all are properties of the canvas
* instance). These specify which filters *may* be used by libpng. During
* encoding, libpng will select the best filter from this list of allowed
* filters. Defaults to `canvas.PNG_ALL_FILTERS`.
*/
filters?: number
/**
* _For creating indexed PNGs._ The palette of colors. Entries should be in
* RGBA order.
*/
palette?: Uint8ClampedArray
/**
* _For creating indexed PNGs._ The index of the background color. Defaults
* to 0.
*/
backgroundIndex?: number
/** pixels per inch */
resolution?: number
}
export interface JpegConfig {
/** Specifies the quality, between 0 and 1. Defaults to 0.75. */
quality?: number
/** Enables progressive encoding. Defaults to `false`. */
progressive?: boolean
/** Enables 2x2 chroma subsampling. Defaults to `true`. */
chromaSubsampling?: boolean
}
export interface PdfConfig {
title?: string
author?: string
subject?: string
keywords?: string
creator?: string
creationDate?: Date
modDate?: Date
}
export interface NodeCanvasRenderingContext2DSettings {
alpha?: boolean
pixelFormat?: 'RGBA32' | 'RGB24' | 'A8' | 'RGB16_565' | 'A1' | 'RGB30'
}
export class Canvas {
width: number
height: number
/** _Non standard._ The type of the canvas. */
readonly type: 'image'|'pdf'|'svg'
/** _Non standard._ Getter. The stride used by the canvas. */
readonly stride: number;
/** Constant used in PNG encoding methods. */
static readonly PNG_NO_FILTERS: number
/** Constant used in PNG encoding methods. */
static readonly PNG_ALL_FILTERS: number
/** Constant used in PNG encoding methods. */
static readonly PNG_FILTER_NONE: number
/** Constant used in PNG encoding methods. */
static readonly PNG_FILTER_SUB: number
/** Constant used in PNG encoding methods. */
static readonly PNG_FILTER_UP: number
/** Constant used in PNG encoding methods. */
static readonly PNG_FILTER_AVG: number
/** Constant used in PNG encoding methods. */
static readonly PNG_FILTER_PAETH: number
constructor(width: number, height: number, type?: 'image'|'pdf'|'svg')
getContext(contextId: '2d', contextAttributes?: NodeCanvasRenderingContext2DSettings): CanvasRenderingContext2D
/**
* For image canvases, encodes the canvas as a PNG. For PDF canvases,
* encodes the canvas as a PDF. For SVG canvases, encodes the canvas as an
* SVG.
*/
toBuffer(cb: (err: Error|null, result: Buffer) => void): void
toBuffer(cb: (err: Error|null, result: Buffer) => void, mimeType: 'image/png', config?: PngConfig): void
toBuffer(cb: (err: Error|null, result: Buffer) => void, mimeType: 'image/jpeg', config?: JpegConfig): void
/**
* For image canvases, encodes the canvas as a PNG. For PDF canvases,
* encodes the canvas as a PDF. For SVG canvases, encodes the canvas as an
* SVG.
*/
toBuffer(): Buffer
toBuffer(mimeType: 'image/png', config?: PngConfig): Buffer
toBuffer(mimeType: 'image/jpeg', config?: JpegConfig): Buffer
toBuffer(mimeType: 'application/pdf', config?: PdfConfig): Buffer
/**
* Returns the unencoded pixel data, top-to-bottom. On little-endian (most)
* systems, the array will be ordered BGRA; on big-endian systems, it will
* be ARGB.
*/
toBuffer(mimeType: 'raw'): Buffer
createPNGStream(config?: PngConfig): PNGStream
createJPEGStream(config?: JpegConfig): JPEGStream
createPDFStream(config?: PdfConfig): PDFStream
/** Defaults to PNG image. */
toDataURL(): string
toDataURL(mimeType: 'image/png'): string
toDataURL(mimeType: 'image/jpeg', quality?: number): string
/** _Non-standard._ Defaults to PNG image. */
toDataURL(cb: (err: Error|null, result: string) => void): void
/** _Non-standard._ */
toDataURL(mimeType: 'image/png', cb: (err: Error|null, result: string) => void): void
/** _Non-standard._ */
toDataURL(mimeType: 'image/jpeg', cb: (err: Error|null, result: string) => void): void
/** _Non-standard._ */
toDataURL(mimeType: 'image/jpeg', config: JpegConfig, cb: (err: Error|null, result: string) => void): void
/** _Non-standard._ */
toDataURL(mimeType: 'image/jpeg', quality: number, cb: (err: Error|null, result: string) => void): void
}
export interface TextMetrics {
readonly alphabeticBaseline: number;
readonly actualBoundingBoxAscent: number;
readonly actualBoundingBoxDescent: number;
readonly actualBoundingBoxLeft: number;
readonly actualBoundingBoxRight: number;
readonly emHeightAscent: number;
readonly emHeightDescent: number;
readonly fontBoundingBoxAscent: number;
readonly fontBoundingBoxDescent: number;
readonly width: number;
}
export type CanvasFillRule = 'evenodd' | 'nonzero';
export type GlobalCompositeOperation =
| 'clear'
| 'copy'
| 'destination'
| 'source-over'
| 'destination-over'
| 'source-in'
| 'destination-in'
| 'source-out'
| 'destination-out'
| 'source-atop'
| 'destination-atop'
| 'xor'
| 'lighter'
| 'normal'
| 'multiply'
| 'screen'
| 'overlay'
| 'darken'
| 'lighten'
| 'color-dodge'
| 'color-burn'
| 'hard-light'
| 'soft-light'
| 'difference'
| 'exclusion'
| 'hue'
| 'saturation'
| 'color'
| 'luminosity'
| 'saturate';
export type CanvasLineCap = 'butt' | 'round' | 'square';
export type CanvasLineJoin = 'bevel' | 'miter' | 'round';
export type CanvasTextBaseline = 'alphabetic' | 'bottom' | 'hanging' | 'ideographic' | 'middle' | 'top';
export type CanvasTextAlign = 'center' | 'end' | 'left' | 'right' | 'start';
export class CanvasRenderingContext2D {
drawImage(image: Canvas|Image, dx: number, dy: number): void
drawImage(image: Canvas|Image, dx: number, dy: number, dw: number, dh: number): void
drawImage(image: Canvas|Image, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void
putImageData(imagedata: ImageData, dx: number, dy: number): void;
putImageData(imagedata: ImageData, dx: number, dy: number, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number): void;
getImageData(sx: number, sy: number, sw: number, sh: number): ImageData;
createImageData(sw: number, sh: number): ImageData;
createImageData(imagedata: ImageData): ImageData;
/**
* For PDF canvases, adds another page. If width and/or height are omitted,
* the canvas's initial size is used.
*/
addPage(width?: number, height?: number): void
save(): void;
restore(): void;
rotate(angle: number): void;
translate(x: number, y: number): void;
transform(a: number, b: number, c: number, d: number, e: number, f: number): void;
getTransform(): DOMMatrix;
resetTransform(): void;
setTransform(transform?: DOMMatrix): void;
setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void;
isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean;
scale(x: number, y: number): void;
clip(fillRule?: CanvasFillRule): void;
fill(fillRule?: CanvasFillRule): void;
stroke(): void;
fillText(text: string, x: number, y: number, maxWidth?: number): void;
strokeText(text: string, x: number, y: number, maxWidth?: number): void;
fillRect(x: number, y: number, w: number, h: number): void;
strokeRect(x: number, y: number, w: number, h: number): void;
clearRect(x: number, y: number, w: number, h: number): void;
rect(x: number, y: number, w: number, h: number): void;
roundRect(x: number, y: number, w: number, h: number, radii?: number | number[]): void;
measureText(text: string): TextMetrics;
moveTo(x: number, y: number): void;
lineTo(x: number, y: number): void;
bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void;
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void;
beginPath(): void;
closePath(): void;
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void;
ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
setLineDash(segments: number[]): void;
getLineDash(): number[];
createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): CanvasPattern
createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient;
createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;
beginTag(tagName: string, attributes?: string): void;
endTag(tagName: string): void;
/**
* _Non-standard_. Defaults to 'good'. Affects pattern (gradient, image,
* etc.) rendering quality.
*/
patternQuality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear'
imageSmoothingEnabled: boolean;
globalCompositeOperation: GlobalCompositeOperation;
globalAlpha: number;
shadowColor: string;
miterLimit: number;
lineWidth: number;
lineCap: CanvasLineCap;
lineJoin: CanvasLineJoin;
lineDashOffset: number;
shadowOffsetX: number;
shadowOffsetY: number;
shadowBlur: number;
/** _Non-standard_. Sets the antialiasing mode. */
antialias: 'default' | 'gray' | 'none' | 'subpixel'
/**
* Defaults to 'path'. The effect depends on the canvas type:
*
* * **Standard (image)** `'glyph'` and `'path'` both result in rasterized
* text. Glyph mode is faster than path, but may result in lower-quality
* text, especially when rotated or translated.
*
* * **PDF** `'glyph'` will embed text instead of paths into the PDF. This
* is faster to encode, faster to open with PDF viewers, yields a smaller
* file size and makes the text selectable. The subset of the font needed
* to render the glyphs will be embedded in the PDF. This is usually the
* mode you want to use with PDF canvases.
*
* * **SVG** glyph does not cause `<text>` elements to be produced as one
* might expect ([cairo bug](https://gitlab.freedesktop.org/cairo/cairo/issues/253)).
* Rather, glyph will create a `<defs>` section with a `<symbol>` for each
* glyph, then those glyphs be reused via `<use>` elements. `'path'` mode
* creates a `<path>` element for each text string. glyph mode is faster
* and yields a smaller file size.
*
* In glyph mode, `ctx.strokeText()` and `ctx.fillText()` behave the same
* (aside from using the stroke and fill style, respectively).
*/
textDrawingMode: 'path' | 'glyph'
/**
* _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to
* transformations affecting more than just patterns.
*/
quality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear'
/** Returns or sets a `DOMMatrix` for the current transformation matrix. */
currentTransform: DOMMatrix
fillStyle: string | CanvasGradient | CanvasPattern;
strokeStyle: string | CanvasGradient | CanvasPattern;
font: string;
textBaseline: CanvasTextBaseline;
textAlign: CanvasTextAlign;
canvas: Canvas;
direction: 'ltr' | 'rtl';
lang: string;
}
export class CanvasGradient {
addColorStop(offset: number, color: string): void;
}
export class CanvasPattern {
setTransform(transform?: DOMMatrix): void;
}
// This does not extend HTMLImageElement because there are dozens of inherited
// methods and properties that we do not provide.
export class Image {
/** Track image data */
static readonly MODE_IMAGE: number
/** Track MIME data */
static readonly MODE_MIME: number
/**
* The URL, `data:` URI or local file path of the image to be loaded, or a
* Buffer instance containing an encoded image.
*/
src: string | Buffer
/** Retrieves whether the object is fully loaded. */
readonly complete: boolean
/** Sets or retrieves the height of the image. */
height: number
/** Sets or retrieves the width of the image. */
width: number
/** The original height of the image resource before sizing. */
readonly naturalHeight: number
/** The original width of the image resource before sizing. */
readonly naturalWidth: number
/**
* Applies to JPEG images drawn to PDF canvases only. Setting
* `img.dataMode = Image.MODE_MIME` or `Image.MODE_MIME|Image.MODE_IMAGE`
* enables image MIME data tracking. When MIME data is tracked, PDF canvases
* can embed JPEGs directly into the output, rather than re-encoding into
* PNG. This can drastically reduce filesize and speed up rendering.
*/
dataMode: number
onload: (() => void) | null;
onerror: ((err: Error) => void) | null;
}
/**
* Creates a Canvas instance. This function works in both Node.js and Web
* browsers, where there is no Canvas constructor.
* @param type Optionally specify to create a PDF or SVG canvas. Defaults to an
* image canvas.
*/
export function createCanvas(width: number, height: number, type?: 'pdf'|'svg'): Canvas
/**
* Creates an ImageData instance. This function works in both Node.js and Web
* browsers.
* @param data An array containing the pixel representation of the image.
* @param height If omitted, the height is calculated based on the array's size
* and `width`.
*/
export function createImageData(data: Uint8ClampedArray, width: number, height?: number): ImageData
/**
* _Non-standard._ Creates an ImageData instance for an alternative pixel
* format, such as RGB16_565
* @param data An array containing the pixel representation of the image.
* @param height If omitted, the height is calculated based on the array's size
* and `width`.
*/
export function createImageData(data: Uint16Array, width: number, height?: number): ImageData
/**
* Creates an ImageData instance. This function works in both Node.js and Web
* browsers.
*/
export function createImageData(width: number, height: number): ImageData
/**
* Convenience function for loading an image with a Promise interface. This
* function works in both Node.js and Web browsers; however, the `src` must be
* a string in Web browsers (it can only be a Buffer in Node.js).
* @param src URL, `data: ` URI or (Node.js only) a local file path or Buffer
* instance.
*/
export function loadImage(src: string|Buffer, options?: any): Promise<Image>
/**
* Registers a font that is not installed as a system font. This must be used
* before creating Canvas instances.
* @param path Path to local font file.
* @param fontFace Description of the font face, corresponding to CSS properties
* used in `@font-face` rules.
*/
export function registerFont(path: string, fontFace: {family: string, weight?: string, style?: string}): void
/**
* Unloads all fonts
*/
export function deregisterAllFonts(): void;
/** This class must not be constructed directly; use `canvas.createPNGStream()`. */
export class PNGStream extends Readable {}
/** This class must not be constructed directly; use `canvas.createJPEGStream()`. */
export class JPEGStream extends Readable {}
/** This class must not be constructed directly; use `canvas.createPDFStream()`. */
export class PDFStream extends Readable {}
// TODO: this is wrong. See matrixTransform in lib/DOMMatrix.js
type DOMMatrixInit = DOMMatrix | string | number[];
interface DOMPointInit {
w?: number;
x?: number;
y?: number;
z?: number;
}
export class DOMPoint {
w: number;
x: number;
y: number;
z: number;
matrixTransform(matrix?: DOMMatrixInit): DOMPoint;
toJSON(): any;
static fromPoint(other?: DOMPointInit): DOMPoint;
}
export class DOMMatrix {
constructor(init?: string | number[]);
toString(): string;
multiply(other?: DOMMatrix): DOMMatrix;
multiplySelf(other?: DOMMatrix): DOMMatrix;
preMultiplySelf(other?: DOMMatrix): DOMMatrix;
translate(tx?: number, ty?: number, tz?: number): DOMMatrix;
translateSelf(tx?: number, ty?: number, tz?: number): DOMMatrix;
scale(scaleX?: number, scaleY?: number, scaleZ?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix;
scale3d(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix;
scale3dSelf(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix;
scaleSelf(scaleX?: number, scaleY?: number, scaleZ?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix;
/**
* @deprecated
*/
scaleNonUniform(scaleX?: number, scaleY?: number): DOMMatrix;
rotateFromVector(x?: number, y?: number): DOMMatrix;
rotateFromVectorSelf(x?: number, y?: number): DOMMatrix;
rotate(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix;
rotateSelf(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix;
rotateAxisAngle(x?: number, y?: number, z?: number, angle?: number): DOMMatrix;
rotateAxisAngleSelf(x?: number, y?: number, z?: number, angle?: number): DOMMatrix;
skewX(sx?: number): DOMMatrix;
skewXSelf(sx?: number): DOMMatrix;
skewY(sy?: number): DOMMatrix;
skewYSelf(sy?: number): DOMMatrix;
flipX(): DOMMatrix;
flipY(): DOMMatrix;
inverse(): DOMMatrix;
invertSelf(): DOMMatrix;
setMatrixValue(transformList: string): DOMMatrix;
transformPoint(point?: DOMPoint): DOMPoint;
toJSON(): any;
toFloat32Array(): Float32Array;
toFloat64Array(): Float64Array;
readonly is2D: boolean;
readonly isIdentity: boolean;
a: number;
b: number;
c: number;
d: number;
e: number;
f: number;
m11: number;
m12: number;
m13: number;
m14: number;
m21: number;
m22: number;
m23: number;
m24: number;
m31: number;
m32: number;
m33: number;
m34: number;
m41: number;
m42: number;
m43: number;
m44: number;
static fromMatrix(other: DOMMatrix): DOMMatrix;
static fromFloat32Array(a: Float32Array): DOMMatrix;
static fromFloat64Array(a: Float64Array): DOMMatrix;
}
export class ImageData {
constructor(sw: number, sh: number);
constructor(data: Uint8ClampedArray, sw: number, sh?: number);
readonly data: Uint8ClampedArray;
readonly height: number;
readonly width: number;
}
// Not documented: backends
/** Library version. */
export const version: string
/** Cairo version. */
export const cairoVersion: string
/** jpeglib version, if built with JPEG support. */
export const jpegVersion: string | undefined
/** giflib version, if built with GIF support. */
export const gifVersion: string | undefined
/** freetype version. */
export const freetypeVersion: string
/** rsvg version. */
export const rsvgVersion: string | undefined
+94
View File
@@ -0,0 +1,94 @@
const Canvas = require('./lib/canvas')
const Image = require('./lib/image')
const CanvasRenderingContext2D = require('./lib/context2d')
const CanvasPattern = require('./lib/pattern')
const packageJson = require('./package.json')
const bindings = require('./lib/bindings')
const fs = require('fs')
const PNGStream = require('./lib/pngstream')
const PDFStream = require('./lib/pdfstream')
const JPEGStream = require('./lib/jpegstream')
const { DOMPoint, DOMMatrix } = require('./lib/DOMMatrix')
bindings.setDOMMatrix(DOMMatrix)
function createCanvas (width, height, type) {
return new Canvas(width, height, type)
}
function createImageData (array, width, height) {
return new bindings.ImageData(array, width, height)
}
function loadImage (src) {
return new Promise((resolve, reject) => {
const image = new Image()
function cleanup () {
image.onload = null
image.onerror = null
}
image.onload = () => { cleanup(); resolve(image) }
image.onerror = (err) => { cleanup(); reject(err) }
image.src = src
})
}
/**
* Resolve paths for registerFont. Must be called *before* creating a Canvas
* instance.
* @param src {string} Path to font file.
* @param fontFace {{family: string, weight?: string, style?: string}} Object
* specifying font information. `weight` and `style` default to `"normal"`.
*/
function registerFont (src, fontFace) {
// TODO this doesn't need to be on Canvas; it should just be a static method
// of `bindings`.
return Canvas._registerFont(fs.realpathSync(src), fontFace)
}
/**
* Unload all fonts from pango to free up memory
*/
function deregisterAllFonts () {
return Canvas._deregisterAllFonts()
}
exports.Canvas = Canvas
exports.Context2d = CanvasRenderingContext2D // Legacy/compat export
exports.CanvasRenderingContext2D = CanvasRenderingContext2D
exports.CanvasGradient = bindings.CanvasGradient
exports.CanvasPattern = CanvasPattern
exports.Image = Image
exports.ImageData = bindings.ImageData
exports.PNGStream = PNGStream
exports.PDFStream = PDFStream
exports.JPEGStream = JPEGStream
exports.DOMMatrix = DOMMatrix
exports.DOMPoint = DOMPoint
exports.registerFont = registerFont
exports.deregisterAllFonts = deregisterAllFonts
exports.createCanvas = createCanvas
exports.createImageData = createImageData
exports.loadImage = loadImage
exports.backends = bindings.Backends
/** Library version. */
exports.version = packageJson.version
/** Cairo version. */
exports.cairoVersion = bindings.cairoVersion
/** jpeglib version. */
exports.jpegVersion = bindings.jpegVersion
/** gif_lib version. */
exports.gifVersion = bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined
/** freetype version. */
exports.freetypeVersion = bindings.freetypeVersion
/** rsvg version. */
exports.rsvgVersion = bindings.rsvgVersion
/** pango version. */
exports.pangoVersion = bindings.pangoVersion
+678
View File
@@ -0,0 +1,678 @@
'use strict'
const util = require('util')
// DOMMatrix per https://drafts.fxtf.org/geometry/#DOMMatrix
class DOMPoint {
constructor (x, y, z, w) {
if (typeof x === 'object' && x !== null) {
w = x.w
z = x.z
y = x.y
x = x.x
}
this.x = typeof x === 'number' ? x : 0
this.y = typeof y === 'number' ? y : 0
this.z = typeof z === 'number' ? z : 0
this.w = typeof w === 'number' ? w : 1
}
matrixTransform(init) {
// TODO: this next line is wrong. matrixTransform is supposed to only take
// an object with the DOMMatrix properties called DOMMatrixInit
const m = init instanceof DOMMatrix ? init : new DOMMatrix(init)
return m.transformPoint(this)
}
toJSON() {
return {
x: this.x,
y: this.y,
z: this.z,
w: this.w
}
}
static fromPoint(other) {
return new this(other.x, other.y, other.z, other.w)
}
}
// Constants to index into _values (col-major)
const M11 = 0; const M12 = 1; const M13 = 2; const M14 = 3
const M21 = 4; const M22 = 5; const M23 = 6; const M24 = 7
const M31 = 8; const M32 = 9; const M33 = 10; const M34 = 11
const M41 = 12; const M42 = 13; const M43 = 14; const M44 = 15
const DEGREE_PER_RAD = 180 / Math.PI
const RAD_PER_DEGREE = Math.PI / 180
function parseMatrix (init) {
let parsed = init.replace('matrix(', '')
parsed = parsed.split(',', 7) // 6 + 1 to handle too many params
if (parsed.length !== 6) throw new Error(`Failed to parse ${init}`)
parsed = parsed.map(parseFloat)
return [
parsed[0], parsed[1], 0, 0,
parsed[2], parsed[3], 0, 0,
0, 0, 1, 0,
parsed[4], parsed[5], 0, 1
]
}
function parseMatrix3d (init) {
let parsed = init.replace('matrix3d(', '')
parsed = parsed.split(',', 17) // 16 + 1 to handle too many params
if (parsed.length !== 16) throw new Error(`Failed to parse ${init}`)
return parsed.map(parseFloat)
}
function parseTransform (tform) {
const type = tform.split('(', 1)[0]
switch (type) {
case 'matrix':
return parseMatrix(tform)
case 'matrix3d':
return parseMatrix3d(tform)
// TODO This is supposed to support any CSS transform value.
default:
throw new Error(`${type} parsing not implemented`)
}
}
class DOMMatrix {
constructor (init) {
this._is2D = true
this._values = new Float64Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
])
let i
if (typeof init === 'string') { // parse CSS transformList
if (init === '') return // default identity matrix
const tforms = init.split(/\)\s+/, 20).map(parseTransform)
if (tforms.length === 0) return
init = tforms[0]
for (i = 1; i < tforms.length; i++) init = multiply(tforms[i], init)
}
i = 0
if (init && init.length === 6) {
setNumber2D(this, M11, init[i++])
setNumber2D(this, M12, init[i++])
setNumber2D(this, M21, init[i++])
setNumber2D(this, M22, init[i++])
setNumber2D(this, M41, init[i++])
setNumber2D(this, M42, init[i++])
} else if (init && init.length === 16) {
setNumber2D(this, M11, init[i++])
setNumber2D(this, M12, init[i++])
setNumber3D(this, M13, init[i++])
setNumber3D(this, M14, init[i++])
setNumber2D(this, M21, init[i++])
setNumber2D(this, M22, init[i++])
setNumber3D(this, M23, init[i++])
setNumber3D(this, M24, init[i++])
setNumber3D(this, M31, init[i++])
setNumber3D(this, M32, init[i++])
setNumber3D(this, M33, init[i++])
setNumber3D(this, M34, init[i++])
setNumber2D(this, M41, init[i++])
setNumber2D(this, M42, init[i++])
setNumber3D(this, M43, init[i++])
setNumber3D(this, M44, init[i])
} else if (init !== undefined) {
throw new TypeError('Expected string or array.')
}
}
toString () {
return this.is2D
? `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})`
: `matrix3d(${this._values.join(', ')})`
}
multiply (other) {
return newInstance(this._values).multiplySelf(other)
}
multiplySelf (other) {
this._values = multiply(other._values, this._values)
if (!other.is2D) this._is2D = false
return this
}
preMultiplySelf (other) {
this._values = multiply(this._values, other._values)
if (!other.is2D) this._is2D = false
return this
}
translate (tx, ty, tz) {
return newInstance(this._values).translateSelf(tx, ty, tz)
}
translateSelf (tx, ty, tz) {
if (typeof tx !== 'number') tx = 0
if (typeof ty !== 'number') ty = 0
if (typeof tz !== 'number') tz = 0
this._values = multiply([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1
], this._values)
if (tz !== 0) this._is2D = false
return this
}
scale (scaleX, scaleY, scaleZ, originX, originY, originZ) {
return newInstance(this._values).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ)
}
scale3d (scale, originX, originY, originZ) {
return newInstance(this._values).scale3dSelf(scale, originX, originY, originZ)
}
scale3dSelf (scale, originX, originY, originZ) {
return this.scaleSelf(scale, scale, scale, originX, originY, originZ)
}
/**
* @deprecated
*/
scaleNonUniform(scaleX, scaleY) {
return this.scale(scaleX, scaleY)
}
scaleSelf (scaleX, scaleY, scaleZ, originX, originY, originZ) {
// Not redundant with translate's checks because we need to negate the values later.
if (typeof originX !== 'number') originX = 0
if (typeof originY !== 'number') originY = 0
if (typeof originZ !== 'number') originZ = 0
this.translateSelf(originX, originY, originZ)
if (typeof scaleX !== 'number') scaleX = 1
if (typeof scaleY !== 'number') scaleY = scaleX
if (typeof scaleZ !== 'number') scaleZ = 1
this._values = multiply([
scaleX, 0, 0, 0,
0, scaleY, 0, 0,
0, 0, scaleZ, 0,
0, 0, 0, 1
], this._values)
this.translateSelf(-originX, -originY, -originZ)
if (scaleZ !== 1 || originZ !== 0) this._is2D = false
return this
}
rotateFromVector (x, y) {
return newInstance(this._values).rotateFromVectorSelf(x, y)
}
rotateFromVectorSelf (x, y) {
if (typeof x !== 'number') x = 0
if (typeof y !== 'number') y = 0
const theta = (x === 0 && y === 0) ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD
return this.rotateSelf(theta)
}
rotate (rotX, rotY, rotZ) {
return newInstance(this._values).rotateSelf(rotX, rotY, rotZ)
}
rotateSelf (rotX, rotY, rotZ) {
if (rotY === undefined && rotZ === undefined) {
rotZ = rotX
rotX = rotY = 0
}
if (typeof rotY !== 'number') rotY = 0
if (typeof rotZ !== 'number') rotZ = 0
if (rotX !== 0 || rotY !== 0) this._is2D = false
rotX *= RAD_PER_DEGREE
rotY *= RAD_PER_DEGREE
rotZ *= RAD_PER_DEGREE
let c, s
c = Math.cos(rotZ)
s = Math.sin(rotZ)
this._values = multiply([
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
], this._values)
c = Math.cos(rotY)
s = Math.sin(rotY)
this._values = multiply([
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
], this._values)
c = Math.cos(rotX)
s = Math.sin(rotX)
this._values = multiply([
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
], this._values)
return this
}
rotateAxisAngle (x, y, z, angle) {
return newInstance(this._values).rotateAxisAngleSelf(x, y, z, angle)
}
rotateAxisAngleSelf (x, y, z, angle) {
if (typeof x !== 'number') x = 0
if (typeof y !== 'number') y = 0
if (typeof z !== 'number') z = 0
// Normalize axis
const length = Math.sqrt(x * x + y * y + z * z)
if (length === 0) return this
if (length !== 1) {
x /= length
y /= length
z /= length
}
angle *= RAD_PER_DEGREE
const c = Math.cos(angle)
const s = Math.sin(angle)
const t = 1 - c
const tx = t * x
const ty = t * y
// NB: This is the generic transform. If the axis is a major axis, there are
// faster transforms.
this._values = multiply([
tx * x + c, tx * y + s * z, tx * z - s * y, 0,
tx * y - s * z, ty * y + c, ty * z + s * x, 0,
tx * z + s * y, ty * z - s * x, t * z * z + c, 0,
0, 0, 0, 1
], this._values)
if (x !== 0 || y !== 0) this._is2D = false
return this
}
skewX (sx) {
return newInstance(this._values).skewXSelf(sx)
}
skewXSelf (sx) {
if (typeof sx !== 'number') return this
const t = Math.tan(sx * RAD_PER_DEGREE)
this._values = multiply([
1, 0, 0, 0,
t, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
], this._values)
return this
}
skewY (sy) {
return newInstance(this._values).skewYSelf(sy)
}
skewYSelf (sy) {
if (typeof sy !== 'number') return this
const t = Math.tan(sy * RAD_PER_DEGREE)
this._values = multiply([
1, t, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
], this._values)
return this
}
flipX () {
return newInstance(multiply([
-1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
], this._values))
}
flipY () {
return newInstance(multiply([
1, 0, 0, 0,
0, -1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
], this._values))
}
inverse () {
return newInstance(this._values).invertSelf()
}
invertSelf () {
const m = this._values
const inv = m.map(v => 0)
inv[0] = m[5] * m[10] * m[15] -
m[5] * m[11] * m[14] -
m[9] * m[6] * m[15] +
m[9] * m[7] * m[14] +
m[13] * m[6] * m[11] -
m[13] * m[7] * m[10]
inv[4] = -m[4] * m[10] * m[15] +
m[4] * m[11] * m[14] +
m[8] * m[6] * m[15] -
m[8] * m[7] * m[14] -
m[12] * m[6] * m[11] +
m[12] * m[7] * m[10]
inv[8] = m[4] * m[9] * m[15] -
m[4] * m[11] * m[13] -
m[8] * m[5] * m[15] +
m[8] * m[7] * m[13] +
m[12] * m[5] * m[11] -
m[12] * m[7] * m[9]
inv[12] = -m[4] * m[9] * m[14] +
m[4] * m[10] * m[13] +
m[8] * m[5] * m[14] -
m[8] * m[6] * m[13] -
m[12] * m[5] * m[10] +
m[12] * m[6] * m[9]
// If the determinant is zero, this matrix cannot be inverted, and all
// values should be set to NaN, with the is2D flag set to false.
const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12]
if (det === 0) {
this._values = m.map(v => NaN)
this._is2D = false
return this
}
inv[1] = -m[1] * m[10] * m[15] +
m[1] * m[11] * m[14] +
m[9] * m[2] * m[15] -
m[9] * m[3] * m[14] -
m[13] * m[2] * m[11] +
m[13] * m[3] * m[10]
inv[5] = m[0] * m[10] * m[15] -
m[0] * m[11] * m[14] -
m[8] * m[2] * m[15] +
m[8] * m[3] * m[14] +
m[12] * m[2] * m[11] -
m[12] * m[3] * m[10]
inv[9] = -m[0] * m[9] * m[15] +
m[0] * m[11] * m[13] +
m[8] * m[1] * m[15] -
m[8] * m[3] * m[13] -
m[12] * m[1] * m[11] +
m[12] * m[3] * m[9]
inv[13] = m[0] * m[9] * m[14] -
m[0] * m[10] * m[13] -
m[8] * m[1] * m[14] +
m[8] * m[2] * m[13] +
m[12] * m[1] * m[10] -
m[12] * m[2] * m[9]
inv[2] = m[1] * m[6] * m[15] -
m[1] * m[7] * m[14] -
m[5] * m[2] * m[15] +
m[5] * m[3] * m[14] +
m[13] * m[2] * m[7] -
m[13] * m[3] * m[6]
inv[6] = -m[0] * m[6] * m[15] +
m[0] * m[7] * m[14] +
m[4] * m[2] * m[15] -
m[4] * m[3] * m[14] -
m[12] * m[2] * m[7] +
m[12] * m[3] * m[6]
inv[10] = m[0] * m[5] * m[15] -
m[0] * m[7] * m[13] -
m[4] * m[1] * m[15] +
m[4] * m[3] * m[13] +
m[12] * m[1] * m[7] -
m[12] * m[3] * m[5]
inv[14] = -m[0] * m[5] * m[14] +
m[0] * m[6] * m[13] +
m[4] * m[1] * m[14] -
m[4] * m[2] * m[13] -
m[12] * m[1] * m[6] +
m[12] * m[2] * m[5]
inv[3] = -m[1] * m[6] * m[11] +
m[1] * m[7] * m[10] +
m[5] * m[2] * m[11] -
m[5] * m[3] * m[10] -
m[9] * m[2] * m[7] +
m[9] * m[3] * m[6]
inv[7] = m[0] * m[6] * m[11] -
m[0] * m[7] * m[10] -
m[4] * m[2] * m[11] +
m[4] * m[3] * m[10] +
m[8] * m[2] * m[7] -
m[8] * m[3] * m[6]
inv[11] = -m[0] * m[5] * m[11] +
m[0] * m[7] * m[9] +
m[4] * m[1] * m[11] -
m[4] * m[3] * m[9] -
m[8] * m[1] * m[7] +
m[8] * m[3] * m[5]
inv[15] = m[0] * m[5] * m[10] -
m[0] * m[6] * m[9] -
m[4] * m[1] * m[10] +
m[4] * m[2] * m[9] +
m[8] * m[1] * m[6] -
m[8] * m[2] * m[5]
inv.forEach((v, i) => { inv[i] = v / det })
this._values = inv
return this
}
setMatrixValue (transformList) {
const temp = new DOMMatrix(transformList)
this._values = temp._values
this._is2D = temp._is2D
return this
}
transformPoint (point) {
point = new DOMPoint(point)
const x = point.x
const y = point.y
const z = point.z
const w = point.w
const values = this._values
const nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w
const ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w
const nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w
const nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w
return new DOMPoint(nx, ny, nz, nw)
}
toFloat32Array () {
return Float32Array.from(this._values)
}
toFloat64Array () {
return this._values.slice(0)
}
static fromMatrix (init) {
if (!(init instanceof DOMMatrix)) throw new TypeError('Expected DOMMatrix')
return new DOMMatrix(init._values)
}
static fromFloat32Array (init) {
if (!(init instanceof Float32Array)) throw new TypeError('Expected Float32Array')
return new DOMMatrix(init)
}
static fromFloat64Array (init) {
if (!(init instanceof Float64Array)) throw new TypeError('Expected Float64Array')
return new DOMMatrix(init)
}
[util.inspect.custom || 'inspect'] (depth, options) {
if (depth < 0) return '[DOMMatrix]'
return `DOMMatrix [
a: ${this.a}
b: ${this.b}
c: ${this.c}
d: ${this.d}
e: ${this.e}
f: ${this.f}
m11: ${this.m11}
m12: ${this.m12}
m13: ${this.m13}
m14: ${this.m14}
m21: ${this.m21}
m22: ${this.m22}
m23: ${this.m23}
m23: ${this.m23}
m31: ${this.m31}
m32: ${this.m32}
m33: ${this.m33}
m34: ${this.m34}
m41: ${this.m41}
m42: ${this.m42}
m43: ${this.m43}
m44: ${this.m44}
is2D: ${this.is2D}
isIdentity: ${this.isIdentity} ]`
}
}
/**
* Checks that `value` is a number and sets the value.
*/
function setNumber2D (receiver, index, value) {
if (typeof value !== 'number') throw new TypeError('Expected number')
return (receiver._values[index] = value)
}
/**
* Checks that `value` is a number, sets `_is2D = false` if necessary and sets
* the value.
*/
function setNumber3D (receiver, index, value) {
if (typeof value !== 'number') throw new TypeError('Expected number')
if (index === M33 || index === M44) {
if (value !== 1) receiver._is2D = false
} else if (value !== 0) receiver._is2D = false
return (receiver._values[index] = value)
}
Object.defineProperties(DOMMatrix.prototype, {
m11: { get () { return this._values[M11] }, set (v) { return setNumber2D(this, M11, v) } },
m12: { get () { return this._values[M12] }, set (v) { return setNumber2D(this, M12, v) } },
m13: { get () { return this._values[M13] }, set (v) { return setNumber3D(this, M13, v) } },
m14: { get () { return this._values[M14] }, set (v) { return setNumber3D(this, M14, v) } },
m21: { get () { return this._values[M21] }, set (v) { return setNumber2D(this, M21, v) } },
m22: { get () { return this._values[M22] }, set (v) { return setNumber2D(this, M22, v) } },
m23: { get () { return this._values[M23] }, set (v) { return setNumber3D(this, M23, v) } },
m24: { get () { return this._values[M24] }, set (v) { return setNumber3D(this, M24, v) } },
m31: { get () { return this._values[M31] }, set (v) { return setNumber3D(this, M31, v) } },
m32: { get () { return this._values[M32] }, set (v) { return setNumber3D(this, M32, v) } },
m33: { get () { return this._values[M33] }, set (v) { return setNumber3D(this, M33, v) } },
m34: { get () { return this._values[M34] }, set (v) { return setNumber3D(this, M34, v) } },
m41: { get () { return this._values[M41] }, set (v) { return setNumber2D(this, M41, v) } },
m42: { get () { return this._values[M42] }, set (v) { return setNumber2D(this, M42, v) } },
m43: { get () { return this._values[M43] }, set (v) { return setNumber3D(this, M43, v) } },
m44: { get () { return this._values[M44] }, set (v) { return setNumber3D(this, M44, v) } },
a: { get () { return this.m11 }, set (v) { return (this.m11 = v) } },
b: { get () { return this.m12 }, set (v) { return (this.m12 = v) } },
c: { get () { return this.m21 }, set (v) { return (this.m21 = v) } },
d: { get () { return this.m22 }, set (v) { return (this.m22 = v) } },
e: { get () { return this.m41 }, set (v) { return (this.m41 = v) } },
f: { get () { return this.m42 }, set (v) { return (this.m42 = v) } },
is2D: { get () { return this._is2D } }, // read-only
isIdentity: {
get () {
const values = this._values
return (values[M11] === 1 && values[M12] === 0 && values[M13] === 0 && values[M14] === 0 &&
values[M21] === 0 && values[M22] === 1 && values[M23] === 0 && values[M24] === 0 &&
values[M31] === 0 && values[M32] === 0 && values[M33] === 1 && values[M34] === 0 &&
values[M41] === 0 && values[M42] === 0 && values[M43] === 0 && values[M44] === 1)
}
},
toJSON: {
value() {
return {
a: this.a,
b: this.b,
c: this.c,
d: this.d,
e: this.e,
f: this.f,
m11: this.m11,
m12: this.m12,
m13: this.m13,
m14: this.m14,
m21: this.m21,
m22: this.m22,
m23: this.m23,
m23: this.m23,
m31: this.m31,
m32: this.m32,
m33: this.m33,
m34: this.m34,
m41: this.m41,
m42: this.m42,
m43: this.m43,
m44: this.m44,
is2D: this.is2D,
isIdentity: this.isIdentity,
}
}
}
})
/**
* Instantiates a DOMMatrix, bypassing the constructor.
* @param {Float64Array} values Value to assign to `_values`. This is assigned
* without copying (okay because all usages are followed by a multiply).
*/
function newInstance (values) {
const instance = Object.create(DOMMatrix.prototype)
instance.constructor = DOMMatrix
instance._is2D = true
instance._values = values
return instance
}
function multiply (A, B) {
const dest = new Float64Array(16)
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
let sum = 0
for (let k = 0; k < 4; k++) {
sum += A[i * 4 + k] * B[k * 4 + j]
}
dest[i * 4 + j] = sum
}
}
return dest
}
module.exports = { DOMMatrix, DOMPoint }
+43
View File
@@ -0,0 +1,43 @@
'use strict'
const bindings = require('../build/Release/canvas.node')
module.exports = bindings
Object.defineProperty(bindings.Canvas.prototype, Symbol.toStringTag, {
value: 'HTMLCanvasElement',
configurable: true
})
Object.defineProperty(bindings.Image.prototype, Symbol.toStringTag, {
value: 'HTMLImageElement',
configurable: true
})
bindings.ImageData.prototype.toString = function () {
return '[object ImageData]'
}
Object.defineProperty(bindings.ImageData.prototype, Symbol.toStringTag, {
value: 'ImageData',
configurable: true
})
bindings.CanvasGradient.prototype.toString = function () {
return '[object CanvasGradient]'
}
Object.defineProperty(bindings.CanvasGradient.prototype, Symbol.toStringTag, {
value: 'CanvasGradient',
configurable: true
})
Object.defineProperty(bindings.CanvasPattern.prototype, Symbol.toStringTag, {
value: 'CanvasPattern',
configurable: true
})
Object.defineProperty(bindings.CanvasRenderingContext2d.prototype, Symbol.toStringTag, {
value: 'CanvasRenderingContext2d',
configurable: true
})
+113
View File
@@ -0,0 +1,113 @@
'use strict'
/*!
* Canvas
* Copyright (c) 2010 LearnBoost <tj@learnboost.com>
* MIT Licensed
*/
const bindings = require('./bindings')
const Canvas = module.exports = bindings.Canvas
const Context2d = require('./context2d')
const PNGStream = require('./pngstream')
const PDFStream = require('./pdfstream')
const JPEGStream = require('./jpegstream')
const FORMATS = ['image/png', 'image/jpeg']
const util = require('util')
// TODO || is for Node.js pre-v6.6.0
Canvas.prototype[util.inspect.custom || 'inspect'] = function () {
return `[Canvas ${this.width}x${this.height}]`
}
Canvas.prototype.getContext = function (contextType, contextAttributes) {
if (contextType == '2d') {
const ctx = this._context2d || (this._context2d = new Context2d(this, contextAttributes))
this.context = ctx
ctx.canvas = this
return ctx
}
}
Canvas.prototype.pngStream =
Canvas.prototype.createPNGStream = function (options) {
return new PNGStream(this, options)
}
Canvas.prototype.pdfStream =
Canvas.prototype.createPDFStream = function (options) {
return new PDFStream(this, options)
}
Canvas.prototype.jpegStream =
Canvas.prototype.createJPEGStream = function (options) {
return new JPEGStream(this, options)
}
Canvas.prototype.toDataURL = function (a1, a2, a3) {
// valid arg patterns (args -> [type, opts, fn]):
// [] -> ['image/png', null, null]
// [qual] -> ['image/png', null, null]
// [undefined] -> ['image/png', null, null]
// ['image/png'] -> ['image/png', null, null]
// ['image/png', qual] -> ['image/png', null, null]
// [fn] -> ['image/png', null, fn]
// [type, fn] -> [type, null, fn]
// [undefined, fn] -> ['image/png', null, fn]
// ['image/png', qual, fn] -> ['image/png', null, fn]
// ['image/jpeg', fn] -> ['image/jpeg', null, fn]
// ['image/jpeg', opts, fn] -> ['image/jpeg', opts, fn]
// ['image/jpeg', qual, fn] -> ['image/jpeg', {quality: qual}, fn]
// ['image/jpeg', undefined, fn] -> ['image/jpeg', null, fn]
// ['image/jpeg'] -> ['image/jpeg', null, fn]
// ['image/jpeg', opts] -> ['image/jpeg', opts, fn]
// ['image/jpeg', qual] -> ['image/jpeg', {quality: qual}, fn]
let type = 'image/png'
let opts = {}
let fn
if (typeof a1 === 'function') {
fn = a1
} else {
if (typeof a1 === 'string' && FORMATS.includes(a1.toLowerCase())) {
type = a1.toLowerCase()
}
if (typeof a2 === 'function') {
fn = a2
} else {
if (typeof a2 === 'object') {
opts = a2
} else if (typeof a2 === 'number') {
opts = { quality: Math.max(0, Math.min(1, a2)) }
}
if (typeof a3 === 'function') {
fn = a3
} else if (undefined !== a3) {
throw new TypeError(`${typeof a3} is not a function`)
}
}
}
if (this.width === 0 || this.height === 0) {
// Per spec, if the bitmap has no pixels, return this string:
const str = 'data:,'
if (fn) {
setTimeout(() => fn(null, str))
return
} else {
return str
}
}
if (fn) {
this.toBuffer((err, buf) => {
if (err) return fn(err)
fn(null, `data:${type};base64,${buf.toString('base64')}`)
}, type, opts)
} else {
return `data:${type};base64,${this.toBuffer(type, opts).toString('base64')}`
}
}
+11
View File
@@ -0,0 +1,11 @@
'use strict'
/*!
* Canvas - Context2d
* Copyright (c) 2010 LearnBoost <tj@learnboost.com>
* MIT Licensed
*/
const bindings = require('./bindings')
module.exports = bindings.CanvasRenderingContext2d
+97
View File
@@ -0,0 +1,97 @@
'use strict'
/*!
* Canvas - Image
* Copyright (c) 2010 LearnBoost <tj@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
const bindings = require('./bindings')
const Image = module.exports = bindings.Image
const util = require('util')
const { GetSource, SetSource } = bindings
Object.defineProperty(Image.prototype, 'src', {
/**
* src setter. Valid values:
* * `data:` URI
* * Local file path
* * HTTP or HTTPS URL
* * Buffer containing image data (i.e. not a `data:` URI stored in a Buffer)
*
* @param {String|Buffer} val filename, buffer, data URI, URL
* @api public
*/
set (val) {
if (typeof val === 'string') {
if (/^\s*data:/.test(val)) { // data: URI
const commaI = val.indexOf(',')
// 'base64' must come before the comma
const isBase64 = val.lastIndexOf('base64', commaI) !== -1
const content = val.slice(commaI + 1)
setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val)
} else if (/^\s*https?:\/\//.test(val)) { // remote URL
const onerror = err => {
if (typeof this.onerror === 'function') {
this.onerror(err)
} else {
throw err
}
}
fetch(val, {
method: 'GET',
headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36' }
})
.then(res => {
if (!res.ok) {
throw new Error(`Server responded with ${res.status}`)
}
return res.arrayBuffer()
})
.then(data => {
setSource(this, Buffer.from(data))
})
.catch(onerror)
} else { // local file path assumed
setSource(this, val)
}
} else if (Buffer.isBuffer(val)) {
setSource(this, val)
} else {
const err = new Error("Invalid image source")
if (typeof this.onerror === 'function') this.onerror(err)
else throw err
}
},
get () {
// TODO https://github.com/Automattic/node-canvas/issues/118
return getSource(this)
},
configurable: true
})
// TODO || is for Node.js pre-v6.6.0
Image.prototype[util.inspect.custom || 'inspect'] = function () {
return '[Image' +
(this.complete ? ':' + this.width + 'x' + this.height : '') +
(this.src ? ' ' + this.src : '') +
(this.complete ? ' complete' : '') +
']'
}
function getSource (img) {
return img._originalSource || GetSource.call(img)
}
function setSource (img, src, origSrc) {
SetSource.call(img, src)
img._originalSource = origSrc
}
+41
View File
@@ -0,0 +1,41 @@
'use strict'
/*!
* Canvas - JPEGStream
* Copyright (c) 2010 LearnBoost <tj@learnboost.com>
* MIT Licensed
*/
const { Readable } = require('stream')
function noop () {}
class JPEGStream extends Readable {
constructor (canvas, options) {
super()
if (canvas.streamJPEGSync === undefined) {
throw new Error('node-canvas was built without JPEG support.')
}
this.options = options
this.canvas = canvas
}
_read () {
// For now we're not controlling the c++ code's data emission, so we only
// call canvas.streamJPEGSync once and let it emit data at will.
this._read = noop
this.canvas.streamJPEGSync(this.options, (err, chunk) => {
if (err) {
this.emit('error', err)
} else if (chunk) {
this.push(chunk)
} else {
this.push(null)
}
})
}
};
module.exports = JPEGStream
+15
View File
@@ -0,0 +1,15 @@
'use strict'
/*!
* Canvas - CanvasPattern
* Copyright (c) 2010 LearnBoost <tj@learnboost.com>
* MIT Licensed
*/
const bindings = require('./bindings')
module.exports = bindings.CanvasPattern
bindings.CanvasPattern.prototype.toString = function () {
return '[object CanvasPattern]'
}
+35
View File
@@ -0,0 +1,35 @@
'use strict'
/*!
* Canvas - PDFStream
*/
const { Readable } = require('stream')
function noop () {}
class PDFStream extends Readable {
constructor (canvas, options) {
super()
this.canvas = canvas
this.options = options
}
_read () {
// For now we're not controlling the c++ code's data emission, so we only
// call canvas.streamPDFSync once and let it emit data at will.
this._read = noop
this.canvas.streamPDFSync((err, chunk, len) => {
if (err) {
this.emit('error', err)
} else if (len) {
this.push(chunk)
} else {
this.push(null)
}
}, this.options)
}
}
module.exports = PDFStream
+42
View File
@@ -0,0 +1,42 @@
'use strict'
/*!
* Canvas - PNGStream
* Copyright (c) 2010 LearnBoost <tj@learnboost.com>
* MIT Licensed
*/
const { Readable } = require('stream')
function noop () {}
class PNGStream extends Readable {
constructor (canvas, options) {
super()
if (options &&
options.palette instanceof Uint8ClampedArray &&
options.palette.length % 4 !== 0) {
throw new Error('Palette length must be a multiple of 4.')
}
this.canvas = canvas
this.options = options || {}
}
_read () {
// For now we're not controlling the c++ code's data emission, so we only
// call canvas.streamPNGSync once and let it emit data at will.
this._read = noop
this.canvas.streamPNGSync((err, chunk, len) => {
if (err) {
this.emit('error', err)
} else if (len) {
this.push(chunk)
} else {
this.push(null)
}
}, this.options)
}
}
module.exports = PNGStream
+71
View File
@@ -0,0 +1,71 @@
{
"name": "canvas",
"description": "Canvas graphics API backed by Cairo",
"version": "3.2.3",
"author": "TJ Holowaychuk <tj@learnboost.com>",
"main": "index.js",
"browser": "browser.js",
"types": "index.d.ts",
"contributors": [
"Nathan Rajlich <nathan@tootallnate.net>",
"Rod Vagg <r@va.gg>",
"Juriy Zaytsev <kangax@gmail.com>"
],
"keywords": [
"canvas",
"graphic",
"graphics",
"pixman",
"cairo",
"image",
"images",
"pdf"
],
"homepage": "https://github.com/Automattic/node-canvas",
"repository": "git://github.com/Automattic/node-canvas.git",
"scripts": {
"prebenchmark": "node-gyp build",
"benchmark": "node benchmarks/run.js",
"lint": "standard examples/*.js test/server.js test/public/*.js benchmarks/run.js lib/context2d.js util/has_lib.js browser.js index.js",
"test": "mocha test/*.test.js",
"pretest-server": "node-gyp build",
"test-server": "node test/server.js",
"generate-wpt": "node ./test/wpt/generate.js",
"test-wpt": "mocha test/wpt/generated/*.js",
"install": "prebuild-install -r napi || node-gyp rebuild",
"tsd": "tsd"
},
"files": [
"binding.gyp",
"browser.js",
"index.d.ts",
"index.js",
"lib/",
"src/",
"util/"
],
"dependencies": {
"node-addon-api": "^7.0.0",
"prebuild-install": "^7.1.3"
},
"devDependencies": {
"@types/node": "^10.12.18",
"assert-rejects": "^1.0.0",
"express": "^4.16.3",
"js-yaml": "^4.1.0",
"mocha": "^5.2.0",
"pixelmatch": "^4.0.2",
"standard": "^12.0.1",
"tsd": "^0.29.0",
"typescript": "^4.2.2"
},
"engines": {
"node": "^18.12.0 || >= 20.9.0"
},
"binary": {
"napi_versions": [
7
]
},
"license": "MIT"
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "backend/Backend.h"
#include <napi.h>
class Backends : public Napi::ObjectWrap<Backends> {
public:
static void Initialize(Napi::Env env, Napi::Object exports);
};
+1026
View File
@@ -0,0 +1,1026 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#include "Canvas.h"
#include "InstanceData.h"
#include <algorithm> // std::min
#include <assert.h>
#include <cairo-pdf.h>
#include <cairo-svg.h>
#include "CanvasRenderingContext2d.h"
#include "closure.h"
#include <cstring>
#include <cctype>
#include <ctime>
#include <glib.h>
#include "PNG.h"
#include "register_font.h"
#include <sstream>
#include <stdlib.h>
#include <string>
#include <unordered_set>
#include "Util.h"
#include <vector>
#include "node_buffer.h"
#include "FontParser.h"
#ifdef HAVE_JPEG
#include "JPEGStream.h"
#endif
#define GENERIC_FACE_ERROR \
"The second argument to registerFont is required, and should be an object " \
"with at least a family (string) and optionally weight (string/number) " \
"and style (string)."
#define CAIRO_MAX_SIZE 32767
using namespace std;
std::vector<FontFace> Canvas::font_face_list;
// Increases each time a font is (de)registered
int Canvas::fontSerial = 1;
/*
* Initialize Canvas.
*/
void
Canvas::Initialize(Napi::Env& env, Napi::Object& exports) {
Napi::HandleScope scope(env);
InstanceData* data = env.GetInstanceData<InstanceData>();
// Constructor
Napi::Function ctor = DefineClass(env, "Canvas", {
InstanceMethod<&Canvas::ToBuffer>("toBuffer", napi_default_method),
InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync", napi_default_method),
InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync", napi_default_method),
#ifdef HAVE_JPEG
InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync", napi_default_method),
#endif
InstanceAccessor<&Canvas::GetType>("type", napi_default_jsproperty),
InstanceAccessor<&Canvas::GetStride>("stride", napi_default_jsproperty),
InstanceAccessor<&Canvas::GetWidth, &Canvas::SetWidth>("width", napi_default_jsproperty),
InstanceAccessor<&Canvas::GetHeight, &Canvas::SetHeight>("height", napi_default_jsproperty),
StaticValue("PNG_NO_FILTERS", Napi::Number::New(env, PNG_NO_FILTERS), napi_default_jsproperty),
StaticValue("PNG_FILTER_NONE", Napi::Number::New(env, PNG_FILTER_NONE), napi_default_jsproperty),
StaticValue("PNG_FILTER_SUB", Napi::Number::New(env, PNG_FILTER_SUB), napi_default_jsproperty),
StaticValue("PNG_FILTER_UP", Napi::Number::New(env, PNG_FILTER_UP), napi_default_jsproperty),
StaticValue("PNG_FILTER_AVG", Napi::Number::New(env, PNG_FILTER_AVG), napi_default_jsproperty),
StaticValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH), napi_default_jsproperty),
StaticValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS), napi_default_jsproperty),
StaticMethod<&Canvas::RegisterFont>("_registerFont", napi_default_method),
StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method),
StaticMethod<&Canvas::ParseFont>("parseFont", napi_default_method)
});
data->CanvasCtor = Napi::Persistent(ctor);
exports.Set("Canvas", ctor);
}
/*
* Initialize a Canvas with the given width and height.
*/
Canvas::Canvas(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Canvas>(info), env(info.Env()) {
InstanceData* data = env.GetInstanceData<InstanceData>();
ctor = Napi::Persistent(data->CanvasCtor.Value());
_surface = nullptr;
_closure = nullptr;
width = 0;
height = 0;
format = CAIRO_FORMAT_ARGB32;
if (info[0].IsNumber()) {
uint32_t width = info[0].As<Napi::Number>().Uint32Value();
uint32_t height = 0;
if (info[1].IsNumber()) height = info[1].As<Napi::Number>().Uint32Value();
if (width > CAIRO_MAX_SIZE) {
std::string msg = "Canvas width cannot exceed " + std::to_string(CAIRO_MAX_SIZE);
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
return;
}
if (height > CAIRO_MAX_SIZE) {
std::string msg = "Canvas height cannot exceed " + std::to_string(CAIRO_MAX_SIZE);
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
return;
}
this->width = width;
this->height = height;
if (info[2].IsString()) {
std::string str = info[2].As<Napi::String>();
if (str == "pdf") {
type = CANVAS_TYPE_PDF;
} else if (str == "svg") {
type = CANVAS_TYPE_SVG;
} else {
type = CANVAS_TYPE_IMAGE;
}
} else {
type = CANVAS_TYPE_IMAGE;
}
} else {
type = CANVAS_TYPE_IMAGE;
}
cairo_status_t status = cairo_surface_status(ensureSurface());
if (status != CAIRO_STATUS_SUCCESS) {
Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException();
return;
}
}
Canvas::~Canvas() {
destroySurface();
}
/*
* Get type string.
*/
Napi::Value
Canvas::GetType(const Napi::CallbackInfo& info) {
switch (type) {
case CANVAS_TYPE_PDF:
return Napi::String::New(env, "pdf");
case CANVAS_TYPE_SVG:
return Napi::String::New(env, "svg");
default:
return Napi::String::New(env, "image");
}
}
/*
* Get stride.
*/
Napi::Value
Canvas::GetStride(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, cairo_image_surface_get_stride(ensureSurface()));
}
/*
* Get width.
*/
Napi::Value
Canvas::GetWidth(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, getWidth());
}
/*
* Set width.
*/
void
Canvas::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (value.IsNumber()) {
uint32_t width = value.As<Napi::Number>().Uint32Value();
if (width <= CAIRO_MAX_SIZE) {
resurface(info.This().As<Napi::Object>(), width, this->height);
}
}
}
/*
* Get height.
*/
Napi::Value
Canvas::GetHeight(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, getHeight());
}
/*
* Set height.
*/
void
Canvas::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (value.IsNumber()) {
uint32_t height = value.As<Napi::Number>().Uint32Value();
if (height <= CAIRO_MAX_SIZE) {
resurface(info.This().As<Napi::Object>(), this->width, height);
}
}
}
/*
* EIO toBuffer callback.
*/
void
Canvas::ToPngBufferAsync(Closure* base) {
PngClosure* closure = static_cast<PngClosure*>(base);
closure->status = canvas_write_to_png_stream(
closure->canvas->ensureSurface(),
PngClosure::writeVec,
closure);
}
#ifdef HAVE_JPEG
void
Canvas::ToJpegBufferAsync(Closure* base) {
JpegClosure* closure = static_cast<JpegClosure*>(base);
write_to_jpeg_buffer(closure->canvas->ensureSurface(), closure);
}
#endif
static void
parsePNGArgs(Napi::Value arg, PngClosure& pngargs) {
if (arg.IsObject()) {
Napi::Object obj = arg.As<Napi::Object>();
Napi::Value cLevel;
if (obj.Get("compressionLevel").UnwrapTo(&cLevel) && cLevel.IsNumber()) {
uint32_t val = cLevel.As<Napi::Number>().Uint32Value();
// See quote below from spec section 4.12.5.5.
if (val <= 9) pngargs.compressionLevel = val;
}
Napi::Value rez;
if (obj.Get("resolution").UnwrapTo(&rez) && rez.IsNumber()) {
uint32_t val = rez.As<Napi::Number>().Uint32Value();
if (val > 0) pngargs.resolution = val;
}
Napi::Value filters;
if (obj.Get("filters").UnwrapTo(&filters) && filters.IsNumber()) {
pngargs.filters = filters.As<Napi::Number>().Uint32Value();
}
Napi::Value palette;
if (obj.Get("palette").UnwrapTo(&palette) && palette.IsTypedArray()) {
Napi::TypedArray palette_ta = palette.As<Napi::TypedArray>();
if (palette_ta.TypedArrayType() == napi_uint8_clamped_array) {
pngargs.nPaletteColors = palette_ta.ElementLength();
if (pngargs.nPaletteColors % 4 != 0) {
throw "Palette length must be a multiple of 4.";
}
pngargs.palette = palette_ta.As<Napi::Uint8Array>().Data();
pngargs.nPaletteColors /= 4;
// Optional background color index:
Napi::Value backgroundIndexVal;
if (obj.Get("backgroundIndex").UnwrapTo(&backgroundIndexVal) && backgroundIndexVal.IsNumber()) {
pngargs.backgroundIndex = backgroundIndexVal.As<Napi::Number>().Uint32Value();
}
}
}
}
}
#ifdef HAVE_JPEG
static void parseJPEGArgs(Napi::Value arg, JpegClosure& jpegargs) {
// "If Type(quality) is not Number, or if quality is outside that range, the
// user agent must use its default quality value, as if the quality argument
// had not been given." - 4.12.5.5
if (arg.IsObject()) {
Napi::Object obj = arg.As<Napi::Object>();
Napi::Value qual;
if (obj.Get("quality").UnwrapTo(&qual) && qual.IsNumber()) {
double quality = qual.As<Napi::Number>().DoubleValue();
if (quality >= 0.0 && quality <= 1.0) {
jpegargs.quality = static_cast<uint32_t>(100.0 * quality);
}
}
Napi::Value chroma;
if (obj.Get("chromaSubsampling").UnwrapTo(&chroma)) {
if (chroma.IsBoolean()) {
bool subsample = chroma.As<Napi::Boolean>().Value();
jpegargs.chromaSubsampling = subsample ? 2 : 1;
} else if (chroma.IsNumber()) {
jpegargs.chromaSubsampling = chroma.As<Napi::Number>().Uint32Value();
}
}
Napi::Value progressive;
if (obj.Get("progressive").UnwrapTo(&progressive) && progressive.IsBoolean()) {
jpegargs.progressive = progressive.As<Napi::Boolean>().Value();
}
}
}
#endif
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
static inline void setPdfMetaStr(cairo_surface_t* surf, Napi::Object opts,
cairo_pdf_metadata_t t, const char* propName) {
Napi::Value propValue;
if (opts.Get(propName).UnwrapTo(&propValue) && propValue.IsString()) {
// (copies char data)
cairo_pdf_surface_set_metadata(surf, t, propValue.As<Napi::String>().Utf8Value().c_str());
}
}
static inline void setPdfMetaDate(cairo_surface_t* surf, Napi::Object opts,
cairo_pdf_metadata_t t, const char* propName) {
Napi::Value propValue;
if (opts.Get(propName).UnwrapTo(&propValue) && propValue.IsDate()) {
auto date = static_cast<time_t>(propValue.As<Napi::Date>().ValueOf() / 1000); // ms -> s
char buf[sizeof "2011-10-08T07:07:09Z"];
strftime(buf, sizeof buf, "%FT%TZ", gmtime(&date));
cairo_pdf_surface_set_metadata(surf, t, buf);
}
}
static void setPdfMetadata(Canvas* canvas, Napi::Object opts) {
cairo_surface_t* surf = canvas->ensureSurface();
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_TITLE, "title");
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_AUTHOR, "author");
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_SUBJECT, "subject");
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_KEYWORDS, "keywords");
setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_CREATOR, "creator");
setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_CREATE_DATE, "creationDate");
setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_MOD_DATE, "modDate");
}
#endif // CAIRO 16+
/*
* Converts/encodes data to a Buffer. Async when a callback function is passed.
* PDF canvases:
(any) => Buffer
("application/pdf", config) => Buffer
* SVG canvases:
(any) => Buffer
* ARGB data:
("raw") => Buffer
* PNG-encoded
() => Buffer
(undefined|"image/png", {compressionLevel?: number, filter?: number}) => Buffer
((err: null|Error, buffer) => any)
((err: null|Error, buffer) => any, undefined|"image/png", {compressionLevel?: number, filter?: number})
* JPEG-encoded
("image/jpeg") => Buffer
("image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) => Buffer
((err: null|Error, buffer) => any, "image/jpeg")
((err: null|Error, buffer) => any, "image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number})
*/
Napi::Value
Canvas::ToBuffer(const Napi::CallbackInfo& info) {
cairo_status_t status;
// Vector canvases, sync only
if (isPDF() || isSVG()) {
// mime type may be present, but it's not checked
PdfSvgClosure* closure = static_cast<PdfSvgClosure*>(_closure);
if (isPDF()) {
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
if (info[1].IsObject()) { // toBuffer("application/pdf", config)
setPdfMetadata(this, info[1].As<Napi::Object>());
}
#endif // CAIRO 16+
}
cairo_surface_t *surf = ensureSurface();
cairo_surface_finish(surf);
cairo_status_t status = cairo_surface_status(surf);
if (status != CAIRO_STATUS_SUCCESS) {
Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException();
return env.Undefined();
}
return Napi::Buffer<uint8_t>::Copy(env, &closure->vec[0], closure->vec.size());
}
// Raw ARGB data -- just a memcpy()
if (info[0].StrictEquals(Napi::String::New(env, "raw"))) {
cairo_surface_t *surface = ensureSurface();
cairo_surface_flush(surface);
if (nBytes() > node::Buffer::kMaxLength) {
Napi::Error::New(env, "Data exceeds maximum buffer length.").ThrowAsJavaScriptException();
return env.Undefined();
}
return Napi::Buffer<uint8_t>::Copy(env, cairo_image_surface_get_data(surface), nBytes());
}
// Sync PNG, default
if (info[0].IsUndefined() || info[0].StrictEquals(Napi::String::New(env, "image/png"))) {
try {
PngClosure closure(this);
parsePNGArgs(info[1], closure);
if (closure.nPaletteColors == 0xFFFFFFFF) {
Napi::Error::New(env, "Palette length must be a multiple of 4.").ThrowAsJavaScriptException();
return env.Undefined();
}
status = canvas_write_to_png_stream(ensureSurface(), PngClosure::writeVec, &closure);
if (!env.IsExceptionPending()) {
if (status) {
throw status; // TODO: throw in js?
} else {
// TODO it's possible to avoid this copy
return Napi::Buffer<uint8_t>::Copy(env, &closure.vec[0], closure.vec.size());
}
}
} catch (cairo_status_t ex) {
CairoError(ex).ThrowAsJavaScriptException();
} catch (const char* ex) {
Napi::Error::New(env, ex).ThrowAsJavaScriptException();
}
return env.Undefined();
}
// Async PNG
if (info[0].IsFunction() &&
(info[1].IsUndefined() || info[1].StrictEquals(Napi::String::New(env, "image/png")))) {
PngClosure* closure;
try {
closure = new PngClosure(this);
parsePNGArgs(info[2], *closure);
} catch (cairo_status_t ex) {
CairoError(ex).ThrowAsJavaScriptException();
return env.Undefined();
} catch (const char* ex) {
Napi::Error::New(env, ex).ThrowAsJavaScriptException();
return env.Undefined();
}
Ref();
closure->cb = Napi::Persistent(info[0].As<Napi::Function>());
// Make sure the surface exists since we won't have an isolate context in the async block:
ensureSurface();
EncodingWorker* worker = new EncodingWorker(env);
worker->Init(&ToPngBufferAsync, closure);
worker->Queue();
return env.Undefined();
}
#ifdef HAVE_JPEG
// Sync JPEG
Napi::Value jpegStr = Napi::String::New(env, "image/jpeg");
if (info[0].StrictEquals(jpegStr)) {
try {
JpegClosure closure(this);
parseJPEGArgs(info[1], closure);
write_to_jpeg_buffer(ensureSurface(), &closure);
if (!env.IsExceptionPending()) {
// TODO it's possible to avoid this copy.
return Napi::Buffer<uint8_t>::Copy(env, &closure.vec[0], closure.vec.size());
}
} catch (cairo_status_t ex) {
CairoError(ex).ThrowAsJavaScriptException();
return env.Undefined();
}
return env.Undefined();
}
// Async JPEG
if (info[0].IsFunction() && info[1].StrictEquals(jpegStr)) {
JpegClosure* closure = new JpegClosure(this);
parseJPEGArgs(info[2], *closure);
Ref();
closure->cb = Napi::Persistent(info[0].As<Napi::Function>());
// Make sure the surface exists since we won't have an isolate context in the async block:
ensureSurface();
EncodingWorker* worker = new EncodingWorker(env);
worker->Init(&ToJpegBufferAsync, closure);
worker->Queue();
return env.Undefined();
}
#endif
return env.Undefined();
}
/*
* Canvas::StreamPNG callback.
*/
static cairo_status_t
streamPNG(void *c, const uint8_t *data, unsigned len) {
PngClosure* closure = (PngClosure*) c;
Napi::Env env = closure->canvas->env;
Napi::HandleScope scope(env);
Napi::AsyncContext async(env, "canvas:StreamPNG");
Napi::Value buf = Napi::Buffer<uint8_t>::Copy(env, data, len);
closure->cb.MakeCallback(env.Global(), { env.Null(), buf, Napi::Number::New(env, len) }, async);
return CAIRO_STATUS_SUCCESS;
}
/*
* Stream PNG data synchronously. TODO async
* StreamPngSync(this, options: {palette?: Uint8ClampedArray, backgroundIndex?: uint32, compressionLevel: uint32, filters: uint32})
*/
void
Canvas::StreamPNGSync(const Napi::CallbackInfo& info) {
if (!info[0].IsFunction()) {
Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException();
return;
}
PngClosure closure(this);
parsePNGArgs(info[1], closure);
closure.cb = Napi::Persistent(info[0].As<Napi::Function>());
cairo_status_t status = canvas_write_to_png_stream(ensureSurface(), streamPNG, &closure);
if (!env.IsExceptionPending()) {
if (status) {
closure.cb.Call(env.Global(), { CairoError(status).Value() });
} else {
closure.cb.Call(env.Global(), { env.Null(), env.Null(), Napi::Number::New(env, 0) });
}
}
}
struct PdfStreamInfo {
Napi::Function fn;
uint32_t len;
uint8_t* data;
};
/*
* Canvas::StreamPDF callback.
*/
static cairo_status_t
streamPDF(void *c, const uint8_t *data, unsigned len) {
PdfStreamInfo* streaminfo = static_cast<PdfStreamInfo*>(c);
Napi::Env env = streaminfo->fn.Env();
Napi::HandleScope scope(env);
Napi::AsyncContext async(env, "canvas:StreamPDF");
// TODO this is technically wrong, we're returning a pointer to the data in a
// vector in a class with automatic storage duration. If the canvas goes out
// of scope while we're in the handler, a use-after-free could happen.
Napi::Value buf = Napi::Buffer<uint8_t>::New(env, (uint8_t *)(data), len);
streaminfo->fn.MakeCallback(env.Global(), { env.Null(), buf, Napi::Number::New(env, len) }, async);
return CAIRO_STATUS_SUCCESS;
}
cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PdfStreamInfo* streaminfo) {
size_t whole_chunks = streaminfo->len / PAGE_SIZE;
size_t remainder = streaminfo->len - whole_chunks * PAGE_SIZE;
for (size_t i = 0; i < whole_chunks; ++i) {
write_func(streaminfo, &streaminfo->data[i * PAGE_SIZE], PAGE_SIZE);
}
if (remainder) {
write_func(streaminfo, &streaminfo->data[whole_chunks * PAGE_SIZE], remainder);
}
return CAIRO_STATUS_SUCCESS;
}
/*
* Stream PDF data synchronously.
*/
void
Canvas::StreamPDFSync(const Napi::CallbackInfo& info) {
if (!info[0].IsFunction()) {
Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException();
return;
}
if (!isPDF()) {
Napi::TypeError::New(env, "wrong canvas type").ThrowAsJavaScriptException();
return;
}
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
if (info[1].IsObject()) {
setPdfMetadata(this, info[1].As<Napi::Object>());
}
#endif
cairo_surface_finish(ensureSurface());
PdfSvgClosure *closure = static_cast<PdfSvgClosure *>(_closure);
Napi::Function fn = info[0].As<Napi::Function>();
PdfStreamInfo streaminfo;
streaminfo.fn = fn;
streaminfo.data = &closure->vec[0];
streaminfo.len = closure->vec.size();
cairo_status_t status = canvas_write_to_pdf_stream(ensureSurface(), streamPDF, &streaminfo);
if (!env.IsExceptionPending()) {
if (status) {
fn.Call(env.Global(), { CairoError(status).Value() });
} else {
fn.Call(env.Global(), { env.Null(), env.Null(), Napi::Number::New(env, 0) });
}
}
}
/*
* Stream JPEG data synchronously.
*/
#ifdef HAVE_JPEG
static uint32_t getSafeBufSize(Canvas* canvas) {
// Don't allow the buffer size to exceed the size of the canvas (#674)
// TODO not sure if this is really correct, but it fixed #674
return (std::min)((uint32_t)canvas->getWidth() * canvas->getHeight() * 4, (uint32_t)PAGE_SIZE);
}
void
Canvas::StreamJPEGSync(const Napi::CallbackInfo& info) {
if (!info[1].IsFunction()) {
Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException();
return;
}
JpegClosure closure(this);
parseJPEGArgs(info[0], closure);
closure.cb = Napi::Persistent(info[1].As<Napi::Function>());
uint32_t bufsize = getSafeBufSize(this);
write_to_jpeg_stream(ensureSurface(), bufsize, &closure);
}
#endif
char *
str_value(Napi::Maybe<Napi::Value> maybe, const char *fallback, bool can_be_number) {
Napi::Value val;
if (maybe.UnwrapTo(&val)) {
if (val.IsString() || (can_be_number && val.IsNumber())) {
Napi::String strVal;
if (val.ToString().UnwrapTo(&strVal)) return strdup(strVal.Utf8Value().c_str());
} else if (fallback) {
return strdup(fallback);
}
}
return NULL;
}
void
Canvas::RegisterFont(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (!info[0].IsString()) {
Napi::Error::New(env, "Wrong argument type").ThrowAsJavaScriptException();
return;
} else if (!info[1].IsObject()) {
Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException();
return;
}
std::string filePath = info[0].As<Napi::String>();
PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *)(filePath.c_str()));
if (!sys_desc) {
Napi::Error::New(env, "Could not parse font file").ThrowAsJavaScriptException();
return;
}
PangoFontDescription *user_desc = pango_font_description_new();
// now check the attrs, there are many ways to be wrong
Napi::Object js_user_desc = info[1].As<Napi::Object>();
// TODO: use FontParser on these values just like the FontFace API works
char *family = str_value(js_user_desc.Get("family"), NULL, false);
char *weight = str_value(js_user_desc.Get("weight"), "normal", true);
char *style = str_value(js_user_desc.Get("style"), "normal", false);
if (family && weight && style) {
pango_font_description_set_weight(user_desc, Canvas::GetWeightFromCSSString(weight));
pango_font_description_set_style(user_desc, Canvas::GetStyleFromCSSString(style));
pango_font_description_set_family(user_desc, family);
auto found = std::find_if(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) {
return pango_font_description_equal(f.sys_desc, sys_desc);
});
if (found != font_face_list.end()) {
pango_font_description_free(found->user_desc);
found->user_desc = user_desc;
} else if (register_font((unsigned char *) filePath.c_str())) {
FontFace face;
face.user_desc = user_desc;
face.sys_desc = sys_desc;
strncpy((char *)face.file_path, (char *) filePath.c_str(), 1023);
font_face_list.push_back(face);
} else {
pango_font_description_free(user_desc);
Napi::Error::New(env, "Could not load font to the system's font host").ThrowAsJavaScriptException();
}
} else {
pango_font_description_free(user_desc);
if (!env.IsExceptionPending()) {
Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException();
}
}
free(family);
free(weight);
free(style);
fontSerial++;
}
void
Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
// Unload all fonts from pango to free up memory
bool success = true;
std::for_each(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) {
if (!deregister_font( (unsigned char *)f.file_path )) success = false;
pango_font_description_free(f.user_desc);
pango_font_description_free(f.sys_desc);
});
font_face_list.clear();
fontSerial++;
if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException();
}
/*
* Do not use! This is only exported for testing
*/
Napi::Value
Canvas::ParseFont(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) return env.Undefined();
Napi::String str;
if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined();
bool ok;
auto props = FontParser::parse(str, &ok);
if (!ok) return env.Undefined();
Napi::Object obj = Napi::Object::New(env);
obj.Set("size", Napi::Number::New(env, props.fontSize));
Napi::Array families = Napi::Array::New(env);
obj.Set("families", families);
unsigned int index = 0;
for (auto& family : props.fontFamily) {
families[index++] = Napi::String::New(env, family);
}
obj.Set("weight", Napi::Number::New(env, props.fontWeight));
obj.Set("variant", Napi::Number::New(env, static_cast<int>(props.fontVariant)));
obj.Set("style", Napi::Number::New(env, static_cast<int>(props.fontStyle)));
return obj;
}
/*
* Get a PangoStyle from a CSS string (like "italic")
*/
PangoStyle
Canvas::GetStyleFromCSSString(const char *style) {
PangoStyle s = PANGO_STYLE_NORMAL;
if (strlen(style) > 0) {
if (0 == strcmp("italic", style)) {
s = PANGO_STYLE_ITALIC;
} else if (0 == strcmp("oblique", style)) {
s = PANGO_STYLE_OBLIQUE;
}
}
return s;
}
/*
* Get a PangoWeight from a CSS string ("bold", "100", etc)
*/
PangoWeight
Canvas::GetWeightFromCSSString(const char *weight) {
PangoWeight w = PANGO_WEIGHT_NORMAL;
if (strlen(weight) > 0) {
if (0 == strcmp("bold", weight)) {
w = PANGO_WEIGHT_BOLD;
} else if (0 == strcmp("100", weight)) {
w = PANGO_WEIGHT_THIN;
} else if (0 == strcmp("200", weight)) {
w = PANGO_WEIGHT_ULTRALIGHT;
} else if (0 == strcmp("300", weight)) {
w = PANGO_WEIGHT_LIGHT;
} else if (0 == strcmp("400", weight)) {
w = PANGO_WEIGHT_NORMAL;
} else if (0 == strcmp("500", weight)) {
w = PANGO_WEIGHT_MEDIUM;
} else if (0 == strcmp("600", weight)) {
w = PANGO_WEIGHT_SEMIBOLD;
} else if (0 == strcmp("700", weight)) {
w = PANGO_WEIGHT_BOLD;
} else if (0 == strcmp("800", weight)) {
w = PANGO_WEIGHT_ULTRABOLD;
} else if (0 == strcmp("900", weight)) {
w = PANGO_WEIGHT_HEAVY;
}
}
return w;
}
/*
* Given a user description, return a description that will select the
* font either from the system or @font-face
*/
PangoFontDescription *
Canvas::ResolveFontDescription(const PangoFontDescription *desc) {
// One of the user-specified families could map to multiple SFNT family names
// if someone registered two different fonts under the same family name.
// https://drafts.csswg.org/css-fonts-3/#font-style-matching
FontFace best;
istringstream families(pango_font_description_get_family(desc));
unordered_set<string> seen_families;
string resolved_families;
bool first = true;
for (string family; getline(families, family, ','); ) {
string renamed_families;
for (auto& ff : font_face_list) {
string pangofamily = string(pango_font_description_get_family(ff.user_desc));
if (streq_casein(family, pangofamily)) {
const char* sys_desc_family_name = pango_font_description_get_family(ff.sys_desc);
bool unseen = seen_families.find(sys_desc_family_name) == seen_families.end();
bool better = best.user_desc == nullptr || pango_font_description_better_match(desc, best.user_desc, ff.user_desc);
// Avoid sending duplicate SFNT font names due to a bug in Pango for macOS:
// https://bugzilla.gnome.org/show_bug.cgi?id=762873
if (unseen) {
seen_families.insert(sys_desc_family_name);
if (better) {
renamed_families = string(sys_desc_family_name) + (renamed_families.size() ? "," : "") + renamed_families;
} else {
renamed_families = renamed_families + (renamed_families.size() ? "," : "") + sys_desc_family_name;
}
}
if (first && better) best = ff;
}
}
if (resolved_families.size()) resolved_families += ',';
resolved_families += renamed_families.size() ? renamed_families : family;
first = false;
}
PangoFontDescription* ret = pango_font_description_copy(best.sys_desc ? best.sys_desc : desc);
pango_font_description_set_family(ret, resolved_families.c_str());
return ret;
}
// This returns an approximate value only, suitable for
// Napi::MemoryManagement:: AdjustExternalMemory.
// The formats that don't map to intrinsic types (RGB30, A1) round up.
uint8_t
Canvas::approxBytesPerPixel() {
switch (format) {
case CAIRO_FORMAT_ARGB32:
case CAIRO_FORMAT_RGB24:
return 4;
#ifdef CAIRO_FORMAT_RGB30
case CAIRO_FORMAT_RGB30:
return 3;
#endif
case CAIRO_FORMAT_RGB16_565:
return 2;
case CAIRO_FORMAT_A8:
case CAIRO_FORMAT_A1:
return 1;
default:
return 0;
}
}
void
Canvas::setFormat(cairo_format_t format) {
if (this->format != format) {
destroySurface();
this->format = format;
}
}
cairo_format_t
Canvas::getFormat() {
return isImage() ? format : CAIRO_FORMAT_INVALID;
}
/*
* Re-alloc the surface, destroying the previous.
*/
void
Canvas::resurface(Napi::Object This, uint16_t width, uint16_t height) {
Napi::HandleScope scope(env);
Napi::Value context;
if (type == CANVAS_TYPE_PDF) {
ensureSurface();
cairo_pdf_surface_set_size(_surface, width, height);
this->width = width;
this->height = height;
} else {
destroySurface();
this->width = width;
this->height = height;
ensureSurface();
if (This.Get("context").UnwrapTo(&context) && context.IsObject()) {
// Reset context
Context2d *context2d = Context2d::Unwrap(context.As<Napi::Object>());
cairo_t *prev = context2d->context();
context2d->setContext(createCairoContext());
context2d->resetState();
cairo_destroy(prev);
}
}
}
cairo_surface_t *
Canvas::ensureSurface() {
if (_surface) {
return _surface;
}
assert(!_closure);
if (type == CANVAS_TYPE_PDF) {
_closure = new PdfSvgClosure(this);
_surface = cairo_pdf_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height);
} else if (type == CANVAS_TYPE_SVG) {
_closure = new PdfSvgClosure(this);
_surface = cairo_svg_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height);
} else {
_surface = cairo_image_surface_create(format, width, height);
Napi::MemoryManagement::AdjustExternalMemory(env, (int64_t)approxBytesPerPixel() * width * height);
}
assert(_surface);
return _surface;
}
void
Canvas::destroySurface() {
if (_surface) {
// flush any operations that may use the closure that is freed below
cairo_surface_finish(_surface);
if (type == CANVAS_TYPE_IMAGE) {
Napi::MemoryManagement::AdjustExternalMemory(env, -(int64_t)approxBytesPerPixel() * width * height);
}
cairo_surface_destroy(_surface);
_surface = nullptr;
}
if (_closure) {
delete _closure;
_closure = nullptr;
}
}
/**
* Wrapper around cairo_create()
* (do not call cairo_create directly, call this instead)
*/
cairo_t*
Canvas::createCairoContext() {
cairo_t* ret = cairo_create(ensureSurface());
cairo_set_line_width(ret, 1); // Cairo defaults to 2
return ret;
}
/*
* Construct an Error from the given cairo status.
*/
Napi::Error
Canvas::CairoError(cairo_status_t status) {
return Napi::Error::New(env, cairo_status_to_string(status));
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#pragma once
struct Closure;
struct PdfSvgClosure;
#include "closure.h"
#include <cairo.h>
#include "dll_visibility.h"
#include <napi.h>
#include <pango/pangocairo.h>
#include <vector>
#include <cstddef>
/*
* Canvas types.
*/
typedef enum {
CANVAS_TYPE_IMAGE,
CANVAS_TYPE_PDF,
CANVAS_TYPE_SVG
} canvas_type_t;
/*
* FontFace describes a font file in terms of one PangoFontDescription that
* will resolve to it and one that the user describes it as (like @font-face)
*/
class FontFace {
public:
PangoFontDescription *sys_desc = nullptr;
PangoFontDescription *user_desc = nullptr;
unsigned char file_path[1024];
};
enum text_baseline_t : uint8_t {
TEXT_BASELINE_ALPHABETIC = 0,
TEXT_BASELINE_TOP = 1,
TEXT_BASELINE_BOTTOM = 2,
TEXT_BASELINE_MIDDLE = 3,
TEXT_BASELINE_IDEOGRAPHIC = 4,
TEXT_BASELINE_HANGING = 5
};
enum text_align_t : int8_t {
TEXT_ALIGNMENT_LEFT = -1,
TEXT_ALIGNMENT_CENTER = 0,
TEXT_ALIGNMENT_RIGHT = 1,
TEXT_ALIGNMENT_START = -2,
TEXT_ALIGNMENT_END = 2
};
enum canvas_draw_mode_t : uint8_t {
TEXT_DRAW_PATHS,
TEXT_DRAW_GLYPHS
};
/*
* Canvas.
*/
class Canvas : public Napi::ObjectWrap<Canvas> {
public:
Canvas(const Napi::CallbackInfo& info);
~Canvas();
static void Initialize(Napi::Env& env, Napi::Object& target);
Napi::Value ToBuffer(const Napi::CallbackInfo& info);
Napi::Value GetType(const Napi::CallbackInfo& info);
Napi::Value GetStride(const Napi::CallbackInfo& info);
Napi::Value GetWidth(const Napi::CallbackInfo& info);
Napi::Value GetHeight(const Napi::CallbackInfo& info);
void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value);
void StreamPNGSync(const Napi::CallbackInfo& info);
void StreamPDFSync(const Napi::CallbackInfo& info);
void StreamJPEGSync(const Napi::CallbackInfo& info);
static void RegisterFont(const Napi::CallbackInfo& info);
static void DeregisterAllFonts(const Napi::CallbackInfo& info);
static Napi::Value ParseFont(const Napi::CallbackInfo& info);
Napi::Error CairoError(cairo_status_t status);
static void ToPngBufferAsync(Closure* closure);
static void ToJpegBufferAsync(Closure* closure);
static PangoWeight GetWeightFromCSSString(const char *weight);
static PangoStyle GetStyleFromCSSString(const char *style);
static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc);
inline bool isPDF() { return type == CANVAS_TYPE_PDF; }
inline bool isSVG() { return type == CANVAS_TYPE_SVG; }
inline bool isImage() { return type == CANVAS_TYPE_IMAGE; }
inline void *closure() { return _closure; }
cairo_t* createCairoContext();
DLL_PUBLIC inline uint8_t *data() { return cairo_image_surface_get_data(ensureSurface()); }
DLL_PUBLIC inline int stride() { return cairo_image_surface_get_stride(ensureSurface()); }
DLL_PUBLIC inline std::size_t nBytes() {
return static_cast<std::size_t>(height) * stride();
}
DLL_PUBLIC inline uint16_t getWidth() { return width; }
DLL_PUBLIC inline uint16_t getHeight() { return height; }
uint8_t approxBytesPerPixel();
void setFormat(cairo_format_t format);
cairo_format_t getFormat();
void resurface(Napi::Object This, uint16_t width, uint16_t height);
cairo_surface_t *ensureSurface();
void destroySurface();
Napi::Env env;
static int fontSerial;
private:
cairo_surface_t *_surface;
PdfSvgClosure *_closure;
Napi::FunctionReference ctor;
static std::vector<FontFace> font_face_list;
uint16_t width;
uint16_t height;
canvas_type_t type;
cairo_format_t format;
};
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <string>
#include <napi.h>
class CanvasError {
public:
std::string message;
std::string syscall;
std::string path;
int cerrno = 0;
void set(const char* iMessage = NULL, const char* iSyscall = NULL, int iErrno = 0, const char* iPath = NULL) {
if (iMessage) message.assign(iMessage);
if (iSyscall) syscall.assign(iSyscall);
cerrno = iErrno;
if (iPath) path.assign(iPath);
}
void reset() {
message.clear();
syscall.clear();
path.clear();
cerrno = 0;
}
bool empty() {
return cerrno == 0 && message.empty();
}
Napi::Error toError(Napi::Env env) {
if (cerrno) {
Napi::Error err = Napi::Error::New(env, strerror(cerrno));
if (!syscall.empty()) err.Value().Set("syscall", syscall);
if (!path.empty()) err.Value().Set("path", path);
return err;
} else {
return Napi::Error::New(env, message);
}
}
};
+113
View File
@@ -0,0 +1,113 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#include "CanvasGradient.h"
#include "InstanceData.h"
#include "Canvas.h"
#include "color.h"
using namespace Napi;
/*
* Initialize CanvasGradient.
*/
void
Gradient::Initialize(Napi::Env& env, Napi::Object& exports) {
Napi::HandleScope scope(env);
InstanceData* data = env.GetInstanceData<InstanceData>();
Napi::Function ctor = DefineClass(env, "CanvasGradient", {
InstanceMethod<&Gradient::AddColorStop>("addColorStop", napi_default_method)
});
exports.Set("CanvasGradient", ctor);
data->CanvasGradientCtor = Napi::Persistent(ctor);
}
/*
* Initialize a new CanvasGradient.
*/
Gradient::Gradient(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Gradient>(info), env(info.Env()) {
// Linear
if (
4 == info.Length() &&
info[0].IsNumber() &&
info[1].IsNumber() &&
info[2].IsNumber() &&
info[3].IsNumber()
) {
double x0 = info[0].As<Napi::Number>().DoubleValue();
double y0 = info[1].As<Napi::Number>().DoubleValue();
double x1 = info[2].As<Napi::Number>().DoubleValue();
double y1 = info[3].As<Napi::Number>().DoubleValue();
_pattern = cairo_pattern_create_linear(x0, y0, x1, y1);
return;
}
// Radial
if (
6 == info.Length() &&
info[0].IsNumber() &&
info[1].IsNumber() &&
info[2].IsNumber() &&
info[3].IsNumber() &&
info[4].IsNumber() &&
info[5].IsNumber()
) {
double x0 = info[0].As<Napi::Number>().DoubleValue();
double y0 = info[1].As<Napi::Number>().DoubleValue();
double r0 = info[2].As<Napi::Number>().DoubleValue();
double x1 = info[3].As<Napi::Number>().DoubleValue();
double y1 = info[4].As<Napi::Number>().DoubleValue();
double r1 = info[5].As<Napi::Number>().DoubleValue();
_pattern = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1);
return;
}
Napi::TypeError::New(env, "invalid arguments").ThrowAsJavaScriptException();
}
/*
* Add color stop.
*/
void
Gradient::AddColorStop(const Napi::CallbackInfo& info) {
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "offset required").ThrowAsJavaScriptException();
return;
}
if (!info[1].IsString()) {
Napi::TypeError::New(env, "color string required").ThrowAsJavaScriptException();
return;
}
short ok;
std::string str = info[1].As<Napi::String>();
uint32_t rgba = rgba_from_string(str.c_str(), &ok);
if (ok) {
rgba_t color = rgba_create(rgba);
cairo_pattern_add_color_stop_rgba(
_pattern
, info[0].As<Napi::Number>().DoubleValue()
, color.r
, color.g
, color.b
, color.a);
} else {
Napi::TypeError::New(env, "parse color failed").ThrowAsJavaScriptException();
}
}
/*
* Destroy the pattern.
*/
Gradient::~Gradient() {
if (_pattern) cairo_pattern_destroy(_pattern);
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#pragma once
#include <napi.h>
#include <cairo.h>
class Gradient : public Napi::ObjectWrap<Gradient> {
public:
static void Initialize(Napi::Env& env, Napi::Object& target);
Gradient(const Napi::CallbackInfo& info);
void AddColorStop(const Napi::CallbackInfo& info);
inline cairo_pattern_t *pattern(){ return _pattern; }
~Gradient();
Napi::Env env;
private:
cairo_pattern_t *_pattern;
};
+129
View File
@@ -0,0 +1,129 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#include "CanvasPattern.h"
#include "Canvas.h"
#include "Image.h"
#include "InstanceData.h"
using namespace Napi;
const cairo_user_data_key_t *pattern_repeat_key;
/*
* Initialize CanvasPattern.
*/
void
Pattern::Initialize(Napi::Env& env, Napi::Object& exports) {
Napi::HandleScope scope(env);
InstanceData* data = env.GetInstanceData<InstanceData>();
// Constructor
Napi::Function ctor = DefineClass(env, "CanvasPattern", {
InstanceMethod<&Pattern::setTransform>("setTransform", napi_default_method)
});
// Prototype
exports.Set("CanvasPattern", ctor);
data->CanvasPatternCtor = Napi::Persistent(ctor);
}
/*
* Initialize a new CanvasPattern.
*/
Pattern::Pattern(const Napi::CallbackInfo& info) : ObjectWrap<Pattern>(info), env(info.Env()) {
if (!info[0].IsObject()) {
Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException();
return;
}
Napi::Object obj = info[0].As<Napi::Object>();
InstanceData* data = env.GetInstanceData<InstanceData>();
cairo_surface_t *surface;
// Image
if (obj.InstanceOf(data->ImageCtor.Value()).UnwrapOr(false)) {
Image *img = Image::Unwrap(obj);
if (!img->isComplete()) {
Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException();
return;
}
surface = img->surface();
// Canvas
} else if (obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) {
Canvas *canvas = Canvas::Unwrap(obj);
surface = canvas->ensureSurface();
// Invalid
} else {
if (!env.IsExceptionPending()) {
Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException();
}
return;
}
_pattern = cairo_pattern_create_for_surface(surface);
if (info[1].IsString()) {
if ("no-repeat" == info[1].As<Napi::String>().Utf8Value()) {
_repeat = NO_REPEAT;
} else if ("repeat-x" == info[1].As<Napi::String>().Utf8Value()) {
_repeat = REPEAT_X;
} else if ("repeat-y" == info[1].As<Napi::String>().Utf8Value()) {
_repeat = REPEAT_Y;
}
}
cairo_pattern_set_user_data(_pattern, pattern_repeat_key, &_repeat, NULL);
}
/*
* Set the pattern-space to user-space transform.
*/
void
Pattern::setTransform(const Napi::CallbackInfo& info) {
if (!info[0].IsObject()) {
Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException();
return;
}
Napi::Object mat = info[0].As<Napi::Object>();
InstanceData* data = env.GetInstanceData<InstanceData>();
if (!mat.InstanceOf(data->DOMMatrixCtor.Value()).UnwrapOr(false)) {
if (!env.IsExceptionPending()) {
Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException();
}
return;
}
Napi::Value one = Napi::Number::New(env, 1);
Napi::Value zero = Napi::Number::New(env, 0);
cairo_matrix_t matrix;
cairo_matrix_init(&matrix,
mat.Get("a").UnwrapOr(one).As<Napi::Number>().DoubleValue(),
mat.Get("b").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
mat.Get("c").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
mat.Get("d").UnwrapOr(one).As<Napi::Number>().DoubleValue(),
mat.Get("e").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
mat.Get("f").UnwrapOr(zero).As<Napi::Number>().DoubleValue()
);
cairo_matrix_invert(&matrix);
cairo_pattern_set_matrix(_pattern, &matrix);
}
repeat_type_t Pattern::get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern) {
void *ud = cairo_pattern_get_user_data(pattern, pattern_repeat_key);
return *reinterpret_cast<repeat_type_t*>(ud);
}
/*
* Destroy the pattern.
*/
Pattern::~Pattern() {
if (_pattern) cairo_pattern_destroy(_pattern);
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright (c) 2011 LearnBoost <tj@learnboost.com>
#pragma once
#include <cairo.h>
#include <napi.h>
/*
* Canvas types.
*/
typedef enum {
NO_REPEAT, // match CAIRO_EXTEND_NONE
REPEAT, // match CAIRO_EXTEND_REPEAT
REPEAT_X, // needs custom processing
REPEAT_Y // needs custom processing
} repeat_type_t;
extern const cairo_user_data_key_t *pattern_repeat_key;
class Pattern : public Napi::ObjectWrap<Pattern> {
public:
Pattern(const Napi::CallbackInfo& info);
static void Initialize(Napi::Env& env, Napi::Object& target);
void setTransform(const Napi::CallbackInfo& info);
static repeat_type_t get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern);
inline cairo_pattern_t *pattern(){ return _pattern; }
~Pattern();
Napi::Env env;
private:
cairo_pattern_t *_pattern;
repeat_type_t _repeat = REPEAT;
};
+3527
View File
@@ -0,0 +1,3527 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#include "CanvasRenderingContext2d.h"
#include <algorithm>
#include <cairo-pdf.h>
#include "Canvas.h"
#include "CanvasGradient.h"
#include "CanvasPattern.h"
#include "InstanceData.h"
#include "FontParser.h"
#include <cmath>
#include <cstdlib>
#include "Image.h"
#include "ImageData.h"
#include <limits>
#include <map>
#include "Point.h"
#include <string>
#include "Util.h"
#include <vector>
/*
* Rectangle arg assertions.
*/
#define RECT_ARGS \
double args[4]; \
if(!checkArgs(info, args, 4)) \
return; \
double x = args[0]; \
double y = args[1]; \
double width = args[2]; \
double height = args[3];
constexpr double twoPi = M_PI * 2.;
/*
* Simple helper macro for a rather verbose function call.
*/
#define PANGO_LAYOUT_GET_METRICS(LAYOUT) pango_context_get_metrics( \
pango_layout_get_context(LAYOUT), \
pango_layout_get_font_description(LAYOUT), \
pango_language_from_string(state->lang.c_str()))
inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){
Napi::Env env = info.Env();
int argsEnd = std::min(9, offset + argsNum);
bool areArgsValid = true;
napi_value argv[9];
size_t argc = 9;
napi_get_cb_info(env, static_cast<napi_callback_info>(info), &argc, argv, nullptr, nullptr);
for (int i = offset; i < argsEnd; i++) {
napi_valuetype type;
double val = 0;
napi_typeof(env, argv[i], &type);
if (type == napi_number) {
// fast path
napi_get_value_double(env, argv[i], &val);
} else {
napi_value num;
if (napi_coerce_to_number(env, argv[i], &num) == napi_ok) {
napi_get_value_double(env, num, &val);
}
}
if (areArgsValid) {
if (!std::isfinite(val)) {
// We should continue the loop instead of returning immediately
// See https://html.spec.whatwg.org/multipage/canvas.html
areArgsValid = false;
continue;
}
args[i - offset] = val;
}
}
return areArgsValid;
}
/*
* Initialize Context2d.
*/
void
Context2d::Initialize(Napi::Env& env, Napi::Object& exports) {
Napi::HandleScope scope(env);
InstanceData* data = env.GetInstanceData<InstanceData>();
Napi::Function ctor = DefineClass(env, "CanvasRenderingContext2D", {
InstanceMethod<&Context2d::DrawImage>("drawImage", napi_default_method),
InstanceMethod<&Context2d::PutImageData>("putImageData", napi_default_method),
InstanceMethod<&Context2d::GetImageData>("getImageData", napi_default_method),
InstanceMethod<&Context2d::CreateImageData>("createImageData", napi_default_method),
InstanceMethod<&Context2d::AddPage>("addPage", napi_default_method),
InstanceMethod<&Context2d::Save>("save", napi_default_method),
InstanceMethod<&Context2d::Restore>("restore", napi_default_method),
InstanceMethod<&Context2d::Rotate>("rotate", napi_default_method),
InstanceMethod<&Context2d::Translate>("translate", napi_default_method),
InstanceMethod<&Context2d::Transform>("transform", napi_default_method),
InstanceMethod<&Context2d::GetTransform>("getTransform", napi_default_method),
InstanceMethod<&Context2d::ResetTransform>("resetTransform", napi_default_method),
InstanceMethod<&Context2d::SetTransform>("setTransform", napi_default_method),
InstanceMethod<&Context2d::IsPointInPath>("isPointInPath", napi_default_method),
InstanceMethod<&Context2d::Scale>("scale", napi_default_method),
InstanceMethod<&Context2d::Clip>("clip", napi_default_method),
InstanceMethod<&Context2d::Fill>("fill", napi_default_method),
InstanceMethod<&Context2d::Stroke>("stroke", napi_default_method),
InstanceMethod<&Context2d::FillText>("fillText", napi_default_method),
InstanceMethod<&Context2d::StrokeText>("strokeText", napi_default_method),
InstanceMethod<&Context2d::FillRect>("fillRect", napi_default_method),
InstanceMethod<&Context2d::StrokeRect>("strokeRect", napi_default_method),
InstanceMethod<&Context2d::ClearRect>("clearRect", napi_default_method),
InstanceMethod<&Context2d::Rect>("rect", napi_default_method),
InstanceMethod<&Context2d::RoundRect>("roundRect", napi_default_method),
InstanceMethod<&Context2d::MeasureText>("measureText", napi_default_method),
InstanceMethod<&Context2d::MoveTo>("moveTo", napi_default_method),
InstanceMethod<&Context2d::LineTo>("lineTo", napi_default_method),
InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo", napi_default_method),
InstanceMethod<&Context2d::QuadraticCurveTo>("quadraticCurveTo", napi_default_method),
InstanceMethod<&Context2d::BeginPath>("beginPath", napi_default_method),
InstanceMethod<&Context2d::ClosePath>("closePath", napi_default_method),
InstanceMethod<&Context2d::Arc>("arc", napi_default_method),
InstanceMethod<&Context2d::ArcTo>("arcTo", napi_default_method),
InstanceMethod<&Context2d::Ellipse>("ellipse", napi_default_method),
InstanceMethod<&Context2d::SetLineDash>("setLineDash", napi_default_method),
InstanceMethod<&Context2d::GetLineDash>("getLineDash", napi_default_method),
InstanceMethod<&Context2d::CreatePattern>("createPattern", napi_default_method),
InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient", napi_default_method),
InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient", napi_default_method),
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
InstanceMethod<&Context2d::BeginTag>("beginTag", napi_default_method),
InstanceMethod<&Context2d::EndTag>("endTag", napi_default_method),
#endif
InstanceAccessor<&Context2d::GetFormat>("pixelFormat", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetGlobalCompositeOperation, &Context2d::SetGlobalCompositeOperation>("globalCompositeOperation", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetGlobalAlpha, &Context2d::SetGlobalAlpha>("globalAlpha", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetShadowColor, &Context2d::SetShadowColor>("shadowColor", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetMiterLimit, &Context2d::SetMiterLimit>("miterLimit", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetLineWidth, &Context2d::SetLineWidth>("lineWidth", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetLineCap, &Context2d::SetLineCap>("lineCap", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetLineJoin, &Context2d::SetLineJoin>("lineJoin", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetLineDashOffset, &Context2d::SetLineDashOffset>("lineDashOffset", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetShadowOffsetX, &Context2d::SetShadowOffsetX>("shadowOffsetX", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetLanguage, &Context2d::SetLanguage>("lang", napi_default_jsproperty)
});
exports.Set("CanvasRenderingContext2d", ctor);
data->Context2dCtor = Napi::Persistent(ctor);
}
/*
* Create a cairo context.
*/
Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Context2d>(info), env(info.Env()) {
InstanceData* data = env.GetInstanceData<InstanceData>();
if (!info[0].IsObject()) {
Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException();
return;
}
Napi::Object obj = info[0].As<Napi::Object>();
if (!obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) {
if (!env.IsExceptionPending()) {
Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException();
}
return;
}
_canvas = Canvas::Unwrap(obj);
if (_canvas->isImage()) {
cairo_format_t format = CAIRO_FORMAT_ARGB32;
if (info[1].IsObject()) {
Napi::Object ctxAttributes = info[1].As<Napi::Object>();
Napi::Value pixelFormat;
if (ctxAttributes.Get("pixelFormat").UnwrapTo(&pixelFormat) && pixelFormat.IsString()) {
std::string utf8PixelFormat = pixelFormat.As<Napi::String>();
if (utf8PixelFormat == "RGBA32") format = CAIRO_FORMAT_ARGB32;
else if (utf8PixelFormat == "RGB24") format = CAIRO_FORMAT_RGB24;
else if (utf8PixelFormat == "A8") format = CAIRO_FORMAT_A8;
else if (utf8PixelFormat == "RGB16_565") format = CAIRO_FORMAT_RGB16_565;
else if (utf8PixelFormat == "A1") format = CAIRO_FORMAT_A1;
#ifdef CAIRO_FORMAT_RGB30
else if (utf8PixelFormat == "RGB30") format = CAIRO_FORMAT_RGB30;
#endif
}
// alpha: false forces use of RGB24
Napi::Value alpha;
if (ctxAttributes.Get("alpha").UnwrapTo(&alpha) && alpha.IsBoolean() && !alpha.As<Napi::Boolean>().Value()) {
format = CAIRO_FORMAT_RGB24;
}
}
_canvas->setFormat(format);
}
_context = _canvas->createCairoContext();
_layout = pango_cairo_create_layout(_context);
// As of January 2023, Pango rounds glyph positions which renders text wider
// or narrower than the browser. See #2184 for more information
#if PANGO_VERSION_CHECK(1, 44, 0)
pango_context_set_round_glyph_positions(pango_layout_get_context(_layout), FALSE);
#endif
pango_layout_set_auto_dir(_layout, FALSE);
states.emplace();
state = &states.top();
pango_layout_set_font_description(_layout, state->fontDescription);
}
/*
* Destroy cairo context.
*/
Context2d::~Context2d() {
if (_layout) g_object_unref(_layout);
if (_context) cairo_destroy(_context);
_resetPersistentHandles();
}
/*
* Reset canvas state.
*/
void Context2d::resetState() {
states.pop();
states.emplace();
pango_layout_set_font_description(_layout, state->fontDescription);
_resetPersistentHandles();
}
void Context2d::_resetPersistentHandles() {
_fillStyle.Reset();
_strokeStyle.Reset();
}
/*
* Save cairo / canvas state.
*/
void
Context2d::save() {
cairo_save(_context);
states.emplace(states.top());
state = &states.top();
}
/*
* Restore cairo / canvas state.
*/
void
Context2d::restore() {
if (states.size() > 1) {
cairo_restore(_context);
states.pop();
state = &states.top();
pango_layout_set_font_description(_layout, state->fontDescription);
}
}
/*
* Save flat path.
*/
void
Context2d::savePath() {
_path = cairo_copy_path_flat(_context);
cairo_new_path(_context);
}
/*
* Restore flat path.
*/
void
Context2d::restorePath() {
cairo_new_path(_context);
cairo_append_path(_context, _path);
cairo_path_destroy(_path);
}
/*
* Create temporary surface for gradient or pattern transparency
*/
cairo_pattern_t*
create_transparent_gradient(cairo_pattern_t *source, float alpha) {
double x0;
double y0;
double x1;
double y1;
double r0;
double r1;
int count;
int i;
double offset;
double r;
double g;
double b;
double a;
cairo_pattern_t *newGradient;
cairo_pattern_type_t type = cairo_pattern_get_type(source);
cairo_pattern_get_color_stop_count(source, &count);
if (type == CAIRO_PATTERN_TYPE_LINEAR) {
cairo_pattern_get_linear_points (source, &x0, &y0, &x1, &y1);
newGradient = cairo_pattern_create_linear(x0, y0, x1, y1);
} else if (type == CAIRO_PATTERN_TYPE_RADIAL) {
cairo_pattern_get_radial_circles(source, &x0, &y0, &r0, &x1, &y1, &r1);
newGradient = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1);
} else {
return NULL;
}
for ( i = 0; i < count; i++ ) {
cairo_pattern_get_color_stop_rgba(source, i, &offset, &r, &g, &b, &a);
cairo_pattern_add_color_stop_rgba(newGradient, offset, r, g, b, a * alpha);
}
return newGradient;
}
cairo_pattern_t*
create_transparent_pattern(cairo_pattern_t *source, float alpha) {
cairo_surface_t *surface;
cairo_pattern_get_surface(source, &surface);
int width = cairo_image_surface_get_width(surface);
int height = cairo_image_surface_get_height(surface);
cairo_surface_t *mask_surface = cairo_image_surface_create(
CAIRO_FORMAT_ARGB32,
width,
height);
cairo_t *mask_context = cairo_create(mask_surface);
if (cairo_status(mask_context) != CAIRO_STATUS_SUCCESS) {
return NULL;
}
cairo_set_source(mask_context, source);
cairo_paint_with_alpha(mask_context, alpha);
cairo_destroy(mask_context);
cairo_pattern_t* newPattern = cairo_pattern_create_for_surface(mask_surface);
cairo_surface_destroy(mask_surface);
return newPattern;
}
/*
* Fill and apply shadow.
*/
void
Context2d::setFillRule(Napi::Value value) {
cairo_fill_rule_t rule = CAIRO_FILL_RULE_WINDING;
if (value.IsString()) {
std::string str = value.As<Napi::String>().Utf8Value();
if (str == "evenodd") {
rule = CAIRO_FILL_RULE_EVEN_ODD;
}
}
cairo_set_fill_rule(_context, rule);
}
void
Context2d::fill(bool preserve) {
cairo_pattern_t *new_pattern;
bool needsRestore = false;
if (state->fillPattern) {
if (state->globalAlpha < 1) {
new_pattern = create_transparent_pattern(state->fillPattern, state->globalAlpha);
if (new_pattern == NULL) {
Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException();
// failed to allocate
return;
}
cairo_set_source(_context, new_pattern);
cairo_pattern_destroy(new_pattern);
} else {
cairo_pattern_set_filter(state->fillPattern, state->patternQuality);
cairo_set_source(_context, state->fillPattern);
}
repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->fillPattern);
if (repeat == NO_REPEAT) {
cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE);
} else if (repeat == REPEAT) {
cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT);
} else {
cairo_save(_context);
cairo_path_t *savedPath = cairo_copy_path(_context);
cairo_surface_t *patternSurface = nullptr;
cairo_pattern_get_surface(cairo_get_source(_context), &patternSurface);
double width, height;
if (repeat == REPEAT_X) {
double x1, x2;
cairo_path_extents(_context, &x1, nullptr, &x2, nullptr);
width = x2 - x1;
height = cairo_image_surface_get_height(patternSurface);
} else {
double y1, y2;
cairo_path_extents(_context, nullptr, &y1, nullptr, &y2);
width = cairo_image_surface_get_width(patternSurface);
height = y2 - y1;
}
cairo_new_path(_context);
cairo_rectangle(_context, 0, 0, width, height);
cairo_clip(_context);
cairo_append_path(_context, savedPath);
cairo_path_destroy(savedPath);
cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT);
needsRestore = true;
}
} else if (state->fillGradient) {
if (state->globalAlpha < 1) {
new_pattern = create_transparent_gradient(state->fillGradient, state->globalAlpha);
if (new_pattern == NULL) {
Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException();
// failed to recognize gradient
return;
}
cairo_pattern_set_filter(new_pattern, state->patternQuality);
cairo_set_source(_context, new_pattern);
cairo_pattern_destroy(new_pattern);
} else {
cairo_pattern_set_filter(state->fillGradient, state->patternQuality);
cairo_set_source(_context, state->fillGradient);
}
} else {
setSourceRGBA(state->fill);
}
if (preserve) {
hasShadow()
? shadow(cairo_fill_preserve)
: cairo_fill_preserve(_context);
} else {
hasShadow()
? shadow(cairo_fill)
: cairo_fill(_context);
}
if (needsRestore) {
cairo_restore(_context);
}
}
/*
* Stroke and apply shadow.
*/
void
Context2d::stroke(bool preserve) {
cairo_pattern_t *new_pattern;
if (state->strokePattern) {
if (state->globalAlpha < 1) {
new_pattern = create_transparent_pattern(state->strokePattern, state->globalAlpha);
if (new_pattern == NULL) {
Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException();
// failed to allocate
return;
}
cairo_set_source(_context, new_pattern);
cairo_pattern_destroy(new_pattern);
} else {
cairo_pattern_set_filter(state->strokePattern, state->patternQuality);
cairo_set_source(_context, state->strokePattern);
}
repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->strokePattern);
if (NO_REPEAT == repeat) {
cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE);
} else {
cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT);
}
} else if (state->strokeGradient) {
if (state->globalAlpha < 1) {
new_pattern = create_transparent_gradient(state->strokeGradient, state->globalAlpha);
if (new_pattern == NULL) {
Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException();
// failed to recognize gradient
return;
}
cairo_pattern_set_filter(new_pattern, state->patternQuality);
cairo_set_source(_context, new_pattern);
cairo_pattern_destroy(new_pattern);
} else {
cairo_pattern_set_filter(state->strokeGradient, state->patternQuality);
cairo_set_source(_context, state->strokeGradient);
}
} else {
setSourceRGBA(state->stroke);
}
if (preserve) {
hasShadow()
? shadow(cairo_stroke_preserve)
: cairo_stroke_preserve(_context);
} else {
hasShadow()
? shadow(cairo_stroke)
: cairo_stroke(_context);
}
}
/*
* Apply shadow with the given draw fn.
*/
void
Context2d::shadow(void (fn)(cairo_t *cr)) {
cairo_path_t *path = cairo_copy_path_flat(_context);
cairo_save(_context);
// shadowOffset is unaffected by current transform
cairo_matrix_t path_matrix;
cairo_get_matrix(_context, &path_matrix);
cairo_identity_matrix(_context);
// Apply shadow
cairo_push_group(_context);
// No need to invoke blur if shadowBlur is 0
if (state->shadowBlur) {
// find out extent of path
double x1, y1, x2, y2;
if (fn == cairo_fill || fn == cairo_fill_preserve) {
cairo_fill_extents(_context, &x1, &y1, &x2, &y2);
} else {
cairo_stroke_extents(_context, &x1, &y1, &x2, &y2);
}
// create new image surface that size + padding for blurring
double dx = x2-x1, dy = y2-y1;
cairo_user_to_device_distance(_context, &dx, &dy);
int pad = state->shadowBlur * 2;
cairo_surface_t *shadow_surface = cairo_image_surface_create(
CAIRO_FORMAT_ARGB32,
dx + 2 * pad,
dy + 2 * pad);
cairo_t *shadow_context = cairo_create(shadow_surface);
// transform path to the right place
cairo_translate(shadow_context, pad-x1, pad-y1);
cairo_transform(shadow_context, &path_matrix);
// set lineCap lineJoin lineDash
cairo_set_line_cap(shadow_context, cairo_get_line_cap(_context));
cairo_set_line_join(shadow_context, cairo_get_line_join(_context));
double offset;
int dashes = cairo_get_dash_count(_context);
std::vector<double> a(dashes);
cairo_get_dash(_context, a.data(), &offset);
cairo_set_dash(shadow_context, a.data(), dashes, offset);
// draw the path and blur
cairo_set_line_width(shadow_context, cairo_get_line_width(_context));
cairo_new_path(shadow_context);
cairo_append_path(shadow_context, path);
setSourceRGBA(shadow_context, state->shadow);
fn(shadow_context);
blur(shadow_surface, state->shadowBlur);
// paint to original context
cairo_set_source_surface(_context, shadow_surface,
x1 - pad + state->shadowOffsetX + 1,
y1 - pad + state->shadowOffsetY + 1);
cairo_paint(_context);
cairo_destroy(shadow_context);
cairo_surface_destroy(shadow_surface);
} else {
// Offset first, then apply path's transform
cairo_translate(
_context
, state->shadowOffsetX
, state->shadowOffsetY);
cairo_transform(_context, &path_matrix);
// Apply shadow
cairo_new_path(_context);
cairo_append_path(_context, path);
setSourceRGBA(state->shadow);
fn(_context);
}
// Paint the shadow
cairo_pop_group_to_source(_context);
cairo_paint(_context);
// Restore state
cairo_restore(_context);
cairo_new_path(_context);
cairo_append_path(_context, path);
fn(_context);
cairo_path_destroy(path);
}
/*
* Set source RGBA for the current context
*/
void
Context2d::setSourceRGBA(rgba_t color) {
setSourceRGBA(_context, color);
}
/*
* Set source RGBA
*/
void
Context2d::setSourceRGBA(cairo_t *ctx, rgba_t color) {
cairo_set_source_rgba(
ctx
, color.r
, color.g
, color.b
, color.a * state->globalAlpha);
}
/*
* Check if the context has a drawable shadow.
*/
bool
Context2d::hasShadow() {
return state->shadow.a
&& (state->shadowBlur || state->shadowOffsetX || state->shadowOffsetY);
}
/*
* Blur the given surface with the given radius.
*/
void
Context2d::blur(cairo_surface_t *surface, int radius) {
// Steve Hanov, 2009
// Released into the public domain.
radius = radius * 0.57735f + 0.5f;
// get width, height
int width = cairo_image_surface_get_width( surface );
int height = cairo_image_surface_get_height( surface );
const unsigned int size = width * height * sizeof(unsigned);
unsigned* precalc = (unsigned*)malloc(size);
cairo_surface_flush( surface );
unsigned char* src = cairo_image_surface_get_data( surface );
double mul=1.f/((radius*2)*(radius*2));
int channel;
// The number of times to perform the averaging. According to wikipedia,
// three iterations is good enough to pass for a gaussian.
const int MAX_ITERATIONS = 3;
int iteration;
for ( iteration = 0; iteration < MAX_ITERATIONS; iteration++ ) {
for( channel = 0; channel < 4; channel++ ) {
int x,y;
// precomputation step.
unsigned char* pix = src;
unsigned* pre = precalc;
bool modified = false;
pix += channel;
for (y=0;y<height;y++) {
for (x=0;x<width;x++) {
int tot=pix[0];
if (x>0) tot+=pre[-1];
if (y>0) tot+=pre[-width];
if (x>0 && y>0) tot-=pre[-width-1];
*pre++=tot;
if (!modified) modified = true;
pix += 4;
}
}
if (!modified) {
memset(precalc, 0, size);
}
// blur step.
pix = src + (int)radius * width * 4 + (int)radius * 4 + channel;
for (y=radius;y<height-radius;y++) {
for (x=radius;x<width-radius;x++) {
int l = x < radius ? 0 : x - radius;
int t = y < radius ? 0 : y - radius;
int r = x + radius >= width ? width - 1 : x + radius;
int b = y + radius >= height ? height - 1 : y + radius;
int tot = precalc[r+b*width] + precalc[l+t*width] -
precalc[l+b*width] - precalc[r+t*width];
*pix=(unsigned char)(tot*mul);
pix += 4;
}
pix += (int)radius * 2 * 4;
}
}
}
cairo_surface_mark_dirty(surface);
free(precalc);
}
/*
* Get format (string).
*/
Napi::Value
Context2d::GetFormat(const Napi::CallbackInfo& info) {
std::string pixelFormatString;
switch (canvas()->getFormat()) {
case CAIRO_FORMAT_ARGB32: pixelFormatString = "RGBA32"; break;
case CAIRO_FORMAT_RGB24: pixelFormatString = "RGB24"; break;
case CAIRO_FORMAT_A8: pixelFormatString = "A8"; break;
case CAIRO_FORMAT_A1: pixelFormatString = "A1"; break;
case CAIRO_FORMAT_RGB16_565: pixelFormatString = "RGB16_565"; break;
#ifdef CAIRO_FORMAT_RGB30
case CAIRO_FORMAT_RGB30: pixelFormatString = "RGB30"; break;
#endif
default: return env.Null();
}
return Napi::String::New(env, pixelFormatString);
}
/*
* Create a new page.
*/
void
Context2d::AddPage(const Napi::CallbackInfo& info) {
if (!canvas()->isPDF()) {
Napi::Error::New(env, "only PDF canvases support .addPage()").ThrowAsJavaScriptException();
return;
}
cairo_show_page(context());
Napi::Number zero = Napi::Number::New(env, 0);
int width = info[0].ToNumber().UnwrapOr(zero).Int32Value();
int height = info[1].ToNumber().UnwrapOr(zero).Int32Value();
if (width < 1) width = canvas()->getWidth();
if (height < 1) height = canvas()->getHeight();
cairo_pdf_surface_set_size(canvas()->ensureSurface(), width, height);
}
/*
* Get text direction.
*/
Napi::Value
Context2d::GetDirection(const Napi::CallbackInfo& info) {
return Napi::String::New(env, state->direction);
}
/*
* Set text direction.
*/
void
Context2d::SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (!value.IsString()) return;
std::string dir = value.As<Napi::String>();
if (dir != "ltr" && dir != "rtl") return;
state->direction = dir;
}
/*
* Get language.
*/
Napi::Value
Context2d::GetLanguage(const Napi::CallbackInfo& info) {
return Napi::String::New(env, state->lang);
}
/*
* Set language.
*/
void
Context2d::SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (!value.IsString()) return;
std::string lang = value.As<Napi::String>();
state->lang = lang;
}
/*
* Put image data.
*
* - imageData, dx, dy
* - imageData, dx, dy, sx, sy, sw, sh
*
*/
void
Context2d::PutImageData(const Napi::CallbackInfo& info) {
if (!info[0].IsObject()) {
Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException();
return;
}
Napi::Object obj = info[0].As<Napi::Object>();
InstanceData* data = env.GetInstanceData<InstanceData>();
if (!obj.InstanceOf(data->ImageDataCtor.Value()).UnwrapOr(false)) {
if (!env.IsExceptionPending()) {
Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException();
}
return;
}
ImageData *imageData = ImageData::Unwrap(obj);
Napi::Number zero = Napi::Number::New(env, 0);
uint8_t *src = imageData->data();
uint8_t *dst = canvas()->data();
if (dst == nullptr) {
Napi::Error::New(env, "Not an image canvas").ThrowAsJavaScriptException();
return;
}
int dstStride = canvas()->stride();
int Bpp = dstStride / canvas()->getWidth();
int srcStride = Bpp * imageData->width();
int64_t sx = 0
, sy = 0
, sw = 0
, sh = 0
, dx = info[1].ToNumber().UnwrapOr(zero).Int32Value()
, dy = info[2].ToNumber().UnwrapOr(zero).Int32Value()
, rows
, cols;
switch (info.Length()) {
// imageData, dx, dy
case 3:
sw = imageData->width();
sh = imageData->height();
break;
// imageData, dx, dy, sx, sy, sw, sh
case 7:
sx = info[3].ToNumber().UnwrapOr(zero).Int32Value();
sy = info[4].ToNumber().UnwrapOr(zero).Int32Value();
sw = info[5].ToNumber().UnwrapOr(zero).Int32Value();
sh = info[6].ToNumber().UnwrapOr(zero).Int32Value();
// fix up negative height, width
if (sw < 0) sx += sw, sw = -sw;
if (sh < 0) sy += sh, sh = -sh;
// clamp the left edge
if (sx < 0) sw += sx, sx = 0;
if (sy < 0) sh += sy, sy = 0;
// clamp the right edge
if (sx + sw > imageData->width()) sw = imageData->width() - sx;
if (sy + sh > imageData->height()) sh = imageData->height() - sy;
// start destination at source offset
dx += sx;
dy += sy;
break;
default:
Napi::Error::New(env, "invalid arguments").ThrowAsJavaScriptException();
return;
}
// chop off outlying source data
if (dx < 0) sw += dx, sx -= dx, dx = 0;
if (dy < 0) sh += dy, sy -= dy, dy = 0;
// clamp width at canvas size
// Need to wrap std::min calls using parens to prevent macro expansion on
// windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error
cols = (std::min)(sw, (int)canvas()->getWidth() - dx);
rows = (std::min)(sh, (int)canvas()->getHeight() - dy);
if (cols <= 0 || rows <= 0) return;
switch (canvas()->getFormat()) {
case CAIRO_FORMAT_ARGB32: {
src += sy * srcStride + sx * 4;
dst += dstStride * dy + 4 * dx;
for (int y = 0; y < rows; ++y) {
uint8_t *dstRow = dst;
uint8_t *srcRow = src;
for (int x = 0; x < cols; ++x) {
// rgba
uint8_t r = *srcRow++;
uint8_t g = *srcRow++;
uint8_t b = *srcRow++;
uint8_t a = *srcRow++;
// argb
// performance optimization: fully transparent/opaque pixels can be
// processed more efficiently.
if (a == 0) {
*dstRow++ = 0;
*dstRow++ = 0;
*dstRow++ = 0;
*dstRow++ = 0;
} else if (a == 255) {
*dstRow++ = b;
*dstRow++ = g;
*dstRow++ = r;
*dstRow++ = a;
} else {
float alpha = (float)a / 255;
*dstRow++ = b * alpha;
*dstRow++ = g * alpha;
*dstRow++ = r * alpha;
*dstRow++ = a;
}
}
dst += dstStride;
src += srcStride;
}
break;
}
case CAIRO_FORMAT_RGB24: {
src += sy * srcStride + sx * 4;
dst += dstStride * dy + 4 * dx;
for (int y = 0; y < rows; ++y) {
uint8_t *dstRow = dst;
uint8_t *srcRow = src;
for (int x = 0; x < cols; ++x) {
// rgba
uint8_t r = *srcRow++;
uint8_t g = *srcRow++;
uint8_t b = *srcRow++;
srcRow++;
// argb
*dstRow++ = b;
*dstRow++ = g;
*dstRow++ = r;
*dstRow++ = 255;
}
dst += dstStride;
src += srcStride;
}
break;
}
case CAIRO_FORMAT_A8: {
src += sy * srcStride + sx;
dst += dstStride * dy + dx;
if (srcStride == dstStride && cols == dstStride) {
// fast path: strides are the same and doing a full-width put
memcpy(dst, src, cols * rows);
} else {
for (int y = 0; y < rows; ++y) {
memcpy(dst, src, cols);
dst += dstStride;
src += srcStride;
}
}
break;
}
case CAIRO_FORMAT_A1: {
// TODO Should this be totally packed, or maintain a stride divisible by 4?
Napi::Error::New(env, "putImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException();
break;
}
case CAIRO_FORMAT_RGB16_565: {
src += sy * srcStride + sx * 2;
dst += dstStride * dy + 2 * dx;
for (int y = 0; y < rows; ++y) {
memcpy(dst, src, cols * 2);
dst += dstStride;
src += srcStride;
}
break;
}
#ifdef CAIRO_FORMAT_RGB30
case CAIRO_FORMAT_RGB30: {
// TODO
Napi::Error::New(env, "putImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException();
break;
}
#endif
default: {
Napi::Error::New(env, "Invalid pixel format").ThrowAsJavaScriptException();
return;
}
}
cairo_surface_mark_dirty_rectangle(
canvas()->ensureSurface()
, dx
, dy
, cols
, rows);
}
/*
* Get image data.
*
* - sx, sy, sw, sh
*
*/
Napi::Value
Context2d::GetImageData(const Napi::CallbackInfo& info) {
Napi::Number zero = Napi::Number::New(env, 0);
Canvas *canvas = this->canvas();
// 64 bit integers for when (1) both sx, sw or sy, sh are negative (2) sx and
// sy are decreased (3) signs are flipped
int64_t sx = info[0].ToNumber().UnwrapOr(zero).Int32Value();
int64_t sy = info[1].ToNumber().UnwrapOr(zero).Int32Value();
int64_t sw = info[2].ToNumber().UnwrapOr(zero).Int32Value();
int64_t sh = info[3].ToNumber().UnwrapOr(zero).Int32Value();
if (!sw) {
Napi::Error::New(env, "IndexSizeError: The source width is 0.").ThrowAsJavaScriptException();
return env.Undefined();
}
if (!sh) {
Napi::Error::New(env, "IndexSizeError: The source height is 0.").ThrowAsJavaScriptException();
return env.Undefined();
}
int width = canvas->getWidth();
int height = canvas->getHeight();
if (!width) {
Napi::TypeError::New(env, "Canvas width is 0").ThrowAsJavaScriptException();
return env.Undefined();
}
if (!height) {
Napi::TypeError::New(env, "Canvas height is 0").ThrowAsJavaScriptException();
return env.Undefined();
}
// WebKit and Firefox have this behavior:
// Flip the coordinates so the origin is top/left-most:
if (sw < 0) {
sx += sw;
sw = -sw;
}
if (sh < 0) {
sy += sh;
sh = -sh;
}
// Width and height to actually copy
int64_t cw = sw;
int64_t ch = sh;
// Offsets in the destination image
int64_t ox = 0;
int64_t oy = 0;
// Clamp the copy width and height if the copy would go outside the image
if (sx + sw > width) cw = width - sx;
if (sy + sh > height) ch = height - sy;
// Clamp the copy origin if the copy would go outside the image
if (sx < 0) {
ox = -sx;
cw += sx;
sx = 0;
}
if (sy < 0) {
oy = -sy;
ch += sy;
sy = 0;
}
int srcStride = canvas->stride();
int bpp = srcStride / width;
// Note: barely fits: INT32_MAX * INT32_MAX * 4. Bpp can't be bigger than 4!
uint64_t size = (uint64_t)sw * sh * bpp;
int64_t dstStride = sw * bpp;
if (size > INT32_MAX) {
// INT32_MAX is what Firefox limits the buffer to
std::string msg = "buffer exceeds " + std::to_string(INT32_MAX) + " bytes";
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
return env.Undefined();
}
uint8_t *src = canvas->data();
if (src == nullptr) {
Napi::Error::New(env, "Not an image canvas").ThrowAsJavaScriptException();
return env.Null();
}
Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, size);
Napi::TypedArray dataArray;
if (canvas->getFormat() == CAIRO_FORMAT_RGB16_565) {
dataArray = Napi::Uint16Array::New(env, size >> 1, buffer, 0);
} else {
dataArray = Napi::Uint8Array::New(env, size, buffer, 0, napi_uint8_clamped_array);
}
uint8_t *dst = (uint8_t *)buffer.Data();
if (!(cw > 0 && ch > 0)) goto return_empty;
switch (canvas->getFormat()) {
case CAIRO_FORMAT_ARGB32: {
dst += oy * dstStride + ox * 4;
// Rearrange alpha (argb -> rgba), undo alpha pre-multiplication,
// and store in big-endian format
for (int y = 0; y < ch; ++y) {
uint32_t *row = (uint32_t *)(src + srcStride * (y + sy));
for (int x = 0; x < cw; ++x) {
int bx = x * 4;
uint32_t *pixel = row + x + sx;
uint8_t a = *pixel >> 24;
uint8_t r = *pixel >> 16;
uint8_t g = *pixel >> 8;
uint8_t b = *pixel;
dst[bx + 3] = a;
// Performance optimization: fully transparent/opaque pixels can be
// processed more efficiently.
if (a == 0 || a == 255) {
dst[bx + 0] = r;
dst[bx + 1] = g;
dst[bx + 2] = b;
} else {
// Undo alpha pre-multiplication
float alphaR = (float)255 / a;
dst[bx + 0] = (int)((float)r * alphaR);
dst[bx + 1] = (int)((float)g * alphaR);
dst[bx + 2] = (int)((float)b * alphaR);
}
}
dst += dstStride;
}
break;
}
case CAIRO_FORMAT_RGB24: {
dst += oy * dstStride + ox * 4;
// Rearrange alpha (argb -> rgba) and store in big-endian format
for (int y = 0; y < ch; ++y) {
uint32_t *row = (uint32_t *)(src + srcStride * (y + sy));
for (int x = 0; x < cw; ++x) {
int bx = x * 4;
uint32_t *pixel = row + x + sx;
uint8_t r = *pixel >> 16;
uint8_t g = *pixel >> 8;
uint8_t b = *pixel;
dst[bx + 0] = r;
dst[bx + 1] = g;
dst[bx + 2] = b;
dst[bx + 3] = 255;
}
dst += dstStride;
}
break;
}
case CAIRO_FORMAT_A8: {
dst += oy * dstStride + ox;
for (int y = 0; y < ch; ++y) {
uint8_t *row = (uint8_t *)(src + srcStride * (y + sy));
memcpy(dst, row + sx, cw);
dst += dstStride;
}
break;
}
case CAIRO_FORMAT_A1: {
// TODO Should this be totally packed, or maintain a stride divisible by 4?
Napi::Error::New(env, "getImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException();
break;
}
case CAIRO_FORMAT_RGB16_565: {
dst += oy * dstStride + ox * 2;
for (int y = 0; y < ch; ++y) {
uint16_t *row = (uint16_t *)(src + srcStride * (y + sy));
memcpy(dst, row + sx, cw * 2);
dst += dstStride;
}
break;
}
#ifdef CAIRO_FORMAT_RGB30
case CAIRO_FORMAT_RGB30: {
// TODO
Napi::Error::New(env, "getImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException();
break;
}
#endif
default: {
// Unlikely
Napi::Error::New(env, "Invalid pixel format").ThrowAsJavaScriptException();
return env.Null();
}
}
return_empty:
Napi::Number swHandle = Napi::Number::New(env, sw);
Napi::Number shHandle = Napi::Number::New(env, sh);
Napi::Function ctor = env.GetInstanceData<InstanceData>()->ImageDataCtor.Value();
Napi::Maybe<Napi::Object> ret = ctor.New({ dataArray, swHandle, shHandle });
return ret.IsJust() ? ret.Unwrap() : env.Undefined();
}
/**
* Create `ImageData` with the given dimensions or
* `ImageData` instance for dimensions.
*/
Napi::Value
Context2d::CreateImageData(const Napi::CallbackInfo& info){
Canvas *canvas = this->canvas();
Napi::Number zero = Napi::Number::New(env, 0);
uint32_t width, height;
if (info[0].IsObject()) {
Napi::Object obj = info[0].As<Napi::Object>();
width = obj.Get("width").UnwrapOr(zero).ToNumber().UnwrapOr(zero).Uint32Value();
height = obj.Get("height").UnwrapOr(zero).ToNumber().UnwrapOr(zero).Uint32Value();
} else {
width = info[0].ToNumber().UnwrapOr(zero).Uint32Value();
height = info[1].ToNumber().UnwrapOr(zero).Uint32Value();
}
int stride = canvas->stride();
double Bpp = static_cast<double>(stride) / canvas->getWidth();
int64_t nBytes = Bpp * width * height + .5;
if (nBytes > INT32_MAX) {
// INT32_MAX is what Firefox limits the buffer to
std::string msg = "buffer exceeds " + std::to_string(INT32_MAX) + " bytes";
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
return env.Undefined();
}
Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, nBytes);
Napi::Value arr;
if (canvas->getFormat() == CAIRO_FORMAT_RGB16_565)
arr = Napi::Uint16Array::New(env, nBytes / 2, ab, 0);
else
arr = Napi::Uint8Array::New(env, nBytes, ab, 0, napi_uint8_clamped_array);
Napi::Function ctor = env.GetInstanceData<InstanceData>()->ImageDataCtor.Value();
Napi::Maybe<Napi::Object> ret = ctor.New({ arr, Napi::Number::New(env, width), Napi::Number::New(env, height) });
return ret.IsJust() ? ret.Unwrap() : env.Undefined();
}
/*
* Take a transform matrix and return its components
* 0: angle, 1: scaleX, 2: scaleY, 3: skewX, 4: translateX, 5: translateY
*/
void decompose_matrix(cairo_matrix_t matrix, double *destination) {
double denom = pow(matrix.xx, 2) + pow(matrix.yx, 2);
destination[0] = atan2(matrix.yx, matrix.xx);
destination[1] = sqrt(denom);
destination[2] = (matrix.xx * matrix.yy - matrix.xy * matrix.yx) / destination[1];
destination[3] = atan2(matrix.xx * matrix.xy + matrix.yx * matrix.yy, denom);
destination[4] = matrix.x0;
destination[5] = matrix.y0;
}
/*
* Draw image src image to the destination (context).
*
* - dx, dy
* - dx, dy, dw, dh
* - sx, sy, sw, sh, dx, dy, dw, dh
*
*/
void
Context2d::DrawImage(const Napi::CallbackInfo& info) {
int infoLen = info.Length();
if (infoLen != 3 && infoLen != 5 && infoLen != 9) {
Napi::TypeError::New(env, "Invalid arguments").ThrowAsJavaScriptException();
return;
}
if (!info[0].IsObject()) {
Napi::TypeError::New(env, "The first argument must be an object").ThrowAsJavaScriptException();
return;
}
double args[8];
if(!checkArgs(info, args, infoLen - 1, 1))
return;
double sx = 0
, sy = 0
, sw = 0
, sh = 0
, dx = 0
, dy = 0
, dw = 0
, dh = 0
, source_w = 0
, source_h = 0;
cairo_surface_t *surface;
Napi::Object obj = info[0].As<Napi::Object>();
// Image
if (obj.InstanceOf(env.GetInstanceData<InstanceData>()->ImageCtor.Value()).UnwrapOr(false)) {
Image *img = Image::Unwrap(obj);
if (!img->isComplete()) {
Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException();
return;
}
source_w = sw = img->width;
source_h = sh = img->height;
surface = img->surface();
// Canvas
} else if (obj.InstanceOf(env.GetInstanceData<InstanceData>()->CanvasCtor.Value()).UnwrapOr(false)) {
Canvas *canvas = Canvas::Unwrap(obj);
source_w = sw = canvas->getWidth();
source_h = sh = canvas->getHeight();
surface = canvas->ensureSurface();
// Invalid
} else {
if (!env.IsExceptionPending()) {
Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException();
}
return;
}
cairo_t *ctx = context();
// Arguments
switch (infoLen) {
// img, sx, sy, sw, sh, dx, dy, dw, dh
case 9:
sx = args[0];
sy = args[1];
sw = args[2];
sh = args[3];
dx = args[4];
dy = args[5];
dw = args[6];
dh = args[7];
break;
// img, dx, dy, dw, dh
case 5:
dx = args[0];
dy = args[1];
dw = args[2];
dh = args[3];
break;
// img, dx, dy
case 3:
dx = args[0];
dy = args[1];
dw = sw;
dh = sh;
break;
}
if (!(sw && sh && dw && dh))
return;
// Start draw
cairo_save(ctx);
cairo_matrix_t matrix;
double transforms[6];
cairo_get_matrix(ctx, &matrix);
decompose_matrix(matrix, transforms);
// extract the scale value from the current transform so that we know how many pixels we
// need for our extra canvas in the drawImage operation.
double current_scale_x = std::abs(transforms[1]);
double current_scale_y = std::abs(transforms[2]);
double extra_dx = 0;
double extra_dy = 0;
double fx = dw / sw * current_scale_x; // transforms[1] is scale on X
double fy = dh / sh * current_scale_y; // transforms[2] is scale on X
bool needScale = dw != sw || dh != sh;
bool needCut = sw != source_w || sh != source_h || sx < 0 || sy < 0;
bool sameCanvas = surface == canvas()->ensureSurface();
bool needsExtraSurface = sameCanvas || needCut || needScale;
cairo_surface_t *surfTemp = NULL;
cairo_t *ctxTemp = NULL;
if (needsExtraSurface) {
// we want to create the extra surface as small as possible.
// fx and fy are the total scaling we need to apply to sw, sh.
// from sw and sh we want to remove the part that is outside the source_w and soruce_h
double real_w = sw;
double real_h = sh;
double translate_x = 0;
double translate_y = 0;
// if sx or sy are negative, a part of the area represented by sw and sh is empty
// because there are empty pixels, so we cut it out.
// On the other hand if sx or sy are positive, but sw and sh extend outside the real
// source pixels, we cut the area in that case too.
if (sx < 0) {
extra_dx = -sx * fx;
real_w = sw + sx;
} else if (sx + sw > source_w) {
real_w = sw - (sx + sw - source_w);
}
if (sy < 0) {
extra_dy = -sy * fy;
real_h = sh + sy;
} else if (sy + sh > source_h) {
real_h = sh - (sy + sh - source_h);
}
// if after cutting we are still bigger than source pixels, we restrict again
if (real_w > source_w) {
real_w = source_w;
}
if (real_h > source_h) {
real_h = source_h;
}
// TODO: find a way to limit the surfTemp to real_w and real_h if fx and fy are bigger than 1.
// there are no more pixel than the one available in the source, no need to create a bigger surface.
surfTemp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, round(real_w * fx), round(real_h * fy));
ctxTemp = cairo_create(surfTemp);
cairo_scale(ctxTemp, fx, fy);
if (sx > 0) {
translate_x = sx;
}
if (sy > 0) {
translate_y = sy;
}
cairo_set_source_surface(ctxTemp, surface, -translate_x, -translate_y);
cairo_pattern_set_filter(cairo_get_source(ctxTemp), state->imageSmoothingEnabled ? state->patternQuality : CAIRO_FILTER_NEAREST);
cairo_pattern_set_extend(cairo_get_source(ctxTemp), CAIRO_EXTEND_REFLECT);
cairo_paint_with_alpha(ctxTemp, 1);
surface = surfTemp;
}
// apply shadow if there is one
if (hasShadow()) {
if(state->shadowBlur) {
// we need to create a new surface in order to blur
int pad = state->shadowBlur * 2;
cairo_surface_t *shadow_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dw + 2 * pad, dh + 2 * pad);
cairo_t *shadow_context = cairo_create(shadow_surface);
// mask and blur
setSourceRGBA(shadow_context, state->shadow);
cairo_mask_surface(shadow_context, surface, pad, pad);
blur(shadow_surface, state->shadowBlur);
// paint
// @note: ShadowBlur looks different in each browser. This implementation matches chrome as close as possible.
// The 1.4 offset comes from visual tests with Chrome. I have read the spec and part of the shadowBlur
// implementation, and its not immediately clear why an offset is necessary, but without it, the result
// in chrome is different.
cairo_set_source_surface(ctx, shadow_surface,
dx + state->shadowOffsetX - pad + 1.4,
dy + state->shadowOffsetY - pad + 1.4);
cairo_paint(ctx);
// cleanup
cairo_destroy(shadow_context);
cairo_surface_destroy(shadow_surface);
} else {
setSourceRGBA(state->shadow);
cairo_mask_surface(ctx, surface,
dx + (state->shadowOffsetX),
dy + (state->shadowOffsetY));
}
}
double scaled_dx = dx;
double scaled_dy = dy;
if (needsExtraSurface && (current_scale_x != 1 || current_scale_y != 1)) {
// in this case our surface contains already current_scale_x, we need to scale back
cairo_scale(ctx, 1 / current_scale_x, 1 / current_scale_y);
scaled_dx *= current_scale_x;
scaled_dy *= current_scale_y;
}
// Paint
cairo_set_source_surface(ctx, surface, scaled_dx + extra_dx, scaled_dy + extra_dy);
cairo_pattern_set_filter(cairo_get_source(ctx), state->imageSmoothingEnabled ? state->patternQuality : CAIRO_FILTER_NEAREST);
cairo_pattern_set_extend(cairo_get_source(ctx), CAIRO_EXTEND_NONE);
cairo_paint_with_alpha(ctx, state->globalAlpha);
cairo_restore(ctx);
if (needsExtraSurface) {
cairo_destroy(ctxTemp);
cairo_surface_destroy(surfTemp);
}
}
/*
* Get global alpha.
*/
Napi::Value
Context2d::GetGlobalAlpha(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, state->globalAlpha);
}
/*
* Set global alpha.
*/
void
Context2d::SetGlobalAlpha(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Maybe<Napi::Number> numberValue = value.ToNumber();
if (numberValue.IsJust()) {
double n = numberValue.Unwrap().DoubleValue();
if (n >= 0 && n <= 1) state->globalAlpha = n;
}
}
/*
* Get global composite operation.
*/
Napi::Value
Context2d::GetGlobalCompositeOperation(const Napi::CallbackInfo& info) {
cairo_t *ctx = context();
const char *op{};
switch (cairo_get_operator(ctx)) {
// composite modes:
case CAIRO_OPERATOR_CLEAR: op = "clear"; break;
case CAIRO_OPERATOR_SOURCE: op = "copy"; break;
case CAIRO_OPERATOR_DEST: op = "destination"; break;
case CAIRO_OPERATOR_OVER: op = "source-over"; break;
case CAIRO_OPERATOR_DEST_OVER: op = "destination-over"; break;
case CAIRO_OPERATOR_IN: op = "source-in"; break;
case CAIRO_OPERATOR_DEST_IN: op = "destination-in"; break;
case CAIRO_OPERATOR_OUT: op = "source-out"; break;
case CAIRO_OPERATOR_DEST_OUT: op = "destination-out"; break;
case CAIRO_OPERATOR_ATOP: op = "source-atop"; break;
case CAIRO_OPERATOR_DEST_ATOP: op = "destination-atop"; break;
case CAIRO_OPERATOR_XOR: op = "xor"; break;
case CAIRO_OPERATOR_ADD: op = "lighter"; break;
// blend modes:
// Note: "source-over" and "normal" are synonyms. Chrome and FF both report
// "source-over" after setting gCO to "normal".
// case CAIRO_OPERATOR_OVER: op = "normal";
case CAIRO_OPERATOR_MULTIPLY: op = "multiply"; break;
case CAIRO_OPERATOR_SCREEN: op = "screen"; break;
case CAIRO_OPERATOR_OVERLAY: op = "overlay"; break;
case CAIRO_OPERATOR_DARKEN: op = "darken"; break;
case CAIRO_OPERATOR_LIGHTEN: op = "lighten"; break;
case CAIRO_OPERATOR_COLOR_DODGE: op = "color-dodge"; break;
case CAIRO_OPERATOR_COLOR_BURN: op = "color-burn"; break;
case CAIRO_OPERATOR_HARD_LIGHT: op = "hard-light"; break;
case CAIRO_OPERATOR_SOFT_LIGHT: op = "soft-light"; break;
case CAIRO_OPERATOR_DIFFERENCE: op = "difference"; break;
case CAIRO_OPERATOR_EXCLUSION: op = "exclusion"; break;
case CAIRO_OPERATOR_HSL_HUE: op = "hue"; break;
case CAIRO_OPERATOR_HSL_SATURATION: op = "saturation"; break;
case CAIRO_OPERATOR_HSL_COLOR: op = "color"; break;
case CAIRO_OPERATOR_HSL_LUMINOSITY: op = "luminosity"; break;
// non-standard:
case CAIRO_OPERATOR_SATURATE: op = "saturate"; break;
default: op = "source-over";
}
return Napi::String::New(env, op);
}
/*
* Set pattern quality.
*/
void
Context2d::SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (value.IsString()) {
std::string quality = value.As<Napi::String>().Utf8Value();
if (quality == "fast") {
state->patternQuality = CAIRO_FILTER_FAST;
} else if (quality == "good") {
state->patternQuality = CAIRO_FILTER_GOOD;
} else if (quality == "best") {
state->patternQuality = CAIRO_FILTER_BEST;
} else if (quality == "nearest") {
state->patternQuality = CAIRO_FILTER_NEAREST;
} else if (quality == "bilinear") {
state->patternQuality = CAIRO_FILTER_BILINEAR;
}
}
}
/*
* Get pattern quality.
*/
Napi::Value
Context2d::GetPatternQuality(const Napi::CallbackInfo& info) {
const char *quality;
switch (state->patternQuality) {
case CAIRO_FILTER_FAST: quality = "fast"; break;
case CAIRO_FILTER_BEST: quality = "best"; break;
case CAIRO_FILTER_NEAREST: quality = "nearest"; break;
case CAIRO_FILTER_BILINEAR: quality = "bilinear"; break;
default: quality = "good";
}
return Napi::String::New(env, quality);
}
/*
* Set ImageSmoothingEnabled value.
*/
void
Context2d::SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Boolean boolValue;
if (value.ToBoolean().UnwrapTo(&boolValue)) state->imageSmoothingEnabled = boolValue.Value();
}
/*
* Get pattern quality.
*/
Napi::Value
Context2d::GetImageSmoothingEnabled(const Napi::CallbackInfo& info) {
return Napi::Boolean::New(env, state->imageSmoothingEnabled);
}
/*
* Set global composite operation.
*/
void
Context2d::SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value) {
cairo_t *ctx = this->context();
Napi::String opStr;
if (value.ToString().UnwrapTo(&opStr)) { // Unlike CSS colors, this *is* case-sensitive
const std::map<std::string, cairo_operator_t> blendmodes = {
// composite modes:
{"clear", CAIRO_OPERATOR_CLEAR},
{"copy", CAIRO_OPERATOR_SOURCE},
{"destination", CAIRO_OPERATOR_DEST}, // this seems to have been omitted from the spec
{"source-over", CAIRO_OPERATOR_OVER},
{"destination-over", CAIRO_OPERATOR_DEST_OVER},
{"source-in", CAIRO_OPERATOR_IN},
{"destination-in", CAIRO_OPERATOR_DEST_IN},
{"source-out", CAIRO_OPERATOR_OUT},
{"destination-out", CAIRO_OPERATOR_DEST_OUT},
{"source-atop", CAIRO_OPERATOR_ATOP},
{"destination-atop", CAIRO_OPERATOR_DEST_ATOP},
{"xor", CAIRO_OPERATOR_XOR},
{"lighter", CAIRO_OPERATOR_ADD},
// blend modes:
{"normal", CAIRO_OPERATOR_OVER},
{"multiply", CAIRO_OPERATOR_MULTIPLY},
{"screen", CAIRO_OPERATOR_SCREEN},
{"overlay", CAIRO_OPERATOR_OVERLAY},
{"darken", CAIRO_OPERATOR_DARKEN},
{"lighten", CAIRO_OPERATOR_LIGHTEN},
{"color-dodge", CAIRO_OPERATOR_COLOR_DODGE},
{"color-burn", CAIRO_OPERATOR_COLOR_BURN},
{"hard-light", CAIRO_OPERATOR_HARD_LIGHT},
{"soft-light", CAIRO_OPERATOR_SOFT_LIGHT},
{"difference", CAIRO_OPERATOR_DIFFERENCE},
{"exclusion", CAIRO_OPERATOR_EXCLUSION},
{"hue", CAIRO_OPERATOR_HSL_HUE},
{"saturation", CAIRO_OPERATOR_HSL_SATURATION},
{"color", CAIRO_OPERATOR_HSL_COLOR},
{"luminosity", CAIRO_OPERATOR_HSL_LUMINOSITY},
// non-standard:
{"saturate", CAIRO_OPERATOR_SATURATE}
};
auto op = blendmodes.find(opStr.Utf8Value());
if (op != blendmodes.end()) cairo_set_operator(ctx, op->second);
}
}
/*
* Get shadow offset x.
*/
Napi::Value
Context2d::GetShadowOffsetX(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, state->shadowOffsetX);
}
/*
* Set shadow offset x.
*/
void
Context2d::SetShadowOffsetX(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Number numberValue;
if (value.ToNumber().UnwrapTo(&numberValue)) state->shadowOffsetX = numberValue.DoubleValue();
}
/*
* Get shadow offset y.
*/
Napi::Value
Context2d::GetShadowOffsetY(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, state->shadowOffsetY);
}
/*
* Set shadow offset y.
*/
void
Context2d::SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Number numberValue;
if (value.ToNumber().UnwrapTo(&numberValue)) state->shadowOffsetY = numberValue.DoubleValue();
}
/*
* Get shadow blur.
*/
Napi::Value
Context2d::GetShadowBlur(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, state->shadowBlur);
}
/*
* Set shadow blur.
*/
void
Context2d::SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Number n;
if (value.ToNumber().UnwrapTo(&n)) {
double v = n.DoubleValue();
if (v >= 0 && v <= std::numeric_limits<decltype(state->shadowBlur)>::max()) {
state->shadowBlur = v;
}
}
}
/*
* Get current antialiasing setting.
*/
Napi::Value
Context2d::GetAntiAlias(const Napi::CallbackInfo& info) {
const char *aa;
switch (cairo_get_antialias(context())) {
case CAIRO_ANTIALIAS_NONE: aa = "none"; break;
case CAIRO_ANTIALIAS_GRAY: aa = "gray"; break;
case CAIRO_ANTIALIAS_SUBPIXEL: aa = "subpixel"; break;
default: aa = "default";
}
return Napi::String::New(env, aa);
}
/*
* Set antialiasing.
*/
void
Context2d::SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::String stringValue;
if (value.ToString().UnwrapTo(&stringValue)) {
std::string str = stringValue.Utf8Value();
cairo_t *ctx = context();
cairo_antialias_t a;
if (str == "none") {
a = CAIRO_ANTIALIAS_NONE;
} else if (str == "default") {
a = CAIRO_ANTIALIAS_DEFAULT;
} else if (str == "gray") {
a = CAIRO_ANTIALIAS_GRAY;
} else if (str == "subpixel") {
a = CAIRO_ANTIALIAS_SUBPIXEL;
} else {
a = cairo_get_antialias(ctx);
}
cairo_set_antialias(ctx, a);
}
}
/*
* Get text drawing mode.
*/
Napi::Value
Context2d::GetTextDrawingMode(const Napi::CallbackInfo& info) {
const char *mode;
if (state->textDrawingMode == TEXT_DRAW_PATHS) {
mode = "path";
} else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) {
mode = "glyph";
} else {
mode = "unknown";
}
return Napi::String::New(env, mode);
}
/*
* Set text drawing mode.
*/
void
Context2d::SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::String stringValue;
if (value.ToString().UnwrapTo(&stringValue)) {
std::string str = stringValue.Utf8Value();
if (str == "path") {
state->textDrawingMode = TEXT_DRAW_PATHS;
} else if (str == "glyph") {
state->textDrawingMode = TEXT_DRAW_GLYPHS;
}
}
}
/*
* Get filter.
*/
Napi::Value
Context2d::GetQuality(const Napi::CallbackInfo& info) {
const char *filter;
switch (cairo_pattern_get_filter(cairo_get_source(context()))) {
case CAIRO_FILTER_FAST: filter = "fast"; break;
case CAIRO_FILTER_BEST: filter = "best"; break;
case CAIRO_FILTER_NEAREST: filter = "nearest"; break;
case CAIRO_FILTER_BILINEAR: filter = "bilinear"; break;
default: filter = "good";
}
return Napi::String::New(env, filter);
}
/*
* Set filter.
*/
void
Context2d::SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::String stringValue;
if (value.ToString().UnwrapTo(&stringValue)) {
std::string str = stringValue.Utf8Value();
cairo_filter_t filter;
if (str == "fast") {
filter = CAIRO_FILTER_FAST;
} else if (str == "best") {
filter = CAIRO_FILTER_BEST;
} else if (str == "nearest") {
filter = CAIRO_FILTER_NEAREST;
} else if (str == "bilinear") {
filter = CAIRO_FILTER_BILINEAR;
} else {
filter = CAIRO_FILTER_GOOD;
}
cairo_pattern_set_filter(cairo_get_source(context()), filter);
}
}
/*
* Helper for get current transform matrix
*/
Napi::Value
Context2d::get_current_transform() {
Napi::Float64Array arr = Napi::Float64Array::New(env, 6);
double *dest = arr.Data();
cairo_matrix_t matrix;
cairo_get_matrix(context(), &matrix);
dest[0] = matrix.xx;
dest[1] = matrix.yx;
dest[2] = matrix.xy;
dest[3] = matrix.yy;
dest[4] = matrix.x0;
dest[5] = matrix.y0;
Napi::Maybe<Napi::Object> ret = env.GetInstanceData<InstanceData>()->DOMMatrixCtor.Value().New({ arr });
return ret.IsJust() ? ret.Unwrap() : env.Undefined();
}
/*
* Helper for get/set transform.
*/
void parse_matrix_from_object(cairo_matrix_t &matrix, Napi::Object mat) {
Napi::Value zero = Napi::Number::New(mat.Env(), 0);
cairo_matrix_init(&matrix,
mat.Get("a").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
mat.Get("b").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
mat.Get("c").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
mat.Get("d").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
mat.Get("e").UnwrapOr(zero).As<Napi::Number>().DoubleValue(),
mat.Get("f").UnwrapOr(zero).As<Napi::Number>().DoubleValue()
);
}
/*
* Get current transform.
*/
Napi::Value
Context2d::GetCurrentTransform(const Napi::CallbackInfo& info) {
return get_current_transform();
}
/*
* Set current transform.
*/
void
Context2d::SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Object mat;
if (value.ToObject().UnwrapTo(&mat)) {
if (!mat.InstanceOf(env.GetInstanceData<InstanceData>()->DOMMatrixCtor.Value()).UnwrapOr(false)) {
if (!env.IsExceptionPending()) {
Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException();
}
return;
}
cairo_matrix_t matrix;
parse_matrix_from_object(matrix, mat);
cairo_transform(context(), &matrix);
}
}
/*
* Get current fill style.
*/
Napi::Value
Context2d::GetFillStyle(const Napi::CallbackInfo& info) {
Napi::Value style;
if (_fillStyle.IsEmpty())
style = _getFillColor();
else
style = _fillStyle.Value();
return style;
}
/*
* Set current fill style.
*/
void
Context2d::SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (value.IsString()) {
_fillStyle.Reset();
_setFillColor(value.As<Napi::String>());
} else if (value.IsObject()) {
InstanceData *data = env.GetInstanceData<InstanceData>();
Napi::Object obj = value.As<Napi::Object>();
if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) {
_fillStyle.Reset(obj);
Gradient *grad = Gradient::Unwrap(obj);
state->fillGradient = grad->pattern();
} else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) {
_fillStyle.Reset(obj);
Pattern *pattern = Pattern::Unwrap(obj);
state->fillPattern = pattern->pattern();
}
}
}
/*
* Get current stroke style.
*/
Napi::Value
Context2d::GetStrokeStyle(const Napi::CallbackInfo& info) {
Napi::Value style;
if (_strokeStyle.IsEmpty())
style = _getStrokeColor();
else
style = _strokeStyle.Value();
return style;
}
/*
* Set current stroke style.
*/
void
Context2d::SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (value.IsString()) {
_strokeStyle.Reset();
_setStrokeColor(value.As<Napi::String>());
} else if (value.IsObject()) {
InstanceData *data = env.GetInstanceData<InstanceData>();
Napi::Object obj = value.As<Napi::Object>();
if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) {
_strokeStyle.Reset(obj);
Gradient *grad = Gradient::Unwrap(obj);
state->strokeGradient = grad->pattern();
} else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) {
_strokeStyle.Reset(value);
Pattern *pattern = Pattern::Unwrap(obj);
state->strokePattern = pattern->pattern();
}
}
}
/*
* Get miter limit.
*/
Napi::Value
Context2d::GetMiterLimit(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, cairo_get_miter_limit(context()));
}
/*
* Set miter limit.
*/
void
Context2d::SetMiterLimit(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Maybe<Napi::Number> numberValue = value.ToNumber();
if (numberValue.IsJust()) {
double n = numberValue.Unwrap().DoubleValue();
if (n > 0) cairo_set_miter_limit(context(), n);
}
}
/*
* Get line width.
*/
Napi::Value
Context2d::GetLineWidth(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, cairo_get_line_width(context()));
}
/*
* Set line width.
*/
void
Context2d::SetLineWidth(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Maybe<Napi::Number> numberValue = value.ToNumber();
if (numberValue.IsJust()) {
double n = numberValue.Unwrap().DoubleValue();
if (n > 0 && n != std::numeric_limits<double>::infinity()) {
cairo_set_line_width(context(), n);
}
}
}
/*
* Get line join.
*/
Napi::Value
Context2d::GetLineJoin(const Napi::CallbackInfo& info) {
const char *join;
switch (cairo_get_line_join(context())) {
case CAIRO_LINE_JOIN_BEVEL: join = "bevel"; break;
case CAIRO_LINE_JOIN_ROUND: join = "round"; break;
default: join = "miter";
}
return Napi::String::New(env, join);
}
/*
* Set line join.
*/
void
Context2d::SetLineJoin(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Maybe<Napi::String> stringValue = value.ToString();
cairo_t *ctx = context();
if (stringValue.IsJust()) {
std::string type = stringValue.Unwrap().Utf8Value();
if (type == "round") {
cairo_set_line_join(ctx, CAIRO_LINE_JOIN_ROUND);
} else if (type == "bevel") {
cairo_set_line_join(ctx, CAIRO_LINE_JOIN_BEVEL);
} else {
cairo_set_line_join(ctx, CAIRO_LINE_JOIN_MITER);
}
}
}
/*
* Get line cap.
*/
Napi::Value
Context2d::GetLineCap(const Napi::CallbackInfo& info) {
const char *cap;
switch (cairo_get_line_cap(context())) {
case CAIRO_LINE_CAP_ROUND: cap = "round"; break;
case CAIRO_LINE_CAP_SQUARE: cap = "square"; break;
default: cap = "butt";
}
return Napi::String::New(env, cap);
}
/*
* Set line cap.
*/
void
Context2d::SetLineCap(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Maybe<Napi::String> stringValue = value.ToString();
cairo_t *ctx = context();
if (stringValue.IsJust()) {
std::string type = stringValue.Unwrap().Utf8Value();
if (type == "round") {
cairo_set_line_cap(ctx, CAIRO_LINE_CAP_ROUND);
} else if (type == "square") {
cairo_set_line_cap(ctx, CAIRO_LINE_CAP_SQUARE);
} else {
cairo_set_line_cap(ctx, CAIRO_LINE_CAP_BUTT);
}
}
}
/*
* Check if the given point is within the current path.
*/
Napi::Value
Context2d::IsPointInPath(const Napi::CallbackInfo& info) {
if (info[0].IsNumber() && info[1].IsNumber()) {
cairo_t *ctx = context();
double x = info[0].As<Napi::Number>(), y = info[1].As<Napi::Number>();
setFillRule(info[2]);
return Napi::Boolean::New(env, cairo_in_fill(ctx, x, y) || cairo_in_stroke(ctx, x, y));
}
return Napi::Boolean::New(env, false);
}
/*
* Set shadow color.
*/
void
Context2d::SetShadowColor(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Maybe<Napi::String> stringValue = value.ToString();
short ok;
if (stringValue.IsJust()) {
std::string str = stringValue.Unwrap().Utf8Value();
uint32_t rgba = rgba_from_string(str.c_str(), &ok);
if (ok) state->shadow = rgba_create(rgba);
}
}
/*
* Get shadow color.
*/
Napi::Value
Context2d::GetShadowColor(const Napi::CallbackInfo& info) {
char buf[64];
rgba_to_string(state->shadow, buf, sizeof(buf));
return Napi::String::New(env, buf);
}
/*
* Set fill color, used internally for fillStyle=
*/
void
Context2d::_setFillColor(Napi::Value arg) {
Napi::Maybe<Napi::String> stringValue = arg.ToString();
short ok;
if (stringValue.IsJust()) {
Napi::String str = stringValue.Unwrap();
char buf[128] = {0};
napi_status status = napi_get_value_string_utf8(env, str, buf, sizeof(buf) - 1, nullptr);
if (status != napi_ok) return;
uint32_t rgba = rgba_from_string(buf, &ok);
if (!ok) return;
state->fillPattern = state->fillGradient = NULL;
state->fill = rgba_create(rgba);
}
}
/*
* Get fill color.
*/
Napi::Value
Context2d::_getFillColor() {
char buf[64];
rgba_to_string(state->fill, buf, sizeof(buf));
return Napi::String::New(env, buf);
}
/*
* Set stroke color, used internally for strokeStyle=
*/
void
Context2d::_setStrokeColor(Napi::Value arg) {
short ok;
std::string str = arg.As<Napi::String>();
uint32_t rgba = rgba_from_string(str.c_str(), &ok);
if (!ok) return;
state->strokePattern = state->strokeGradient = NULL;
state->stroke = rgba_create(rgba);
}
/*
* Get stroke color.
*/
Napi::Value
Context2d::_getStrokeColor() {
char buf[64];
rgba_to_string(state->stroke, buf, sizeof(buf));
return Napi::String::New(env, buf);
}
Napi::Value
Context2d::CreatePattern(const Napi::CallbackInfo& info) {
Napi::Function ctor = env.GetInstanceData<InstanceData>()->CanvasPatternCtor.Value();
Napi::Maybe<Napi::Object> ret = ctor.New({ info[0], info[1] });
return ret.IsJust() ? ret.Unwrap() : env.Undefined();
}
Napi::Value
Context2d::CreateLinearGradient(const Napi::CallbackInfo& info) {
Napi::Function ctor = env.GetInstanceData<InstanceData>()->CanvasGradientCtor.Value();
Napi::Maybe<Napi::Object> ret = ctor.New({ info[0], info[1], info[2], info[3] });
return ret.IsJust() ? ret.Unwrap() : env.Undefined();
}
Napi::Value
Context2d::CreateRadialGradient(const Napi::CallbackInfo& info) {
Napi::Function ctor = env.GetInstanceData<InstanceData>()->CanvasGradientCtor.Value();
Napi::Maybe<Napi::Object> ret = ctor.New({ info[0], info[1], info[2], info[3], info[4], info[5] });
return ret.IsJust() ? ret.Unwrap() : env.Undefined();
}
/*
* Bezier curve.
*/
void
Context2d::BezierCurveTo(const Napi::CallbackInfo& info) {
double args[6];
if(!checkArgs(info, args, 6))
return;
cairo_curve_to(context()
, args[0]
, args[1]
, args[2]
, args[3]
, args[4]
, args[5]);
}
/*
* Quadratic curve approximation from libsvg-cairo.
*/
void
Context2d::QuadraticCurveTo(const Napi::CallbackInfo& info) {
double args[4];
if(!checkArgs(info, args, 4))
return;
cairo_t *ctx = context();
double x, y
, x1 = args[0]
, y1 = args[1]
, x2 = args[2]
, y2 = args[3];
cairo_get_current_point(ctx, &x, &y);
if (0 == x && 0 == y) {
x = x1;
y = y1;
}
cairo_curve_to(ctx
, x + 2.0 / 3.0 * (x1 - x), y + 2.0 / 3.0 * (y1 - y)
, x2 + 2.0 / 3.0 * (x1 - x2), y2 + 2.0 / 3.0 * (y1 - y2)
, x2
, y2);
}
/*
* Save state.
*/
void
Context2d::Save(const Napi::CallbackInfo& info) {
save();
}
/*
* Restore state.
*/
void
Context2d::Restore(const Napi::CallbackInfo& info) {
restore();
}
/*
* Creates a new subpath.
*/
void
Context2d::BeginPath(const Napi::CallbackInfo& info) {
cairo_new_path(context());
}
/*
* Marks the subpath as closed.
*/
void
Context2d::ClosePath(const Napi::CallbackInfo& info) {
cairo_close_path(context());
}
/*
* Rotate transformation.
*/
void
Context2d::Rotate(const Napi::CallbackInfo& info) {
double args[1];
if(!checkArgs(info, args, 1))
return;
cairo_rotate(context(), args[0]);
}
/*
* Modify the CTM.
*/
void
Context2d::Transform(const Napi::CallbackInfo& info) {
double args[6];
if(!checkArgs(info, args, 6))
return;
cairo_matrix_t matrix;
cairo_matrix_init(&matrix
, args[0]
, args[1]
, args[2]
, args[3]
, args[4]
, args[5]);
cairo_transform(context(), &matrix);
}
/*
* Get the CTM
*/
Napi::Value
Context2d::GetTransform(const Napi::CallbackInfo& info) {
return get_current_transform();
}
/*
* Reset the CTM, used internally by setTransform().
*/
void
Context2d::ResetTransform(const Napi::CallbackInfo& info) {
cairo_identity_matrix(context());
}
/*
* Reset transform matrix to identity, then apply the given args.
*/
void
Context2d::SetTransform(const Napi::CallbackInfo& info) {
Napi::Object mat;
if (info.Length() == 1 && info[0].ToObject().UnwrapTo(&mat)) {
if (!mat.InstanceOf(env.GetInstanceData<InstanceData>()->DOMMatrixCtor.Value()).UnwrapOr(false)) {
if (!env.IsExceptionPending()) {
Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException();
}
return;
}
cairo_matrix_t matrix;
parse_matrix_from_object(matrix, mat);
cairo_set_matrix(context(), &matrix);
} else {
cairo_identity_matrix(context());
Context2d::Transform(info);
}
}
/*
* Translate transformation.
*/
void
Context2d::Translate(const Napi::CallbackInfo& info) {
double args[2];
if(!checkArgs(info, args, 2))
return;
cairo_translate(context(), args[0], args[1]);
}
/*
* Scale transformation.
*/
void
Context2d::Scale(const Napi::CallbackInfo& info) {
double args[2];
if(!checkArgs(info, args, 2))
return;
cairo_scale(context(), args[0], args[1]);
}
/*
* Use path as clipping region.
*/
void
Context2d::Clip(const Napi::CallbackInfo& info) {
setFillRule(info[0]);
cairo_t *ctx = context();
cairo_clip_preserve(ctx);
}
/*
* Fill the path.
*/
void
Context2d::Fill(const Napi::CallbackInfo& info) {
setFillRule(info[0]);
fill(true);
}
/*
* Stroke the path.
*/
void
Context2d::Stroke(const Napi::CallbackInfo& info) {
stroke(true);
}
/*
* Helper for fillText/strokeText
*/
double
get_text_scale(PangoLayout *layout, double maxWidth) {
PangoRectangle logical_rect;
pango_layout_get_pixel_extents(layout, NULL, &logical_rect);
if (logical_rect.width > maxWidth) {
return maxWidth / logical_rect.width;
} else {
return 1.0;
}
}
/*
* Make sure the layout's font list is up-to-date
*/
void
Context2d::checkFonts() {
// If fonts have been registered, the PangoContext is using an outdated FontMap
if (canvas()->fontSerial != fontSerial) {
pango_context_set_font_map(
pango_layout_get_context(_layout),
pango_cairo_font_map_get_default()
);
fontSerial = canvas()->fontSerial;
}
}
void
Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) {
int argsNum = info.Length() >= 4 ? 3 : 2;
if (argsNum == 3 && info[3].IsUndefined())
argsNum = 2;
double args[3];
if(!checkArgs(info, args, argsNum, 1))
return;
Napi::String strValue;
if (!info[0].ToString().UnwrapTo(&strValue)) return;
std::string str = strValue.Utf8Value();
double x = args[0];
double y = args[1];
double scaled_by = 1;
PangoLayout *layout = this->layout();
checkFonts();
pango_layout_set_text(layout, str.c_str(), -1);
if (state->lang != "") {
pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str()));
}
pango_cairo_update_layout(context(), layout);
PangoDirection pango_dir = state->direction == "ltr" ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL;
pango_context_set_base_dir(pango_layout_get_context(_layout), pango_dir);
if (argsNum == 3) {
if (args[2] <= 0) return;
scaled_by = get_text_scale(layout, args[2]);
cairo_save(context());
cairo_scale(context(), scaled_by, 1);
}
savePath();
if (state->textDrawingMode == TEXT_DRAW_GLYPHS) {
if (stroke == true) { this->stroke(); } else { this->fill(); }
setTextPath(x / scaled_by, y);
} else if (state->textDrawingMode == TEXT_DRAW_PATHS) {
setTextPath(x / scaled_by, y);
if (stroke == true) { this->stroke(); } else { this->fill(); }
}
restorePath();
if (argsNum == 3) {
cairo_restore(context());
}
}
/*
* Fill text at (x, y).
*/
void
Context2d::FillText(const Napi::CallbackInfo& info) {
paintText(info, false);
}
/*
* Stroke text at (x ,y).
*/
void
Context2d::StrokeText(const Napi::CallbackInfo& info) {
paintText(info, true);
}
/*
* Gets the baseline adjustment in device pixels
*/
inline double getBaselineAdjustment(PangoLayout* layout, short baseline) {
PangoRectangle logical_rect;
pango_layout_line_get_extents(pango_layout_get_line(layout, 0), NULL, &logical_rect);
double scale = 1.0 / PANGO_SCALE;
double ascent = scale * pango_layout_get_baseline(layout);
double descent = scale * logical_rect.height - ascent;
switch (baseline) {
case TEXT_BASELINE_ALPHABETIC:
return ascent;
case TEXT_BASELINE_MIDDLE:
return (ascent + descent) / 2.0;
case TEXT_BASELINE_BOTTOM:
return ascent + descent;
default:
return 0;
}
}
text_align_t
Context2d::resolveTextAlignment() {
text_align_t alignment = state->textAlignment;
// Convert start/end to left/right based on direction
if (alignment == TEXT_ALIGNMENT_START) {
return (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT;
} else if (alignment == TEXT_ALIGNMENT_END) {
return (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT;
}
return alignment;
}
/*
* Set text path for the string in the layout at (x, y).
* This function is called by paintText and won't behave correctly
* if is not called from there.
* it needs pango_layout_set_text and pango_cairo_update_layout to be called before
*/
void
Context2d::setTextPath(double x, double y) {
PangoRectangle logical_rect;
text_align_t alignment = resolveTextAlignment();
switch (alignment) {
case TEXT_ALIGNMENT_CENTER:
pango_layout_get_pixel_extents(_layout, NULL, &logical_rect);
x -= logical_rect.width / 2;
break;
case TEXT_ALIGNMENT_RIGHT:
pango_layout_get_pixel_extents(_layout, NULL, &logical_rect);
x -= logical_rect.width;
break;
default: // TEXT_ALIGNMENT_LEFT
break;
}
y -= getBaselineAdjustment(_layout, state->textBaseline);
cairo_move_to(_context, x, y);
if (state->textDrawingMode == TEXT_DRAW_PATHS) {
pango_cairo_layout_path(_context, _layout);
} else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) {
pango_cairo_show_layout(_context, _layout);
}
}
/*
* Adds a point to the current subpath.
*/
void
Context2d::LineTo(const Napi::CallbackInfo& info) {
double args[2];
if(!checkArgs(info, args, 2))
return;
cairo_line_to(context(), args[0], args[1]);
}
/*
* Creates a new subpath at the given point.
*/
void
Context2d::MoveTo(const Napi::CallbackInfo& info) {
double args[2];
if(!checkArgs(info, args, 2))
return;
cairo_move_to(context(), args[0], args[1]);
}
/*
* Get font.
*/
Napi::Value
Context2d::GetFont(const Napi::CallbackInfo& info) {
return Napi::String::New(env, state->font);
}
/*
* Set font:
* - weight
* - style
* - size
* - unit
* - family
*/
void
Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (!value.IsString()) return;
std::string str = value.As<Napi::String>().Utf8Value();
if (!str.length()) return;
bool success;
auto props = FontParser::parse(str, &success);
if (!success) return;
PangoFontDescription *desc = pango_font_description_copy(state->fontDescription);
pango_font_description_free(state->fontDescription);
PangoStyle style = props.fontStyle == FontStyle::Italic ? PANGO_STYLE_ITALIC
: props.fontStyle == FontStyle::Oblique ? PANGO_STYLE_OBLIQUE
: PANGO_STYLE_NORMAL;
pango_font_description_set_style(desc, style);
pango_font_description_set_weight(desc, static_cast<PangoWeight>(props.fontWeight));
std::string family = props.fontFamily.empty() ? "" : props.fontFamily[0];
for (size_t i = 1; i < props.fontFamily.size(); i++) {
family += "," + props.fontFamily[i];
}
if (family.length() > 0) {
// See #1643 - Pango understands "sans" whereas CSS uses "sans-serif"
std::string s1(family);
std::string s2("sans-serif");
if (streq_casein(s1, s2)) {
pango_font_description_set_family(desc, "sans");
} else {
pango_font_description_set_family(desc, family.c_str());
}
}
PangoFontDescription *sys_desc = Canvas::ResolveFontDescription(desc);
pango_font_description_free(desc);
if (props.fontSize > 0) pango_font_description_set_absolute_size(sys_desc, props.fontSize * PANGO_SCALE);
state->fontDescription = sys_desc;
pango_layout_set_font_description(_layout, sys_desc);
state->font = str;
}
/*
* Get text baseline.
*/
Napi::Value
Context2d::GetTextBaseline(const Napi::CallbackInfo& info) {
const char* baseline;
switch (state->textBaseline) {
default:
case TEXT_BASELINE_ALPHABETIC: baseline = "alphabetic"; break;
case TEXT_BASELINE_TOP: baseline = "top"; break;
case TEXT_BASELINE_BOTTOM: baseline = "bottom"; break;
case TEXT_BASELINE_MIDDLE: baseline = "middle"; break;
case TEXT_BASELINE_IDEOGRAPHIC: baseline = "ideographic"; break;
case TEXT_BASELINE_HANGING: baseline = "hanging"; break;
}
return Napi::String::New(env, baseline);
}
/*
* Set text baseline.
*/
void
Context2d::SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (!value.IsString()) return;
std::string opStr = value.As<Napi::String>();
const std::map<std::string, text_baseline_t> modes = {
{"alphabetic", TEXT_BASELINE_ALPHABETIC},
{"top", TEXT_BASELINE_TOP},
{"bottom", TEXT_BASELINE_BOTTOM},
{"middle", TEXT_BASELINE_MIDDLE},
{"ideographic", TEXT_BASELINE_IDEOGRAPHIC},
{"hanging", TEXT_BASELINE_HANGING}
};
auto op = modes.find(opStr);
if (op == modes.end()) return;
state->textBaseline = op->second;
}
/*
* Get text align.
*/
Napi::Value
Context2d::GetTextAlign(const Napi::CallbackInfo& info) {
const char* align;
switch (state->textAlignment) {
case TEXT_ALIGNMENT_LEFT: align = "left"; break;
case TEXT_ALIGNMENT_START: align = "start"; break;
case TEXT_ALIGNMENT_CENTER: align = "center"; break;
case TEXT_ALIGNMENT_RIGHT: align = "right"; break;
case TEXT_ALIGNMENT_END: align = "end"; break;
default: align = "start";
}
return Napi::String::New(env, align);
}
/*
* Set text align.
*/
void
Context2d::SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (!value.IsString()) return;
std::string opStr = value.As<Napi::String>();
const std::map<std::string, text_align_t> modes = {
{"center", TEXT_ALIGNMENT_CENTER},
{"left", TEXT_ALIGNMENT_LEFT},
{"start", TEXT_ALIGNMENT_START},
{"right", TEXT_ALIGNMENT_RIGHT},
{"end", TEXT_ALIGNMENT_END}
};
auto op = modes.find(opStr);
if (op == modes.end()) return;
state->textAlignment = op->second;
}
/*
* Return the given text extents.
* TODO: Support for:
* hangingBaseline, ideographicBaseline,
* fontBoundingBoxAscent, fontBoundingBoxDescent
*/
Napi::Value
Context2d::MeasureText(const Napi::CallbackInfo& info) {
cairo_t *ctx = this->context();
Napi::String str;
if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined();
Napi::Object obj = Napi::Object::New(env);
PangoRectangle _ink_rect, _logical_rect;
float_rectangle ink_rect, logical_rect;
PangoFontMetrics *metrics;
PangoLayout *layout = this->layout();
checkFonts();
pango_layout_set_text(layout, str.Utf8Value().c_str(), -1);
if (state->lang != "") {
pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str()));
}
pango_cairo_update_layout(ctx, layout);
// Normally you could use pango_layout_get_pixel_extents and be done, or use
// pango_extents_to_pixels, but both of those round the pixels, so we have to
// divide by PANGO_SCALE manually
pango_layout_get_extents(layout, &_ink_rect, &_logical_rect);
float inverse_pango_scale = 1. / PANGO_SCALE;
logical_rect.x = _logical_rect.x * inverse_pango_scale;
logical_rect.y = _logical_rect.y * inverse_pango_scale;
logical_rect.width = _logical_rect.width * inverse_pango_scale;
logical_rect.height = _logical_rect.height * inverse_pango_scale;
ink_rect.x = _ink_rect.x * inverse_pango_scale;
ink_rect.y = _ink_rect.y * inverse_pango_scale;
ink_rect.width = _ink_rect.width * inverse_pango_scale;
ink_rect.height = _ink_rect.height * inverse_pango_scale;
metrics = PANGO_LAYOUT_GET_METRICS(layout);
text_align_t alignment = resolveTextAlignment();
double x_offset;
switch (alignment) {
case TEXT_ALIGNMENT_CENTER:
x_offset = logical_rect.width / 2.;
break;
case TEXT_ALIGNMENT_RIGHT:
x_offset = logical_rect.width;
break;
case TEXT_ALIGNMENT_LEFT:
default:
x_offset = 0.0;
}
double y_offset = getBaselineAdjustment(layout, state->textBaseline);
obj.Set("width", Napi::Number::New(env, logical_rect.width));
obj.Set("actualBoundingBoxLeft", Napi::Number::New(env, PANGO_LBEARING(ink_rect) + x_offset));
obj.Set("actualBoundingBoxRight", Napi::Number::New(env, PANGO_RBEARING(ink_rect) - x_offset));
obj.Set("actualBoundingBoxAscent", Napi::Number::New(env, y_offset + PANGO_ASCENT(ink_rect)));
obj.Set("actualBoundingBoxDescent", Napi::Number::New(env, PANGO_DESCENT(ink_rect) - y_offset));
obj.Set("emHeightAscent", Napi::Number::New(env, -(PANGO_ASCENT(logical_rect) - y_offset)));
obj.Set("emHeightDescent", Napi::Number::New(env, PANGO_DESCENT(logical_rect) - y_offset));
obj.Set("alphabeticBaseline", Napi::Number::New(env, -(pango_font_metrics_get_ascent(metrics) * inverse_pango_scale - y_offset)));
pango_font_metrics_unref(metrics);
return obj;
}
/*
* Set line dash
* ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash
*/
void
Context2d::SetLineDash(const Napi::CallbackInfo& info) {
if (!info[0].IsArray()) return;
Napi::Array dash = info[0].As<Napi::Array>();
uint32_t dashes = dash.Length() & 1 ? dash.Length() * 2 : dash.Length();
uint32_t zero_dashes = 0;
std::vector<double> a(dashes);
for (uint32_t i=0; i<dashes; i++) {
Napi::Number d;
if (!dash.Get(i % dash.Length()).UnwrapTo(&d) || !d.IsNumber()) return;
a[i] = d.As<Napi::Number>().DoubleValue();
if (a[i] == 0) zero_dashes++;
if (a[i] < 0 || !std::isfinite(a[i])) return;
}
cairo_t *ctx = this->context();
double offset;
cairo_get_dash(ctx, NULL, &offset);
if (zero_dashes == dashes) {
std::vector<double> b(0);
cairo_set_dash(ctx, b.data(), 0, offset);
} else {
cairo_set_dash(ctx, a.data(), dashes, offset);
}
}
/*
* Get line dash
* ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash
*/
Napi::Value
Context2d::GetLineDash(const Napi::CallbackInfo& info) {
cairo_t *ctx = this->context();
int dashes = cairo_get_dash_count(ctx);
std::vector<double> a(dashes);
cairo_get_dash(ctx, a.data(), NULL);
Napi::Array dash = Napi::Array::New(env, dashes);
for (int i=0; i<dashes; i++) {
dash.Set(Napi::Number::New(env, i), Napi::Number::New(env, a[i]));
}
return dash;
}
/*
* Set line dash offset
* ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash
*/
void
Context2d::SetLineDashOffset(const Napi::CallbackInfo& info, const Napi::Value& value) {
Napi::Number numberValue;
if (!value.ToNumber().UnwrapTo(&numberValue)) return;
double offset = numberValue.DoubleValue();
if (!std::isfinite(offset)) return;
cairo_t *ctx = this->context();
int dashes = cairo_get_dash_count(ctx);
std::vector<double> a(dashes);
cairo_get_dash(ctx, a.data(), NULL);
cairo_set_dash(ctx, a.data(), dashes, offset);
}
/*
* Get line dash offset
* ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash
*/
Napi::Value
Context2d::GetLineDashOffset(const Napi::CallbackInfo& info) {
cairo_t *ctx = this->context();
double offset;
cairo_get_dash(ctx, NULL, &offset);
return Napi::Number::New(env, offset);
}
/*
* Fill the rectangle defined by x, y, width and height.
*/
void
Context2d::FillRect(const Napi::CallbackInfo& info) {
RECT_ARGS;
if (0 == width || 0 == height) return;
cairo_t *ctx = context();
savePath();
cairo_rectangle(ctx, x, y, width, height);
fill();
restorePath();
}
/*
* Stroke the rectangle defined by x, y, width and height.
*/
void
Context2d::StrokeRect(const Napi::CallbackInfo& info) {
RECT_ARGS;
if (0 == width && 0 == height) return;
cairo_t *ctx = context();
savePath();
cairo_rectangle(ctx, x, y, width, height);
stroke();
restorePath();
}
/*
* Clears all pixels defined by x, y, width and height.
*/
void
Context2d::ClearRect(const Napi::CallbackInfo& info) {
RECT_ARGS;
if (0 == width || 0 == height) return;
cairo_t *ctx = context();
cairo_save(ctx);
savePath();
cairo_rectangle(ctx, x, y, width, height);
cairo_set_operator(ctx, CAIRO_OPERATOR_CLEAR);
cairo_fill(ctx);
restorePath();
cairo_restore(ctx);
}
/*
* Adds a rectangle subpath.
*/
void
Context2d::Rect(const Napi::CallbackInfo& info) {
RECT_ARGS;
cairo_t *ctx = context();
if (width == 0) {
cairo_move_to(ctx, x, y);
cairo_line_to(ctx, x, y + height);
} else if (height == 0) {
cairo_move_to(ctx, x, y);
cairo_line_to(ctx, x + width, y);
} else {
cairo_rectangle(ctx, x, y, width, height);
}
}
// Draws an arc with two potentially different radii.
inline static
void elli_arc(cairo_t* ctx, double xc, double yc, double rx, double ry, double a1, double a2, bool clockwise=true) {
if (rx == 0. || ry == 0.) {
cairo_line_to(ctx, xc + rx, yc + ry);
} else {
cairo_save(ctx);
cairo_translate(ctx, xc, yc);
cairo_scale(ctx, rx, ry);
if (clockwise)
cairo_arc(ctx, 0., 0., 1., a1, a2);
else
cairo_arc_negative(ctx, 0., 0., 1., a2, a1);
cairo_restore(ctx);
}
}
inline static
bool getRadius(Point<double>& p, const Napi::Value& v) {
Napi::Env env = v.Env();
if (v.IsObject()) { // 5.1 DOMPointInit
Napi::Value rx;
Napi::Value ry;
auto rxMaybe = v.As<Napi::Object>().Get("x");
auto ryMaybe = v.As<Napi::Object>().Get("y");
if (rxMaybe.UnwrapTo(&rx) && rx.IsNumber() && ryMaybe.UnwrapTo(&ry) && ry.IsNumber()) {
auto rxv = rx.As<Napi::Number>().DoubleValue();
auto ryv = ry.As<Napi::Number>().DoubleValue();
if (!std::isfinite(rxv) || !std::isfinite(ryv))
return true;
if (rxv < 0 || ryv < 0) {
Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException();
return true;
}
p.x = rxv;
p.y = ryv;
return false;
}
} else if (v.IsNumber()) { // 5.2 unrestricted double
auto rv = v.As<Napi::Number>().DoubleValue();
if (!std::isfinite(rv))
return true;
if (rv < 0) {
Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException();
return true;
}
p.x = p.y = rv;
return false;
}
return true;
}
/**
* https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect
* x, y, w, h, [radius|[radii]]
*/
void
Context2d::RoundRect(const Napi::CallbackInfo& info) {
RECT_ARGS;
cairo_t *ctx = this->context();
// 4. Let normalizedRadii be an empty list
Point<double> normalizedRadii[4];
size_t nRadii = 4;
if (info[4].IsUndefined()) {
for (size_t i = 0; i < 4; i++)
normalizedRadii[i].x = normalizedRadii[i].y = 0.;
} else if (info[4].IsArray()) {
auto radiiList = info[4].As<Napi::Array>();
nRadii = radiiList.Length();
if (!(nRadii >= 1 && nRadii <= 4)) {
Napi::RangeError::New(env, "radii must be a list of one, two, three or four radii.").ThrowAsJavaScriptException();
return;
}
// 5. For each radius of radii
for (size_t i = 0; i < nRadii; i++) {
Napi::Value r;
if (!radiiList.Get(i).UnwrapTo(&r) || getRadius(normalizedRadii[i], r))
return;
}
} else {
// 2. If radii is a double, then set radii to <<radii>>
if (getRadius(normalizedRadii[0], info[4]))
return;
for (size_t i = 1; i < 4; i++) {
normalizedRadii[i].x = normalizedRadii[0].x;
normalizedRadii[i].y = normalizedRadii[0].y;
}
}
Point<double> upperLeft, upperRight, lowerRight, lowerLeft;
if (nRadii == 4) {
upperLeft = normalizedRadii[0];
upperRight = normalizedRadii[1];
lowerRight = normalizedRadii[2];
lowerLeft = normalizedRadii[3];
} else if (nRadii == 3) {
upperLeft = normalizedRadii[0];
upperRight = normalizedRadii[1];
lowerLeft = normalizedRadii[1];
lowerRight = normalizedRadii[2];
} else if (nRadii == 2) {
upperLeft = normalizedRadii[0];
lowerRight = normalizedRadii[0];
upperRight = normalizedRadii[1];
lowerLeft = normalizedRadii[1];
} else {
upperLeft = normalizedRadii[0];
upperRight = normalizedRadii[0];
lowerRight = normalizedRadii[0];
lowerLeft = normalizedRadii[0];
}
bool clockwise = true;
if (width < 0) {
clockwise = false;
x += width;
width = -width;
std::swap(upperLeft, upperRight);
std::swap(lowerLeft, lowerRight);
}
if (height < 0) {
clockwise = !clockwise;
y += height;
height = -height;
std::swap(upperLeft, lowerLeft);
std::swap(upperRight, lowerRight);
}
// 11. Corner curves must not overlap. Scale radii to prevent this.
{
auto top = upperLeft.x + upperRight.x;
auto right = upperRight.y + lowerRight.y;
auto bottom = lowerRight.x + lowerLeft.x;
auto left = upperLeft.y + lowerLeft.y;
auto scale = std::min({ width / top, height / right, width / bottom, height / left });
if (scale < 1.) {
upperLeft.x *= scale;
upperLeft.y *= scale;
upperRight.x *= scale;
upperRight.y *= scale;
lowerLeft.x *= scale;
lowerLeft.y *= scale;
lowerRight.x *= scale;
lowerRight.y *= scale;
}
}
// 12. Draw
cairo_move_to(ctx, x + upperLeft.x, y);
if (clockwise) {
cairo_line_to(ctx, x + width - upperRight.x, y);
elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0.);
cairo_line_to(ctx, x + width, y + height - lowerRight.y);
elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2.);
cairo_line_to(ctx, x + lowerLeft.x, y + height);
elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI);
cairo_line_to(ctx, x, y + upperLeft.y);
elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2.);
} else {
elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2., false);
cairo_line_to(ctx, x, y + upperLeft.y);
elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI, false);
cairo_line_to(ctx, x + lowerLeft.x, y + height);
elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2., false);
cairo_line_to(ctx, x + width, y + height - lowerRight.y);
elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0., false);
cairo_line_to(ctx, x + width - upperRight.x, y);
}
cairo_close_path(ctx);
}
// Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp
static void canonicalizeAngle(double& startAngle, double& endAngle) {
// Make 0 <= startAngle < 2*PI
double newStartAngle = std::fmod(startAngle, twoPi);
if (newStartAngle < 0) {
newStartAngle += twoPi;
// Check for possible catastrophic cancellation in cases where
// newStartAngle was a tiny negative number (c.f. crbug.com/503422)
if (newStartAngle >= twoPi)
newStartAngle -= twoPi;
}
double delta = newStartAngle - startAngle;
startAngle = newStartAngle;
endAngle = endAngle + delta;
}
// Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp
static double adjustEndAngle(double startAngle, double endAngle, bool counterclockwise) {
double newEndAngle = endAngle;
/* http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-arc
* If the counterclockwise argument is false and endAngle-startAngle is equal to or greater than 2pi, or,
* if the counterclockwise argument is true and startAngle-endAngle is equal to or greater than 2pi,
* then the arc is the whole circumference of this ellipse, and the point at startAngle along this circle's circumference,
* measured in radians clockwise from the ellipse's semi-major axis, acts as both the start point and the end point.
*/
if (!counterclockwise && endAngle - startAngle >= twoPi)
newEndAngle = startAngle + twoPi;
else if (counterclockwise && startAngle - endAngle >= twoPi)
newEndAngle = startAngle - twoPi;
/*
* Otherwise, the arc is the path along the circumference of this ellipse from the start point to the end point,
* going anti-clockwise if the counterclockwise argument is true, and clockwise otherwise.
* Since the points are on the ellipse, as opposed to being simply angles from zero,
* the arc can never cover an angle greater than 2pi radians.
*/
/* NOTE: When startAngle = 0, endAngle = 2Pi and counterclockwise = true, the spec does not indicate clearly.
* We draw the entire circle, because some web sites use arc(x, y, r, 0, 2*Math.PI, true) to draw circle.
* We preserve backward-compatibility.
*/
else if (!counterclockwise && startAngle > endAngle)
newEndAngle = startAngle + (twoPi - std::fmod(startAngle - endAngle, twoPi));
else if (counterclockwise && startAngle < endAngle)
newEndAngle = startAngle - (twoPi - std::fmod(endAngle - startAngle, twoPi));
return newEndAngle;
}
/*
* Adds an arc at x, y with the given radii and start/end angles.
*/
void
Context2d::Arc(const Napi::CallbackInfo& info) {
double args[5];
if(!checkArgs(info, args, 5))
return;
auto x = args[0];
auto y = args[1];
auto radius = args[2];
auto startAngle = args[3];
auto endAngle = args[4];
if (radius < 0) {
Napi::RangeError::New(env, "The radius provided is negative.").ThrowAsJavaScriptException();
return;
}
Napi::Boolean counterclockwiseValue;
if (!info[5].ToBoolean().UnwrapTo(&counterclockwiseValue)) return;
bool counterclockwise = counterclockwiseValue.Value();
cairo_t *ctx = context();
canonicalizeAngle(startAngle, endAngle);
endAngle = adjustEndAngle(startAngle, endAngle, counterclockwise);
if (counterclockwise) {
cairo_arc_negative(ctx, x, y, radius, startAngle, endAngle);
} else {
cairo_arc(ctx, x, y, radius, startAngle, endAngle);
}
}
/*
* Adds an arcTo point (x0,y0) to (x1,y1) with the given radius.
*
* Implementation influenced by WebKit.
*/
void
Context2d::ArcTo(const Napi::CallbackInfo& info) {
double args[5];
if(!checkArgs(info, args, 5))
return;
cairo_t *ctx = context();
// Current path point
double x, y;
cairo_get_current_point(ctx, &x, &y);
Point<float> p0(x, y);
// Point (x0,y0)
Point<float> p1(args[0], args[1]);
// Point (x1,y1)
Point<float> p2(args[2], args[3]);
float radius = args[4];
if ((p1.x == p0.x && p1.y == p0.y)
|| (p1.x == p2.x && p1.y == p2.y)
|| radius == 0.f) {
cairo_line_to(ctx, p1.x, p1.y);
return;
}
Point<float> p1p0((p0.x - p1.x),(p0.y - p1.y));
Point<float> p1p2((p2.x - p1.x),(p2.y - p1.y));
float p1p0_length = sqrtf(p1p0.x * p1p0.x + p1p0.y * p1p0.y);
float p1p2_length = sqrtf(p1p2.x * p1p2.x + p1p2.y * p1p2.y);
double cos_phi = (p1p0.x * p1p2.x + p1p0.y * p1p2.y) / (p1p0_length * p1p2_length);
// all points on a line logic
if (-1 == cos_phi) {
cairo_line_to(ctx, p1.x, p1.y);
return;
}
if (1 == cos_phi) {
// add infinite far away point
unsigned int max_length = 65535;
double factor_max = max_length / p1p0_length;
Point<float> ep((p0.x + factor_max * p1p0.x), (p0.y + factor_max * p1p0.y));
cairo_line_to(ctx, ep.x, ep.y);
return;
}
float tangent = radius / tan(acos(cos_phi) / 2);
float factor_p1p0 = tangent / p1p0_length;
Point<float> t_p1p0((p1.x + factor_p1p0 * p1p0.x), (p1.y + factor_p1p0 * p1p0.y));
Point<float> orth_p1p0(p1p0.y, -p1p0.x);
float orth_p1p0_length = sqrt(orth_p1p0.x * orth_p1p0.x + orth_p1p0.y * orth_p1p0.y);
float factor_ra = radius / orth_p1p0_length;
double cos_alpha = (orth_p1p0.x * p1p2.x + orth_p1p0.y * p1p2.y) / (orth_p1p0_length * p1p2_length);
if (cos_alpha < 0.f)
orth_p1p0 = Point<float>(-orth_p1p0.x, -orth_p1p0.y);
Point<float> p((t_p1p0.x + factor_ra * orth_p1p0.x), (t_p1p0.y + factor_ra * orth_p1p0.y));
orth_p1p0 = Point<float>(-orth_p1p0.x, -orth_p1p0.y);
float sa = acos(orth_p1p0.x / orth_p1p0_length);
if (orth_p1p0.y < 0.f)
sa = 2 * M_PI - sa;
bool anticlockwise = false;
float factor_p1p2 = tangent / p1p2_length;
Point<float> t_p1p2((p1.x + factor_p1p2 * p1p2.x), (p1.y + factor_p1p2 * p1p2.y));
Point<float> orth_p1p2((t_p1p2.x - p.x),(t_p1p2.y - p.y));
float orth_p1p2_length = sqrtf(orth_p1p2.x * orth_p1p2.x + orth_p1p2.y * orth_p1p2.y);
float ea = acos(orth_p1p2.x / orth_p1p2_length);
if (orth_p1p2.y < 0) ea = 2 * M_PI - ea;
if ((sa > ea) && ((sa - ea) < M_PI)) anticlockwise = true;
if ((sa < ea) && ((ea - sa) > M_PI)) anticlockwise = true;
cairo_line_to(ctx, t_p1p0.x, t_p1p0.y);
if (anticlockwise && M_PI * 2 != radius) {
cairo_arc_negative(ctx
, p.x
, p.y
, radius
, sa
, ea);
} else {
cairo_arc(ctx
, p.x
, p.y
, radius
, sa
, ea);
}
}
/*
* Adds an ellipse to the path which is centered at (x, y) position with the
* radii radiusX and radiusY starting at startAngle and ending at endAngle
* going in the given direction by anticlockwise (defaulting to clockwise).
*/
void
Context2d::Ellipse(const Napi::CallbackInfo& info) {
double args[7];
if(!checkArgs(info, args, 7))
return;
double radiusX = args[2];
double radiusY = args[3];
if (radiusX == 0 || radiusY == 0) return;
double x = args[0];
double y = args[1];
double rotation = args[4];
double startAngle = args[5];
double endAngle = args[6];
Napi::Boolean anticlockwiseValue;
if (!info[7].ToBoolean().UnwrapTo(&anticlockwiseValue)) return;
bool anticlockwise = anticlockwiseValue.Value();
cairo_t *ctx = context();
// See https://www.cairographics.org/cookbook/ellipses/
double xRatio = radiusX / radiusY;
cairo_matrix_t save_matrix;
cairo_get_matrix(ctx, &save_matrix);
cairo_translate(ctx, x, y);
cairo_rotate(ctx, rotation);
cairo_scale(ctx, xRatio, 1.0);
cairo_translate(ctx, -x, -y);
if (anticlockwise && M_PI * 2 != args[4]) {
cairo_arc_negative(ctx,
x,
y,
radiusY,
startAngle,
endAngle);
} else {
cairo_arc(ctx,
x,
y,
radiusY,
startAngle,
endAngle);
}
cairo_set_matrix(ctx, &save_matrix);
}
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
void
Context2d::BeginTag(const Napi::CallbackInfo& info) {
std::string tagName = "";
std::string attributes = "";
if (info.Length() == 0) {
Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException();
return;
} else {
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException();
return;
} else {
tagName = info[0].As<Napi::String>().Utf8Value();
}
if (info.Length() > 1) {
if (!info[1].IsString()) {
Napi::TypeError::New(env, "Attributes must be a string matching Cairo's attribute format").ThrowAsJavaScriptException();
return;
} else {
attributes = info[1].As<Napi::String>().Utf8Value();
}
}
}
cairo_tag_begin(_context, tagName.c_str(), attributes.c_str());
}
void
Context2d::EndTag(const Napi::CallbackInfo& info) {
if (info.Length() == 0) {
Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException();
return;
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException();
return;
}
std::string tagName = info[0].As<Napi::String>().Utf8Value();
cairo_tag_end(_context, tagName.c_str());
}
#endif
+238
View File
@@ -0,0 +1,238 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#pragma once
#include "cairo.h"
#include "Canvas.h"
#include "color.h"
#include "napi.h"
#include <pango/pangocairo.h>
#include <stack>
/*
* State struct.
*
* Used in conjunction with Save() / Restore() since
* cairo's gstate maintains only a single source pattern at a time.
*/
struct canvas_state_t {
rgba_t fill = { 0, 0, 0, 1 };
rgba_t stroke = { 0, 0, 0, 1 };
rgba_t shadow = { 0, 0, 0, 0 };
double shadowOffsetX = 0.;
double shadowOffsetY = 0.;
cairo_pattern_t* fillPattern = nullptr;
cairo_pattern_t* strokePattern = nullptr;
cairo_pattern_t* fillGradient = nullptr;
cairo_pattern_t* strokeGradient = nullptr;
PangoFontDescription* fontDescription = nullptr;
std::string font = "10px sans-serif";
cairo_filter_t patternQuality = CAIRO_FILTER_GOOD;
float globalAlpha = 1.f;
int shadowBlur = 0;
text_align_t textAlignment = TEXT_ALIGNMENT_START;
text_baseline_t textBaseline = TEXT_BASELINE_ALPHABETIC;
canvas_draw_mode_t textDrawingMode = TEXT_DRAW_PATHS;
bool imageSmoothingEnabled = true;
std::string direction = "ltr";
std::string lang = "";
canvas_state_t() {
fontDescription = pango_font_description_from_string("sans");
pango_font_description_set_absolute_size(fontDescription, 10 * PANGO_SCALE);
}
canvas_state_t(const canvas_state_t& other) {
fill = other.fill;
stroke = other.stroke;
patternQuality = other.patternQuality;
fillPattern = other.fillPattern;
strokePattern = other.strokePattern;
fillGradient = other.fillGradient;
strokeGradient = other.strokeGradient;
globalAlpha = other.globalAlpha;
textAlignment = other.textAlignment;
textBaseline = other.textBaseline;
shadow = other.shadow;
shadowBlur = other.shadowBlur;
shadowOffsetX = other.shadowOffsetX;
shadowOffsetY = other.shadowOffsetY;
textDrawingMode = other.textDrawingMode;
fontDescription = pango_font_description_copy(other.fontDescription);
font = other.font;
imageSmoothingEnabled = other.imageSmoothingEnabled;
direction = other.direction;
lang = other.lang;
}
~canvas_state_t() {
pango_font_description_free(fontDescription);
}
};
/*
* Equivalent to a PangoRectangle but holds floats instead of ints
* (software pixels are stored here instead of pango units)
*
* Should be compatible with PANGO_ASCENT, PANGO_LBEARING, etc.
*/
typedef struct {
float x;
float y;
float width;
float height;
} float_rectangle;
class Context2d : public Napi::ObjectWrap<Context2d> {
public:
std::stack<canvas_state_t> states;
canvas_state_t *state;
Context2d(const Napi::CallbackInfo& info);
static void Initialize(Napi::Env& env, Napi::Object& target);
void DrawImage(const Napi::CallbackInfo& info);
void PutImageData(const Napi::CallbackInfo& info);
void Save(const Napi::CallbackInfo& info);
void Restore(const Napi::CallbackInfo& info);
void Rotate(const Napi::CallbackInfo& info);
void Translate(const Napi::CallbackInfo& info);
void Scale(const Napi::CallbackInfo& info);
void Transform(const Napi::CallbackInfo& info);
Napi::Value GetTransform(const Napi::CallbackInfo& info);
void ResetTransform(const Napi::CallbackInfo& info);
void SetTransform(const Napi::CallbackInfo& info);
Napi::Value IsPointInPath(const Napi::CallbackInfo& info);
void BeginPath(const Napi::CallbackInfo& info);
void ClosePath(const Napi::CallbackInfo& info);
void AddPage(const Napi::CallbackInfo& info);
void Clip(const Napi::CallbackInfo& info);
void Fill(const Napi::CallbackInfo& info);
void Stroke(const Napi::CallbackInfo& info);
void FillText(const Napi::CallbackInfo& info);
void StrokeText(const Napi::CallbackInfo& info);
static Napi::Value SetFont(const Napi::CallbackInfo& info);
static Napi::Value SetFillColor(const Napi::CallbackInfo& info);
static Napi::Value SetStrokeColor(const Napi::CallbackInfo& info);
static Napi::Value SetStrokePattern(const Napi::CallbackInfo& info);
static Napi::Value SetTextAlignment(const Napi::CallbackInfo& info);
void SetLineDash(const Napi::CallbackInfo& info);
Napi::Value GetLineDash(const Napi::CallbackInfo& info);
Napi::Value MeasureText(const Napi::CallbackInfo& info);
void BezierCurveTo(const Napi::CallbackInfo& info);
void QuadraticCurveTo(const Napi::CallbackInfo& info);
void LineTo(const Napi::CallbackInfo& info);
void MoveTo(const Napi::CallbackInfo& info);
void FillRect(const Napi::CallbackInfo& info);
void StrokeRect(const Napi::CallbackInfo& info);
void ClearRect(const Napi::CallbackInfo& info);
void Rect(const Napi::CallbackInfo& info);
void RoundRect(const Napi::CallbackInfo& info);
void Arc(const Napi::CallbackInfo& info);
void ArcTo(const Napi::CallbackInfo& info);
void Ellipse(const Napi::CallbackInfo& info);
Napi::Value GetImageData(const Napi::CallbackInfo& info);
Napi::Value CreateImageData(const Napi::CallbackInfo& info);
static Napi::Value GetStrokeColor(const Napi::CallbackInfo& info);
Napi::Value CreatePattern(const Napi::CallbackInfo& info);
Napi::Value CreateLinearGradient(const Napi::CallbackInfo& info);
Napi::Value CreateRadialGradient(const Napi::CallbackInfo& info);
Napi::Value GetFormat(const Napi::CallbackInfo& info);
Napi::Value GetPatternQuality(const Napi::CallbackInfo& info);
Napi::Value GetImageSmoothingEnabled(const Napi::CallbackInfo& info);
Napi::Value GetGlobalCompositeOperation(const Napi::CallbackInfo& info);
Napi::Value GetGlobalAlpha(const Napi::CallbackInfo& info);
Napi::Value GetShadowColor(const Napi::CallbackInfo& info);
Napi::Value GetMiterLimit(const Napi::CallbackInfo& info);
Napi::Value GetLineCap(const Napi::CallbackInfo& info);
Napi::Value GetLineJoin(const Napi::CallbackInfo& info);
Napi::Value GetLineWidth(const Napi::CallbackInfo& info);
Napi::Value GetLineDashOffset(const Napi::CallbackInfo& info);
Napi::Value GetShadowOffsetX(const Napi::CallbackInfo& info);
Napi::Value GetShadowOffsetY(const Napi::CallbackInfo& info);
Napi::Value GetShadowBlur(const Napi::CallbackInfo& info);
Napi::Value GetAntiAlias(const Napi::CallbackInfo& info);
Napi::Value GetTextDrawingMode(const Napi::CallbackInfo& info);
Napi::Value GetQuality(const Napi::CallbackInfo& info);
Napi::Value GetCurrentTransform(const Napi::CallbackInfo& info);
Napi::Value GetFillStyle(const Napi::CallbackInfo& info);
Napi::Value GetStrokeStyle(const Napi::CallbackInfo& info);
Napi::Value GetFont(const Napi::CallbackInfo& info);
Napi::Value GetTextBaseline(const Napi::CallbackInfo& info);
Napi::Value GetTextAlign(const Napi::CallbackInfo& info);
Napi::Value GetLanguage(const Napi::CallbackInfo& info);
void SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetGlobalAlpha(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetShadowColor(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetMiterLimit(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetLineCap(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetLineJoin(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetLineWidth(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetLineDashOffset(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetShadowOffsetX(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetFont(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value);
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
void BeginTag(const Napi::CallbackInfo& info);
void EndTag(const Napi::CallbackInfo& info);
#endif
Napi::Value GetDirection(const Napi::CallbackInfo& info);
void SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value);
inline void setContext(cairo_t *ctx) { _context = ctx; }
inline cairo_t *context(){ return _context; }
inline Canvas *canvas(){ return _canvas; }
inline bool hasShadow();
void inline setSourceRGBA(rgba_t color);
void inline setSourceRGBA(cairo_t *ctx, rgba_t color);
void setTextPath(double x, double y);
void blur(cairo_surface_t *surface, int radius);
void shadow(void (fn)(cairo_t *cr));
void shadowStart();
void shadowApply();
void savePath();
void restorePath();
void saveState();
void restoreState();
void inline setFillRule(Napi::Value value);
void fill(bool preserve = false);
void stroke(bool preserve = false);
void save();
void restore();
void setFontFromState();
void resetState();
inline PangoLayout *layout(){ return _layout; }
~Context2d();
Napi::Env env;
private:
void _resetPersistentHandles();
Napi::Value _getFillColor();
Napi::Value _getStrokeColor();
Napi::Value get_current_transform();
void _setFillColor(Napi::Value arg);
void _setFillPattern(Napi::Value arg);
void _setStrokeColor(Napi::Value arg);
void _setStrokePattern(Napi::Value arg);
void checkFonts();
void paintText(const Napi::CallbackInfo&, bool);
text_align_t resolveTextAlignment();
Napi::Reference<Napi::Value> _fillStyle;
Napi::Reference<Napi::Value> _strokeStyle;
Canvas *_canvas;
cairo_t *_context = nullptr;
cairo_path_t *_path;
PangoLayout *_layout = nullptr;
int fontSerial = 1;
};
+233
View File
@@ -0,0 +1,233 @@
// This is used for classifying characters according to the definition of tokens
// in the CSS standards, but could be extended for any other future uses
#pragma once
#include <cstdint>
namespace CharData {
static constexpr uint8_t Whitespace = 0x1;
static constexpr uint8_t Newline = 0x2;
static constexpr uint8_t Hex = 0x4;
static constexpr uint8_t Nmstart = 0x8;
static constexpr uint8_t Nmchar = 0x10;
static constexpr uint8_t Sign = 0x20;
static constexpr uint8_t Digit = 0x40;
static constexpr uint8_t NumStart = 0x80;
};
using namespace CharData;
constexpr const uint8_t charData[256] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-8
Whitespace, // 9 (HT)
Whitespace | Newline, // 10 (LF)
0, // 11 (VT)
Whitespace | Newline, // 12 (FF)
Whitespace | Newline, // 13 (CR)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14-31
Whitespace, // 32 (Space)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 33-42
Sign | NumStart, // 43 (+)
0, // 44
Nmchar | Sign | NumStart, // 45 (-)
0, 0, // 46-47
Nmchar | Digit | NumStart | Hex, // 48 (0)
Nmchar | Digit | NumStart | Hex, // 49 (1)
Nmchar | Digit | NumStart | Hex, // 50 (2)
Nmchar | Digit | NumStart | Hex, // 51 (3)
Nmchar | Digit | NumStart | Hex, // 52 (4)
Nmchar | Digit | NumStart | Hex, // 53 (5)
Nmchar | Digit | NumStart | Hex, // 54 (6)
Nmchar | Digit | NumStart | Hex, // 55 (7)
Nmchar | Digit | NumStart | Hex, // 56 (8)
Nmchar | Digit | NumStart | Hex, // 57 (9)
0, 0, 0, 0, 0, 0, 0, // 58-64
Nmstart | Nmchar | Hex, // 65 (A)
Nmstart | Nmchar | Hex, // 66 (B)
Nmstart | Nmchar | Hex, // 67 (C)
Nmstart | Nmchar | Hex, // 68 (D)
Nmstart | Nmchar | Hex, // 69 (E)
Nmstart | Nmchar | Hex, // 70 (F)
Nmstart | Nmchar, // 71 (G)
Nmstart | Nmchar, // 72 (H)
Nmstart | Nmchar, // 73 (I)
Nmstart | Nmchar, // 74 (J)
Nmstart | Nmchar, // 75 (K)
Nmstart | Nmchar, // 76 (L)
Nmstart | Nmchar, // 77 (M)
Nmstart | Nmchar, // 78 (N)
Nmstart | Nmchar, // 79 (O)
Nmstart | Nmchar, // 80 (P)
Nmstart | Nmchar, // 81 (Q)
Nmstart | Nmchar, // 82 (R)
Nmstart | Nmchar, // 83 (S)
Nmstart | Nmchar, // 84 (T)
Nmstart | Nmchar, // 85 (U)
Nmstart | Nmchar, // 86 (V)
Nmstart | Nmchar, // 87 (W)
Nmstart | Nmchar, // 88 (X)
Nmstart | Nmchar, // 89 (Y)
Nmstart | Nmchar, // 90 (Z)
0, // 91
Nmstart, // 92 (\)
0, 0, // 93-94
Nmstart | Nmchar, // 95 (_)
0, // 96
Nmstart | Nmchar | Hex, // 97 (a)
Nmstart | Nmchar | Hex, // 98 (b)
Nmstart | Nmchar | Hex, // 99 (c)
Nmstart | Nmchar | Hex, // 100 (d)
Nmstart | Nmchar | Hex, // 101 (e)
Nmstart | Nmchar | Hex, // 102 (f)
Nmstart | Nmchar, // 103 (g)
Nmstart | Nmchar, // 104 (h)
Nmstart | Nmchar, // 105 (i)
Nmstart | Nmchar, // 106 (j)
Nmstart | Nmchar, // 107 (k)
Nmstart | Nmchar, // 108 (l)
Nmstart | Nmchar, // 109 (m)
Nmstart | Nmchar, // 110 (n)
Nmstart | Nmchar, // 111 (o)
Nmstart | Nmchar, // 112 (p)
Nmstart | Nmchar, // 113 (q)
Nmstart | Nmchar, // 114 (r)
Nmstart | Nmchar, // 115 (s)
Nmstart | Nmchar, // 116 (t)
Nmstart | Nmchar, // 117 (u)
Nmstart | Nmchar, // 118 (v)
Nmstart | Nmchar, // 119 (w)
Nmstart | Nmchar, // 120 (x)
Nmstart | Nmchar, // 121 (y)
Nmstart | Nmchar, // 122 (z)
0, 0, 0, 0, 0, // 123-127
// Non-ASCII
Nmstart | Nmchar, // 128
Nmstart | Nmchar, // 129
Nmstart | Nmchar, // 130
Nmstart | Nmchar, // 131
Nmstart | Nmchar, // 132
Nmstart | Nmchar, // 133
Nmstart | Nmchar, // 134
Nmstart | Nmchar, // 135
Nmstart | Nmchar, // 136
Nmstart | Nmchar, // 137
Nmstart | Nmchar, // 138
Nmstart | Nmchar, // 139
Nmstart | Nmchar, // 140
Nmstart | Nmchar, // 141
Nmstart | Nmchar, // 142
Nmstart | Nmchar, // 143
Nmstart | Nmchar, // 144
Nmstart | Nmchar, // 145
Nmstart | Nmchar, // 146
Nmstart | Nmchar, // 147
Nmstart | Nmchar, // 148
Nmstart | Nmchar, // 149
Nmstart | Nmchar, // 150
Nmstart | Nmchar, // 151
Nmstart | Nmchar, // 152
Nmstart | Nmchar, // 153
Nmstart | Nmchar, // 154
Nmstart | Nmchar, // 155
Nmstart | Nmchar, // 156
Nmstart | Nmchar, // 157
Nmstart | Nmchar, // 158
Nmstart | Nmchar, // 159
Nmstart | Nmchar, // 160
Nmstart | Nmchar, // 161
Nmstart | Nmchar, // 162
Nmstart | Nmchar, // 163
Nmstart | Nmchar, // 164
Nmstart | Nmchar, // 165
Nmstart | Nmchar, // 166
Nmstart | Nmchar, // 167
Nmstart | Nmchar, // 168
Nmstart | Nmchar, // 169
Nmstart | Nmchar, // 170
Nmstart | Nmchar, // 171
Nmstart | Nmchar, // 172
Nmstart | Nmchar, // 173
Nmstart | Nmchar, // 174
Nmstart | Nmchar, // 175
Nmstart | Nmchar, // 176
Nmstart | Nmchar, // 177
Nmstart | Nmchar, // 178
Nmstart | Nmchar, // 179
Nmstart | Nmchar, // 180
Nmstart | Nmchar, // 181
Nmstart | Nmchar, // 182
Nmstart | Nmchar, // 183
Nmstart | Nmchar, // 184
Nmstart | Nmchar, // 185
Nmstart | Nmchar, // 186
Nmstart | Nmchar, // 187
Nmstart | Nmchar, // 188
Nmstart | Nmchar, // 189
Nmstart | Nmchar, // 190
Nmstart | Nmchar, // 191
Nmstart | Nmchar, // 192
Nmstart | Nmchar, // 193
Nmstart | Nmchar, // 194
Nmstart | Nmchar, // 195
Nmstart | Nmchar, // 196
Nmstart | Nmchar, // 197
Nmstart | Nmchar, // 198
Nmstart | Nmchar, // 199
Nmstart | Nmchar, // 200
Nmstart | Nmchar, // 201
Nmstart | Nmchar, // 202
Nmstart | Nmchar, // 203
Nmstart | Nmchar, // 204
Nmstart | Nmchar, // 205
Nmstart | Nmchar, // 206
Nmstart | Nmchar, // 207
Nmstart | Nmchar, // 208
Nmstart | Nmchar, // 209
Nmstart | Nmchar, // 210
Nmstart | Nmchar, // 211
Nmstart | Nmchar, // 212
Nmstart | Nmchar, // 213
Nmstart | Nmchar, // 214
Nmstart | Nmchar, // 215
Nmstart | Nmchar, // 216
Nmstart | Nmchar, // 217
Nmstart | Nmchar, // 218
Nmstart | Nmchar, // 219
Nmstart | Nmchar, // 220
Nmstart | Nmchar, // 221
Nmstart | Nmchar, // 222
Nmstart | Nmchar, // 223
Nmstart | Nmchar, // 224
Nmstart | Nmchar, // 225
Nmstart | Nmchar, // 226
Nmstart | Nmchar, // 227
Nmstart | Nmchar, // 228
Nmstart | Nmchar, // 229
Nmstart | Nmchar, // 230
Nmstart | Nmchar, // 231
Nmstart | Nmchar, // 232
Nmstart | Nmchar, // 233
Nmstart | Nmchar, // 234
Nmstart | Nmchar, // 235
Nmstart | Nmchar, // 236
Nmstart | Nmchar, // 237
Nmstart | Nmchar, // 238
Nmstart | Nmchar, // 239
Nmstart | Nmchar, // 240
Nmstart | Nmchar, // 241
Nmstart | Nmchar, // 242
Nmstart | Nmchar, // 243
Nmstart | Nmchar, // 244
Nmstart | Nmchar, // 245
Nmstart | Nmchar, // 246
Nmstart | Nmchar, // 247
Nmstart | Nmchar, // 248
Nmstart | Nmchar, // 249
Nmstart | Nmchar, // 250
Nmstart | Nmchar, // 251
Nmstart | Nmchar, // 252
Nmstart | Nmchar, // 253
Nmstart | Nmchar, // 254
Nmstart | Nmchar // 255
};
+605
View File
@@ -0,0 +1,605 @@
// This is written to exactly parse the `font` shorthand in CSS2:
// https://www.w3.org/TR/CSS22/fonts.html#font-shorthand
// https://www.w3.org/TR/CSS22/syndata.html#tokenization
//
// We may want to update it for CSS 3 (e.g. font-stretch, or updated
// tokenization) but I've only ever seen one or two issues filed in node-canvas
// due to parsing in my 8 years on the project
#include "FontParser.h"
#include "CharData.h"
#include <cctype>
#include <unordered_map>
Token::Token(Type type, std::string value) : type_(type), value_(std::move(value)) {}
Token::Token(Type type, double value) : type_(type), value_(value) {}
Token::Token(Type type) : type_(type), value_(std::string{}) {}
const std::string&
Token::getString() const {
static const std::string empty;
auto* str = std::get_if<std::string>(&value_);
return str ? *str : empty;
}
double
Token::getNumber() const {
auto* num = std::get_if<double>(&value_);
return num ? *num : 0.0f;
}
Tokenizer::Tokenizer(std::string_view input) : input_(input) {}
std::string
Tokenizer::utf8Encode(uint32_t codepoint) {
std::string result;
if (codepoint < 0x80) {
result += static_cast<char>(codepoint);
} else if (codepoint < 0x800) {
result += static_cast<char>((codepoint >> 6) | 0xc0);
result += static_cast<char>((codepoint & 0x3f) | 0x80);
} else if (codepoint < 0x10000) {
result += static_cast<char>((codepoint >> 12) | 0xe0);
result += static_cast<char>(((codepoint >> 6) & 0x3f) | 0x80);
result += static_cast<char>((codepoint & 0x3f) | 0x80);
} else {
result += static_cast<char>((codepoint >> 18) | 0xf0);
result += static_cast<char>(((codepoint >> 12) & 0x3f) | 0x80);
result += static_cast<char>(((codepoint >> 6) & 0x3f) | 0x80);
result += static_cast<char>((codepoint & 0x3f) | 0x80);
}
return result;
}
char
Tokenizer::peek() const {
return position_ < input_.length() ? input_[position_] : '\0';
}
char
Tokenizer::advance() {
return position_ < input_.length() ? input_[position_++] : '\0';
}
Token
Tokenizer::parseNumber() {
enum class State {
Start,
AfterSign,
Digits,
AfterDecimal,
AfterE,
AfterESign,
ExponentDigits
};
size_t start = position_;
size_t ePosition = 0;
State state = State::Start;
bool valid = false;
while (position_ < input_.length()) {
char c = peek();
uint8_t flags = charData[static_cast<uint8_t>(c)];
switch (state) {
case State::Start:
if (flags & CharData::Sign) {
position_++;
state = State::AfterSign;
} else if (flags & CharData::Digit) {
position_++;
state = State::Digits;
valid = true;
} else if (c == '.') {
position_++;
state = State::AfterDecimal;
} else {
goto done;
}
break;
case State::AfterSign:
if (flags & CharData::Digit) {
position_++;
state = State::Digits;
valid = true;
} else if (c == '.') {
position_++;
state = State::AfterDecimal;
} else {
goto done;
}
break;
case State::Digits:
if (flags & CharData::Digit) {
position_++;
} else if (c == '.') {
position_++;
state = State::AfterDecimal;
} else if (c == 'e' || c == 'E') {
ePosition = position_;
position_++;
state = State::AfterE;
valid = false;
} else {
goto done;
}
break;
case State::AfterDecimal:
if (flags & CharData::Digit) {
position_++;
valid = true;
state = State::Digits;
} else {
goto done;
}
break;
case State::AfterE:
if (flags & CharData::Sign) {
position_++;
state = State::AfterESign;
} else if (flags & CharData::Digit) {
position_++;
valid = true;
state = State::ExponentDigits;
} else {
position_ = ePosition;
valid = true;
goto done;
}
break;
case State::AfterESign:
if (flags & CharData::Digit) {
position_++;
valid = true;
state = State::ExponentDigits;
} else {
position_ = ePosition;
valid = true;
goto done;
}
break;
case State::ExponentDigits:
if (flags & CharData::Digit) {
position_++;
} else {
goto done;
}
break;
}
}
done:
if (!valid) {
position_ = start;
return Token(Token::Type::Invalid);
}
std::string number_str(input_.substr(start, position_ - start));
double value = std::stod(number_str);
return Token(Token::Type::Number, value);
}
// Note that identifiers are always lower-case. This helps us make easier/more
// efficient comparisons, but means that font-families specified as identifiers
// will be lower-cased. Since font selection isn't case sensitive, this
// shouldn't ever be a problem.
Token
Tokenizer::parseIdentifier() {
std::string identifier;
auto flags = CharData::Nmstart;
auto start = position_;
while (position_ < input_.length()) {
char c = peek();
if (c == '\\') {
advance();
if (!parseEscape(identifier)) {
position_ = start;
return Token(Token::Type::Invalid);
}
flags = CharData::Nmchar;
} else if (charData[static_cast<uint8_t>(c)] & flags) {
identifier += advance() + (c >= 'A' && c <= 'Z' ? 32 : 0);
flags = CharData::Nmchar;
} else {
break;
}
}
return Token(Token::Type::Identifier, identifier);
}
uint32_t
Tokenizer::parseUnicode() {
uint32_t value = 0;
size_t count = 0;
while (position_ < input_.length() && count < 6) {
char c = peek();
uint32_t digit;
if (c >= '0' && c <= '9') {
digit = c - '0';
} else if (c >= 'a' && c <= 'f') {
digit = c - 'a' + 10;
} else if (c >= 'A' && c <= 'F') {
digit = c - 'A' + 10;
} else {
break;
}
value = value * 16 + digit;
advance();
count++;
}
// Optional whitespace after hex escape
char c = peek();
if (c == '\r') {
advance();
if (peek() == '\n') advance();
} else if (isWhitespace(c)) {
advance();
}
return value;
}
bool
Tokenizer::parseEscape(std::string& str) {
char c = peek();
auto flags = charData[static_cast<uint8_t>(c)];
if (flags & CharData::Hex) {
str += utf8Encode(parseUnicode());
return true;
} else if (!(flags & CharData::Newline) && !(flags & CharData::Hex)) {
str += advance();
return true;
}
return false;
}
Token
Tokenizer::parseString(char quote) {
advance();
std::string value;
auto start = position_;
while (position_ < input_.length()) {
char c = peek();
if (c == quote) {
advance();
return Token(Token::Type::QuotedString, value);
} else if (c == '\\') {
advance();
c = peek();
if (c == '\r') {
advance();
if (peek() == '\n') advance();
} else if (isNewline(c)) {
advance();
} else {
if (!parseEscape(value)) {
position_ = start;
return Token(Token::Type::Invalid);
}
}
} else {
value += advance();
}
}
position_ = start;
return Token(Token::Type::Invalid);
}
Token
Tokenizer::nextToken() {
if (position_ >= input_.length()) {
return Token(Token::Type::EndOfInput);
}
char c = peek();
auto flags = charData[static_cast<uint8_t>(c)];
if (isWhitespace(c)) {
std::string whitespace;
while (position_ < input_.length() && isWhitespace(peek())) {
whitespace += advance();
}
return Token(Token::Type::Whitespace, whitespace);
}
if (flags & CharData::NumStart) {
Token token = parseNumber();
if (token.type() != Token::Type::Invalid) return token;
}
if (flags & CharData::Nmstart) {
Token token = parseIdentifier();
if (token.type() != Token::Type::Invalid) return token;
}
if (c == '"') {
Token token = parseString('"');
if (token.type() != Token::Type::Invalid) return token;
}
if (c == '\'') {
Token token = parseString('\'');
if (token.type() != Token::Type::Invalid) return token;
}
switch (advance()) {
case '/': return Token(Token::Type::Slash);
case ',': return Token(Token::Type::Comma);
case '%': return Token(Token::Type::Percent);
default: return Token(Token::Type::Invalid);
}
}
FontParser::FontParser(std::string_view input)
: tokenizer_(input)
, currentToken_(tokenizer_.nextToken())
, nextToken_(tokenizer_.nextToken()) {}
const std::unordered_map<std::string, uint16_t> FontParser::weightMap = {
{"normal", 400},
{"bold", 700},
{"lighter", 100},
{"bolder", 700}
};
const std::unordered_map<std::string, double> FontParser::unitMap = {
{"cm", 37.8f},
{"mm", 3.78f},
{"in", 96.0f},
{"pt", 96.0f / 72.0f},
{"pc", 96.0f / 6.0f},
{"em", 16.0f},
{"px", 1.0f}
};
void
FontParser::advance() {
currentToken_ = nextToken_;
nextToken_ = tokenizer_.nextToken();
}
void
FontParser::skipWs() {
while (currentToken_.type() == Token::Type::Whitespace) advance();
}
bool
FontParser::check(Token::Type type) const {
return currentToken_.type() == type;
}
bool
FontParser::checkWs() const {
return nextToken_.type() == Token::Type::Whitespace
|| nextToken_.type() == Token::Type::EndOfInput;
}
bool
FontParser::parseFontStyle(FontProperties& props) {
if (check(Token::Type::Identifier)) {
const auto& value = currentToken_.getString();
if (value == "italic") {
props.fontStyle = FontStyle::Italic;
advance();
return true;
} else if (value == "oblique") {
props.fontStyle = FontStyle::Oblique;
advance();
return true;
} else if (value == "normal") {
props.fontStyle = FontStyle::Normal;
advance();
return true;
}
}
return false;
}
bool
FontParser::parseFontVariant(FontProperties& props) {
if (check(Token::Type::Identifier)) {
const auto& value = currentToken_.getString();
if (value == "small-caps") {
props.fontVariant = FontVariant::SmallCaps;
advance();
return true;
} else if (value == "normal") {
props.fontVariant = FontVariant::Normal;
advance();
return true;
}
}
return false;
}
bool
FontParser::parseFontWeight(FontProperties& props) {
if (check(Token::Type::Number)) {
double weightFloat = currentToken_.getNumber();
int weight = static_cast<int>(weightFloat);
if (weight < 1 || weight > 1000) return false;
props.fontWeight = static_cast<uint16_t>(weight);
advance();
return true;
} else if (check(Token::Type::Identifier)) {
const auto& value = currentToken_.getString();
if (auto it = weightMap.find(value); it != weightMap.end()) {
props.fontWeight = it->second;
advance();
return true;
}
}
return false;
}
bool
FontParser::parseFontSize(FontProperties& props) {
if (!check(Token::Type::Number)) return false;
props.fontSize = currentToken_.getNumber();
advance();
double multiplier = 1.0f;
if (check(Token::Type::Identifier)) {
const auto& unit = currentToken_.getString();
if (auto it = unitMap.find(unit); it != unitMap.end()) {
multiplier = it->second;
advance();
} else {
return false;
}
} else if (check(Token::Type::Percent)) {
multiplier = 16.0f / 100.0f;
advance();
} else {
return false;
}
// Technically if we consumed some tokens but couldn't parse the font-size,
// we should rewind the tokenizer, but I don't think the grammar allows for
// any valid alternates in this specific case
props.fontSize *= multiplier;
return true;
}
// line-height is not used by canvas ever, but should still parse
bool
FontParser::parseLineHeight(FontProperties& props) {
if (check(Token::Type::Slash)) {
advance();
skipWs();
if (check(Token::Type::Number)) {
advance();
if (check(Token::Type::Percent)) {
advance();
} else if (check(Token::Type::Identifier)) {
auto identifier = currentToken_.getString();
if (auto it = unitMap.find(identifier); it != unitMap.end()) {
advance();
} else {
return false;
}
} else {
return false;
}
} else if (check(Token::Type::Identifier) && currentToken_.getString() == "normal") {
advance();
} else {
return false;
}
}
return true;
}
bool
FontParser::parseFontFamily(FontProperties& props) {
while (!check(Token::Type::EndOfInput)) {
std::string family = "";
std::string trailingWs = "";
bool found = false;
while (
check(Token::Type::QuotedString) ||
check(Token::Type::Identifier) ||
check(Token::Type::Whitespace)
) {
if (check(Token::Type::Whitespace)) {
if (found) trailingWs += currentToken_.getString();
} else { // Identifier, QuotedString
if (found) {
family += trailingWs;
trailingWs.clear();
}
family += currentToken_.getString();
found = true;
}
advance();
}
if (!found) return false; // only whitespace or non-id/string found
props.fontFamily.push_back(family);
if (check(Token::Type::Comma)) advance();
}
return true;
}
FontProperties
FontParser::parse(const std::string& fontString, bool* success) {
FontParser parser(fontString);
auto result = parser.parseFont();
if (success) *success = !parser.hasError_;
return result;
}
FontProperties
FontParser::parseFont() {
FontProperties props;
uint8_t state = 0b111;
skipWs();
for (size_t i = 0; i < 3 && checkWs(); i++) {
if ((state & 0b001) && parseFontStyle(props)) {
state &= 0b110;
goto match;
}
if ((state & 0b010) && parseFontVariant(props)) {
state &= 0b101;
goto match;
}
if ((state & 0b100) && parseFontWeight(props)) {
state &= 0b011;
goto match;
}
break; // all attempts exhausted
match: skipWs(); // success: move to the next non-ws token
}
if (parseFontSize(props)) {
skipWs();
if (parseLineHeight(props) && parseFontFamily(props)) {
return props;
}
}
hasError_ = true;
return props;
}
+115
View File
@@ -0,0 +1,115 @@
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <memory>
#include <variant>
#include <unordered_map>
#include "CharData.h"
enum class FontStyle {
Normal,
Italic,
Oblique
};
enum class FontVariant {
Normal,
SmallCaps
};
struct FontProperties {
double fontSize{16.0f};
std::vector<std::string> fontFamily;
uint16_t fontWeight{400};
FontVariant fontVariant{FontVariant::Normal};
FontStyle fontStyle{FontStyle::Normal};
};
class Token {
public:
enum class Type {
Invalid,
Number,
Percent,
Identifier,
Slash,
Comma,
QuotedString,
Whitespace,
EndOfInput
};
Token(Type type, std::string value);
Token(Type type, double value);
Token(Type type);
Type type() const { return type_; }
const std::string& getString() const;
double getNumber() const;
private:
Type type_;
std::variant<std::string, double> value_;
};
class Tokenizer {
public:
Tokenizer(std::string_view input);
Token nextToken();
private:
std::string_view input_;
size_t position_{0};
// Util
std::string utf8Encode(uint32_t codepoint);
inline bool isWhitespace(char c) const {
return charData[static_cast<uint8_t>(c)] & CharData::Whitespace;
}
inline bool isNewline(char c) const {
return charData[static_cast<uint8_t>(c)] & CharData::Newline;
}
// Moving through the string
char peek() const;
char advance();
// Tokenize
Token parseNumber();
Token parseIdentifier();
uint32_t parseUnicode();
bool parseEscape(std::string& str);
Token parseString(char quote);
};
class FontParser {
public:
static FontProperties parse(const std::string& fontString, bool* success = nullptr);
private:
static const std::unordered_map<std::string, uint16_t> weightMap;
static const std::unordered_map<std::string, double> unitMap;
FontParser(std::string_view input);
void advance();
void skipWs();
bool check(Token::Type type) const;
bool checkWs() const;
bool parseFontStyle(FontProperties& props);
bool parseFontVariant(FontProperties& props);
bool parseFontWeight(FontProperties& props);
bool parseFontSize(FontProperties& props);
bool parseLineHeight(FontProperties& props);
bool parseFontFamily(FontProperties& props);
FontProperties parseFont();
Tokenizer tokenizer_;
Token currentToken_;
Token nextToken_;
bool hasError_{false};
};
+1719
View File
@@ -0,0 +1,1719 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#include "Image.h"
#include "InstanceData.h"
#include "bmp/BMPParser.h"
#include "Canvas.h"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <node_buffer.h>
#include <sys/stat.h>
/* Cairo limit:
* https://lists.cairographics.org/archives/cairo/2010-December/021422.html
*/
static constexpr int canvas_max_side = (1 << 15) - 1;
#ifdef HAVE_GIF
typedef struct {
uint8_t *buf;
unsigned len;
unsigned pos;
} gif_data_t;
#endif
#ifdef HAVE_JPEG
#include <csetjmp>
struct canvas_jpeg_error_mgr: jpeg_error_mgr {
Image* image;
jmp_buf setjmp_buffer;
};
#endif
/*
* Read closure used by loadFromBuffer.
*/
typedef struct {
Napi::Env env;
unsigned len;
uint8_t *buf;
} read_closure_t;
/*
* Initialize Image.
*/
void
Image::Initialize(Napi::Env& env, Napi::Object& exports) {
InstanceData *data = env.GetInstanceData<InstanceData>();
Napi::HandleScope scope(env);
Napi::Function ctor = DefineClass(env, "Image", {
InstanceAccessor<&Image::GetComplete>("complete", napi_default_jsproperty),
InstanceAccessor<&Image::GetWidth, &Image::SetWidth>("width", napi_default_jsproperty),
InstanceAccessor<&Image::GetHeight, &Image::SetHeight>("height", napi_default_jsproperty),
InstanceAccessor<&Image::GetNaturalWidth>("naturalWidth", napi_default_jsproperty),
InstanceAccessor<&Image::GetNaturalHeight>("naturalHeight", napi_default_jsproperty),
InstanceAccessor<&Image::GetDataMode, &Image::SetDataMode>("dataMode", napi_default_jsproperty),
StaticValue("MODE_IMAGE", Napi::Number::New(env, DATA_IMAGE), napi_default_jsproperty),
StaticValue("MODE_MIME", Napi::Number::New(env, DATA_MIME), napi_default_jsproperty)
});
// Used internally in lib/image.js
exports.Set("GetSource", Napi::Function::New(env, &GetSource));
exports.Set("SetSource", Napi::Function::New(env, &SetSource));
data->ImageCtor = Napi::Persistent(ctor);
exports.Set("Image", ctor);
}
/*
* Initialize a new Image.
*/
Image::Image(const Napi::CallbackInfo& info) : ObjectWrap<Image>(info), env(info.Env()) {
data_mode = DATA_IMAGE;
info.This().ToObject().Unwrap().Set("onload", env.Null());
info.This().ToObject().Unwrap().Set("onerror", env.Null());
filename = NULL;
_data = nullptr;
_data_len = 0;
_surface = NULL;
width = height = 0;
naturalWidth = naturalHeight = 0;
state = DEFAULT;
#ifdef HAVE_RSVG
_rsvg = NULL;
_is_svg = false;
_svg_last_width = _svg_last_height = 0;
#endif
}
/*
* Get complete boolean.
*/
Napi::Value
Image::GetComplete(const Napi::CallbackInfo& info) {
return Napi::Boolean::New(env, true);
}
/*
* Get dataMode.
*/
Napi::Value
Image::GetDataMode(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, data_mode);
}
/*
* Set dataMode.
*/
void
Image::SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (value.IsNumber()) {
int mode = value.As<Napi::Number>().Uint32Value();
data_mode = (data_mode_t) mode;
}
}
/*
* Get natural width
*/
Napi::Value
Image::GetNaturalWidth(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, naturalWidth);
}
/*
* Get width.
*/
Napi::Value
Image::GetWidth(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, width);
}
/*
* Set width.
*/
void
Image::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (value.IsNumber()) {
width = value.As<Napi::Number>().Uint32Value();
}
}
/*
* Get natural height
*/
Napi::Value
Image::GetNaturalHeight(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, naturalHeight);
}
/*
* Get height.
*/
Napi::Value
Image::GetHeight(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, height);
}
/*
* Set height.
*/
void
Image::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) {
if (value.IsNumber()) {
height = value.As<Napi::Number>().Uint32Value();
}
}
/*
* Get src path.
*/
Napi::Value
Image::GetSource(const Napi::CallbackInfo& info){
Napi::Env env = info.Env();
Image *img = Image::Unwrap(info.This().As<Napi::Object>());
return Napi::String::New(env, img->filename ? img->filename : "");
}
/*
* Clean up assets and variables.
*/
void
Image::clearData() {
if (_surface) {
cairo_surface_destroy(_surface);
Napi::MemoryManagement::AdjustExternalMemory(env, -_data_len);
_data_len = 0;
_surface = NULL;
}
delete[] _data;
_data = nullptr;
free(filename);
filename = NULL;
#ifdef HAVE_RSVG
if (_rsvg != NULL) {
g_object_unref(_rsvg);
_rsvg = NULL;
}
#endif
width = height = 0;
naturalWidth = naturalHeight = 0;
state = DEFAULT;
}
/*
* Set src path.
*/
void
Image::SetSource(const Napi::CallbackInfo& info){
Napi::Env env = info.Env();
Napi::Object This = info.This().As<Napi::Object>();
Image *img = Image::Unwrap(This);
cairo_status_t status = CAIRO_STATUS_READ_ERROR;
Napi::Value value = info[0];
img->clearData();
// Clear errno in case some unrelated previous syscall failed
errno = 0;
// url string
if (value.IsString()) {
std::string src = value.As<Napi::String>().Utf8Value();
if (img->filename) free(img->filename);
img->filename = strdup(src.c_str());
status = img->load();
// Buffer
} else if (value.IsBuffer()) {
uint8_t *buf = value.As<Napi::Buffer<uint8_t>>().Data();
unsigned len = value.As<Napi::Buffer<uint8_t>>().Length();
status = img->loadFromBuffer(buf, len);
}
if (status) {
Napi::Value onerrorFn;
if (This.Get("onerror").UnwrapTo(&onerrorFn) && onerrorFn.IsFunction()) {
Napi::Error arg;
if (img->errorInfo.empty()) {
arg = Napi::Error::New(env, Napi::String::New(env, cairo_status_to_string(status)));
} else {
arg = img->errorInfo.toError(env);
}
onerrorFn.As<Napi::Function>().Call({ arg.Value() });
}
} else {
img->loaded();
Napi::Value onloadFn;
if (This.Get("onload").UnwrapTo(&onloadFn) && onloadFn.IsFunction()) {
onloadFn.As<Napi::Function>().Call({});
}
}
}
/*
* Load image data from `buf` by sniffing
* the bytes to determine format.
*/
cairo_status_t
Image::loadFromBuffer(uint8_t *buf, unsigned len) {
if (len == 0) return CAIRO_STATUS_READ_ERROR;
uint8_t data[4] = {0};
memcpy(data, buf, (len < 4 ? len : 4) * sizeof(uint8_t));
if (isPNG(data)) return loadPNGFromBuffer(buf);
if (isGIF(data)) {
#ifdef HAVE_GIF
return loadGIFFromBuffer(buf, len);
#else
this->errorInfo.set("node-canvas was built without GIF support");
return CAIRO_STATUS_READ_ERROR;
#endif
}
if (isJPEG(data)) {
#ifdef HAVE_JPEG
if (DATA_IMAGE == data_mode) return loadJPEGFromBuffer(buf, len);
if (DATA_MIME == data_mode) return decodeJPEGBufferIntoMimeSurface(buf, len);
if ((DATA_IMAGE | DATA_MIME) == data_mode) {
cairo_status_t status;
status = loadJPEGFromBuffer(buf, len);
if (status) return status;
return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG);
}
#else // HAVE_JPEG
this->errorInfo.set("node-canvas was built without JPEG support");
return CAIRO_STATUS_READ_ERROR;
#endif
}
// confirm svg using first 1000 chars
// if a very long comment precedes the root <svg> tag, isSVG returns false
unsigned head_len = (len < 1000 ? len : 1000);
if (isSVG(buf, head_len)) {
#ifdef HAVE_RSVG
return loadSVGFromBuffer(buf, len);
#else
this->errorInfo.set("node-canvas was built without SVG support");
return CAIRO_STATUS_READ_ERROR;
#endif
}
if (isBMP(buf, len))
return loadBMPFromBuffer(buf, len);
this->errorInfo.set("Unsupported image type");
return CAIRO_STATUS_READ_ERROR;
}
/*
* Load PNG data from `buf`.
*/
cairo_status_t
Image::loadPNGFromBuffer(uint8_t *buf) {
read_closure_t closure{ env, 0, buf };
_surface = cairo_image_surface_create_from_png_stream(readPNG, &closure);
cairo_status_t status = cairo_surface_status(_surface);
if (status) return status;
return CAIRO_STATUS_SUCCESS;
}
/*
* Read PNG data.
*/
cairo_status_t
Image::readPNG(void *c, uint8_t *data, unsigned int len) {
read_closure_t *closure = (read_closure_t *) c;
memcpy(data, closure->buf + closure->len, len);
closure->len += len;
return CAIRO_STATUS_SUCCESS;
}
/*
* Destroy image and associated surface.
*/
Image::~Image() {
clearData();
}
/*
* Initiate image loading.
*/
cairo_status_t
Image::load() {
if (LOADING != state) {
state = LOADING;
return loadSurface();
}
return CAIRO_STATUS_READ_ERROR;
}
/*
* Set state, assign dimensions.
*/
void
Image::loaded() {
Napi::HandleScope scope(env);
state = COMPLETE;
width = naturalWidth = cairo_image_surface_get_width(_surface);
height = naturalHeight = cairo_image_surface_get_height(_surface);
_data_len = naturalHeight * cairo_image_surface_get_stride(_surface);
Napi::MemoryManagement::AdjustExternalMemory(env, _data_len);
}
/*
* Returns this image's surface.
*/
cairo_surface_t *Image::surface() {
#ifdef HAVE_RSVG
if (_is_svg && (_svg_last_width != width || _svg_last_height != height)) {
if (_surface != NULL) {
cairo_surface_destroy(_surface);
_surface = NULL;
}
cairo_status_t status = renderSVGToSurface();
if (status != CAIRO_STATUS_SUCCESS) {
g_object_unref(_rsvg);
Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException();
return NULL;
}
}
#endif
return _surface;
}
/*
* Load cairo surface from the image src.
*
* TODO: support more formats
* TODO: use node IO or at least thread pool
*/
cairo_status_t
Image::loadSurface() {
FILE *stream = fopen(filename, "rb");
if (!stream) {
this->errorInfo.set(NULL, "fopen", errno, filename);
return CAIRO_STATUS_READ_ERROR;
}
uint8_t buf[5];
if (1 != fread(&buf, 5, 1, stream)) {
fclose(stream);
return CAIRO_STATUS_READ_ERROR;
}
rewind(stream);
// png
if (isPNG(buf)) {
fclose(stream);
return loadPNG();
}
if (isGIF(buf)) {
#ifdef HAVE_GIF
return loadGIF(stream);
#else
this->errorInfo.set("node-canvas was built without GIF support");
return CAIRO_STATUS_READ_ERROR;
#endif
}
if (isJPEG(buf)) {
#ifdef HAVE_JPEG
return loadJPEG(stream);
#else
this->errorInfo.set("node-canvas was built without JPEG support");
return CAIRO_STATUS_READ_ERROR;
#endif
}
// confirm svg using first 1000 chars
// if a very long comment precedes the root <svg> tag, isSVG returns false
uint8_t head[1000] = {0};
fseek(stream, 0 , SEEK_END);
long len = ftell(stream);
unsigned head_len = (len < 1000 ? len : 1000);
unsigned head_size = head_len * sizeof(uint8_t);
rewind(stream);
if (head_size != fread(&head, 1, head_size, stream)) {
fclose(stream);
return CAIRO_STATUS_READ_ERROR;
}
rewind(stream);
if (isSVG(head, head_len)) {
#ifdef HAVE_RSVG
return loadSVG(stream);
#else
this->errorInfo.set("node-canvas was built without SVG support");
return CAIRO_STATUS_READ_ERROR;
#endif
}
if (isBMP(buf, 2))
return loadBMP(stream);
fclose(stream);
this->errorInfo.set("Unsupported image type");
return CAIRO_STATUS_READ_ERROR;
}
/*
* Load PNG.
*/
cairo_status_t
Image::loadPNG() {
_surface = cairo_image_surface_create_from_png(filename);
return cairo_surface_status(_surface);
}
// GIF support
#ifdef HAVE_GIF
/*
* Return the alpha color for `gif` at `frame`, or -1.
*/
int
get_gif_transparent_color(GifFileType *gif, int frame) {
ExtensionBlock *ext = gif->SavedImages[frame].ExtensionBlocks;
int len = gif->SavedImages[frame].ExtensionBlockCount;
for (int x = 0; x < len; ++x, ++ext) {
if ((ext->Function == GRAPHICS_EXT_FUNC_CODE) && (ext->Bytes[0] & 1)) {
return ext->Bytes[3] == 0 ? 0 : (uint8_t) ext->Bytes[3];
}
}
return -1;
}
/*
* Memory GIF reader callback.
*/
int
read_gif_from_memory(GifFileType *gif, GifByteType *buf, int len) {
gif_data_t *data = (gif_data_t *) gif->UserData;
if ((data->pos + len) > data->len) len = data->len - data->pos;
memcpy(buf, data->pos + data->buf, len);
data->pos += len;
return len;
}
/*
* Load GIF.
*/
cairo_status_t
Image::loadGIF(FILE *stream) {
struct stat s;
int fd = fileno(stream);
// stat
if (fstat(fd, &s) < 0) {
fclose(stream);
return CAIRO_STATUS_READ_ERROR;
}
uint8_t *buf = (uint8_t *) malloc(s.st_size);
if (!buf) {
fclose(stream);
this->errorInfo.set(NULL, "malloc", errno);
return CAIRO_STATUS_NO_MEMORY;
}
size_t read = fread(buf, s.st_size, 1, stream);
fclose(stream);
cairo_status_t result = CAIRO_STATUS_READ_ERROR;
if (1 == read) result = loadGIFFromBuffer(buf, s.st_size);
free(buf);
return result;
}
/*
* Load give from `buf` and the given `len`.
*/
cairo_status_t
Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) {
int i = 0;
GifFileType* gif;
gif_data_t gifd = { buf, len, 0 };
#if GIFLIB_MAJOR >= 5
int errorcode;
if ((gif = DGifOpen((void*) &gifd, read_gif_from_memory, &errorcode)) == NULL)
return CAIRO_STATUS_READ_ERROR;
#else
if ((gif = DGifOpen((void*) &gifd, read_gif_from_memory)) == NULL)
return CAIRO_STATUS_READ_ERROR;
#endif
if (GIF_OK != DGifSlurp(gif)) {
GIF_CLOSE_FILE(gif);
return CAIRO_STATUS_READ_ERROR;
}
if (gif->SWidth > canvas_max_side || gif->SHeight > canvas_max_side) {
GIF_CLOSE_FILE(gif);
return CAIRO_STATUS_INVALID_SIZE;
}
width = naturalWidth = gif->SWidth;
height = naturalHeight = gif->SHeight;
uint8_t *data = new uint8_t[naturalWidth * naturalHeight * 4];
if (!data) {
GIF_CLOSE_FILE(gif);
this->errorInfo.set(NULL, "malloc", errno);
return CAIRO_STATUS_NO_MEMORY;
}
GifImageDesc *img = &gif->SavedImages[i].ImageDesc;
// local colormap takes precedence over global
ColorMapObject *colormap = img->ColorMap
? img->ColorMap
: gif->SColorMap;
if (colormap == nullptr) {
GIF_CLOSE_FILE(gif);
return CAIRO_STATUS_READ_ERROR;
}
int bgColor = 0;
int alphaColor = get_gif_transparent_color(gif, i);
if (gif->SColorMap) bgColor = (uint8_t) gif->SBackGroundColor;
else if(alphaColor >= 0) bgColor = alphaColor;
uint8_t *src_data = (uint8_t*) gif->SavedImages[i].RasterBits;
uint32_t *dst_data = (uint32_t*) data;
if (!gif->Image.Interlace) {
if (naturalWidth == img->Width && naturalHeight == img->Height) {
for (int y = 0; y < naturalHeight; ++y) {
for (int x = 0; x < naturalWidth; ++x) {
*dst_data = ((*src_data == alphaColor) ? 0 : 255) << 24
| colormap->Colors[*src_data].Red << 16
| colormap->Colors[*src_data].Green << 8
| colormap->Colors[*src_data].Blue;
dst_data++;
src_data++;
}
}
} else {
// Image does not take up whole "screen" so we need to fill-in the background
int bottom = img->Top + img->Height;
int right = img->Left + img->Width;
uint32_t bgPixel =
((bgColor == alphaColor) ? 0 : 255) << 24
| colormap->Colors[bgColor].Red << 16
| colormap->Colors[bgColor].Green << 8
| colormap->Colors[bgColor].Blue;
for (int y = 0; y < naturalHeight; ++y) {
for (int x = 0; x < naturalWidth; ++x) {
if (y < img->Top || y >= bottom || x < img->Left || x >= right) {
*dst_data = bgPixel;
dst_data++;
} else {
*dst_data = ((*src_data == alphaColor) ? 0 : 255) << 24
| colormap->Colors[*src_data].Red << 16
| colormap->Colors[*src_data].Green << 8
| colormap->Colors[*src_data].Blue;
dst_data++;
src_data++;
}
}
}
}
} else {
// Image is interlaced so that it streams nice over 14.4k and 28.8k modems :)
// We first load in 1/8 of the image, followed by another 1/8, followed by
// 1/4 and finally the remaining 1/2.
int ioffs[] = { 0, 4, 2, 1 };
int ijumps[] = { 8, 8, 4, 2 };
uint8_t *src_ptr = src_data;
uint32_t *dst_ptr;
for(int z = 0; z < 4; z++) {
for(int y = ioffs[z]; y < naturalHeight; y += ijumps[z]) {
dst_ptr = dst_data + naturalWidth * y;
for(int x = 0; x < naturalWidth; ++x) {
*dst_ptr = ((*src_ptr == alphaColor) ? 0 : 255) << 24
| (colormap->Colors[*src_ptr].Red) << 16
| (colormap->Colors[*src_ptr].Green) << 8
| (colormap->Colors[*src_ptr].Blue);
dst_ptr++;
src_ptr++;
}
}
}
}
GIF_CLOSE_FILE(gif);
// New image surface
_surface = cairo_image_surface_create_for_data(
data
, CAIRO_FORMAT_ARGB32
, naturalWidth
, naturalHeight
, cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth));
cairo_status_t status = cairo_surface_status(_surface);
if (status) {
delete[] data;
return status;
}
_data = data;
return CAIRO_STATUS_SUCCESS;
}
#endif /* HAVE_GIF */
// JPEG support
#ifdef HAVE_JPEG
// libjpeg 6.2 does not have jpeg_mem_src; define it ourselves here unless
// libjpeg 8 is installed.
#if JPEG_LIB_VERSION < 80 && !defined(MEM_SRCDST_SUPPORTED)
/* Read JPEG image from a memory segment */
static void
init_source(j_decompress_ptr cinfo) {}
static boolean
fill_input_buffer(j_decompress_ptr cinfo) {
ERREXIT(cinfo, JERR_INPUT_EMPTY);
return TRUE;
}
static void
skip_input_data(j_decompress_ptr cinfo, long num_bytes) {
struct jpeg_source_mgr* src = (struct jpeg_source_mgr*) cinfo->src;
if (num_bytes > 0) {
src->next_input_byte += (size_t) num_bytes;
src->bytes_in_buffer -= (size_t) num_bytes;
}
}
static void term_source (j_decompress_ptr cinfo) {}
static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) {
struct jpeg_source_mgr* src;
if (cinfo->src == NULL) {
cinfo->src = (struct jpeg_source_mgr *)
(*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_PERMANENT,
sizeof(struct jpeg_source_mgr));
}
src = (struct jpeg_source_mgr*) cinfo->src;
src->init_source = init_source;
src->fill_input_buffer = fill_input_buffer;
src->skip_input_data = skip_input_data;
src->resync_to_restart = jpeg_resync_to_restart; /* use default method */
src->term_source = term_source;
src->bytes_in_buffer = nbytes;
src->next_input_byte = (JOCTET*)buffer;
}
#endif
class BufferReader : public Image::Reader {
public:
BufferReader(uint8_t* buf, unsigned len) : _buf(buf), _len(len), _idx(0) {}
bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); }
uint8_t getNext() override {
return _buf[_idx++];
}
void skipBytes(unsigned n) override { _idx += n; }
private:
uint8_t* _buf; // we do not own this
unsigned _len;
unsigned _idx;
};
class StreamReader : public Image::Reader {
public:
StreamReader(FILE *stream) : _stream(stream), _len(0), _idx(0) {
fseek(_stream, 0, SEEK_END);
_len = ftell(_stream);
fseek(_stream, 0, SEEK_SET);
}
bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); }
uint8_t getNext() override {
++_idx;
return getc(_stream);
}
void skipBytes(unsigned n) override {
_idx += n;
fseek(_stream, _idx, SEEK_SET);
}
private:
FILE* _stream;
unsigned _len;
unsigned _idx;
};
void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) {
int stride = naturalWidth * 4;
for (int y = 0; y < naturalHeight; ++y) {
jpeg_read_scanlines(args, &src, 1);
uint32_t *row = (uint32_t*)(data + stride * y);
for (int x = 0; x < naturalWidth; ++x) {
int bx = args->output_components * x;
row[x] = decode(src + bx);
}
}
}
/*
* Takes an initialised jpeg_decompress_struct and decodes the
* data into _surface.
*/
cairo_status_t
Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args, Orientation orientation) {
const int channels = 4;
cairo_status_t status = CAIRO_STATUS_SUCCESS;
uint8_t *data = new uint8_t[naturalWidth * naturalHeight * channels];
if (!data) {
jpeg_abort_decompress(args);
jpeg_destroy_decompress(args);
this->errorInfo.set(NULL, "malloc", errno);
return CAIRO_STATUS_NO_MEMORY;
}
uint8_t *src = new uint8_t[naturalWidth * args->output_components];
if (!src) {
free(data);
jpeg_abort_decompress(args);
jpeg_destroy_decompress(args);
this->errorInfo.set(NULL, "malloc", errno);
return CAIRO_STATUS_NO_MEMORY;
}
// These are the three main cases to handle. libjpeg converts YCCK to CMYK
// and YCbCr to RGB by default.
switch (args->out_color_space) {
case JCS_CMYK:
jpegToARGB(args, data, src, [](uint8_t const* src) {
uint16_t k = static_cast<uint16_t>(src[3]);
uint8_t r = k * src[0] / 255;
uint8_t g = k * src[1] / 255;
uint8_t b = k * src[2] / 255;
return 255 << 24 | r << 16 | g << 8 | b;
});
break;
case JCS_RGB:
jpegToARGB(args, data, src, [](uint8_t const* src) {
uint8_t r = src[0], g = src[1], b = src[2];
return 255 << 24 | r << 16 | g << 8 | b;
});
break;
case JCS_GRAYSCALE:
jpegToARGB(args, data, src, [](uint8_t const* src) {
uint8_t v = src[0];
return 255 << 24 | v << 16 | v << 8 | v;
});
break;
default:
this->errorInfo.set("Unsupported JPEG encoding");
status = CAIRO_STATUS_READ_ERROR;
break;
}
updateDimensionsForOrientation(orientation);
if (!status) {
_surface = cairo_image_surface_create_for_data(
data
, CAIRO_FORMAT_ARGB32
, naturalWidth
, naturalHeight
, cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth));
}
jpeg_finish_decompress(args);
jpeg_destroy_decompress(args);
status = cairo_surface_status(_surface);
rotatePixels(data, naturalWidth, naturalHeight, channels, orientation);
delete[] src;
if (status) {
delete[] data;
return status;
}
_data = data;
return CAIRO_STATUS_SUCCESS;
}
/*
* Callback to recover from jpeg errors
*/
static void canvas_jpeg_error_exit(j_common_ptr cinfo) {
canvas_jpeg_error_mgr *cjerr = static_cast<canvas_jpeg_error_mgr*>(cinfo->err);
cjerr->output_message(cinfo);
// Return control to the setjmp point
longjmp(cjerr->setjmp_buffer, 1);
}
// Capture libjpeg errors instead of writing stdout
static void canvas_jpeg_output_message(j_common_ptr cinfo) {
canvas_jpeg_error_mgr *cjerr = static_cast<canvas_jpeg_error_mgr*>(cinfo->err);
char buff[JMSG_LENGTH_MAX];
cjerr->format_message(cinfo, buff);
// (Only the last message will be returned to JS land.)
cjerr->image->errorInfo.set(buff);
}
/*
* Takes a jpeg data buffer and assigns it as mime data to a
* dummy surface
*/
cairo_status_t
Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) {
// TODO: remove this duplicate logic
// JPEG setup
struct jpeg_decompress_struct args;
struct canvas_jpeg_error_mgr err;
err.image = this;
args.err = jpeg_std_error(&err);
args.err->error_exit = canvas_jpeg_error_exit;
args.err->output_message = canvas_jpeg_output_message;
// Establish the setjmp return context for canvas_jpeg_error_exit to use
if (setjmp(err.setjmp_buffer)) {
// If we get here, the JPEG code has signaled an error.
// We need to clean up the JPEG object, close the input file, and return.
jpeg_destroy_decompress(&args);
return CAIRO_STATUS_READ_ERROR;
}
jpeg_create_decompress(&args);
jpeg_mem_src(&args, buf, len);
jpeg_read_header(&args, 1);
jpeg_start_decompress(&args);
width = naturalWidth = args.output_width;
height = naturalHeight = args.output_height;
// Data alloc
// 8 pixels per byte using Alpha Channel format to reduce memory requirement.
int buf_size = naturalHeight * cairo_format_stride_for_width(CAIRO_FORMAT_A1, naturalWidth);
uint8_t *data = new uint8_t[buf_size];
if (!data) {
this->errorInfo.set(NULL, "malloc", errno);
return CAIRO_STATUS_NO_MEMORY;
}
BufferReader reader(buf, len);
Orientation orientation = getExifOrientation(reader);
updateDimensionsForOrientation(orientation);
// New image surface
_surface = cairo_image_surface_create_for_data(
data
, CAIRO_FORMAT_A1
, naturalWidth
, naturalHeight
, cairo_format_stride_for_width(CAIRO_FORMAT_A1, naturalWidth));
// Cleanup
jpeg_abort_decompress(&args);
jpeg_destroy_decompress(&args);
cairo_status_t status = cairo_surface_status(_surface);
if (status) {
delete[] data;
return status;
}
rotatePixels(data, naturalWidth, naturalHeight, 1, orientation);
_data = data;
return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG);
}
/*
* Helper function for disposing of a mime data closure.
*/
void
clearMimeData(void *closure) {
Napi::MemoryManagement::AdjustExternalMemory(
static_cast<read_closure_t *>(closure)->env,
-static_cast<int>((static_cast<read_closure_t *>(closure)->len)));
free(static_cast<read_closure_t *>(closure)->buf);
free(closure);
}
/*
* Assign a given buffer as mime data against the surface.
* The provided buffer will be copied, and the copy will
* be automatically freed when the surface is destroyed.
*/
cairo_status_t
Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) {
uint8_t *mime_data = (uint8_t *) malloc(len);
if (!mime_data) {
this->errorInfo.set(NULL, "malloc", errno);
return CAIRO_STATUS_NO_MEMORY;
}
read_closure_t *mime_closure = (read_closure_t *) malloc(sizeof(read_closure_t));
if (!mime_closure) {
free(mime_data);
this->errorInfo.set(NULL, "malloc", errno);
return CAIRO_STATUS_NO_MEMORY;
}
memcpy(mime_data, data, len);
mime_closure->env = env;
mime_closure->buf = mime_data;
mime_closure->len = len;
Napi::MemoryManagement::AdjustExternalMemory(env, len);
return cairo_surface_set_mime_data(_surface
, mime_type
, mime_data
, len
, clearMimeData
, mime_closure);
}
/*
* Load jpeg from buffer.
*/
cairo_status_t
Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) {
BufferReader reader(buf, len);
Orientation orientation = getExifOrientation(reader);
// TODO: remove this duplicate logic
// JPEG setup
struct jpeg_decompress_struct args;
struct canvas_jpeg_error_mgr err;
err.image = this;
args.err = jpeg_std_error(&err);
args.err->error_exit = canvas_jpeg_error_exit;
args.err->output_message = canvas_jpeg_output_message;
// Establish the setjmp return context for canvas_jpeg_error_exit to use
if (setjmp(err.setjmp_buffer)) {
// If we get here, the JPEG code has signaled an error.
// We need to clean up the JPEG object, close the input file, and return.
jpeg_destroy_decompress(&args);
return CAIRO_STATUS_READ_ERROR;
}
jpeg_create_decompress(&args);
jpeg_mem_src(&args, buf, len);
jpeg_read_header(&args, 1);
jpeg_start_decompress(&args);
width = naturalWidth = args.output_width;
height = naturalHeight = args.output_height;
return decodeJPEGIntoSurface(&args, orientation);
}
/*
* Load JPEG, convert RGB to ARGB.
*/
cairo_status_t
Image::loadJPEG(FILE *stream) {
cairo_status_t status;
#if defined(_MSC_VER)
if (false) { // Force using loadJPEGFromBuffer
#else
if (data_mode == DATA_IMAGE) { // Can lazily read in the JPEG.
#endif
Orientation orientation = NORMAL;
{
StreamReader reader(stream);
orientation = getExifOrientation(reader);
rewind(stream);
}
// JPEG setup
struct jpeg_decompress_struct args;
struct canvas_jpeg_error_mgr err;
err.image = this;
args.err = jpeg_std_error(&err);
args.err->error_exit = canvas_jpeg_error_exit;
args.err->output_message = canvas_jpeg_output_message;
// Establish the setjmp return context for canvas_jpeg_error_exit to use
if (setjmp(err.setjmp_buffer)) {
// If we get here, the JPEG code has signaled an error.
// We need to clean up the JPEG object, close the input file, and return.
jpeg_destroy_decompress(&args);
return CAIRO_STATUS_READ_ERROR;
}
jpeg_create_decompress(&args);
jpeg_stdio_src(&args, stream);
jpeg_read_header(&args, 1);
jpeg_start_decompress(&args);
if (args.output_width > canvas_max_side || args.output_height > canvas_max_side) {
jpeg_destroy_decompress(&args);
return CAIRO_STATUS_INVALID_SIZE;
}
width = naturalWidth = args.output_width;
height = naturalHeight = args.output_height;
status = decodeJPEGIntoSurface(&args, orientation);
fclose(stream);
} else { // We'll need the actual source jpeg data, so read fully.
uint8_t *buf;
unsigned len;
fseek(stream, 0, SEEK_END);
len = ftell(stream);
fseek(stream, 0, SEEK_SET);
buf = (uint8_t *) malloc(len);
if (!buf) {
this->errorInfo.set(NULL, "malloc", errno);
return CAIRO_STATUS_NO_MEMORY;
}
if (fread(buf, len, 1, stream) != 1) {
status = CAIRO_STATUS_READ_ERROR;
} else if ((DATA_IMAGE | DATA_MIME) == data_mode) {
status = loadJPEGFromBuffer(buf, len);
if (!status) status = assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG);
} else if (DATA_MIME == data_mode) {
status = decodeJPEGBufferIntoMimeSurface(buf, len);
}
#if defined(_MSC_VER)
else if (DATA_IMAGE == data_mode) {
status = loadJPEGFromBuffer(buf, len);
}
#endif
else {
status = CAIRO_STATUS_READ_ERROR;
}
fclose(stream);
free(buf);
}
return status;
}
/*
* Returns the Exif orientation if one exists, otherwise returns NORMAL
*/
Image::Orientation
Image::getExifOrientation(Reader& jpeg) {
static const char kJpegStartOfImage = (char)0xd8;
static const char kJpegStartOfFrameBaseline = (char)0xc0;
static const char kJpegStartOfFrameProgressive = (char)0xc2;
static const char kJpegHuffmanTable = (char)0xc4;
static const char kJpegQuantizationTable = (char)0xdb;
static const char kJpegRestartInterval = (char)0xdd;
static const char kJpegComment = (char)0xfe;
static const char kJpegStartOfScan = (char)0xda;
static const char kJpegApp0 = (char)0xe0;
static const char kJpegApp1 = (char)0xe1;
// Find the Exif tag (if it exists)
int exif_len = 0;
bool done = false;
while (!done && jpeg.hasBytes(1)) {
while (jpeg.hasBytes(1) && jpeg.getNext() != 0xff) {
// noop
}
if (jpeg.hasBytes(1)) {
char tag = jpeg.getNext();
switch (tag) {
case kJpegStartOfImage:
break; // beginning of file, no extra bytes
case kJpegRestartInterval:
jpeg.skipBytes(4);
break;
case kJpegStartOfFrameBaseline:
case kJpegStartOfFrameProgressive:
case kJpegHuffmanTable:
case kJpegQuantizationTable:
case kJpegComment:
case kJpegApp0:
case kJpegApp1: {
if (jpeg.hasBytes(2)) {
uint16_t tag_len = 0;
tag_len |= jpeg.getNext() << 8;
tag_len |= jpeg.getNext();
// The tag length includes the two bytes for the length
uint16_t tag_content_len = std::max(0, tag_len - 2);
if (tag != kJpegApp1 || !jpeg.hasBytes(tag_content_len)) {
jpeg.skipBytes(tag_content_len); // skip JPEG tags we ignore.
} else if (!jpeg.hasBytes(6)) {
jpeg.skipBytes(tag_content_len); // too short to have "Exif\0\0"
} else {
if (jpeg.getNext() == 'E' && jpeg.getNext() == 'x' &&
jpeg.getNext() == 'i' && jpeg.getNext() == 'f' &&
jpeg.getNext() == '\0' && jpeg.getNext() == '\0') {
exif_len = tag_content_len - 6;
done = true;
} else {
jpeg.skipBytes(tag_content_len); // too short to have "Exif\0\0"
}
}
} else {
done = true; // shouldn't happen: corrupt file or we have a bug
}
break;
}
case kJpegStartOfScan:
default:
done = true; // got to the image, apparently no exif tags here
break;
}
}
}
// Parse exif if it exists. If it does, we have already checked that jpeglen
// is longer than exifStart + exifLen, so we can safely index the data
if (exif_len > 0) {
// The first two bytes of TIFF header are "II" if little-endian ("Intel")
// and "MM" if big-endian ("Motorola")
const bool isLE = (jpeg.getNext() == 'I');
jpeg.skipBytes(3); // +1 for the other I/M, +2 for 0x002a
auto readUint16Little = [](Reader &jpeg) -> uint32_t {
uint16_t val = uint16_t(jpeg.getNext());
val |= uint16_t(jpeg.getNext()) << 8;
return val;
};
auto readUint32Little = [](Reader &jpeg) -> uint32_t {
uint32_t val = uint32_t(jpeg.getNext());
val |= uint32_t(jpeg.getNext()) << 8;
val |= uint32_t(jpeg.getNext()) << 16;
val |= uint32_t(jpeg.getNext()) << 24;
return val;
};
auto readUint16Big = [](Reader &jpeg) -> uint32_t {
uint16_t val = uint16_t(jpeg.getNext()) << 8;
val |= uint16_t(jpeg.getNext());
return val;
};
auto readUint32Big = [](Reader &jpeg) -> uint32_t {
uint32_t val = uint32_t(jpeg.getNext()) << 24;
val |= uint32_t(jpeg.getNext()) << 16;
val |= uint32_t(jpeg.getNext()) << 8;
val |= uint32_t(jpeg.getNext());
return val;
};
// The first two bytes of TIFF header are "II" if little-endian ("Intel")
// and "MM" if big-endian ("Motorola")
auto readUint32 = [readUint32Little, readUint32Big, isLE](Reader &jpeg) -> uint32_t {
return isLE ? readUint32Little(jpeg) : readUint32Big(jpeg);
};
auto readUint16 = [readUint16Little, readUint16Big, isLE](Reader &jpeg) -> uint32_t {
return isLE ? readUint16Little(jpeg) : readUint16Big(jpeg);
};
// offset to the IFD0 (offset from beginning of TIFF header, II/MM,
// which is 8 bytes before where we are after reading the uint32)
jpeg.skipBytes(readUint32(jpeg) - 8);
// Read the IFD0 ("Image File Directory 0")
// | NN | n entries in directory (2 bytes)
// | TT | tt | nnnn | vvvv | entry: tag (2b), data type (2b),
// n components (4b), value/offset (4b)
if (jpeg.hasBytes(2)) {
uint16_t nEntries = readUint16(jpeg);
for (uint16_t i = 0; i < nEntries && jpeg.hasBytes(2); ++i) {
uint16_t tag = readUint16(jpeg);
// The entry is 12 bytes. We already read the 2 bytes for the tag.
jpeg.skipBytes(6); // skip 2 for the data type, skip 4 n components.
if (tag == 0x112) {
switch (readUint16(jpeg)) { // orientation tag is always one uint16
case 1: return NORMAL;
case 2: return MIRROR_HORIZ;
case 3: return ROTATE_180;
case 4: return MIRROR_VERT;
case 5: return MIRROR_HORIZ_AND_ROTATE_270_CW;
case 6: return ROTATE_90_CW;
case 7: return MIRROR_HORIZ_AND_ROTATE_90_CW;
case 8: return ROTATE_270_CW;
default: return NORMAL;
}
} else {
jpeg.skipBytes(4); // skip the four bytes for the value
}
}
}
}
return NORMAL;
}
/*
* Updates the dimensions of the bitmap according to the orientation
*/
void Image::updateDimensionsForOrientation(Orientation orientation) {
switch (orientation) {
case ROTATE_90_CW:
case ROTATE_270_CW:
case MIRROR_HORIZ_AND_ROTATE_90_CW:
case MIRROR_HORIZ_AND_ROTATE_270_CW: {
int tmp = naturalWidth;
naturalWidth = naturalHeight;
naturalHeight = tmp;
tmp = width;
width = height;
height = tmp;
break;
}
case NORMAL:
case MIRROR_HORIZ:
case MIRROR_VERT:
case ROTATE_180:
default: {
break;
}
}
}
/*
* Rotates the pixels to the correct orientation.
*/
void
Image::rotatePixels(uint8_t* pixels, int width, int height, int channels,
Orientation orientation) {
auto swapPixel = [channels](uint8_t* pixels, int src_idx, int dst_idx) {
uint8_t tmp;
for (int i = 0; i < channels; ++i) {
tmp = pixels[src_idx + i];
pixels[src_idx + i] = pixels[dst_idx + i];
pixels[dst_idx + i] = tmp;
}
};
auto mirrorHoriz = [swapPixel](uint8_t* pixels, int width, int height, int channels) {
int midX = width / 2; // ok to truncate if odd, since we don't swap a center pixel
for (int y = 0; y < height; ++y) {
for (int x = 0; x < midX; ++x) {
int orig_idx = (y * width + x) * channels;
int new_idx = (y * width + width - 1 - x) * channels;
swapPixel(pixels, orig_idx, new_idx);
}
}
};
auto mirrorVert = [swapPixel](uint8_t* pixels, int width, int height, int channels) {
int midY = height / 2; // ok to truncate if odd, since we don't swap a center pixel
for (int y = 0; y < midY; ++y) {
for (int x = 0; x < width; ++x) {
int orig_idx = (y * width + x) * channels;
int new_idx = ((height - y - 1) * width + x) * channels;
swapPixel(pixels, orig_idx, new_idx);
}
}
};
auto rotate90 = [](uint8_t* pixels, int width, int height, int channels) {
const int n_bytes = width * height * channels;
uint8_t *unrotated = new uint8_t[n_bytes];
if (!unrotated) {
return;
}
std::memcpy(unrotated, pixels, n_bytes);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width ; ++x) {
int orig_idx = (y * width + x) * channels;
int new_idx = (x * height + height - 1 - y) * channels;
std::memcpy(pixels + new_idx, unrotated + orig_idx, channels);
}
}
};
auto rotate270 = [](uint8_t* pixels, int width, int height, int channels) {
const int n_bytes = width * height * channels;
uint8_t *unrotated = new uint8_t[n_bytes];
if (!unrotated) {
return;
}
std::memcpy(unrotated, pixels, n_bytes);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width ; ++x) {
int orig_idx = (y * width + x) * channels;
int new_idx = ((width - 1 - x) * height + y) * channels;
std::memcpy(pixels + new_idx, unrotated + orig_idx, channels);
}
}
};
switch (orientation) {
case MIRROR_HORIZ:
mirrorHoriz(pixels, width, height, channels);
break;
case MIRROR_VERT:
mirrorVert(pixels, width, height, channels);
break;
case ROTATE_180:
mirrorHoriz(pixels, width, height, channels);
mirrorVert(pixels, width, height, channels);
break;
case ROTATE_90_CW:
rotate90(pixels, height, width, channels); // swap w/h because we need orig w/h
break;
case ROTATE_270_CW:
rotate270(pixels, height, width, channels); // swap w/h because we need orig w/h
break;
case MIRROR_HORIZ_AND_ROTATE_90_CW:
mirrorHoriz(pixels, height, width, channels); // swap w/h because we need orig w/h
rotate90(pixels, height, width, channels);
break;
case MIRROR_HORIZ_AND_ROTATE_270_CW:
mirrorHoriz(pixels, height, width, channels); // swap w/h because we need orig w/h
rotate270(pixels, height, width, channels);
break;
case NORMAL:
default:
break;
}
}
#endif /* HAVE_JPEG */
#ifdef HAVE_RSVG
/*
* Load SVG from buffer
*/
cairo_status_t
Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) {
_is_svg = true;
if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, nullptr))) {
return CAIRO_STATUS_READ_ERROR;
}
double d_width;
double d_height;
rsvg_handle_get_intrinsic_size_in_pixels(_rsvg, &d_width, &d_height);
width = naturalWidth = d_width;
height = naturalHeight = d_height;
if (width <= 0 || height <= 0) {
this->errorInfo.set("Width and height must be set on the svg element");
return CAIRO_STATUS_READ_ERROR;
}
return renderSVGToSurface();
}
/*
* Renders the Rsvg handle to this image's surface
*/
cairo_status_t
Image::renderSVGToSurface() {
cairo_status_t status;
_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
status = cairo_surface_status(_surface);
if (status != CAIRO_STATUS_SUCCESS) {
g_object_unref(_rsvg);
return status;
}
cairo_t *cr = cairo_create(_surface);
status = cairo_status(cr);
if (status != CAIRO_STATUS_SUCCESS) {
g_object_unref(_rsvg);
return status;
}
RsvgRectangle viewport = {
0, // x
0, // y
static_cast<double>(width),
static_cast<double>(height)
};
gboolean render_ok = rsvg_handle_render_document(_rsvg, cr, &viewport, nullptr);
if (!render_ok) {
g_object_unref(_rsvg);
cairo_destroy(cr);
return CAIRO_STATUS_READ_ERROR; // or WRITE?
}
cairo_destroy(cr);
_svg_last_width = width;
_svg_last_height = height;
return status;
}
/*
* Load SVG
*/
cairo_status_t
Image::loadSVG(FILE *stream) {
_is_svg = true;
struct stat s;
int fd = fileno(stream);
// stat
if (fstat(fd, &s) < 0) {
fclose(stream);
return CAIRO_STATUS_READ_ERROR;
}
uint8_t *buf = (uint8_t *) malloc(s.st_size);
if (!buf) {
fclose(stream);
return CAIRO_STATUS_NO_MEMORY;
}
size_t read = fread(buf, s.st_size, 1, stream);
fclose(stream);
cairo_status_t result = CAIRO_STATUS_READ_ERROR;
if (1 == read) result = loadSVGFromBuffer(buf, s.st_size);
free(buf);
return result;
}
#endif /* HAVE_RSVG */
/*
* Load BMP from buffer.
*/
cairo_status_t Image::loadBMPFromBuffer(uint8_t *buf, unsigned len){
BMPParser::Parser parser;
// Reversed ARGB32 with pre-multiplied alpha
uint8_t pixFmt[5] = {2, 1, 0, 3, 1};
parser.parse(buf, len, pixFmt);
if (parser.getStatus() != BMPParser::Status::OK) {
errorInfo.reset();
errorInfo.message = parser.getErrMsg();
return CAIRO_STATUS_READ_ERROR;
}
width = naturalWidth = parser.getWidth();
height = naturalHeight = parser.getHeight();
uint8_t *data = parser.getImgd();
_surface = cairo_image_surface_create_for_data(
data,
CAIRO_FORMAT_ARGB32,
width,
height,
cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width)
);
// No need to delete the data
cairo_status_t status = cairo_surface_status(_surface);
if (status) return status;
_data = data;
parser.clearImgd();
return CAIRO_STATUS_SUCCESS;
}
/*
* Load BMP.
*/
cairo_status_t Image::loadBMP(FILE *stream){
struct stat s;
int fd = fileno(stream);
// Stat
if (fstat(fd, &s) < 0) {
fclose(stream);
return CAIRO_STATUS_READ_ERROR;
}
uint8_t *buf = new uint8_t[s.st_size];
if (!buf) {
fclose(stream);
errorInfo.set(NULL, "malloc", errno);
return CAIRO_STATUS_NO_MEMORY;
}
size_t read = fread(buf, s.st_size, 1, stream);
fclose(stream);
cairo_status_t result = CAIRO_STATUS_READ_ERROR;
if (read == 1) result = loadBMPFromBuffer(buf, s.st_size);
delete[] buf;
return result;
}
/*
* Return UNKNOWN, SVG, GIF, JPEG, or PNG based on the filename.
*/
Image::type
Image::extension(const char *filename) {
size_t len = strlen(filename);
filename += len;
if (len >= 5 && 0 == strcmp(".jpeg", filename - 5)) return Image::JPEG;
if (len >= 4 && 0 == strcmp(".gif", filename - 4)) return Image::GIF;
if (len >= 4 && 0 == strcmp(".jpg", filename - 4)) return Image::JPEG;
if (len >= 4 && 0 == strcmp(".png", filename - 4)) return Image::PNG;
if (len >= 4 && 0 == strcmp(".svg", filename - 4)) return Image::SVG;
return Image::UNKNOWN;
}
/*
* Sniff bytes 0..1 for JPEG's magic number ff d8.
*/
int
Image::isJPEG(uint8_t *data) {
return 0xff == data[0] && 0xd8 == data[1];
}
/*
* Sniff bytes 0..2 for "GIF".
*/
int
Image::isGIF(uint8_t *data) {
return 'G' == data[0] && 'I' == data[1] && 'F' == data[2];
}
/*
* Sniff bytes 1..3 for "PNG".
*/
int
Image::isPNG(uint8_t *data) {
return 'P' == data[1] && 'N' == data[2] && 'G' == data[3];
}
/*
* Skip "<?" and "<!" tags to test if root tag starts "<svg"
*/
int
Image::isSVG(uint8_t *data, unsigned len) {
for (unsigned i = 3; i < len; i++) {
if ('<' == data[i-3]) {
switch (data[i-2]) {
case '?':
case '!':
break;
case 's':
return ('v' == data[i-1] && 'g' == data[i]);
default:
return false;
}
}
}
return false;
}
/*
* Check for valid BMP signatures
*/
int Image::isBMP(uint8_t *data, unsigned len) {
if(len < 2) return false;
std::string sig = std::string(1, (char)data[0]) + (char)data[1];
return sig == "BM" ||
sig == "BA" ||
sig == "CI" ||
sig == "CP" ||
sig == "IC" ||
sig == "PT";
}
+146
View File
@@ -0,0 +1,146 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#pragma once
#include <cairo.h>
#include "CanvasError.h"
#include <functional>
#include <napi.h>
#include <stdint.h> // node < 7 uses libstdc++ on macOS which lacks complete c++11
#ifdef HAVE_JPEG
#include <jpeglib.h>
#include <jerror.h>
#endif
#ifdef HAVE_GIF
#include <gif_lib.h>
#if GIFLIB_MAJOR > 5 || GIFLIB_MAJOR == 5 && GIFLIB_MINOR >= 1
#define GIF_CLOSE_FILE(gif) DGifCloseFile(gif, NULL)
#else
#define GIF_CLOSE_FILE(gif) DGifCloseFile(gif)
#endif
#endif
#ifdef HAVE_RSVG
#include <librsvg/rsvg.h>
// librsvg <= 2.36.1, identified by undefined macro, needs an extra include
#ifndef LIBRSVG_CHECK_VERSION
#include <librsvg/rsvg-cairo.h>
#endif
#endif
using JPEGDecodeL = std::function<uint32_t (uint8_t* const src)>;
class Image : public Napi::ObjectWrap<Image> {
public:
char *filename;
int width, height;
int naturalWidth, naturalHeight;
Napi::Env env;
static Napi::FunctionReference constructor;
static void Initialize(Napi::Env& env, Napi::Object& target);
Image(const Napi::CallbackInfo& info);
Napi::Value GetComplete(const Napi::CallbackInfo& info);
Napi::Value GetWidth(const Napi::CallbackInfo& info);
Napi::Value GetHeight(const Napi::CallbackInfo& info);
Napi::Value GetNaturalWidth(const Napi::CallbackInfo& info);
Napi::Value GetNaturalHeight(const Napi::CallbackInfo& info);
Napi::Value GetDataMode(const Napi::CallbackInfo& info);
void SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value);
static Napi::Value GetSource(const Napi::CallbackInfo& info);
static void SetSource(const Napi::CallbackInfo& info);
inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); }
inline int stride(){ return cairo_image_surface_get_stride(_surface); }
static int isPNG(uint8_t *data);
static int isJPEG(uint8_t *data);
static int isGIF(uint8_t *data);
static int isSVG(uint8_t *data, unsigned len);
static int isBMP(uint8_t *data, unsigned len);
static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len);
inline int isComplete(){ return COMPLETE == state; }
cairo_surface_t *surface();
cairo_status_t loadSurface();
cairo_status_t loadFromBuffer(uint8_t *buf, unsigned len);
cairo_status_t loadPNGFromBuffer(uint8_t *buf);
cairo_status_t loadPNG();
void clearData();
#ifdef HAVE_RSVG
cairo_status_t loadSVGFromBuffer(uint8_t *buf, unsigned len);
cairo_status_t loadSVG(FILE *stream);
cairo_status_t renderSVGToSurface();
#endif
#ifdef HAVE_GIF
cairo_status_t loadGIFFromBuffer(uint8_t *buf, unsigned len);
cairo_status_t loadGIF(FILE *stream);
#endif
#ifdef HAVE_JPEG
enum Orientation {
NORMAL,
MIRROR_HORIZ,
MIRROR_VERT,
ROTATE_180,
ROTATE_90_CW,
ROTATE_270_CW,
MIRROR_HORIZ_AND_ROTATE_90_CW,
MIRROR_HORIZ_AND_ROTATE_270_CW
};
cairo_status_t loadJPEGFromBuffer(uint8_t *buf, unsigned len);
cairo_status_t loadJPEG(FILE *stream);
void jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode);
cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info, Orientation orientation);
cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len);
cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type);
class Reader {
public:
virtual bool hasBytes(unsigned n) const = 0;
virtual uint8_t getNext() = 0;
virtual void skipBytes(unsigned n) = 0;
};
Orientation getExifOrientation(Reader& jpeg);
void updateDimensionsForOrientation(Orientation orientation);
void rotatePixels(uint8_t* pixels, int width, int height, int channels, Orientation orientation);
#endif
cairo_status_t loadBMPFromBuffer(uint8_t *buf, unsigned len);
cairo_status_t loadBMP(FILE *stream);
CanvasError errorInfo;
void loaded();
cairo_status_t load();
~Image();
enum {
DEFAULT
, LOADING
, COMPLETE
} state;
enum data_mode_t {
DATA_IMAGE = 1
, DATA_MIME = 2
} data_mode;
typedef enum {
UNKNOWN
, GIF
, JPEG
, PNG
, SVG
} type;
static type extension(const char *filename);
private:
cairo_surface_t *_surface;
uint8_t *_data = nullptr;
int _data_len;
#ifdef HAVE_RSVG
RsvgHandle *_rsvg;
bool _is_svg;
int _svg_last_width;
int _svg_last_height;
#endif
};
+138
View File
@@ -0,0 +1,138 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#include "ImageData.h"
#include "InstanceData.h"
/*
* Initialize ImageData.
*/
void
ImageData::Initialize(Napi::Env& env, Napi::Object& exports) {
Napi::HandleScope scope(env);
InstanceData *data = env.GetInstanceData<InstanceData>();
Napi::Function ctor = DefineClass(env, "ImageData", {
InstanceAccessor<&ImageData::GetWidth>("width", napi_default_jsproperty),
InstanceAccessor<&ImageData::GetHeight>("height", napi_default_jsproperty)
});
exports.Set("ImageData", ctor);
data->ImageDataCtor = Napi::Persistent(ctor);
}
/*
* Initialize a new ImageData object.
*/
ImageData::ImageData(const Napi::CallbackInfo& info) : Napi::ObjectWrap<ImageData>(info), env(info.Env()) {
Napi::TypedArray dataArray;
uint32_t width;
uint32_t height;
uint32_t length;
if (info[0].IsNumber() && info[1].IsNumber()) {
width = info[0].As<Napi::Number>().Uint32Value();
if (width == 0) {
Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException();
return;
}
height = info[1].As<Napi::Number>().Uint32Value();
if (height == 0) {
Napi::RangeError::New(env, "The source height is zero.").ThrowAsJavaScriptException();
return;
}
if ((uint64_t)width * height > INT32_MAX / 4) {
// INT32_MAX is what Firefox limits ImageData to
std::string msg = "buffer exceeds " + std::to_string(INT32_MAX) + " bytes";
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
return;
}
length = width * height * 4; // ImageData(w, h) constructor assumes 4 BPP; documented.
dataArray = Napi::Uint8Array::New(env, length, napi_uint8_clamped_array);
} else if (
info[0].IsTypedArray() &&
info[0].As<Napi::TypedArray>().TypedArrayType() == napi_uint8_clamped_array &&
info[1].IsNumber()
) {
dataArray = info[0].As<Napi::Uint8Array>();
length = dataArray.ElementLength();
if (length == 0) {
Napi::RangeError::New(env, "The input data has a zero byte length.").ThrowAsJavaScriptException();
return;
}
// Don't assert that the ImageData length is a multiple of four because some
// data formats are not 4 BPP.
width = info[1].As<Napi::Number>().Uint32Value();
if (width == 0) {
Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException();
return;
}
// Don't assert that the byte length is a multiple of 4 * width, ditto.
if (info[2].IsNumber()) { // Explicit height given
height = info[2].As<Napi::Number>().Uint32Value();
} else { // Calculate height assuming 4 BPP
int size = length / 4;
height = size / width;
}
} else if (
info[0].IsTypedArray() &&
info[0].As<Napi::TypedArray>().TypedArrayType() == napi_uint16_array &&
info[1].IsNumber()
) { // Intended for RGB16_565 format
dataArray = info[0].As<Napi::TypedArray>();
length = dataArray.ElementLength();
if (length == 0) {
Napi::RangeError::New(env, "The input data has a zero byte length.").ThrowAsJavaScriptException();
return;
}
width = info[1].As<Napi::Number>().Uint32Value();
if (width == 0) {
Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException();
return;
}
if (info[2].IsNumber()) { // Explicit height given
height = info[2].As<Napi::Number>().Uint32Value();
} else { // Calculate height assuming 2 BPP
int size = length / 2;
height = size / width;
}
} else {
Napi::TypeError::New(env, "Expected (Uint8ClampedArray, width[, height]), (Uint16Array, width[, height]) or (width, height)").ThrowAsJavaScriptException();
return;
}
_width = width;
_height = height;
_data = dataArray.As<Napi::Uint8Array>().Data();
info.This().As<Napi::Object>().Set("data", dataArray);
}
/*
* Get width.
*/
Napi::Value
ImageData::GetWidth(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, width());
}
/*
* Get height.
*/
Napi::Value
ImageData::GetHeight(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, height());
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#pragma once
#include <napi.h>
#include <stdint.h> // node < 7 uses libstdc++ on macOS which lacks complete c++11
class ImageData : public Napi::ObjectWrap<ImageData> {
public:
static void Initialize(Napi::Env& env, Napi::Object& exports);
ImageData(const Napi::CallbackInfo& info);
Napi::Value GetWidth(const Napi::CallbackInfo& info);
Napi::Value GetHeight(const Napi::CallbackInfo& info);
inline uint32_t width() { return _width; }
inline uint32_t height() { return _height; }
inline uint8_t *data() { return _data; }
Napi::Env env;
private:
uint32_t _width;
uint32_t _height;
uint8_t *_data;
};
+12
View File
@@ -0,0 +1,12 @@
#include <napi.h>
struct InstanceData {
Napi::FunctionReference CanvasCtor;
Napi::FunctionReference CanvasGradientCtor;
Napi::FunctionReference DOMMatrixCtor;
Napi::FunctionReference ImageCtor;
Napi::FunctionReference parseFont;
Napi::FunctionReference Context2dCtor;
Napi::FunctionReference ImageDataCtor;
Napi::FunctionReference CanvasPatternCtor;
};
+157
View File
@@ -0,0 +1,157 @@
#pragma once
#include "closure.h"
#include <jpeglib.h>
#include <jerror.h>
/*
* Expanded data destination object for closure output,
* inspired by IJG's jdatadst.c
*/
struct closure_destination_mgr {
jpeg_destination_mgr pub;
JpegClosure* closure;
JOCTET *buffer;
int bufsize;
};
void
init_closure_destination(j_compress_ptr cinfo){
// we really don't have to do anything here
}
boolean
empty_closure_output_buffer(j_compress_ptr cinfo){
closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest;
Napi::Env env = dest->closure->canvas->Env();
Napi::HandleScope scope(env);
Napi::AsyncContext async(env, "canvas:empty_closure_output_buffer");
Napi::Object buf = Napi::Buffer<char>::New(env, (char *)dest->buffer, dest->bufsize);
// emit "data"
dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async);
dest->buffer = (JOCTET *)malloc(dest->bufsize);
cinfo->dest->next_output_byte = dest->buffer;
cinfo->dest->free_in_buffer = dest->bufsize;
return true;
}
void
term_closure_destination(j_compress_ptr cinfo){
closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest;
Napi::Env env = dest->closure->canvas->Env();
Napi::HandleScope scope(env);
Napi::AsyncContext async(env, "canvas:term_closure_destination");
/* emit remaining data */
Napi::Object buf = Napi::Buffer<char>::New(env, (char *)dest->buffer, dest->bufsize - dest->pub.free_in_buffer);
dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async);
// emit "end"
dest->closure->cb.MakeCallback(env.Global(), {env.Null(), env.Null()}, async);
}
void
jpeg_closure_dest(j_compress_ptr cinfo, JpegClosure* closure, int bufsize){
closure_destination_mgr * dest;
/* The destination object is made permanent so that multiple JPEG images
* can be written to the same buffer without re-executing jpeg_mem_dest.
*/
if (cinfo->dest == NULL) { /* first time for this JPEG object? */
cinfo->dest = (struct jpeg_destination_mgr *)
(*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_PERMANENT,
sizeof(closure_destination_mgr));
}
dest = (closure_destination_mgr *) cinfo->dest;
cinfo->dest->init_destination = &init_closure_destination;
cinfo->dest->empty_output_buffer = &empty_closure_output_buffer;
cinfo->dest->term_destination = &term_closure_destination;
dest->closure = closure;
dest->bufsize = bufsize;
dest->buffer = (JOCTET *)malloc(bufsize);
cinfo->dest->next_output_byte = dest->buffer;
cinfo->dest->free_in_buffer = dest->bufsize;
}
void encode_jpeg(jpeg_compress_struct cinfo, cairo_surface_t *surface, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor) {
int w = cairo_image_surface_get_width(surface);
int h = cairo_image_surface_get_height(surface);
cinfo.in_color_space = JCS_RGB;
cinfo.input_components = 3;
cinfo.image_width = w;
cinfo.image_height = h;
jpeg_set_defaults(&cinfo);
if (progressive)
jpeg_simple_progression(&cinfo);
jpeg_set_quality(&cinfo, quality, (quality < 25) ? 0 : 1);
cinfo.comp_info[0].h_samp_factor = chromaHSampFactor;
cinfo.comp_info[0].v_samp_factor = chromaVSampFactor;
JSAMPROW slr;
jpeg_start_compress(&cinfo, TRUE);
unsigned char *dst;
unsigned int *src = (unsigned int *)cairo_image_surface_get_data(surface);
int sl = 0;
dst = (unsigned char *)malloc(w * 3);
while (sl < h) {
unsigned char *dp = dst;
int x = 0;
while (x < w) {
dp[0] = (*src >> 16) & 255;
dp[1] = (*src >> 8) & 255;
dp[2] = *src & 255;
src++;
dp += 3;
x++;
}
slr = dst;
jpeg_write_scanlines(&cinfo, &slr, 1);
sl++;
}
free(dst);
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
}
void
write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, JpegClosure* closure) {
jpeg_compress_struct cinfo;
jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
jpeg_closure_dest(&cinfo, closure, bufsize);
encode_jpeg(
cinfo,
surface,
closure->quality,
closure->progressive,
closure->chromaSubsampling,
closure->chromaSubsampling);
}
void
write_to_jpeg_buffer(cairo_surface_t* surface, JpegClosure* closure) {
jpeg_compress_struct cinfo;
jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
cinfo.client_data = closure;
cinfo.dest = closure->jpeg_dest_mgr;
encode_jpeg(
cinfo,
surface,
closure->quality,
closure->progressive,
closure->chromaSubsampling,
closure->chromaSubsampling);
}
+292
View File
@@ -0,0 +1,292 @@
#pragma once
#include <cairo.h>
#include "closure.h"
#include <cmath> // round
#include <cstdlib>
#include <cstring>
#include <png.h>
#include <pngconf.h>
#if defined(__GNUC__) && (__GNUC__ > 2) && defined(__OPTIMIZE__)
#define likely(expr) (__builtin_expect (!!(expr), 1))
#define unlikely(expr) (__builtin_expect (!!(expr), 0))
#else
#define likely(expr) (expr)
#define unlikely(expr) (expr)
#endif
static void canvas_png_flush(png_structp png_ptr) {
/* Do nothing; fflush() is said to be just a waste of energy. */
(void) png_ptr; /* Stifle compiler warning */
}
/* Converts native endian xRGB => RGBx bytes */
static void canvas_convert_data_to_bytes(png_structp png, png_row_infop row_info, png_bytep data) {
unsigned int i;
for (i = 0; i < row_info->rowbytes; i += 4) {
uint8_t *b = &data[i];
uint32_t pixel;
memcpy(&pixel, b, sizeof (uint32_t));
b[0] = (pixel & 0xff0000) >> 16;
b[1] = (pixel & 0x00ff00) >> 8;
b[2] = (pixel & 0x0000ff) >> 0;
b[3] = 0;
}
}
/* Unpremultiplies data and converts native endian ARGB => RGBA bytes */
static void canvas_unpremultiply_data(png_structp png, png_row_infop row_info, png_bytep data) {
unsigned int i;
for (i = 0; i < row_info->rowbytes; i += 4) {
uint8_t *b = &data[i];
uint32_t pixel;
uint8_t alpha;
memcpy(&pixel, b, sizeof (uint32_t));
alpha = (pixel & 0xff000000) >> 24;
if (alpha == 0) {
b[0] = b[1] = b[2] = b[3] = 0;
} else {
b[0] = (((pixel & 0xff0000) >> 16) * 255 + alpha / 2) / alpha;
b[1] = (((pixel & 0x00ff00) >> 8) * 255 + alpha / 2) / alpha;
b[2] = (((pixel & 0x0000ff) >> 0) * 255 + alpha / 2) / alpha;
b[3] = alpha;
}
}
}
/* Converts RGB16_565 format data to RGBA32 */
static void canvas_convert_565_to_888(png_structp png, png_row_infop row_info, png_bytep data) {
// Loop in reverse to unpack in-place.
for (ptrdiff_t col = row_info->width - 1; col >= 0; col--) {
uint8_t* src = &data[col * sizeof(uint16_t)];
uint8_t* dst = &data[col * 3];
uint16_t pixel;
memcpy(&pixel, src, sizeof(uint16_t));
// Convert and rescale to the full 0-255 range
// See http://stackoverflow.com/a/29326693
const uint8_t red5 = (pixel & 0xF800) >> 11;
const uint8_t green6 = (pixel & 0x7E0) >> 5;
const uint8_t blue5 = (pixel & 0x001F);
dst[0] = ((red5 * 255 + 15) / 31);
dst[1] = ((green6 * 255 + 31) / 63);
dst[2] = ((blue5 * 255 + 15) / 31);
}
}
struct canvas_png_write_closure_t {
cairo_write_func_t write_func;
PngClosure* closure;
};
#ifdef PNG_SETJMP_SUPPORTED
bool setjmp_wrapper(png_structp png) {
return setjmp(png_jmpbuf(png));
}
#endif
static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr write_func, canvas_png_write_closure_t *closure) {
unsigned int i;
cairo_status_t status = CAIRO_STATUS_SUCCESS;
uint8_t *data;
png_structp png;
png_infop info;
png_bytep *volatile rows = NULL;
png_color_16 white;
int png_color_type;
int bpc;
unsigned int width = cairo_image_surface_get_width(surface);
unsigned int height = cairo_image_surface_get_height(surface);
data = cairo_image_surface_get_data(surface);
if (data == NULL) {
status = CAIRO_STATUS_SURFACE_TYPE_MISMATCH;
return status;
}
cairo_surface_flush(surface);
if (width == 0 || height == 0) {
status = CAIRO_STATUS_WRITE_ERROR;
return status;
}
rows = (png_bytep *) malloc(height * sizeof (png_byte*));
if (unlikely(rows == NULL)) {
status = CAIRO_STATUS_NO_MEMORY;
return status;
}
int stride = cairo_image_surface_get_stride(surface);
for (i = 0; i < height; i++) {
rows[i] = (png_byte *) data + i * stride;
}
#ifdef PNG_USER_MEM_SUPPORTED
png = png_create_write_struct_2(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL, NULL, NULL, NULL);
#else
png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
#endif
if (unlikely(png == NULL)) {
status = CAIRO_STATUS_NO_MEMORY;
free(rows);
return status;
}
info = png_create_info_struct (png);
if (unlikely(info == NULL)) {
status = CAIRO_STATUS_NO_MEMORY;
png_destroy_write_struct(&png, &info);
free(rows);
return status;
}
#ifdef PNG_SETJMP_SUPPORTED
if (setjmp_wrapper(png)) {
png_destroy_write_struct(&png, &info);
free(rows);
return status;
}
#endif
png_set_write_fn(png, closure, write_func, canvas_png_flush);
png_set_compression_level(png, closure->closure->compressionLevel);
png_set_filter(png, 0, closure->closure->filters);
if (closure->closure->resolution != 0) {
uint32_t res = static_cast<uint32_t>(round(static_cast<double>(closure->closure->resolution) * 39.3701));
png_set_pHYs(png, info, res, res, PNG_RESOLUTION_METER);
}
cairo_format_t format = cairo_image_surface_get_format(surface);
switch (format) {
case CAIRO_FORMAT_ARGB32:
bpc = 8;
png_color_type = PNG_COLOR_TYPE_RGB_ALPHA;
break;
#ifdef CAIRO_FORMAT_RGB30
case CAIRO_FORMAT_RGB30:
bpc = 10;
png_color_type = PNG_COLOR_TYPE_RGB;
break;
#endif
case CAIRO_FORMAT_RGB24:
bpc = 8;
png_color_type = PNG_COLOR_TYPE_RGB;
break;
case CAIRO_FORMAT_A8:
bpc = 8;
png_color_type = PNG_COLOR_TYPE_GRAY;
break;
case CAIRO_FORMAT_A1:
bpc = 1;
png_color_type = PNG_COLOR_TYPE_GRAY;
#ifndef WORDS_BIGENDIAN
png_set_packswap(png);
#endif
break;
case CAIRO_FORMAT_RGB16_565:
bpc = 8; // 565 gets upconverted to 888
png_color_type = PNG_COLOR_TYPE_RGB;
break;
case CAIRO_FORMAT_INVALID:
default:
status = CAIRO_STATUS_INVALID_FORMAT;
png_destroy_write_struct(&png, &info);
free(rows);
return status;
}
if ((format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) &&
closure->closure->palette != NULL) {
png_color_type = PNG_COLOR_TYPE_PALETTE;
}
png_set_IHDR(png, info, width, height, bpc, png_color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
if (png_color_type == PNG_COLOR_TYPE_PALETTE) {
size_t nColors = closure->closure->nPaletteColors;
uint8_t* colors = closure->closure->palette;
uint8_t backgroundIndex = closure->closure->backgroundIndex;
png_colorp pngPalette = (png_colorp)png_malloc(png, nColors * sizeof(png_colorp));
png_bytep transparency = (png_bytep)png_malloc(png, nColors * sizeof(png_bytep));
for (i = 0; i < nColors; i++) {
pngPalette[i].red = colors[4 * i];
pngPalette[i].green = colors[4 * i + 1];
pngPalette[i].blue = colors[4 * i + 2];
transparency[i] = colors[4 * i + 3];
}
png_set_PLTE(png, info, pngPalette, nColors);
png_set_tRNS(png, info, transparency, nColors, NULL);
png_set_packing(png); // pack pixels
// have libpng free palette and trans:
png_data_freer(png, info, PNG_DESTROY_WILL_FREE_DATA, PNG_FREE_PLTE | PNG_FREE_TRNS);
png_color_16 bkg;
bkg.index = backgroundIndex;
png_set_bKGD(png, info, &bkg);
}
if (png_color_type != PNG_COLOR_TYPE_PALETTE) {
white.gray = (1 << bpc) - 1;
white.red = white.blue = white.green = white.gray;
png_set_bKGD(png, info, &white);
}
/* We have to call png_write_info() before setting up the write
* transformation, since it stores data internally in 'png'
* that is needed for the write transformation functions to work.
*/
png_write_info(png, info);
if (png_color_type == PNG_COLOR_TYPE_RGB_ALPHA) {
png_set_write_user_transform_fn(png, canvas_unpremultiply_data);
} else if (format == CAIRO_FORMAT_RGB16_565) {
png_set_write_user_transform_fn(png, canvas_convert_565_to_888);
} else if (png_color_type == PNG_COLOR_TYPE_RGB) {
png_set_write_user_transform_fn(png, canvas_convert_data_to_bytes);
png_set_filler(png, 0, PNG_FILLER_AFTER);
}
png_write_image(png, rows);
png_write_end(png, info);
png_destroy_write_struct(&png, &info);
free(rows);
return status;
}
static void canvas_stream_write_func(png_structp png, png_bytep data, png_size_t size) {
cairo_status_t status;
struct canvas_png_write_closure_t *png_closure;
png_closure = (struct canvas_png_write_closure_t *) png_get_io_ptr(png);
status = png_closure->write_func(png_closure->closure, data, size);
if (unlikely(status)) {
cairo_status_t *error = (cairo_status_t *) png_get_error_ptr(png);
if (*error == CAIRO_STATUS_SUCCESS) {
*error = status;
}
png_error(png, NULL);
}
}
static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PngClosure* closure) {
struct canvas_png_write_closure_t png_closure;
if (cairo_surface_status(surface)) {
return cairo_surface_status(surface);
}
png_closure.write_func = write_func;
png_closure.closure = closure;
return canvas_write_png(surface, canvas_stream_write_func, &png_closure);
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#pragma once
template <typename T>
class Point {
public:
T x, y;
Point(T x=0, T y=0): x(x), y(y) {}
Point(const Point&) = default;
Point& operator=(const Point&) = default;
};
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <cctype>
inline bool streq_casein(std::string& str1, std::string& str2) {
return str1.size() == str2.size() && std::equal(str1.begin(), str1.end(), str2.begin(), [](char& c1, char& c2) {
return c1 == c2 || std::toupper(c1) == std::toupper(c2);
});
}
+459
View File
@@ -0,0 +1,459 @@
#include "BMPParser.h"
#include <cassert>
#include <cstring>
using namespace std;
using namespace BMPParser;
#define MAX_IMG_SIZE 10000
#define E(cond, msg) if(cond) return setErr(msg)
#define EU(cond, msg) if(cond) return setErrUnsupported(msg)
#define EX(cond, msg) if(cond) return setErrUnknown(msg)
#define I1() get<char>()
#define U1() get<uint8_t>()
#define I2() get<int16_t>()
#define U2() get<uint16_t>()
#define I4() get<int32_t>()
#define U4() get<uint32_t>()
#define I1UC() get<char, false>()
#define U1UC() get<uint8_t, false>()
#define I2UC() get<int16_t, false>()
#define U2UC() get<uint16_t, false>()
#define I4UC() get<int32_t, false>()
#define U4UC() get<uint32_t, false>()
#define CHECK_OVERRUN(ptr, size, type) \
if((ptr) + (size) - data > len){ \
setErr("unexpected end of file"); \
return type(); \
}
Parser::~Parser(){
data = nullptr;
ptr = nullptr;
if(imgd){
delete[] imgd;
imgd = nullptr;
}
}
void Parser::parse(uint8_t *buf, int bufSize, uint8_t *format){
assert(status == Status::EMPTY);
data = ptr = buf;
len = bufSize;
// Start parsing file header
setOp("file header");
// File header signature
string fhSig = getStr(2);
string temp = "file header signature";
EU(fhSig == "BA", temp + " \"BA\"");
EU(fhSig == "CI", temp + " \"CI\"");
EU(fhSig == "CP", temp + " \"CP\"");
EU(fhSig == "IC", temp + " \"IC\"");
EU(fhSig == "PT", temp + " \"PT\"");
EX(fhSig != "BM", temp); // BM
// Length of the file should not be larger than `len`
E(U4() > static_cast<uint32_t>(len), "inconsistent file size");
// Skip unused values
skip(4);
// Offset where the pixel array (bitmap data) can be found
auto imgdOffset = U4();
// Start parsing DIB header
setOp("DIB header");
// Prepare some variables in case they are needed
uint32_t compr = 0;
uint32_t redShift = 0, greenShift = 0, blueShift = 0, alphaShift = 0;
uint32_t redMask = 0, greenMask = 0, blueMask = 0, alphaMask = 0;
double redMultp = 0, greenMultp = 0, blueMultp = 0, alphaMultp = 0;
/**
* Type of the DIB (device-independent bitmap) header
* is determined by its size. Most BMP files use BITMAPINFOHEADER.
*/
auto dibSize = U4();
temp = "DIB header";
EU(dibSize == 64, temp + " \"OS22XBITMAPHEADER\"");
EU(dibSize == 16, temp + " \"OS22XBITMAPHEADER\"");
uint32_t infoHeader = dibSize == 40 ? 1 :
dibSize == 52 ? 2 :
dibSize == 56 ? 3 :
dibSize == 108 ? 4 :
dibSize == 124 ? 5 : 0;
// BITMAPCOREHEADER, BITMAP*INFOHEADER, BITMAP*HEADER
auto isDibValid = dibSize == 12 || infoHeader;
EX(!isDibValid, temp);
// Image width
w = dibSize == 12 ? U2() : I4();
E(!w, "image width is 0");
E(w < 0, "negative image width");
E(w > MAX_IMG_SIZE, "too large image width");
// Image height (specification allows negative values)
h = dibSize == 12 ? U2() : I4();
E(!h, "image height is 0");
E(h > MAX_IMG_SIZE, "too large image height");
bool isHeightNegative = h < 0;
if(isHeightNegative) h = -h;
// Number of color planes (must be 1)
E(U2() != 1, "number of color planes must be 1");
// Bits per pixel (color depth)
auto bpp = U2();
auto isBppValid = bpp == 1 || bpp == 4 || bpp == 8 || bpp == 16 || bpp == 24 || bpp == 32;
EU(!isBppValid, "color depth");
// Calculate image data size and padding
uint32_t expectedImgdSize = (((w * bpp + 31) >> 5) << 2) * h;
uint32_t rowPadding = (-w * bpp & 31) >> 3;
uint32_t imgdSize = 0;
// Color palette data
uint8_t* paletteStart = nullptr;
uint32_t palColNum = 0;
if(infoHeader){
// Compression type
compr = U4();
temp = "compression type";
EU(compr == 1, temp + " \"BI_RLE8\"");
EU(compr == 2, temp + " \"BI_RLE4\"");
EU(compr == 4, temp + " \"BI_JPEG\"");
EU(compr == 5, temp + " \"BI_PNG\"");
EU(compr == 6, temp + " \"BI_ALPHABITFIELDS\"");
EU(compr == 11, temp + " \"BI_CMYK\"");
EU(compr == 12, temp + " \"BI_CMYKRLE8\"");
EU(compr == 13, temp + " \"BI_CMYKRLE4\"");
// BI_RGB and BI_BITFIELDS
auto isComprValid = compr == 0 || compr == 3;
EX(!isComprValid, temp);
// Ensure that BI_BITFIELDS appears only with 16-bit or 32-bit color
E(compr == 3 && !(bpp == 16 || bpp == 32), "compression BI_BITFIELDS can be used only with 16-bit and 32-bit color depth");
// Size of the image data
imgdSize = U4();
// Horizontal and vertical resolution (ignored)
skip(8);
// Number of colors in the palette or 0 if no palette is present
palColNum = U4();
EU(palColNum && bpp > 8, "color palette and bit depth combination");
if(palColNum) paletteStart = data + dibSize + 14;
// Number of important colors used or 0 if all colors are important (generally ignored)
skip(4);
if(infoHeader >= 2){
// If BI_BITFIELDS are used, calculate masks, otherwise ignore them
if(compr == 3){
calcMaskShift(redShift, redMask, redMultp);
calcMaskShift(greenShift, greenMask, greenMultp);
calcMaskShift(blueShift, blueMask, blueMultp);
if(infoHeader >= 3) calcMaskShift(alphaShift, alphaMask, alphaMultp);
if(status == Status::ERROR) return;
}else{
skip(16);
}
// Ensure that the color space is LCS_WINDOWS_COLOR_SPACE or sRGB
if(infoHeader >= 4 && !palColNum){
string colSpace = getStr(4, 1);
EU(colSpace != "Win " && colSpace != "sRGB", "color space \"" + colSpace + "\"");
}
}
}
// Skip to the image data (there may be other chunks between, but they are optional)
E(ptr - data > imgdOffset, "image data overlaps with another structure");
ptr = data + imgdOffset;
// Start parsing image data
setOp("image data");
if(!imgdSize){
// Value 0 is allowed only for BI_RGB compression type
E(compr != 0, "missing image data size");
imgdSize = expectedImgdSize;
}else{
E(imgdSize < expectedImgdSize, "invalid image data size");
}
// Ensure that all image data is present
E(ptr - data + imgdSize > len, "not enough image data");
// Direction of reading rows
int yStart = h - 1;
int yEnd = -1;
int dy = isHeightNegative ? 1 : -1;
// In case of negative height, read rows backward
if(isHeightNegative){
yStart = 0;
yEnd = h;
}
// Allocate output image data array
int buffLen = w * h << 2;
imgd = new (nothrow) uint8_t[buffLen];
E(!imgd, "unable to allocate memory");
// Prepare color values
uint8_t color[4] = {0};
uint8_t &red = color[0];
uint8_t &green = color[1];
uint8_t &blue = color[2];
uint8_t &alpha = color[3];
// Check if pre-multiplied alpha is used
bool premul = format ? format[4] : 0;
// Main loop
for(int y = yStart; y != yEnd; y += dy){
// Use in-byte offset for bpp < 8
uint8_t colOffset = 0;
uint8_t cval = 0;
uint32_t val = 0;
for(int x = 0; x != w; x++){
// Index in the output image data
int i = (x + y * w) << 2;
switch(compr){
case 0: // BI_RGB
switch(bpp){
case 1:
if(colOffset) ptr--;
cval = (U1UC() >> (7 - colOffset)) & 1;
if(palColNum){
uint8_t* entry = paletteStart + (cval << 2);
blue = get<uint8_t>(entry);
green = get<uint8_t>(entry + 1);
red = get<uint8_t>(entry + 2);
if(status == Status::ERROR) return;
}else{
red = green = blue = cval ? 255 : 0;
}
alpha = 255;
colOffset = (colOffset + 1) & 7;
break;
case 4:
if(colOffset) ptr--;
cval = (U1UC() >> (4 - colOffset)) & 15;
if(palColNum){
uint8_t* entry = paletteStart + (cval << 2);
blue = get<uint8_t>(entry);
green = get<uint8_t>(entry + 1);
red = get<uint8_t>(entry + 2);
if(status == Status::ERROR) return;
}else{
red = green = blue = cval << 4;
}
alpha = 255;
colOffset = (colOffset + 4) & 7;
break;
case 8:
cval = U1UC();
if(palColNum){
uint8_t* entry = paletteStart + (cval << 2);
blue = get<uint8_t>(entry);
green = get<uint8_t>(entry + 1);
red = get<uint8_t>(entry + 2);
if(status == Status::ERROR) return;
}else{
red = green = blue = cval;
}
alpha = 255;
break;
case 16:
// RGB555
val = U1UC();
val |= U1UC() << 8;
red = (val >> 10) << 3;
green = (val >> 5) << 3;
blue = val << 3;
alpha = 255;
break;
case 24:
blue = U1UC();
green = U1UC();
red = U1UC();
alpha = 255;
break;
case 32:
blue = U1UC();
green = U1UC();
red = U1UC();
if(infoHeader >= 3){
alpha = U1UC();
}else{
alpha = 255;
skip(1);
}
break;
}
break;
case 3: // BI_BITFIELDS
uint32_t col = bpp == 16 ? U2UC() : U4UC();
red = ((col >> redShift) & redMask) * redMultp + .5;
green = ((col >> greenShift) & greenMask) * greenMultp + .5;
blue = ((col >> blueShift) & blueMask) * blueMultp + .5;
alpha = alphaMask ? ((col >> alphaShift) & alphaMask) * alphaMultp + .5 : 255;
break;
}
/**
* Pixel format:
* red,
* green,
* blue,
* alpha,
* is alpha pre-multiplied
* Default is [0, 1, 2, 3, 0]
*/
if(premul && alpha != 255){
double a = alpha / 255.;
red = static_cast<uint8_t>(red * a + .5);
green = static_cast<uint8_t>(green * a + .5);
blue = static_cast<uint8_t>(blue * a + .5);
}
if(format){
imgd[i] = color[format[0]];
imgd[i + 1] = color[format[1]];
imgd[i + 2] = color[format[2]];
imgd[i + 3] = color[format[3]];
}else{
imgd[i] = red;
imgd[i + 1] = green;
imgd[i + 2] = blue;
imgd[i + 3] = alpha;
}
}
// Skip unused bytes in the current row
skip(rowPadding);
}
if(status == Status::ERROR) return;
status = Status::OK;
};
void Parser::clearImgd(){ imgd = nullptr; }
int32_t Parser::getWidth() const{ return w; }
int32_t Parser::getHeight() const{ return h; }
uint8_t *Parser::getImgd() const{ return imgd; }
Status Parser::getStatus() const{ return status; }
string Parser::getErrMsg() const{
return "Error while processing " + getOp() + " - " + err;
}
template <typename T, bool check> inline T Parser::get(){
if(check)
CHECK_OVERRUN(ptr, sizeof(T), T);
T val;
std::memcpy(&val, ptr, sizeof(T));
ptr += sizeof(T);
return val;
}
template <typename T, bool check> inline T Parser::get(uint8_t* pointer){
if(check)
CHECK_OVERRUN(pointer, sizeof(T), T);
T val = *(T*)pointer;
return val;
}
string Parser::getStr(int size, bool reverse){
CHECK_OVERRUN(ptr, size, string);
string val = "";
while(size--){
if(reverse) val = string(1, static_cast<char>(*ptr++)) + val;
else val += static_cast<char>(*ptr++);
}
return val;
}
inline void Parser::skip(int size){
CHECK_OVERRUN(ptr, size, void);
ptr += size;
}
void Parser::calcMaskShift(uint32_t& shift, uint32_t& mask, double& multp){
mask = U4();
shift = 0;
if(mask == 0) return;
while(~mask & 1){
mask >>= 1;
shift++;
}
E(mask & (mask + 1), "invalid color mask");
multp = 255. / mask;
}
void Parser::setOp(string val){
if(status != Status::EMPTY) return;
op = val;
}
string Parser::getOp() const{
return op;
}
void Parser::setErrUnsupported(string msg){
setErr("unsupported " + msg);
}
void Parser::setErrUnknown(string msg){
setErr("unknown " + msg);
}
void Parser::setErr(string msg){
if(status != Status::EMPTY) return;
err = msg;
status = Status::ERROR;
}
string Parser::getErr() const{
return err;
}
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#ifdef ERROR
#define ERROR_ ERROR
#undef ERROR
#endif
#include <stdint.h> // node < 7 uses libstdc++ on macOS which lacks complete c++11
#include <string>
namespace BMPParser{
enum Status{
EMPTY,
OK,
ERROR,
};
class Parser{
public:
Parser()=default;
~Parser();
void parse(uint8_t *buf, int bufSize, uint8_t *format=nullptr);
void clearImgd();
int32_t getWidth() const;
int32_t getHeight() const;
uint8_t *getImgd() const;
Status getStatus() const;
std::string getErrMsg() const;
private:
Status status = Status::EMPTY;
uint8_t *data = nullptr;
uint8_t *ptr = nullptr;
int len = 0;
int32_t w = 0;
int32_t h = 0;
uint8_t *imgd = nullptr;
std::string err = "";
std::string op = "";
template <typename T, bool check=true> inline T get();
template <typename T, bool check=true> inline T get(uint8_t* pointer);
std::string getStr(int len, bool reverse=false);
inline void skip(int len);
void calcMaskShift(uint32_t& shift, uint32_t& mask, double& multp);
void setOp(std::string val);
std::string getOp() const;
void setErrUnsupported(std::string msg);
void setErrUnknown(std::string msg);
void setErr(std::string msg);
std::string getErr() const;
};
}
#ifdef ERROR_
#define ERROR ERROR_
#undef ERROR_
#endif
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>
+52
View File
@@ -0,0 +1,52 @@
#include "closure.h"
#include "Canvas.h"
#ifdef HAVE_JPEG
void JpegClosure::init_destination(j_compress_ptr cinfo) {
JpegClosure* closure = (JpegClosure*)cinfo->client_data;
closure->vec.resize(PAGE_SIZE);
closure->jpeg_dest_mgr->next_output_byte = &closure->vec[0];
closure->jpeg_dest_mgr->free_in_buffer = closure->vec.size();
}
boolean JpegClosure::empty_output_buffer(j_compress_ptr cinfo) {
JpegClosure* closure = (JpegClosure*)cinfo->client_data;
size_t currentSize = closure->vec.size();
closure->vec.resize(currentSize * 1.5);
closure->jpeg_dest_mgr->next_output_byte = &closure->vec[currentSize];
closure->jpeg_dest_mgr->free_in_buffer = closure->vec.size() - currentSize;
return true;
}
void JpegClosure::term_destination(j_compress_ptr cinfo) {
JpegClosure* closure = (JpegClosure*)cinfo->client_data;
size_t finalSize = closure->vec.size() - closure->jpeg_dest_mgr->free_in_buffer;
closure->vec.resize(finalSize);
}
#endif
void
EncodingWorker::Init(void (*work_fn)(Closure*), Closure* closure) {
this->work_fn = work_fn;
this->closure = closure;
}
void
EncodingWorker::Execute() {
this->work_fn(this->closure);
}
void
EncodingWorker::OnWorkComplete(Napi::Env env, napi_status status) {
Napi::HandleScope scope(env);
if (closure->status) {
closure->cb.Call({ closure->canvas->CairoError(closure->status).Value() });
} else {
Napi::Object buf = Napi::Buffer<uint8_t>::Copy(env, &closure->vec[0], closure->vec.size());
closure->cb.Call({ env.Null(), buf });
}
closure->canvas->Unref();
delete closure;
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#pragma once
class Canvas;
#include <cairo.h>
#include "Canvas.h"
#ifdef HAVE_JPEG
#include <stddef.h>
#include <stdio.h>
#include <jpeglib.h>
#endif
#include <napi.h>
#include <png.h>
#include <stdint.h> // node < 7 uses libstdc++ on macOS which lacks complete c++11
#include <vector>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
/*
* Image encoding closures.
*/
struct Closure {
std::vector<uint8_t> vec;
Napi::FunctionReference cb;
Canvas* canvas = nullptr;
cairo_status_t status = CAIRO_STATUS_SUCCESS;
static cairo_status_t writeVec(void *c, const uint8_t *odata, unsigned len) {
Closure* closure = static_cast<Closure*>(c);
try {
closure->vec.insert(closure->vec.end(), odata, odata + len);
} catch (const std::bad_alloc &) {
return CAIRO_STATUS_NO_MEMORY;
}
return CAIRO_STATUS_SUCCESS;
}
Closure(Canvas* canvas) : canvas(canvas) {};
};
struct PdfSvgClosure : Closure {
PdfSvgClosure(Canvas* canvas) : Closure(canvas) {};
};
struct PngClosure : Closure {
uint32_t compressionLevel = 6;
uint32_t filters = PNG_ALL_FILTERS;
uint32_t resolution = 0; // 0 = unspecified
// Indexed PNGs:
uint32_t nPaletteColors = 0;
uint8_t* palette = nullptr;
uint8_t backgroundIndex = 0;
PngClosure(Canvas* canvas) : Closure(canvas) {};
};
#ifdef HAVE_JPEG
struct JpegClosure : Closure {
uint32_t quality = 75;
uint32_t chromaSubsampling = 2;
bool progressive = false;
jpeg_destination_mgr* jpeg_dest_mgr = nullptr;
static void init_destination(j_compress_ptr cinfo);
static boolean empty_output_buffer(j_compress_ptr cinfo);
static void term_destination(j_compress_ptr cinfo);
JpegClosure(Canvas* canvas) : Closure(canvas) {
jpeg_dest_mgr = new jpeg_destination_mgr;
jpeg_dest_mgr->init_destination = init_destination;
jpeg_dest_mgr->empty_output_buffer = empty_output_buffer;
jpeg_dest_mgr->term_destination = term_destination;
};
~JpegClosure() {
delete jpeg_dest_mgr;
}
};
#endif
class EncodingWorker : public Napi::AsyncWorker {
public:
EncodingWorker(Napi::Env env): Napi::AsyncWorker(env) {};
void Init(void (*work_fn)(Closure*), Closure* closure);
void Execute() override;
void OnWorkComplete(Napi::Env env, napi_status status) override;
private:
void (*work_fn)(Closure*) = nullptr;
Closure* closure = nullptr;
};
+796
View File
@@ -0,0 +1,796 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#include "color.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <map>
#include <string>
// Compatibility with Visual Studio versions prior to VS2015
#if defined(_MSC_VER) && _MSC_VER < 1900
#define snprintf _snprintf
#endif
/*
* Parse integer value
*/
template <typename parsed_t>
static bool
parse_integer(const char** pStr, parsed_t *pParsed) {
parsed_t& c = *pParsed;
const char*& str = *pStr;
int8_t sign=1;
c = 0;
if (*str == '-') {
sign=-1;
++str;
}
else if (*str == '+')
++str;
if (*str >= '0' && *str <= '9') {
do {
c *= 10;
c += *str++ - '0';
} while (*str >= '0' && *str <= '9');
} else {
return false;
}
if (sign<0)
c=-c;
return true;
}
/*
* Parse CSS <number> value
* Adapted from http://crackprogramming.blogspot.co.il/2012/10/implement-atof.html
*/
template <typename parsed_t>
static bool
parse_css_number(const char** pStr, parsed_t *pParsed) {
parsed_t &parsed = *pParsed;
const char*& str = *pStr;
const char* startStr = str;
if (!str || !*str)
return false;
parsed_t integerPart = 0;
parsed_t fractionPart = 0;
int divisorForFraction = 1;
int sign = 1;
int exponent = 0;
int digits = 0;
bool inFraction = false;
if (*str == '-') {
++str;
sign = -1;
}
else if (*str == '+')
++str;
while (*str != '\0') {
if (*str >= '0' && *str <= '9') {
if (digits>=std::numeric_limits<parsed_t>::digits10) {
if (!inFraction)
return false;
}
else {
++digits;
if (inFraction) {
fractionPart = fractionPart*10 + (*str - '0');
divisorForFraction *= 10;
}
else {
integerPart = integerPart*10 + (*str - '0');
}
}
}
else if (*str == '.') {
if (inFraction)
break;
else
inFraction = true;
}
else if (*str == 'e') {
++str;
if (!parse_integer(&str, &exponent))
return false;
break;
}
else
break;
++str;
}
if (str != startStr) {
parsed = sign * (integerPart + fractionPart/divisorForFraction);
for (;exponent>0;--exponent)
parsed *= 10;
for (;exponent<0;++exponent)
parsed /= 10;
return true;
}
return false;
}
/*
* Clip value to the range [minValue, maxValue]
*/
template <typename T>
static T
clip(T value, T minValue, T maxValue) {
if (value > maxValue)
value = maxValue;
if (value < minValue)
value = minValue;
return value;
}
/*
* Wrap value to the range [0, limit]
*/
template <typename T>
static T
wrap_float(T value, T limit) {
return fmod(fmod(value, limit) + limit, limit);
}
/*
* Wrap value to the range [0, limit] - currently-unused integer version of wrap_float
*/
// template <typename T>
// static T wrap_int(T value, T limit) {
// return (value % limit + limit) % limit;
// }
/*
* Parse color channel value
*/
static bool
parse_rgb_channel(const char** pStr, uint8_t *pChannel) {
float f_channel;
if (parse_css_number(pStr, &f_channel)) {
int channel = (int) ceil(f_channel);
*pChannel = clip(channel, 0, 255);
return true;
}
return false;
}
/*
* Parse a value in degrees
*/
static bool
parse_degrees(const char** pStr, float *pDegrees) {
float degrees;
if (parse_css_number(pStr, &degrees)) {
*pDegrees = wrap_float(degrees, 360.0f);
return true;
}
return false;
}
/*
* Parse and clip a percentage value. Returns a float in the range [0, 1].
*/
static bool
parse_clipped_percentage(const char** pStr, float *pFraction) {
float percentage;
bool result = parse_css_number(pStr,&percentage);
const char*& str = *pStr;
if (result) {
if (*str == '%') {
++str;
*pFraction = clip(percentage, 0.0f, 100.0f) / 100.0f;
return result;
}
}
return false;
}
/*
* Macros to help with parsing inside rgba_from_*_string
*/
#define WHITESPACE \
while (' ' == *str) ++str;
#define WHITESPACE_OR_COMMA \
while (' ' == *str || ',' == *str) ++str;
#define WHITESPACE_OR_COMMA_OR_SLASH \
while (' ' == *str || ',' == *str || '/' == *str) ++str;
#define CHANNEL(NAME) \
if (!parse_rgb_channel(&str, &NAME)) \
return 0; \
#define HUE(NAME) \
if (!parse_degrees(&str, &NAME)) \
return 0;
#define SATURATION(NAME) \
if (!parse_clipped_percentage(&str, &NAME)) \
return 0;
#define LIGHTNESS(NAME) SATURATION(NAME)
#define ALPHA(NAME) \
if (*str >= '1' && *str <= '9') { \
NAME = 0; \
float n = .1f; \
while(*str >='0' && *str <= '9') { \
NAME += (*str - '0') * n; \
str++; \
} \
while(*str == ' ')str++; \
if(*str != '%') { \
NAME = 1; \
} \
} else { \
if ('0' == *str) { \
NAME = 0; \
++str; \
} \
if ('.' == *str) { \
++str; \
NAME = 0; \
float n = .1f; \
while (*str >= '0' && *str <= '9') { \
NAME += (*str++ - '0') * n; \
n *= .1f; \
} \
} \
} \
do {} while (0) // require trailing semicolon
/*
* Named colors.
*/
static const std::map<std::string, uint32_t> named_colors = {
{ "transparent", 0xFFFFFF00}
, { "aliceblue", 0xF0F8FFFF }
, { "antiquewhite", 0xFAEBD7FF }
, { "aqua", 0x00FFFFFF }
, { "aquamarine", 0x7FFFD4FF }
, { "azure", 0xF0FFFFFF }
, { "beige", 0xF5F5DCFF }
, { "bisque", 0xFFE4C4FF }
, { "black", 0x000000FF }
, { "blanchedalmond", 0xFFEBCDFF }
, { "blue", 0x0000FFFF }
, { "blueviolet", 0x8A2BE2FF }
, { "brown", 0xA52A2AFF }
, { "burlywood", 0xDEB887FF }
, { "cadetblue", 0x5F9EA0FF }
, { "chartreuse", 0x7FFF00FF }
, { "chocolate", 0xD2691EFF }
, { "coral", 0xFF7F50FF }
, { "cornflowerblue", 0x6495EDFF }
, { "cornsilk", 0xFFF8DCFF }
, { "crimson", 0xDC143CFF }
, { "cyan", 0x00FFFFFF }
, { "darkblue", 0x00008BFF }
, { "darkcyan", 0x008B8BFF }
, { "darkgoldenrod", 0xB8860BFF }
, { "darkgray", 0xA9A9A9FF }
, { "darkgreen", 0x006400FF }
, { "darkgrey", 0xA9A9A9FF }
, { "darkkhaki", 0xBDB76BFF }
, { "darkmagenta", 0x8B008BFF }
, { "darkolivegreen", 0x556B2FFF }
, { "darkorange", 0xFF8C00FF }
, { "darkorchid", 0x9932CCFF }
, { "darkred", 0x8B0000FF }
, { "darksalmon", 0xE9967AFF }
, { "darkseagreen", 0x8FBC8FFF }
, { "darkslateblue", 0x483D8BFF }
, { "darkslategray", 0x2F4F4FFF }
, { "darkslategrey", 0x2F4F4FFF }
, { "darkturquoise", 0x00CED1FF }
, { "darkviolet", 0x9400D3FF }
, { "deeppink", 0xFF1493FF }
, { "deepskyblue", 0x00BFFFFF }
, { "dimgray", 0x696969FF }
, { "dimgrey", 0x696969FF }
, { "dodgerblue", 0x1E90FFFF }
, { "firebrick", 0xB22222FF }
, { "floralwhite", 0xFFFAF0FF }
, { "forestgreen", 0x228B22FF }
, { "fuchsia", 0xFF00FFFF }
, { "gainsboro", 0xDCDCDCFF }
, { "ghostwhite", 0xF8F8FFFF }
, { "gold", 0xFFD700FF }
, { "goldenrod", 0xDAA520FF }
, { "gray", 0x808080FF }
, { "green", 0x008000FF }
, { "greenyellow", 0xADFF2FFF }
, { "grey", 0x808080FF }
, { "honeydew", 0xF0FFF0FF }
, { "hotpink", 0xFF69B4FF }
, { "indianred", 0xCD5C5CFF }
, { "indigo", 0x4B0082FF }
, { "ivory", 0xFFFFF0FF }
, { "khaki", 0xF0E68CFF }
, { "lavender", 0xE6E6FAFF }
, { "lavenderblush", 0xFFF0F5FF }
, { "lawngreen", 0x7CFC00FF }
, { "lemonchiffon", 0xFFFACDFF }
, { "lightblue", 0xADD8E6FF }
, { "lightcoral", 0xF08080FF }
, { "lightcyan", 0xE0FFFFFF }
, { "lightgoldenrodyellow", 0xFAFAD2FF }
, { "lightgray", 0xD3D3D3FF }
, { "lightgreen", 0x90EE90FF }
, { "lightgrey", 0xD3D3D3FF }
, { "lightpink", 0xFFB6C1FF }
, { "lightsalmon", 0xFFA07AFF }
, { "lightseagreen", 0x20B2AAFF }
, { "lightskyblue", 0x87CEFAFF }
, { "lightslategray", 0x778899FF }
, { "lightslategrey", 0x778899FF }
, { "lightsteelblue", 0xB0C4DEFF }
, { "lightyellow", 0xFFFFE0FF }
, { "lime", 0x00FF00FF }
, { "limegreen", 0x32CD32FF }
, { "linen", 0xFAF0E6FF }
, { "magenta", 0xFF00FFFF }
, { "maroon", 0x800000FF }
, { "mediumaquamarine", 0x66CDAAFF }
, { "mediumblue", 0x0000CDFF }
, { "mediumorchid", 0xBA55D3FF }
, { "mediumpurple", 0x9370DBFF }
, { "mediumseagreen", 0x3CB371FF }
, { "mediumslateblue", 0x7B68EEFF }
, { "mediumspringgreen", 0x00FA9AFF }
, { "mediumturquoise", 0x48D1CCFF }
, { "mediumvioletred", 0xC71585FF }
, { "midnightblue", 0x191970FF }
, { "mintcream", 0xF5FFFAFF }
, { "mistyrose", 0xFFE4E1FF }
, { "moccasin", 0xFFE4B5FF }
, { "navajowhite", 0xFFDEADFF }
, { "navy", 0x000080FF }
, { "oldlace", 0xFDF5E6FF }
, { "olive", 0x808000FF }
, { "olivedrab", 0x6B8E23FF }
, { "orange", 0xFFA500FF }
, { "orangered", 0xFF4500FF }
, { "orchid", 0xDA70D6FF }
, { "palegoldenrod", 0xEEE8AAFF }
, { "palegreen", 0x98FB98FF }
, { "paleturquoise", 0xAFEEEEFF }
, { "palevioletred", 0xDB7093FF }
, { "papayawhip", 0xFFEFD5FF }
, { "peachpuff", 0xFFDAB9FF }
, { "peru", 0xCD853FFF }
, { "pink", 0xFFC0CBFF }
, { "plum", 0xDDA0DDFF }
, { "powderblue", 0xB0E0E6FF }
, { "purple", 0x800080FF }
, { "rebeccapurple", 0x663399FF } // Source: CSS Color Level 4 draft
, { "red", 0xFF0000FF }
, { "rosybrown", 0xBC8F8FFF }
, { "royalblue", 0x4169E1FF }
, { "saddlebrown", 0x8B4513FF }
, { "salmon", 0xFA8072FF }
, { "sandybrown", 0xF4A460FF }
, { "seagreen", 0x2E8B57FF }
, { "seashell", 0xFFF5EEFF }
, { "sienna", 0xA0522DFF }
, { "silver", 0xC0C0C0FF }
, { "skyblue", 0x87CEEBFF }
, { "slateblue", 0x6A5ACDFF }
, { "slategray", 0x708090FF }
, { "slategrey", 0x708090FF }
, { "snow", 0xFFFAFAFF }
, { "springgreen", 0x00FF7FFF }
, { "steelblue", 0x4682B4FF }
, { "tan", 0xD2B48CFF }
, { "teal", 0x008080FF }
, { "thistle", 0xD8BFD8FF }
, { "tomato", 0xFF6347FF }
, { "turquoise", 0x40E0D0FF }
, { "violet", 0xEE82EEFF }
, { "wheat", 0xF5DEB3FF }
, { "white", 0xFFFFFFFF }
, { "whitesmoke", 0xF5F5F5FF }
, { "yellow", 0xFFFF00FF }
, { "yellowgreen", 0x9ACD32FF }
};
/*
* Hex digit int val.
*/
static int
h(char c) {
switch (c) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return c - '0';
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
return (c - 'a') + 10;
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
return (c - 'A') + 10;
}
return 0;
}
/*
* Return rgba_t from rgba.
*/
rgba_t
rgba_create(uint32_t rgba) {
rgba_t color;
color.r = (double) (rgba >> 24) / 255;
color.g = (double) (rgba >> 16 & 0xff) / 255;
color.b = (double) (rgba >> 8 & 0xff) / 255;
color.a = (double) (rgba & 0xff) / 255;
return color;
}
/*
* Return a string representation of the color.
*/
void
rgba_to_string(rgba_t rgba, char *buf, size_t len) {
if (1 == rgba.a) {
snprintf(buf, len, "#%.2x%.2x%.2x",
static_cast<int>(round(rgba.r * 255)),
static_cast<int>(round(rgba.g * 255)),
static_cast<int>(round(rgba.b * 255)));
} else {
snprintf(buf, len, "rgba(%d, %d, %d, %.2f)",
static_cast<int>(round(rgba.r * 255)),
static_cast<int>(round(rgba.g * 255)),
static_cast<int>(round(rgba.b * 255)),
rgba.a);
}
}
/*
* Return rgba from (r,g,b,a).
*/
static inline int32_t
rgba_from_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
return
r << 24
| g << 16
| b << 8
| a;
}
/*
* Helper function used in rgba_from_hsla().
* Based on http://dev.w3.org/csswg/css-color-4/#hsl-to-rgb
*/
static float
hue_to_rgb(float t1, float t2, float hue) {
if (hue < 0)
hue += 6;
if (hue >= 6)
hue -= 6;
if (hue < 1)
return (t2 - t1) * hue + t1;
else if (hue < 3)
return t2;
else if (hue < 4)
return (t2 - t1) * (4 - hue) + t1;
else
return t1;
}
/*
* Return rgba from (h,s,l,a).
* Expects h values in the range [0, 360), and s, l, a in the range [0, 1].
* Adapted from http://dev.w3.org/csswg/css-color-4/#hsl-to-rgb
*/
static inline int32_t
rgba_from_hsla(float h_deg, float s, float l, float a) {
uint8_t r, g, b;
float h = (6 * h_deg) / 360.0f, m1, m2;
if (l<=0.5)
m2=l*(s+1);
else
m2=l+s-l*s;
m1 = l*2 - m2;
// Scale and round the RGB components
r = (uint8_t)floor(hue_to_rgb(m1, m2, h + 2) * 255 + 0.5);
g = (uint8_t)floor(hue_to_rgb(m1, m2, h ) * 255 + 0.5);
b = (uint8_t)floor(hue_to_rgb(m1, m2, h - 2) * 255 + 0.5);
return rgba_from_rgba(r, g, b, (uint8_t) (a * 255));
}
/*
* Return rgba from (h,s,l).
* Expects h values in the range [0, 360), and s, l in the range [0, 1].
*/
static inline int32_t
rgba_from_hsl(float h_deg, float s, float l) {
return rgba_from_hsla(h_deg, s, l, 1.0);
}
/*
* Return rgba from (r,g,b).
*/
static int32_t
rgba_from_rgb(uint8_t r, uint8_t g, uint8_t b) {
return rgba_from_rgba(r, g, b, 255);
}
/*
* Return rgba from #RRGGBBAA
*/
static int32_t
rgba_from_hex8_string(const char *str) {
return rgba_from_rgba(
(h(str[0]) << 4) + h(str[1]),
(h(str[2]) << 4) + h(str[3]),
(h(str[4]) << 4) + h(str[5]),
(h(str[6]) << 4) + h(str[7])
);
}
/*
* Return rgb from "#RRGGBB".
*/
static int32_t
rgba_from_hex6_string(const char *str) {
return rgba_from_rgb(
(h(str[0]) << 4) + h(str[1])
, (h(str[2]) << 4) + h(str[3])
, (h(str[4]) << 4) + h(str[5])
);
}
/*
* Return rgba from #RGBA
*/
static int32_t
rgba_from_hex4_string(const char *str) {
return rgba_from_rgba(
(h(str[0]) << 4) + h(str[0]),
(h(str[1]) << 4) + h(str[1]),
(h(str[2]) << 4) + h(str[2]),
(h(str[3]) << 4) + h(str[3])
);
}
/*
* Return rgb from "#RGB"
*/
static int32_t
rgba_from_hex3_string(const char *str) {
return rgba_from_rgb(
(h(str[0]) << 4) + h(str[0])
, (h(str[1]) << 4) + h(str[1])
, (h(str[2]) << 4) + h(str[2])
);
}
/*
* Return rgb from "rgb()"
*/
static int32_t
rgba_from_rgb_string(const char *str, short *ok) {
if (str == strstr(str, "rgb(")) {
str += 4;
WHITESPACE;
uint8_t r = 0, g = 0, b = 0;
float a=1.f;
CHANNEL(r);
WHITESPACE_OR_COMMA;
CHANNEL(g);
WHITESPACE_OR_COMMA;
CHANNEL(b);
WHITESPACE_OR_COMMA_OR_SLASH;
ALPHA(a);
return *ok = 1, rgba_from_rgba(r, g, b, (int) (255 * a));
}
return *ok = 0;
}
/*
* Return rgb from "rgba()"
*/
static int32_t
rgba_from_rgba_string(const char *str, short *ok) {
if (str == strstr(str, "rgba(")) {
str += 5;
WHITESPACE;
uint8_t r = 0, g = 0, b = 0;
float a = 1.f;
CHANNEL(r);
WHITESPACE_OR_COMMA;
CHANNEL(g);
WHITESPACE_OR_COMMA;
CHANNEL(b);
WHITESPACE_OR_COMMA_OR_SLASH;
ALPHA(a);
WHITESPACE;
return *ok = 1, rgba_from_rgba(r, g, b, (int) (a * 255));
}
return *ok = 0;
}
/*
* Return rgb from "hsla()"
*/
static int32_t
rgba_from_hsla_string(const char *str, short *ok) {
if (str == strstr(str, "hsla(")) {
str += 5;
WHITESPACE;
float h_deg = 0;
float s = 0, l = 0;
float a = 0;
HUE(h_deg);
WHITESPACE_OR_COMMA;
SATURATION(s);
WHITESPACE_OR_COMMA;
LIGHTNESS(l);
WHITESPACE_OR_COMMA;
ALPHA(a);
WHITESPACE;
return *ok = 1, rgba_from_hsla(h_deg, s, l, a);
}
return *ok = 0;
}
/*
* Return rgb from "hsl()"
*/
static int32_t
rgba_from_hsl_string(const char *str, short *ok) {
if (str == strstr(str, "hsl(")) {
str += 4;
WHITESPACE;
float h_deg = 0;
float s = 0, l = 0;
HUE(h_deg);
WHITESPACE_OR_COMMA;
SATURATION(s);
WHITESPACE_OR_COMMA;
LIGHTNESS(l);
WHITESPACE;
return *ok = 1, rgba_from_hsl(h_deg, s, l);
}
return *ok = 0;
}
/*
* Return rgb from:
*
* - "#RGB"
* - "#RGBA"
* - "#RRGGBB"
* - "#RRGGBBAA"
*
*/
static int32_t
rgba_from_hex_string(const char *str, short *ok) {
size_t len = strlen(str);
*ok = 1;
switch (len) {
case 8: return rgba_from_hex8_string(str);
case 6: return rgba_from_hex6_string(str);
case 4: return rgba_from_hex4_string(str);
case 3: return rgba_from_hex3_string(str);
}
return *ok = 0;
}
/*
* Return named color value.
*/
static int32_t
rgba_from_name_string(const char *str, short *ok) {
WHITESPACE;
std::string lowered(str);
std::transform(lowered.begin(), lowered.end(), lowered.begin(), tolower);
auto color = named_colors.find(lowered);
if (color != named_colors.end()) {
return *ok = 1, color->second;
}
return *ok = 0;
}
/*
* Return rgb from:
*
* - #RGB
* - #RGBA
* - #RRGGBB
* - #RRGGBBAA
* - rgb(r,g,b)
* - rgba(r,g,b,a)
* - hsl(h,s,l)
* - hsla(h,s,l,a)
* - name
*
*/
int32_t
rgba_from_string(const char *str, short *ok) {
WHITESPACE;
if ('#' == str[0])
return rgba_from_hex_string(++str, ok);
if (str == strstr(str, "rgba"))
return rgba_from_rgba_string(str, ok);
if (str == strstr(str, "rgb"))
return rgba_from_rgb_string(str, ok);
if (str == strstr(str, "hsla"))
return rgba_from_hsla_string(str, ok);
if (str == strstr(str, "hsl"))
return rgba_from_hsl_string(str, ok);
return rgba_from_name_string(str, ok);
}
/*
* Inspect the given rgba color.
*/
void
rgba_inspect(int32_t rgba) {
printf("rgba(%d,%d,%d,%d)\n"
, rgba >> 24 & 0xff
, rgba >> 16 & 0xff
, rgba >> 8 & 0xff
, rgba & 0xff
);
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#pragma once
#include <stdint.h> // node < 7 uses libstdc++ on macOS which lacks complete c++11
#include <cstdlib>
/*
* RGBA struct.
*/
typedef struct {
double r, g, b, a;
} rgba_t;
/*
* Prototypes.
*/
rgba_t
rgba_create(uint32_t rgba);
int32_t
rgba_from_string(const char *str, short *ok);
void
rgba_to_string(rgba_t rgba, char *buf, size_t len);
void
rgba_inspect(int32_t rgba);
+20
View File
@@ -0,0 +1,20 @@
#ifndef DLL_PUBLIC
#if defined _WIN32
#ifdef __GNUC__
#define DLL_PUBLIC __attribute__ ((dllexport))
#else
#define DLL_PUBLIC __declspec(dllexport)
#endif
#define DLL_LOCAL
#else
#if __GNUC__ >= 4
#define DLL_PUBLIC __attribute__ ((visibility ("default")))
#define DLL_LOCAL __attribute__ ((visibility ("hidden")))
#else
#define DLL_PUBLIC
#define DLL_LOCAL
#endif
#endif
#endif
+114
View File
@@ -0,0 +1,114 @@
// Copyright (c) 2010 LearnBoost <tj@learnboost.com>
#include <cstdio>
#include <pango/pango.h>
#include <cairo.h>
#if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1, 10, 0)
// CAIRO_FORMAT_RGB16_565: undeprecated in v1.10.0
// CAIRO_STATUS_INVALID_SIZE: v1.10.0
// CAIRO_FORMAT_INVALID: v1.10.0
// Lots of the compositing operators: v1.10.0
// JPEG MIME tracking: v1.10.0
// Note: CAIRO_FORMAT_RGB30 is v1.12.0 and still optional
#error("cairo v1.10.0 or later is required")
#endif
#include "Canvas.h"
#include "CanvasGradient.h"
#include "CanvasPattern.h"
#include "CanvasRenderingContext2d.h"
#include "Image.h"
#include "ImageData.h"
#include "InstanceData.h"
#include <ft2build.h>
#include FT_FREETYPE_H
/*
* Save some external modules as private references.
*/
static void
setDOMMatrix(const Napi::CallbackInfo& info) {
InstanceData* data = info.Env().GetInstanceData<InstanceData>();
data->DOMMatrixCtor = Napi::Persistent(info[0].As<Napi::Function>());
}
static void
setParseFont(const Napi::CallbackInfo& info) {
InstanceData* data = info.Env().GetInstanceData<InstanceData>();
data->parseFont = Napi::Persistent(info[0].As<Napi::Function>());
}
// Compatibility with Visual Studio versions prior to VS2015
#if defined(_MSC_VER) && _MSC_VER < 1900
#define snprintf _snprintf
#endif
Napi::Object init(Napi::Env env, Napi::Object exports) {
env.SetInstanceData(new InstanceData());
Canvas::Initialize(env, exports);
Image::Initialize(env, exports);
ImageData::Initialize(env, exports);
Context2d::Initialize(env, exports);
Gradient::Initialize(env, exports);
Pattern::Initialize(env, exports);
exports.Set("setDOMMatrix", Napi::Function::New(env, &setDOMMatrix));
exports.Set("setParseFont", Napi::Function::New(env, &setParseFont));
exports.Set("cairoVersion", Napi::String::New(env, cairo_version_string()));
#ifdef HAVE_JPEG
#ifndef JPEG_LIB_VERSION_MAJOR
#ifdef JPEG_LIB_VERSION
#define JPEG_LIB_VERSION_MAJOR (JPEG_LIB_VERSION / 10)
#else
#define JPEG_LIB_VERSION_MAJOR 0
#endif
#endif
#ifndef JPEG_LIB_VERSION_MINOR
#ifdef JPEG_LIB_VERSION
#define JPEG_LIB_VERSION_MINOR (JPEG_LIB_VERSION % 10)
#else
#define JPEG_LIB_VERSION_MINOR 0
#endif
#endif
char jpeg_version[10];
static bool minor_gt_0 = JPEG_LIB_VERSION_MINOR > 0;
if (minor_gt_0) {
snprintf(jpeg_version, 10, "%d%c", JPEG_LIB_VERSION_MAJOR, JPEG_LIB_VERSION_MINOR + 'a' - 1);
} else {
snprintf(jpeg_version, 10, "%d", JPEG_LIB_VERSION_MAJOR);
}
exports.Set("jpegVersion", Napi::String::New(env, jpeg_version));
#endif
#ifdef HAVE_GIF
#ifndef GIF_LIB_VERSION
char gif_version[10];
snprintf(gif_version, 10, "%d.%d.%d", GIFLIB_MAJOR, GIFLIB_MINOR, GIFLIB_RELEASE);
exports.Set("gifVersion", Napi::String::New(env, gif_version));
#else
exports.Set("gifVersion", Napi::String::New(env, GIF_LIB_VERSION));
#endif
#endif
#ifdef HAVE_RSVG
exports.Set("rsvgVersion", Napi::String::New(env, LIBRSVG_VERSION));
#endif
exports.Set("pangoVersion", Napi::String::New(env, PANGO_VERSION_STRING));
char freetype_version[10];
snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH);
exports.Set("freetypeVersion", Napi::String::New(env, freetype_version));
return exports;
}
NODE_API_MODULE(canvas, init);
+352
View File
@@ -0,0 +1,352 @@
#include "register_font.h"
#include <pango/pangocairo.h>
#include <pango/pango-fontmap.h>
#include <pango/pango.h>
#ifdef __APPLE__
#include <CoreText/CoreText.h>
#elif defined(_WIN32)
#include <windows.h>
#include <memory>
#else
#include <fontconfig/fontconfig.h>
#endif
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_TRUETYPE_TABLES_H
#include FT_SFNT_NAMES_H
#include FT_TRUETYPE_IDS_H
#ifndef FT_SFNT_OS2
#define FT_SFNT_OS2 ft_sfnt_os2
#endif
// OSX seems to read the strings in MacRoman encoding and ignore Unicode entries.
// You can verify this by opening a TTF with both Unicode and Macroman on OSX.
// It uses the MacRoman name, while Fontconfig and Windows use Unicode
#ifdef __APPLE__
#define PREFERRED_PLATFORM_ID TT_PLATFORM_MACINTOSH
#define PREFERRED_ENCODING_ID TT_MAC_ID_ROMAN
#else
#define PREFERRED_PLATFORM_ID TT_PLATFORM_MICROSOFT
#define PREFERRED_ENCODING_ID TT_MS_ID_UNICODE_CS
#endif
#define IS_PREFERRED_ENC(X) \
X.platform_id == PREFERRED_PLATFORM_ID && X.encoding_id == PREFERRED_ENCODING_ID
#define GET_NAME_RANK(X) \
(IS_PREFERRED_ENC(X) ? 1 : 0) + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0)
/*
* Return a UTF-8 encoded string given a TrueType name buf+len
* and its platform and encoding
*/
char *
to_utf8(FT_Byte* buf, FT_UInt len, FT_UShort pid, FT_UShort eid) {
size_t ret_len = len * 4; // max chars in a utf8 string
char *ret = (char*)malloc(ret_len + 1); // utf8 string + null
if (!ret) return NULL;
// In my testing of hundreds of fonts from the Google Font repo, the two types
// of fonts are TT_PLATFORM_MICROSOFT with TT_MS_ID_UNICODE_CS encoding, or
// TT_PLATFORM_MACINTOSH with TT_MAC_ID_ROMAN encoding. Usually both, never neither
char const *fromcode;
if (pid == TT_PLATFORM_MACINTOSH && eid == TT_MAC_ID_ROMAN) {
fromcode = "MAC";
} else if (pid == TT_PLATFORM_MICROSOFT && eid == TT_MS_ID_UNICODE_CS) {
fromcode = "UTF-16BE";
} else {
free(ret);
return NULL;
}
GIConv cd = g_iconv_open("UTF-8", fromcode);
if (cd == (GIConv)-1) {
free(ret);
return NULL;
}
size_t inbytesleft = len;
size_t outbytesleft = ret_len;
size_t n_converted = g_iconv(cd, (char**)&buf, &inbytesleft, &ret, &outbytesleft);
ret -= ret_len - outbytesleft; // rewind the pointers to their
buf -= len - inbytesleft; // original starting positions
if (n_converted == (size_t)-1) {
free(ret);
return NULL;
} else {
ret[ret_len - outbytesleft] = '\0';
return ret;
}
}
/*
* Find a family name in the face's name table, preferring the one the
* system, fall back to the other
*/
char *
get_family_name(FT_Face face) {
FT_SfntName name;
int best_rank = -1;
char* best_buf = NULL;
for (unsigned i = 0; i < FT_Get_Sfnt_Name_Count(face); ++i) {
FT_Get_Sfnt_Name(face, i, &name);
if (name.name_id == TT_NAME_ID_FONT_FAMILY || name.name_id == TT_NAME_ID_PREFERRED_FAMILY) {
char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id);
if (buf) {
int rank = GET_NAME_RANK(name);
if (rank > best_rank) {
best_rank = rank;
if (best_buf) free(best_buf);
best_buf = buf;
} else {
free(buf);
}
}
}
}
return best_buf;
}
PangoWeight
get_pango_weight(FT_UShort weight) {
switch (weight) {
case 100: return PANGO_WEIGHT_THIN;
case 200: return PANGO_WEIGHT_ULTRALIGHT;
case 300: return PANGO_WEIGHT_LIGHT;
#if PANGO_VERSION >= PANGO_VERSION_ENCODE(1, 36, 7)
case 350: return PANGO_WEIGHT_SEMILIGHT;
#endif
case 380: return PANGO_WEIGHT_BOOK;
case 400: return PANGO_WEIGHT_NORMAL;
case 500: return PANGO_WEIGHT_MEDIUM;
case 600: return PANGO_WEIGHT_SEMIBOLD;
case 700: return PANGO_WEIGHT_BOLD;
case 800: return PANGO_WEIGHT_ULTRABOLD;
case 900: return PANGO_WEIGHT_HEAVY;
case 1000: return PANGO_WEIGHT_ULTRAHEAVY;
default: return PANGO_WEIGHT_NORMAL;
}
}
PangoStretch
get_pango_stretch(FT_UShort width) {
switch (width) {
case 1: return PANGO_STRETCH_ULTRA_CONDENSED;
case 2: return PANGO_STRETCH_EXTRA_CONDENSED;
case 3: return PANGO_STRETCH_CONDENSED;
case 4: return PANGO_STRETCH_SEMI_CONDENSED;
case 5: return PANGO_STRETCH_NORMAL;
case 6: return PANGO_STRETCH_SEMI_EXPANDED;
case 7: return PANGO_STRETCH_EXPANDED;
case 8: return PANGO_STRETCH_EXTRA_EXPANDED;
case 9: return PANGO_STRETCH_ULTRA_EXPANDED;
default: return PANGO_STRETCH_NORMAL;
}
}
PangoStyle
get_pango_style(FT_Long flags) {
if (flags & FT_STYLE_FLAG_ITALIC) {
return PANGO_STYLE_ITALIC;
} else {
return PANGO_STYLE_NORMAL;
}
}
#ifdef _WIN32
std::unique_ptr<wchar_t[]>
u8ToWide(const char* str) {
int iBufferSize = MultiByteToWideChar(CP_UTF8, 0, str, -1, (wchar_t*)NULL, 0);
if(!iBufferSize){
return nullptr;
}
std::unique_ptr<wchar_t[]> wpBufWString = std::unique_ptr<wchar_t[]>{ new wchar_t[static_cast<size_t>(iBufferSize)] };
if(!MultiByteToWideChar(CP_UTF8, 0, str, -1, wpBufWString.get(), iBufferSize)){
return nullptr;
}
return wpBufWString;
}
static unsigned long
stream_read_func(FT_Stream stream, unsigned long offset, unsigned char* buffer, unsigned long count){
HANDLE hFile = reinterpret_cast<HANDLE>(stream->descriptor.pointer);
DWORD numberOfBytesRead;
OVERLAPPED overlapped;
overlapped.Offset = offset;
overlapped.OffsetHigh = 0;
overlapped.hEvent = NULL;
if(!ReadFile(hFile, buffer, count, &numberOfBytesRead, &overlapped)){
return 0;
}
return numberOfBytesRead;
};
static void
stream_close_func(FT_Stream stream){
HANDLE hFile = reinterpret_cast<HANDLE>(stream->descriptor.pointer);
CloseHandle(hFile);
}
#endif
/*
* Return a PangoFontDescription that will resolve to the font file
*/
PangoFontDescription *
get_pango_font_description(unsigned char* filepath) {
FT_Library library;
FT_Face face;
PangoFontDescription *desc = pango_font_description_new();
#ifdef _WIN32
// FT_New_Face use fopen.
// Unable to find the file when supplied the multibyte string path on the Windows platform and throw error "Could not parse font file".
// This workaround fixes this by reading the font file uses win32 wide character API.
std::unique_ptr<wchar_t[]> wFilepath = u8ToWide((char*)filepath);
if(!wFilepath){
return NULL;
}
HANDLE hFile = CreateFileW(
wFilepath.get(),
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if(!hFile){
return NULL;
}
LARGE_INTEGER liSize;
if(!GetFileSizeEx(hFile, &liSize)) {
CloseHandle(hFile);
return NULL;
}
FT_Open_Args args;
args.flags = FT_OPEN_STREAM;
FT_StreamRec stream;
stream.base = NULL;
stream.size = liSize.QuadPart;
stream.pos = 0;
stream.descriptor.pointer = hFile;
stream.read = stream_read_func;
stream.close = stream_close_func;
args.stream = &stream;
if (
!FT_Init_FreeType(&library) &&
!FT_Open_Face(library, &args, 0, &face)) {
#else
if (!FT_Init_FreeType(&library) && !FT_New_Face(library, (const char*)filepath, 0, &face)) {
#endif
TT_OS2 *table = (TT_OS2*)FT_Get_Sfnt_Table(face, FT_SFNT_OS2);
if (table) {
char *family = get_family_name(face);
if (!family) {
pango_font_description_free(desc);
FT_Done_Face(face);
FT_Done_FreeType(library);
return NULL;
}
pango_font_description_set_family(desc, family);
free(family);
pango_font_description_set_weight(desc, get_pango_weight(table->usWeightClass));
pango_font_description_set_stretch(desc, get_pango_stretch(table->usWidthClass));
pango_font_description_set_style(desc, get_pango_style(face->style_flags));
FT_Done_Face(face);
FT_Done_FreeType(library);
return desc;
}
}
pango_font_description_free(desc);
return NULL;
}
/*
* Register font with the OS
*/
bool
register_font(unsigned char *filepath) {
bool success;
#ifdef __APPLE__
CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false);
success = CTFontManagerRegisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL);
#elif defined(_WIN32)
std::unique_ptr<wchar_t[]> wFilepath = u8ToWide((char*)filepath);
if(wFilepath){
success = AddFontResourceExW(wFilepath.get(), FR_PRIVATE, 0) != 0;
}else{
success = false;
}
#else
success = FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(filepath));
#endif
if (!success) return false;
// Tell Pango to throw away the current FontMap and create a new one. This
// has the effect of registering the new font in Pango by re-looking up all
// font families.
pango_cairo_font_map_set_default(NULL);
return true;
}
/*
* Deregister font from the OS
* Note that Linux (FontConfig) can only dereregister ALL fonts at once.
*/
bool
deregister_font(unsigned char *filepath) {
bool success;
#ifdef __APPLE__
CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false);
success = CTFontManagerUnregisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL);
#elif defined(_WIN32)
std::unique_ptr<wchar_t[]> wFilepath = u8ToWide((char*)filepath);
if(wFilepath){
success = RemoveFontResourceExW(wFilepath.get(), FR_PRIVATE, 0) != 0;
}else{
success = false;
}
#else
FcConfigAppFontClear(FcConfigGetCurrent());
success = true;
#endif
if (!success) return false;
// Tell Pango to throw away the current FontMap and create a new one. This
// has the effect of deregistering the font in Pango by re-looking up all
// font families.
pango_cairo_font_map_set_default(NULL);
return true;
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include <pango/pango.h>
PangoFontDescription *get_pango_font_description(unsigned char *filepath);
bool register_font(unsigned char *filepath);
bool deregister_font(unsigned char *filepath);
+119
View File
@@ -0,0 +1,119 @@
const query = process.argv[2]
const fs = require('fs')
const childProcess = require('child_process')
const SYSTEM_PATHS = [
'/lib',
'/usr/lib',
'/usr/lib64',
'/usr/local/lib',
'/opt/local/lib',
'/opt/homebrew/lib',
'/usr/lib/x86_64-linux-gnu',
'/usr/lib/i386-linux-gnu',
'/usr/lib/arm-linux-gnueabihf',
'/usr/lib/arm-linux-gnueabi',
'/usr/lib/aarch64-linux-gnu'
]
/**
* Checks for lib using ldconfig if present, or searching SYSTEM_PATHS
* otherwise.
* @param {string} lib - library name, e.g. 'jpeg' in 'libjpeg64.so' (see first line)
* @return {boolean} exists
*/
function hasSystemLib (lib) {
const libName = 'lib' + lib + '.+(so|dylib)'
const libNameRegex = new RegExp(libName)
// Try using ldconfig on linux systems
if (hasLdconfig()) {
try {
if (childProcess.execSync('ldconfig -p 2>/dev/null | grep -E "' + libName + '"').length) {
return true
}
} catch (err) {
// noop -- proceed to other search methods
}
}
// Try checking common library locations
return SYSTEM_PATHS.some(function (systemPath) {
try {
const dirListing = fs.readdirSync(systemPath)
return dirListing.some(function (file) {
return libNameRegex.test(file)
})
} catch (err) {
return false
}
})
}
/**
* Checks for ldconfig on the path and /sbin
* @return {boolean} exists
*/
function hasLdconfig () {
try {
// Add /sbin to path as ldconfig is located there on some systems -- e.g.
// Debian (and it can still be used by unprivileged users):
childProcess.execSync('export PATH="$PATH:/sbin"')
process.env.PATH = '...'
// execSync throws on nonzero exit
childProcess.execSync('hash ldconfig 2>/dev/null')
return true
} catch (err) {
return false
}
}
/**
* Checks for freetype2 with --cflags-only-I
* @return Boolean exists
*/
function hasFreetype () {
try {
if (childProcess.execSync('pkg-config cairo --cflags-only-I 2>/dev/null | grep freetype2').length) {
return true
}
} catch (err) {
// noop
}
return false
}
/**
* Checks for lib using pkg-config.
* @param {string} lib - library name
* @return {boolean} exists
*/
function hasPkgconfigLib (lib) {
try {
// execSync throws on nonzero exit
childProcess.execSync('pkg-config --exists "' + lib + '" 2>/dev/null')
return true
} catch (err) {
return false
}
}
function main (query) {
switch (query) {
case 'gif':
case 'cairo':
return hasSystemLib(query)
case 'pango':
return hasPkgconfigLib(query)
case 'freetype':
return hasFreetype()
case 'jpeg':
return hasPkgconfigLib('libjpeg')
case 'rsvg':
return hasPkgconfigLib('librsvg-2.0')
default:
throw new Error('Unknown library: ' + query)
}
}
process.stdout.write(main(query).toString())
+21
View File
@@ -0,0 +1,21 @@
const fs = require('fs')
const paths = ['C:/libjpeg-turbo']
if (process.arch === 'x64') {
paths.unshift('C:/libjpeg-turbo64')
}
paths.forEach(function (path) {
if (exists(path)) {
process.stdout.write(path)
process.exit()
}
})
function exists (path) {
try {
return fs.lstatSync(path).isDirectory()
} catch (e) {
return false
}
}