Socket
Socket
Sign inDemoInstall

skia-canvas

Package Overview
Dependencies
63
Maintainers
1
Versions
25
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.9.21 to 0.9.22

136

lib/index.js

@@ -6,10 +6,8 @@ "use strict"

{inspect} = require('util'),
{extname} = require('path'),
glob = require('glob').sync,
get = require('simple-get'),
REPR = inspect.custom
const crate = require('./v6'),
crate = require('./v6'),
geometry = require('./geometry'),
{parseFont, parseVariant, parseFilter} = require('./parse')
parse = require('./parse'),
REPR = inspect.custom;

@@ -25,3 +23,3 @@ //

alloc(...args){
this.init('new', ...args)
return this.init('new', ...args)
}

@@ -64,2 +62,4 @@

const toString = val => typeof val=='string' ? val : new String(val).toString()
//

@@ -84,28 +84,3 @@ // Helpers to reconcile Skia and DOMMatrix’s disagreement about row/col orientation

//
// Mime type <-> File extension mappings
//
let png = "image/png",
jpg = "image/jpeg",
jpeg = "image/jpeg",
pdf = "application/pdf",
svg = "image/svg+xml",
gif = "image/gif";
function toMime(ext){
return {
png, jpg, jpeg, gif, pdf, svg
}[(ext||'').replace(/^\./, '').toLowerCase()]
}
function fromMime(mime){
return {
[png]: "png", [jpg]: "jpg", [gif]: "gif", [pdf]: "pdf", [svg]: "svg"
}[mime]
}
const toFormat = str => fromMime(toMime(str) || str),
toString = val => typeof val=='string' ? val : new String(val).toString();
//

@@ -121,5 +96,3 @@ // The Canvas API

super().alloc()
let ctx = new CanvasRenderingContext2D(core(this))
Canvas.parent.set(ctx, this)
Canvas.contexts.set(this, [ctx])
Canvas.contexts.set(this, [])
Object.assign(this, {width, height})

@@ -129,3 +102,3 @@ }

getContext(kind){
return (kind=="2d") ? Canvas.contexts.get(this)[0] : null
return (kind=="2d") ? Canvas.contexts.get(this)[0] || this.newPage() : null
}

@@ -135,5 +108,4 @@

set width(w){
let ctx = Canvas.contexts.get(this)[0]
this.ƒ('set_width', (typeof w=='number' && !Number.isNaN(w) && w>=0) ? w : 300)
ctx.ƒ('resetSize', core(this))
this.getContext("2d").ƒ('resetSize', core(this))
}

@@ -143,5 +115,4 @@

set height(h){
let ctx = Canvas.contexts.get(this)[0]
this.ƒ('set_height', h = (typeof h=='number' && !Number.isNaN(h) && h>=0) ? h : 150)
ctx.ƒ('resetSize', core(this))
this.getContext("2d").ƒ('resetSize', core(this))
}

@@ -168,64 +139,44 @@

saveAs(filename, {format, quality=100}={}){
var seq
filename = filename.replace(/{(\d*)}/g, (_, pad) => {
pad = parseInt(pad, 10)
seq = isFinite(pad) ? pad : isFinite(seq) ? seq : -1
return "{}"
})
get async(){ return this.ƒ('get_async') }
set async(flag){ this.ƒ('set_async', flag) }
let ext = format || extname(filename),
fmt = toFormat(ext);
saveAs(filename, opts={}){
opts = typeof opts=='number' ? {quality:opts} : opts
let {format, quality, pages, padding, pattern, density, outline} = parse.output(this.pages, {filename, ...opts}),
args = [pages.map(core), pattern, padding, format, quality, density, outline];
if(!ext){
throw new Error(`Cannot determine image format (use a filename extension or 'format' argument)`)
}if (!fmt){
throw new Error(`Unsupported file format "${ext}" (expected "png", "jpg", "pdf", or "svg")`)
if (this.async){
let worker = new EventEmitter()
this.ƒ("save", (result, msg) => worker.emit(result, msg), ...args)
return new Promise((res, rej) => worker.once('ok', res).once('err', msg => rej(new Error(msg))) )
}else{
this.ƒ("saveSync", ...args)
}
this.ƒ("saveAs", filename, seq, fmt, quality, Canvas.contexts.get(this).map(core))
}
toBuffer(extension, {format="png", quality=100, page}={}){
({format, quality, page} = Object.assign(
{format, quality, page},
typeof extension == 'string' ? {format:extension}
: typeof extension == 'object' ? extension
: {}
));
toBuffer(extension="png", opts={}){
opts = typeof opts=='number' ? {quality:opts} : opts
let {format, quality, pages, density, outline} = parse.output(this.pages, {extension, ...opts}),
args = [pages.map(core), format, quality, density, outline];
let fmt = toFormat(format),
pp = this.pages.length,
idx = page >= 0 ? pp - page
: page < 0 ? pp + page
: undefined
if (!fmt){
throw new Error(`Unsupported file format "${format}" (expected "png", "jpg", "pdf", or "svg")`)
}else if (isFinite(idx) && idx < 0 || idx >= pp){
throw new RangeError(
pp == 1 ? `Canvas only has a ‘page 1’ (${page} is out of bounds)`
: `Canvas has pages 1–${pp} (${page} is out of bounds)`
)
if (this.async){
let worker = new EventEmitter()
this.ƒ("toBuffer", (result, msg) => worker.emit(result, msg), ...args)
return new Promise((res, rej) => worker.once('ok', res).once('err', msg => rej(new Error(msg))) )
}else{
return this.ƒ("toBufferSync", ...args)
}
return this.ƒ("toBuffer", fmt, quality, idx, Canvas.contexts.get(this).map(core))
}
toDataURL(extension, {format="png", quality=100, page}={}){
({format, quality, page} = Object.assign(
{format, quality, page},
typeof extension == 'string' ? {format:extension}
: typeof extension == 'object' ? extension
: {}
));
let fmt = toFormat(format),
mime = toMime(fmt),
buffer = this.toBuffer({format, quality, page});
return `data:${mime};base64,${buffer.toString('base64')}`
toDataURL(extension="png", opts={}){
opts = typeof opts=='number' ? {quality:opts} : opts
let {mime} = parse.output(this.pages, {extension, ...opts}),
urlify = data => `data:${mime};base64,${data.toString('base64')}`,
buffer = this.toBuffer(extension, opts);
return this.async ? buffer.then(urlify) : urlify(buffer)
}
[REPR](depth, options) {
let {width, height} = this
return `Canvas ${inspect({width, height}, options)}`
let {width, height, async, pages} = this
return `Canvas ${inspect({width, height, async, pages}, options)}`
}

@@ -414,3 +365,3 @@ }

get font(){ return this.ƒ('get_font') }
set font(str){ this.ƒ('set_font', parseFont(str)) }
set font(str){ this.ƒ('set_font', parse.font(str)) }
get textAlign(){ return this.ƒ("get_textAlign") }

@@ -424,2 +375,3 @@ set textAlign(mode){ this.ƒ("set_textAlign", mode) }

measureText(text, maxWidth){
text = this.textWrap ? text : text + '\u200b' // include trailing whitespace by default
let [metrics, ...lines] = this.ƒ('measureText', toString(text), maxWidth)

@@ -439,3 +391,3 @@ return new TextMetrics(metrics, lines)

get fontVariant(){ return this.ƒ('get_fontVariant') }
set fontVariant(str){ this.ƒ('set_fontVariant', parseVariant(str)) }
set fontVariant(str){ this.ƒ('set_fontVariant', parse.variant(str)) }
get textTracking(){ return this.ƒ("get_textTracking") }

@@ -460,3 +412,3 @@ set textTracking(ems){ this.ƒ("set_textTracking", ems) }

get filter(){ return this.ƒ('get_filter') }
set filter(str){ this.ƒ('set_filter', parseFilter(str)) }
set filter(str){ this.ƒ('set_filter', parse.filter(str)) }

@@ -463,0 +415,0 @@

"use strict"
const {extname, basename} = require('path')

@@ -298,2 +299,90 @@ //

module.exports = {parseFont, parseVariant, parseSize, parseFilter}
//
// Mime type <-> File extension mappings
//
let png = "image/png",
jpg = "image/jpeg",
jpeg = "image/jpeg",
pdf = "application/pdf",
svg = "image/svg+xml";
function toMime(ext){
return {
png, jpg, jpeg, pdf, svg
}[(ext||'').replace(/^\./, '').toLowerCase()]
}
function fromMime(mime){
return {
[png]: "png", [jpg]: "jpg", [pdf]: "pdf", [svg]: "svg"
}[mime]
}
//
// Validation of the options dict shared by the Canvas saveAs, toBuffer, and toDataURL methods
//
function parseOutput(pages, {filename='', extension='', format, page, quality, density, outline}={}){
var ext = format || extension.replace(/@\d+x$/i,'') || extname(filename),
format = fromMime(toMime(ext) || ext),
mime = toMime(format),
pp = pages.length
if(!ext) throw new Error(`Cannot determine image format (use a filename extension or 'format' argument)`)
if (!format) throw new Error(`Unsupported file format "${ext}" (expected "png", "jpg", "pdf", or "svg")`)
if (!pp) throw new RangeError(`Canvas has no associated contexts (try calling getContext or newPage first)`)
let padding, isSequence, pattern = filename.replace(/{(\d*)}/g, (_, width) => {
isSequence = true
width = parseInt(width, 10)
padding = isFinite(width) ? width : isFinite(padding) ? padding : -1
return "{}"
})
// allow negative indexing if a specific page is specified
let idx = page > 0 ? page - 1
: page < 0 ? pp + page
: undefined;
if (isFinite(idx) && idx < 0 || idx >= pp) throw new RangeError(
pp == 1 ? `Canvas only has a ‘page 1’ (${idx} is out of bounds)`
: `Canvas has pages 1–${pp} (${idx} is out of bounds)`
)
pages = isFinite(idx) ? [pages[idx]]
: isSequence || format=='pdf' ? pages
: pages.slice(-1) // default to the 'current' context
if (quality===undefined){
quality = 0.92
}else{
if (typeof quality!='number' || !isFinite(quality) || quality<0 || quality>1){
throw new TypeError("The quality option must be an number in the 0.0–1.0 range")
}
}
if (density===undefined){
let m = (extension || basename(filename, ext)).match(/@(\d+)x$/i)
density = m ? parseInt(m[1], 10) : 1
}else if (typeof density!='number' || !Number.isInteger(density) || density<1){
throw new TypeError("The density option must be a non-negative integer")
}
if (outline===undefined){
outline = true
}else if (format == 'svg'){
outline = !!outline
}
return {filename, pattern, format, mime, pages, padding, quality, density, outline}
}
module.exports = {
font:parseFont,
variant:parseVariant,
size:parseSize,
filter:parseFilter,
output:parseOutput
}
{
"name": "skia-canvas",
"version": "0.9.21",
"version": "0.9.22",
"description": "A canvas environment for Node",

@@ -5,0 +5,0 @@ "main": "lib/index.js",

@@ -11,4 +11,5 @@ # Skia Canvas

- can generate output in both raster (JPEG & PNG) and vector (PDF & SVG) image formats
- can save images to [files](#saveasfilename-format-quality), return them as [Buffers](#tobufferformat-quality-page), or encode [dataURL](#todataurlformat-quality-page) strings
- can create [multiple ‘pages’](#newpagewidth-height) on a given canvas and then [output](#saveasfilename-format-quality) them as a single, multi-page PDF or an image-sequence saved to multiple files
- can save images to [files][saveAs], return them as [Buffers][toBuffer], or encode [dataURL][toDataURL_ext] strings
- uses native threads and [EventQueues](https://docs.rs/neon/0.8.2-napi/neon/event/struct.EventQueue.html) for asynchronous rendering and file I/O
- can create [multiple ‘pages’][newPage] on a given canvas and then [output][saveAs] them as a single, multi-page PDF or an image-sequence saved to multiple files
- can simplify and combine bézier paths using efficient [boolean operations](https://www.youtube.com/watch?v=OmfliNQsk88)

@@ -26,60 +27,2 @@ - fully supports the [CSS filter effects][filter] image processing operators

## Roadmap
This project is newly-hatched and still has some obvious gaps to fill (feel free to pitch in!).
On the agenda for subsequent updates are:
- Use neon [EventQueues](https://github.com/neon-bindings/neon/blob/main/src/event/event_queue.rs) to provide asynchronous file i/o
- Add SVG image loading using the [µsvg](https://crates.io/crates/usvg) parser
- Add a `density` argument to Canvas and/or the output methods to allow for scaling to other device-pixel-ratios
## Platform Support
The underlying Rust library uses [N-API](https://nodejs.org/api/n-api.html) v6 which allows it to run on Node.js versions:
- 10.20+
- 12.17+
- 14.0 and later
There are pre-compiled binaries for:
- Linux (x86)
- macOS (x86 & Apple silicon)
- Windows (x86)
## Installation
If you’re running on a supported platform, installation should be as simple as:
```console
$ npm install skia-canvas
```
This will download a pre-compiled library from the project’s most recent [release](https://github.com/samizdatco/skia-canvas/releases). If prebuilt binaries aren’t available for your system you’ll need to compile the portions of this library that directly interface with Skia.
Start by installing:
1. The [Rust compiler](https://www.rust-lang.org/tools/install) and cargo package manager using `rustup`
2. A C compiler toolchain like LLVM/Clang, GCC, or MSVC
Once these dependencies are present, running `npm run build` will give you a useable library (after a fairly lengthy compilation process).
## Module Contents
The library exports a number of classes emulating familiar browser objects including:
- [Canvas][Canvas]
- [CanvasGradient][CanvasGradient]
- [CanvasPattern][CanvasPattern]
- [CanvasRenderingContext2D][CanvasRenderingContext2D]
- [DOMMatrix][DOMMatrix]
- [Image][Image]
- [ImageData][ImageData]
- [Path2D][Path2D]
In addition, the module contains:
- [loadImage()](#loadimage) a utility function for loading `Image` objects asynchronously
- [FontLibrary](#fontlibrary) a class allowing you to inspect the system’s installed fonts and load additional ones
### Basic Usage

@@ -124,80 +67,145 @@ ```js

## API Documentation
## Installation
### CanvasRenderingContext2D
If you’re running on a supported platform, installation should be as simple as:
```console
$ npm install skia-canvas
```
Most of your interaction with the canvas will actually be directed toward its ‘rendering context’, a supporting object you can acquire by calling the canvas’s [getContext()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext) method. Documentation for each of the context’s attributes is linked below—properties are printed in **bold** and methods have parentheses attached to the name. The instances where Skia Canvas’s behavior goes beyond the standard are marked by a ⚡ symbol (see the next section for details).
This will download a pre-compiled library from the project’s most recent [release](https://github.com/samizdatco/skia-canvas/releases).
### Dependencies
| Canvas State | Drawing Primitives | Stroke & Fill Style | Compositing Effects |
|----------------------------------------|---------------------------------------------|--------------------------------------|----------------------------------------------------------|
| [**canvas**](#canvas) [⚡](#canvas) | [clearRect()][clearRect()] | [**fillStyle**][fillStyle] | [**filter**][filter] |
| [**globalAlpha**][globalAlpha] | [drawImage()][drawImage()] | [**lineCap**][lineCap] | [**globalCompositeOperation**][globalCompositeOperation] |
| [beginPath()][beginPath()] | [fill()][fill()] | [**lineDashOffset**][lineDashOffset] | [**shadowBlur**][shadowBlur] |
| [clip()][clip()] | [fillRect()][fillRect()] | [**lineJoin**][lineJoin] | [**shadowColor**][shadowColor] |
| [isPointInPath()][isPointInPath()] | [fillText()][fillText()] [⚡][drawText] | [**lineWidth**][lineWidth] | [**shadowOffsetX**][shadowOffsetX] |
| [isPointInStroke()][isPointInStroke()] | [stroke()][stroke()] | [**miterLimit**][miterLimit] | [**shadowOffsetY**][shadowOffsetY] |
| [restore()][restore()] | [strokeRect()][strokeRect()] | [**strokeStyle**][strokeStyle] | |
| [save()][save()] | [strokeText()][strokeText()] [⚡][drawText] | [getLineDash()][getLineDash()] | |
| | | [setLineDash()][setLineDash()] | |
Nearly everything you need is statically linked into the library.
A notable exception is the [Fontconfig](https://www.freedesktop.org/wiki/Software/fontconfig/) library (and its associated [FreeType](https://www.freetype.org) renderer) which must be installed separately if you’re running on Linux.
| Bezier Paths | Typography | Pattern & Image | Transform |
|------------------------------------------|-------------------------------------------------------------|----------------------------------------------------|------------------------------------------|
| [arc()][arc()] | [**direction**][direction] | [**imageSmoothingEnabled**][imageSmoothingEnabled] | [**currentTransform**][currentTransform] |
| [arcTo()][arcTo()] | [**font**][font] [⚡](#font) | [**imageSmoothingQuality**][imageSmoothingQuality] | [getTransform()][getTransform()] |
| [bezierCurveTo()][bezierCurveTo()] | [**fontVariant** ⚡](#fontvariant) | [createConicGradient()][createConicGradient()] | [resetTransform()][resetTransform()] |
| [closePath()][closePath()] | [**textAlign**][textAlign] | [createImageData()][createImageData()] | [rotate()][rotate()] |
| [ellipse()][ellipse()] | [**textBaseline**][textBaseline] | [createLinearGradient()][createLinearGradient()] | [scale()][scale()] |
| [lineTo()][lineTo()] | [**textTracking** ⚡](#texttracking) | [createPattern()][createPattern()] | [setTransform()][setTransform()] |
| [moveTo()][moveTo()] | [**textWrap** ⚡](#textwrap) | [createRadialGradient()][createRadialGradient()] | [transform()][transform()] |
| [quadraticCurveTo()][quadraticCurveTo()] | [measureText()][measureText()] [⚡](#measuretextstr-width) | [getImageData()][getImageData()] | [translate()][translate()] |
| [rect()][rect()] | | [putImageData()][putImageData()] | |
### Platform Support
### Path2D
The underlying Rust library uses [N-API](https://nodejs.org/api/n-api.html) v6 which allows it to run on Node.js versions:
- 10.20+
- 12.17+
- 14.0, 15.0, and later
The context object creates an implicit ‘current’ bézier path which is updated by commands like [lineTo()][lineTo()] and [arcTo()][arcTo()] and is drawn to the canvas by calling [fill()][fill()], [stroke()][stroke()], or [clip()][clip()] without any arguments (aside from an optional [winding][nonzero] [rule][evenodd]). If you start creating a second path by calling [beginPath()][beginPath()] the context discards the prior path, forcing you to recreate it by hand if you need it again later.
Pre-compiled binaries are available for:
The `Path2D` class allows you to create paths independent of the context to be drawn as needed (potentially repeatedly). Its constructor can be called without any arguments to create a new, empty path object. It can also accept a string using [SVG syntax][SVG_path_commands] or a reference to an existing `Path2D` object (which it will return a clone of):
- Linux (x86)
- macOS (x86 & Apple silicon)
- Windows (x86)
### Compiling from source
If prebuilt binaries aren’t available for your system you’ll need to compile the portions of this library that directly interface with Skia.
Start by installing:
1. The [Rust compiler](https://www.rust-lang.org/tools/install) and cargo package manager using [`rustup`](https://rust-lang.github.io/rustup/)
2. A C compiler toolchain like LLVM/Clang or MSVC
3. Python 2.7 (used by Skia's [build process](https://skia.org/docs/user/build/))
4. On Linux: Fontconfig, OpenSSL, X11, and Mesa
[Detailed instructions](https://github.com/rust-skia/rust-skia#building) for setting up these dependencies on different operating systems can be found in the ‘Building’ section of the Rust Skia documentation. Once all the necessary compilers and libraries are present, running `npm run build` will give you a usable library (after a fairly lengthy compilation process).
# API Documentation
> Documentation for the key classes and their attributes are listed below—properties are printed in **bold** and methods have parentheses attached to the name. The instances where Skia Canvas’s behavior goes beyond the standard are marked by a ⚡ symbol, linking to further details below.
The library exports a number of classes emulating familiar browser objects including:
- [Canvas][Canvas] ⧸[⚡](#canvas)
- [CanvasGradient][CanvasGradient]
- [CanvasPattern][CanvasPattern]
- [CanvasRenderingContext2D][CanvasRenderingContext2D] ⧸[⚡](#canvasrenderingcontext2d)
- [DOMMatrix][DOMMatrix]
- [Image][Image]
- [ImageData][ImageData]
- [Path2D][Path2D] ⧸[⚡](#path2d)
In addition, the module contains:
- [loadImage()](#loadimage) a utility function for loading `Image` objects asynchronously
- [FontLibrary](#fontlibrary) a class allowing you to inspect the system’s installed fonts and load additional ones
## Canvas
The Canvas object is a stand-in for the HTML `<canvas>` element. It defines image dimensions and provides a [rendering context](#canvasrenderingcontext2d) to draw to it. Once you’re ready to save or display what you’ve drawn, the canvas can [save][saveAs] it to a file, or hand it off to you as a [data buffer][toBuffer] or [string][toDataURL_ext] to process manually.
| Image Dimensions | Rendering Contexts | Output |
| -- | -- | -- |
| [**width**][canvas_width] | [**pages**][canvas_pages] ⚡ | [**async**][canvas_async] ⚡ |
| [**height**][canvas_height] | [getContext()][getContext] | [**pdf**, **png**, **svg**, **jpg**][shorthands] ⚡ |
| | [newPage()][newPage] ⚡ | [saveAs()][saveAs] ⚡ |
| | | [toBuffer()][toBuffer] ⚡ |
| | | [toDataURL()][toDataURL_mdn] [⚡][toDataURL_ext] |
[canvas_width]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/width
[canvas_height]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/height
[canvas_async]: #async
[canvas_pages]: #pages
[getContext]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
[saveAs]: #saveasfilename-page-format-density1-quality092-outlinefalse
[toBuffer]: #tobufferformat-page-density-quality-outline
[newPage]: #newpagewidth-height
[toDataURL_mdn]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
[toDataURL_ext]: #todataurlformat-page-density-quality-outline
[shorthands]: #pdf-svg-jpg-and-png
#### Creating new `Canvas` objects
Rather than calling a DOM method to create a new canvas, you can simply call the `Canvas` constructor with the width and height (in pixels) of the image you’d like to being drawing.
```js
// three identical (but independent) paths
let p1 = new Path2D("M 10,10 h 100 v 100 h -100 Z")
let p2 = new Path2D(p1)
let p3 = new Path2D()
p3.rect(10, 10, 100, 100)
let defaultCanvas = new Canvas() // without arguments, defaults to 300 × 150 px
let squareCanvas = new Canvas(512, 512) // creates a 512 px square
```
You can then use these objects by passing them as the first argument to the context’s `fill()`, `stroke()`, and `clip()` methods (along with an optional second argument specifying the winding rule).
##### PROPERTIES
| Line Segments | Shapes | Boolean Ops ⚡ | Extents ⚡ |
| -- | -- | -- | -- |
| [moveTo()][p2d_moveTo] | [addPath()][p2d_addPath] | [complement()][bool-ops] | [**bounds**](#bounds) |
| [lineTo()][p2d_lineTo] | [arc()][p2d_arc] | [difference()][bool-ops] | [simplify()](#simplify) |
| [bezierCurveTo()][p2d_bezierCurveTo] | [arcTo()][p2d_arcTo] | [intersect()][bool-ops] |
| [quadraticCurveTo()][p2d_quadraticCurveTo] | [ellipse()][p2d_ellipse] | [union()][bool-ops] |
| [closePath()][p2d_closePath] | [rect()][p2d_rect] | [xor()][bool-ops] |
#### `.async`
When the canvas renders images and writes them to disk, it does so in a background thread so as not to block execution within your script. As a result you’ll generally want to deal with the canvas from within an `async` function and be sure to use the `await` keyword when accessing any of its output methods or shorthand properties:
- [`saveAs()`][saveAs]
- [`toBuffer()`][toBuffer]
- [`toDataURL()`][toDataURL_ext]
- [`.pdf`, `.svg`, `.jpg`, and `.png`][shorthands]
In cases where this is not the desired behavior, you can switch these methods into a synchronous mode for a particular canvas by setting its `async` property to `false`. For instance, both of the example functions below will generate PNG & PDF from the canvas, though the first will be more efficient (particularly for parallel contexts like request-handlers in an HTTP server or batch exports):
```js
let canvas = new Canvas()
console.log(canvas.async) // -> true by default
async function normal(){
let pngURL = await canvas.toDataURL("png")
let pdfBuffer = await canvas.pdf
}
function synchronous(){
canvas.async = false // switch into synchronous mode
let pngURL = canvas.toDataURL("png")
let pdfBuffer = canvas.pdf
}
```
## Non-standard extensions
### Canvas
#### `.pages`
##### `.pages`
The canvas’s `.pages` attribute is an array of [`CanvasRenderingContext2D`][CanvasRenderingContext2D] objects corresponding to each ‘page’ that has been created. The first page is added when the canvas is initialized and additional ones can be added by calling the `newPage()` method. Note that all the pages remain drawable persistently, so you don’t have to constrain yourself to modifying the ‘current’ page as you render your document or image sequence.
##### `.pdf`, `.svg`, `.jpg`, and `.png`
#### `.pdf`, `.svg`, `.jpg`, and `.png`
These properties are syntactic sugar for calling the `toBuffer()` method. Each returns a Node [`Buffer`][Buffer] object with the contents of the canvas in the given format. If more than one page has been added to the canvas, only the most recent one will be included unless you’ve accessed the `.pdf` property in which case the buffer will contain a multi-page PDF.
##### `newPage(width, height)`
##### METHODS
#### `newPage(width, height)`
This method allows for the creation of additional drawing contexts that are fully independent of one another but will be part of the same output batch. It is primarily useful in the context of creating a multi-page PDF but can be used to create multi-file image-sequences in other formats as well. Creating a new page with a different size than the previous one will update the parent Canvas object’s `.width` and `.height` attributes but will not affect any other pages that have been created previously.

@@ -207,33 +215,77 @@

##### `saveAs(filename, {format, quality})`
#### `saveAs(filename, {page, format, density=1, quality=0.92, outline=false})`
The `saveAs` method takes a file path and writes the canvas’s current contents to disk. If the filename ends with an extension that makes its format clear, the second argument is optional. If the filename is ambiguous, you can pass an options object with a `format` string using names like `"png"` and `"jpeg"` or a full mime type like `"application/pdf"`.
The `quality` option is a number between 0 and 100 that controls the level of JPEG compression both when making JPEG files directly and when embedding them in a PDF. If omitted, quality will default to 100 (lossless).
The way multi-page documents are handled depends on the `filename` argument. If the filename contains the string `"{}"`, it will be used as template for generating a numbered sequence of files—one per page. If no curly braces are found in the filename, only a single file will be saved. That single file will be multi-page in the case of PDF output but for other formats it will contain only the most recently added page.
The way multi-page documents are handled depends on the filename argument. If the filename contains the string `"{}"`, it will be used as template for generating a numbered sequence of files—one per page. If no curly braces are found in the filename, only a single file will be saved. That single file will be multi-page in the case of PDF output but for other formats it will contain only the most recently added page.
An integer can optionally be placed between the braces to indicate the number of padding characters to use for numbering. For instance `"page-{}.svg"` will generate files of the form `page-1.svg` whereas `"frame-{4}.png"` will generate files like `frame-0001.png`.
##### `toBuffer(format, {quality, page})`
##### page
The optional `page` argument accepts an integer that allows for the individual selection of pages in a multi-page canvas. Note that page indexing starts with page 1 **not** 0. The page value can also be negative, counting from the end of the canvas’s `.pages` array. For instance, `.saveAs("currentPage.png", {page:-1})` is equivalent to omitting `page` since they both yield the canvas’s most recently added page.
Node [`Buffer`][Buffer] objects containing various image formats can be created by passing either a format string like `"svg"` or a mime-type like `"image/svg+xml"`. The optional `quality` argument behaves the same as in the `saveAs` method.
##### density
By default, the images will be at a 1:1 ratio with the canvas's `width` and `height` dimensions (i.e., a 72 × 72 canvas will yield a 72 pixel × 72 pixel bitmap). But with screens increasingly operating at higher densities, you’ll frequently want to generate images where an on-canvas 'point' may occupy multiple pixels. The optional `density` argument allows you to specify this magnification factor using an integer ≥1. As a shorthand, you can also select a density by choosing a filename using the `@nx` naming convention:
The optional `page` argument accepts an integer that allows for the individual selection of pages in a multi-page canvas. Note that page indexing starts with page 1 **not** 0. The page value can also be negative, counting from the end of the canvas’s `.pages` array. For instance, `.toBuffer("png", {page:-1})` is equivalent to omitting `page` since they both yield the canvas’s most recently added page.
```js
canvas.saveAs('image.png', {density:2}) // choose the density explicitly
canvas.saveAs('image@3x.png') // equivalent to setting the density to 3
```
##### `toDataURL(format, {quality, page})`
##### quality
The `quality` option is a number between 0 and 1.0 that controls the level of JPEG compression both when making JPEG files directly and when embedding them in a PDF. If omitted, quality will default to 0.92.
##### outline
When generating SVG output containing text, you have two options for how to handle the fonts that were used. By default, SVG files will contain `<text>` elements that refer to the fonts by name in the embedded stylesheet. This requires that viewers of the SVG have the same fonts available on their system (or accessible as webfonts). Setting the optional `outline` argument to `true` will trace all the letterforms and ‘burn’ them into the file as bézier paths. This will result in a much larger file (and one in which the original text strings will be unrecoverable), but it will be viewable regardless of the specifics of the system it’s displayed on.
#### `toBuffer(format, {page, density, quality, outline})`
Node [`Buffer`][Buffer] objects containing various image formats can be created by passing either a format string like `"svg"` or a mime-type like `"image/svg+xml"`. An ‘@’ suffix can be added to the format string to specify a pixel-density (for instance, `"jpg@2x"`). The optional arguments behave the same as in the `saveAs` method.
#### `toDataURL(format, {page, density, quality, outline})`
This method accepts the same arguments and behaves similarly to `.toBuffer`. However instead of returning a Buffer, it returns a string of the form `"data:<mime-type>;base64,<image-data>"` which can be used as a `src` attribute in `<img>` tags, embedded into CSS, etc.
### CanvasRenderingContext2D
##### `.font`
## CanvasRenderingContext2D
Most of your interaction with the canvas will actually be directed toward its ‘rendering context’, a supporting object you can acquire by calling the canvas’s [getContext()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext) and [newPage()][newPage] methods.
| Canvas State | Drawing Primitives | Stroke & Fill Style | Compositing Effects |
|----------------------------------------|----------------------------------------------|--------------------------------------|----------------------------------------------------------|
| [**canvas**](#canvas) ⧸[⚡](#canvas) | [clearRect()][clearRect()] | [**fillStyle**][fillStyle] | [**filter**][filter] |
| [**globalAlpha**][globalAlpha] | [drawImage()][drawImage()] | [**lineCap**][lineCap] | [**globalCompositeOperation**][globalCompositeOperation] |
| [beginPath()][beginPath()] | [fill()][fill()] | [**lineDashOffset**][lineDashOffset] | [**shadowBlur**][shadowBlur] |
| [clip()][clip()] | [fillRect()][fillRect()] | [**lineJoin**][lineJoin] | [**shadowColor**][shadowColor] |
| [isPointInPath()][isPointInPath()] | [fillText()][fillText()] ⧸[⚡][drawText] | [**lineWidth**][lineWidth] | [**shadowOffsetX**][shadowOffsetX] |
| [isPointInStroke()][isPointInStroke()] | [stroke()][stroke()] | [**miterLimit**][miterLimit] | [**shadowOffsetY**][shadowOffsetY] |
| [restore()][restore()] | [strokeRect()][strokeRect()] | [**strokeStyle**][strokeStyle] | |
| [save()][save()] | [strokeText()][strokeText()] ⧸[⚡][drawText] | [getLineDash()][getLineDash()] | |
| | | [setLineDash()][setLineDash()] | |
| Bezier Paths | Typography | Pattern & Image | Transform |
|------------------------------------------|-------------------------------------------------------------|----------------------------------------------------|------------------------------------------|
| [arc()][arc()] | [**direction**][direction] | [**imageSmoothingEnabled**][imageSmoothingEnabled] | [**currentTransform**][currentTransform] |
| [arcTo()][arcTo()] | [**font**][font] ⧸[⚡](#font) | [**imageSmoothingQuality**][imageSmoothingQuality] | [getTransform()][getTransform()] |
| [bezierCurveTo()][bezierCurveTo()] | [**fontVariant** ⚡](#fontvariant) | [createConicGradient()][createConicGradient()] | [resetTransform()][resetTransform()] |
| [closePath()][closePath()] | [**textAlign**][textAlign] | [createImageData()][createImageData()] | [rotate()][rotate()] |
| [ellipse()][ellipse()] | [**textBaseline**][textBaseline] | [createLinearGradient()][createLinearGradient()] | [scale()][scale()] |
| [lineTo()][lineTo()] | [**textTracking** ⚡](#texttracking) | [createPattern()][createPattern()] | [setTransform()][setTransform()] |
| [moveTo()][moveTo()] | [**textWrap** ⚡](#textwrap) | [createRadialGradient()][createRadialGradient()] | [transform()][transform()] |
| [quadraticCurveTo()][quadraticCurveTo()] | [measureText()][measureText()] ⧸[⚡](#measuretextstr-width) | [getImageData()][getImageData()] | [translate()][translate()] |
| [rect()][rect()] | | [putImageData()][putImageData()] | |
##### PROPERTIES
#### `.font`
By default any [`line-height`][lineHeight] value included in a font specification (separated from the font size by a `/`) will be preserved but ignored. If the `textWrap` property is set to `true`, the line-height will control the vertical spacing between lines.
##### `.fontVariant`
#### `.fontVariant`
The context’s [`.font`][font] property follows the CSS 2.1 standard and allows the selection of only a single font-variant type: `normal` vs `small-caps`. The full range of CSS 3 [font-variant][font-variant] values can be used if assigned to the context’s `.fontVariant` property (presuming the currently selected font supports them). Note that setting `.font` will also update the current `.fontVariant` value, so be sure to set the variant *after* selecting a typeface.
##### `.textTracking`
#### `.textTracking`

@@ -244,8 +296,10 @@ To loosen or tighten letter-spacing, set the `.textTracking` property to an integer representing the amount of space to add/remove in terms of 1/1000’s of an ‘em’ (a.k.a. the current font size). Positive numbers will space out the text (e.g., `100` is a good value for setting all-caps) while negative values will pull the letters closer together (this is only rarely a good idea).

##### `.textWrap`
#### `.textWrap`
The standard canvas has a rather impoverished typesetting system, allowing for only a single line of text and an approach to width-management that horizontally scales the letterforms (a type-crime if ever there was one). Skia Canvas allows you to opt-out of this single-line world by setting the `.textWrap` property to `true`. Doing so affects the behavior of the `fillText()`, `strokeText()`, and `measureText()` methods as described below.
The standard canvas has a rather impoverished typesetting system, allowing for only a single line of text and an approach to width-management that horizontally scales the letterforms (a type-crime if ever there was one). Skia Canvas allows you to opt-out of this single-line world by setting the `.textWrap` property to `true`. Doing so affects the behavior of the `fillText()`, `strokeText()`, and `measureText()`
##### `fillText(str, x, y, [width])` & `strokeText(str, x, y, [width])`
##### METHODS
#### `fillText(str, x, y, [width])` & `strokeText(str, x, y, [width])`
The text-drawing methods’ behavior is mostly standard unless `.textWrap` has been set to `true`, in which case there are 3 main effects:

@@ -259,3 +313,3 @@

##### `measureText(str, [width])`
#### `measureText(str, [width])`

@@ -275,6 +329,38 @@ The `measureText()` method returns a [TextMetrics][TextMetrics] object describing the dimensions of a run of text *without* actually drawing it to the canvas. Skia Canvas adds an additional property to the metrics object called `.lines` which contains an array describing the geometry of each line individually.

### Path2D
## Path2D
##### `.bounds`
The `Path2D` class allows you to create paths independent of a given [Canvas](#canvas) or [graphics context](#canvasrenderingcontext2d). These paths can be modified over time and drawn repeatedly (potentially on multiple canvases).
| Line Segments | Shapes | Boolean Ops ⚡ | Extents ⚡ |
| -- | -- | -- | -- |
| [moveTo()][p2d_moveTo] | [addPath()][p2d_addPath] | [complement()][bool-ops] | [**bounds**](#bounds) |
| [lineTo()][p2d_lineTo] | [arc()][p2d_arc] | [difference()][bool-ops] | [simplify()](#simplify) |
| [bezierCurveTo()][p2d_bezierCurveTo] | [arcTo()][p2d_arcTo] | [intersect()][bool-ops] |
| [quadraticCurveTo()][p2d_quadraticCurveTo] | [ellipse()][p2d_ellipse] | [union()][bool-ops] |
| [closePath()][p2d_closePath] | [rect()][p2d_rect] | [xor()][bool-ops] |
#### Creating `Path2D` objects
Its constructor can be called without any arguments to create a new, empty path object. It can also accept a string using [SVG syntax][SVG_path_commands] or a reference to an existing `Path2D` object (which it will return a clone of):
```js
// three identical (but independent) paths
let p1 = new Path2D("M 10,10 h 100 v 100 h -100 Z")
let p2 = new Path2D(p1)
let p3 = new Path2D()
p3.rect(10, 10, 100, 100)
```
#### Drawing paths
A canvas’s context always contains an implicit ‘current’ bézier path which is updated by commands like [lineTo()][lineTo()] and [arcTo()][arcTo()] and is drawn to the canvas by calling [fill()][fill()], [stroke()][stroke()], or [clip()][clip()] without any arguments (aside from an optional [winding][nonzero] [rule][evenodd]). If you start creating a second path by calling [beginPath()][beginPath()] the context discards the prior path, forcing you to recreate it by hand if you need it again later.
You can then use these objects by passing them as the first argument to the context’s `fill()`, `stroke()`, and `clip()` methods (along with an optional second argument specifying the winding rule).
##### PROPERTIES
#### `.bounds`
In the browser, Path2D objects offer very little in the way of introspection—they are mostly-opaque recorders of drawing commands that can be ‘played back’ later on. Skia Canvas offers some additional transparency by allowing you to measure the total amount of space the lines will occupy (though you’ll need to account for the current `lineWidth` if you plan to draw the path with `stroke()`).

@@ -287,3 +373,5 @@

##### `complement()`, `difference()`, `intersect()`, `union()`, and `xor()`
##### METHODS
#### `complement()`, `difference()`, `intersect()`, `union()`, and `xor()`
In addition to creating `Path2D` objects through the constructor, you can use pairs of existing paths *in combination* to generate new paths based on their degree of overlap. Based on the method you choose, a different boolean relationship will be used to construct the new path. In all the following examples we’ll be starting off with a pair of overlapping shapes:

@@ -311,3 +399,3 @@ ```js

##### `simplify()`
#### `simplify()`

@@ -325,3 +413,2 @@ In cases where the contours of a single path overlap one another, it’s often useful to have a way of effectively applying a `union` operation *within* the path itself. The `simplify` method traces the path and returns a new copy that removes any overlapping segments:

## Utilities

@@ -328,0 +415,0 @@

@@ -9,3 +9,15 @@ const _ = require('lodash'),

WHITE = [255,255,255,255],
CLEAR = [0,0,0,0]
CLEAR = [0,0,0,0],
MAGIC = {
jpg: Buffer.from([0xFF, 0xD8, 0xFF]),
png: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
pdf: Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d]),
svg: Buffer.from(`<?xml version`, 'utf-8')
},
MIME = {
png: "image/png",
jpg: "image/jpeg",
pdf: "application/pdf",
svg: "image/svg+xml"
};

@@ -40,2 +52,13 @@ describe("Canvas", ()=>{

})
test('async i/o mode', async () => {
expect(canvas.async).toBe(true)
let promise = canvas.png
expect(promise).toBeInstanceOf(Promise)
await expect(promise).resolves.toBeInstanceOf(Buffer)
canvas.async = false
expect(canvas.png).toBeInstanceOf(Buffer)
})
})

@@ -103,3 +126,3 @@

test("export file formats", () => {
test("export file formats", async () => {
expect(() => canvas.saveAs(`${TMP}/output.gif`) ).toThrowError('Unsupported file format');

@@ -109,3 +132,3 @@ expect(() => canvas.saveAs(`${TMP}/output.targa`) ).toThrowError('Unsupported file format');

expect(() => canvas.saveAs(`${TMP}/`) ).toThrowError('Cannot determine image format');
expect(() => canvas.saveAs(`${TMP}/output`, {format:'png'}) ).not.toThrow()
await expect(canvas.saveAs(`${TMP}/output`, {format:'png'}) ).resolves.not.toThrow();
})

@@ -115,3 +138,3 @@

describe("can create", ()=>{
describe("can create | async", ()=>{
let TMP

@@ -127,2 +150,178 @@ beforeEach(() => {

test("JPEGs", async ()=>{
await Promise.all([
canvas.saveAs(`${TMP}/output1.jpg`),
canvas.saveAs(`${TMP}/output2.jpeg`),
canvas.saveAs(`${TMP}/output3.JPG`),
canvas.saveAs(`${TMP}/output4.JPEG`),
canvas.saveAs(`${TMP}/output5`, {format:'jpg'}),
canvas.saveAs(`${TMP}/output6`, {format:'jpeg'}),
canvas.saveAs(`${TMP}/output6.png`, {format:'jpeg'}),
])
let magic = MAGIC.jpg
for (let path of glob(`${TMP}/*`)){
let header = fs.readFileSync(path).slice(0, magic.length)
expect(header.equals(magic)).toBe(true)
}
})
test("PNGs", async ()=>{
await Promise.all([
canvas.saveAs(`${TMP}/output1.png`),
canvas.saveAs(`${TMP}/output2.PNG`),
canvas.saveAs(`${TMP}/output3`, {format:'png'}),
canvas.saveAs(`${TMP}/output4.svg`, {format:'png'}),
])
let magic = MAGIC.png
for (let path of glob(`${TMP}/*`)){
let header = fs.readFileSync(path).slice(0, magic.length)
expect(header.equals(magic)).toBe(true)
}
})
test("SVGs", async ()=>{
await Promise.all([
canvas.saveAs(`${TMP}/output1.svg`),
canvas.saveAs(`${TMP}/output2.SVG`),
canvas.saveAs(`${TMP}/output3`, {format:'svg'}),
canvas.saveAs(`${TMP}/output4.jpeg`, {format:'svg'}),
])
for (let path of glob(`${TMP}/*`)){
let svg = fs.readFileSync(path, 'utf-8')
expect(svg).toMatch(/^<\?xml version/)
}
})
test("PDFs", async ()=>{
await Promise.all([
canvas.saveAs(`${TMP}/output1.pdf`),
canvas.saveAs(`${TMP}/output2.PDF`),
canvas.saveAs(`${TMP}/output3`, {format:'pdf'}),
canvas.saveAs(`${TMP}/output4.jpg`, {format:'pdf'}),
])
let magic = MAGIC.pdf
for (let path of glob(`${TMP}/*`)){
let header = fs.readFileSync(path).slice(0, magic.length)
expect(header.equals(magic)).toBe(true)
}
})
test("image-sequences", async () => {
let colors = ['orange', 'yellow', 'green', 'skyblue', 'purple']
colors.forEach((color, i) => {
let dim = 512 + 100*i
ctx = i ? canvas.newPage(dim, dim) : canvas.newPage()
ctx.fillStyle = color
ctx.arc(100, 100, 25, 0, Math.PI + Math.PI/colors.length*(i+1))
ctx.fill()
expect(ctx.canvas.height).toEqual(dim)
expect(ctx.canvas.width).toEqual(dim)
})
await canvas.saveAs(`${TMP}/output-{2}.png`)
let files = glob(`${TMP}/output-0?.png`)
expect(files.length).toEqual(colors.length+1)
files.forEach((fn, i) => {
let img = new Image()
img.src = fn
expect(img.complete).toBe(true)
// second page inherits the first's size, then they increase
let dim = i<2 ? 512 : 512 + 100 * (i-1)
expect(img.width).toEqual(dim)
expect(img.height).toEqual(dim)
})
})
test("multi-page PDFs", async () => {
let colors = ['orange', 'yellow', 'green', 'skyblue', 'purple']
colors.forEach((color, i) => {
ctx = canvas.newPage()
ctx.fillStyle = color
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = 'white'
ctx.textAlign = 'center'
ctx.fillText(i+1, canvas.width/2, canvas.height/2)
})
let path = `${TMP}/multipage.pdf`
await canvas.saveAs(path)
let header = fs.readFileSync(path).slice(0, MAGIC.pdf.length)
expect(header.equals(MAGIC.pdf)).toBe(true)
})
test("image Buffers", async () => {
for (ext of ["png", "jpg", "pdf", "svg"]){
// use extension to specify type
let path = `${TMP}/output.${ext}`
let buf = await canvas.toBuffer(ext)
expect(buf).toBeInstanceOf(Buffer)
fs.writeFileSync(path, buf)
let header = fs.readFileSync(path).slice(0, MAGIC[ext].length)
expect(header.equals(MAGIC[ext])).toBe(true)
// use mime to specify type
path = `${TMP}/bymime.${ext}`
buf = await canvas.toBuffer(MIME[ext])
expect(buf).toBeInstanceOf(Buffer)
fs.writeFileSync(path, buf)
header = fs.readFileSync(path).slice(0, MAGIC[ext].length)
expect(header.equals(MAGIC[ext])).toBe(true)
}
})
test("data URLs", async () => {
for (ext in MIME){
let magic = MAGIC[ext],
mime = MIME[ext],
[extURL, mimeURL] = await Promise.all([
canvas.toDataURL(ext),
canvas.toDataURL(mime),
]),
header = `data:${mime};base64,`,
data = Buffer.from(extURL.substr(header.length), 'base64')
expect(extURL).toEqual(mimeURL)
expect(extURL.startsWith(header)).toBe(true)
expect(data.slice(0, magic.length)).toEqual(magic)
}
})
test("sensible error messages", async () => {
ctx.fillStyle = 'lightskyblue'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// invalid path
await expect(canvas.saveAs(`${TMP}/deep/path/that/doesn/not/exist.pdf`))
.rejects.toThrow()
// canvas has a zero dimension
let width = 0, height = 128
Object.assign(canvas, {width, height})
expect(canvas).toMatchObject({width, height})
await expect(canvas.saveAs(`${TMP}/zeroed.png`)).rejects.toThrowError("must be non-zero")
})
})
describe("can create | sync", ()=>{
let TMP
beforeEach(() => {
TMP = tmp.dirSync().name
canvas.async = false
ctx.fillStyle = 'red'
ctx.arc(100, 100, 25, 0, Math.PI/2)
ctx.fill()
})
afterEach(() => fs.rmdirSync(TMP, {recursive:true}) )
test("JPEGs", ()=>{

@@ -137,3 +336,3 @@ canvas.saveAs(`${TMP}/output1.jpg`)

let magic = Buffer.from([0xFF, 0xD8, 0xFF])
let magic = MAGIC.jpg
for (let path of glob(`${TMP}/*`)){

@@ -151,3 +350,3 @@ let header = fs.readFileSync(path).slice(0, magic.length)

let magic = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
let magic = MAGIC.png
for (let path of glob(`${TMP}/*`)){

@@ -177,3 +376,3 @@ let header = fs.readFileSync(path).slice(0, magic.length)

let magic = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d])
let magic = MAGIC.pdf
for (let path of glob(`${TMP}/*`)){

@@ -214,2 +413,3 @@ let header = fs.readFileSync(path).slice(0, magic.length)

test("multi-page PDFs", () => {

@@ -225,6 +425,47 @@ let colors = ['orange', 'yellow', 'green', 'skyblue', 'purple']

})
expect(() => canvas.saveAs(`${TMP}/multipage.pdf`) ).not.toThrow()
let path = `${TMP}/multipage.pdf`
expect(() => canvas.saveAs(path) ).not.toThrow()
let header = fs.readFileSync(path).slice(0, MAGIC.pdf.length)
expect(header.equals(MAGIC.pdf)).toBe(true)
})
test("sensible errors for misbegotten exports", () => {
test("image Buffers", () => {
for (ext of ["png", "jpg", "pdf", "svg"]){
// use extension to specify type
let path = `${TMP}/output.${ext}`
let buf = canvas.toBuffer(ext)
expect(buf).toBeInstanceOf(Buffer)
fs.writeFileSync(path, buf)
let header = fs.readFileSync(path).slice(0, MAGIC[ext].length)
expect(header.equals(MAGIC[ext])).toBe(true)
// use mime to specify type
path = `${TMP}/bymime.${ext}`
buf = canvas.toBuffer(MIME[ext])
expect(buf).toBeInstanceOf(Buffer)
fs.writeFileSync(path, buf)
header = fs.readFileSync(path).slice(0, MAGIC[ext].length)
expect(header.equals(MAGIC[ext])).toBe(true)
}
})
test("data URLs", () => {
for (ext in MIME){
let magic = MAGIC[ext],
mime = MIME[ext],
extURL = canvas.toDataURL(ext),
mimeURL = canvas.toDataURL(mime),
header = `data:${mime};base64,`,
data = Buffer.from(extURL.substr(header.length), 'base64')
expect(extURL).toEqual(mimeURL)
expect(extURL.startsWith(header)).toBe(true)
expect(data.slice(0, magic.length)).toEqual(magic)
}
})
test("sensible error messages", () => {
ctx.fillStyle = 'lightskyblue'

@@ -242,6 +483,6 @@ ctx.fillRect(0, 0, canvas.width, canvas.height)

expect(canvas).toMatchObject({width, height})
canvas.saveAs(`${TMP}/zeroed.pdf`)
expect( () => canvas.saveAs(`${TMP}/zeroed.png`)).toThrowError("must be non-zero")
})
})
})

@@ -0,4 +1,6 @@

"use strict"
const _ = require('lodash'),
{Canvas, DOMMatrix, loadImage} = require('../lib'),
{parseFont} = require('../lib/parse');
parse = require('../lib/parse');

@@ -50,3 +52,3 @@ const BLACK = [0,0,0,255],

let font = '16px Baskerville, serif',
canonical = parseFont(font).canonical;
canonical = parse.font(font).canonical;
ctx.font = font

@@ -499,10 +501,18 @@ expect(ctx.font).toBe(canonical)

test("measureText()", () => {
let foo = ctx.measureText('foo').width,
ctx.font = "20px Arial, DejaVu Sans"
let ø = ctx.measureText('').width,
_ = ctx.measureText(' ').width,
__ = ctx.measureText(' ').width,
foo = ctx.measureText('foo').width,
foobar = ctx.measureText('foobar').width,
__foo = ctx.measureText(' foo').width;
__foo = ctx.measureText(' foo').width,
__foo__ = ctx.measureText(' foo ').width
expect(ø).toBeLessThan(_)
expect(_).toBeLessThan(__)
expect(foo).toBeLessThan(foobar)
expect(__foo).toBeGreaterThan(foo)
expect(__foo__).toBeGreaterThan(__foo)
// start from the default, alphabetic baseline
ctx.font = "20px Arial, DejaVu Sans"
var metrics = ctx.measureText("Lordran gypsum")

@@ -519,2 +529,3 @@

// make sure the polarity has flipped for 'bottom' baseline
ctx.textBaseline = "bottom"

@@ -562,3 +573,3 @@ metrics = ctx.measureText("Lordran gypsum")

let expected = _.defaults(spec, {style:"normal", stretch:"normal", variant:"normal"}),
parsed = parseFont(font);
parsed = parse.font(font);
expect(parsed).toMatchObject(expected)

@@ -565,0 +576,0 @@ })

@@ -37,9 +37,9 @@ var path = require('path')

app.get('/render', function (req, res, next) {
app.get('/render', async function (req, res, next) {
var canvas = new Canvas(200, 200)
renderTest(canvas, req.query.name, function (err) {
renderTest(canvas, req.query.name, async function (err) {
if (err) return next(err)
let data = canvas.png
let data = await canvas.png
res.contentType('image/png');

@@ -52,9 +52,9 @@ res.send(data)

app.get('/pdf', function (req, res, next) {
app.get('/pdf', async function (req, res, next) {
var canvas = new Canvas(200, 200)
renderTest(canvas, req.query.name, function (err) {
renderTest(canvas, req.query.name, async function (err) {
if (err) return next(err)
let data = canvas.pdf
let data = await canvas.pdf
res.contentType('application/pdf');

@@ -61,0 +61,0 @@ res.send(data)

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc