Security News
PyPI Introduces Digital Attestations to Strengthen Python Package Security
PyPI now supports digital attestations, enhancing security and trust by allowing package maintainers to verify the authenticity of Python packages.
skia-canvas
Advanced tools
Skia Canvas is a browser-less implementation of the HTML Canvas drawing API for Node.js. It is based on Google’s Skia graphics engine and as a result produces very similar results to Chrome’s <canvas>
element.
While the primary goal of this project is to provide a reliable emulation of the standard API according to the spec, it also extends it in a number of areas that are more relevant to the generation of static graphics files rather than ‘live’ display in a browser.
In particular, Skia Canvas:
is fast and compact since all the heavy lifting is done by native code written in Rust and C++
can generate output in both raster (JPEG & PNG) and vector (PDF & SVG) image formats
can save images to files, return them as Buffers, or encode dataURL strings
uses native threads and EventQueues for asynchronous rendering and file I/O
can create multiple ‘pages’ on a given canvas and then output 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
fully supports the CSS filter effects image processing operators
offers rich typographic control including:
const {Canvas, loadImage} = require('skia-canvas'),
rand = n => Math.floor(n * Math.random());
let canvas = new Canvas(600, 600),
ctx = canvas.getContext("2d"),
{width, height} = canvas;
// draw a sea of blurred dots filling the canvas
ctx.filter = 'blur(12px) hue-rotate(20deg)'
for (let i=0; i<800; i++){
ctx.fillStyle = `hsl(${rand(40)}deg, 80%, 50%)`
ctx.beginPath()
ctx.arc(rand(width), rand(height), rand(20)+5, 0, 2*Math.PI)
ctx.fill()
}
// mask all of the dots that don't overlap with the text
ctx.filter = 'none'
ctx.globalCompositeOperation = 'destination-in'
ctx.font='italic 480px Times, DejaVu Serif'
ctx.textAlign = 'center'
ctx.textBaseline = 'top'
ctx.fillText('¶', width/2, 0)
// draw a background behind the clipped text
ctx.globalCompositeOperation = 'destination-over'
ctx.fillStyle = '#182927'
ctx.fillRect(0,0, width,height)
// save the graphic...
canvas.saveAs("pilcrow.png")
// ...or use a shorthand for canvas.toBuffer("png")
fs.writeFileSync("pilcrow.png", canvas.png)
// ...or embed it in a string
console.log(`<img src="${canvas.toDataURL("png")}">`)
If you’re running on a supported platform, installation should be as simple as:
$ npm install skia-canvas
This will download a pre-compiled library from the project’s most recent release.
Nearly everything you need is statically linked into the library.
A notable exception is the Fontconfig library (and its associated FreeType renderer) which must be installed separately if you’re running on Linux.
The underlying Rust library uses N-API v6 which allows it to run on Node.js versions:
Pre-compiled binaries are available for:
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:
rustup
Detailed instructions 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).
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:
In addition, the module contains:
Image
objects asynchronouslyThe Canvas object is a stand-in for the HTML <canvas>
element. It defines image dimensions and provides a rendering context to draw to it. Once you’re ready to save or display what you’ve drawn, the canvas can save it to a file, or hand it off to you as a data buffer or string to process manually.
Image Dimensions | Rendering Contexts | Output |
---|---|---|
width | pages ⚡ | async ⚡ |
height | getContext() | pdf, png, svg, jpg ⚡ |
newPage() ⚡ | saveAs() ⚡ | |
toBuffer() ⚡ | ||
toDataURL() ⚡ |
Canvas
objectsRather 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.
let defaultCanvas = new Canvas() // without arguments, defaults to 300 × 150 px
let squareCanvas = new Canvas(512, 512) // creates a 512 px square
.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:
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):
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
}
.pages
The canvas’s .pages
attribute is an array of 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
These properties are syntactic sugar for calling the toBuffer()
method. Each returns a Node 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)
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.
The method’s return value is a CanvasRenderingContext2D
object which you can either save a reference to or recover later from the .pages
array.
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 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
.
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.
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:
canvas.saveAs('image.png', {density:2}) // choose the density explicitly
canvas.saveAs('image@3x.png') // equivalent to setting the density to 3
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.
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
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.
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() and newPage() methods.
.font
By default any line-height
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
The context’s .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 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
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).
The tracking value defaults to 0
and settings will persist across changes to the .font
property.
.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()
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:
"\n"
escapes will be honored rather than converted to spaceswidth
argument accepted by fillText
, strokeText
and measureText
will be interpreted as a ‘column width’ and used to word-wrap long lines.font
value will be used to set the inter-line leading rather than simply being ignored.Even when .textWrap
is false
, the text-drawing methods will never choose a more-condensed weight or otherwise attempt to squeeze your entire string into the measure specified by width
. Instead the text will be typeset up through the last word that fits and the rest will be omitted. This can be used in conjunction with the .lines
property of the object returned by measureText()
to incrementally lay out a long string into, for example, a multi-column layout with an even number of lines in each.
measureText(str, [width])
The measureText()
method returns a 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.
Each element of the array contains an object of the form:
{x, y, width, height, baseline, startIndex, endIndex}
The x
, y
, width
, and height
values define a rectangle that fully encloses the text of a given line relative to the ‘origin’ point you would pass to fillText()
or strokeText()
(and reflecting the context’s current .textBaseline
setting).
The baseline
value is a y-axis offset from the text origin to that particular line’s baseline.
The startIndex
and endIndex
values are the indices into the string of the first and last character that were typeset on that line.
The Path2D
class allows you to create paths independent of a given Canvas or graphics context. These paths can be modified over time and drawn repeatedly (potentially on multiple canvases).
Line Segments | Shapes | Boolean Ops ⚡ | Extents ⚡ |
---|---|---|---|
moveTo() | addPath() | complement() | bounds |
lineTo() | arc() | difference() | simplify() |
bezierCurveTo() | arcTo() | intersect() | |
quadraticCurveTo() | ellipse() | union() | |
closePath() | rect() | xor() |
Path2D
objectsIts constructor can be called without any arguments to create a new, empty path object. It can also accept a string using SVG syntax or a reference to an existing Path2D
object (which it will return a clone of):
// 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)
A canvas’s context always contains an implicit ‘current’ bézier path which is updated by commands like lineTo() and arcTo() and is drawn to the canvas by calling fill(), stroke(), or clip() without any arguments (aside from an optional winding rule). If you start creating a second path by calling 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).
.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()
).
The .bounds
property contains an object defining the minimal rectangle containing the path:
{top, left, bottom, right, width, height}
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:
let oval = new Path2D()
oval.arc(100, 100, 100, 0, 2*Math.PI)
let rect = new Path2D()
rect.rect(0, 100, 100, 100)
We can then create a new path by using one of the boolean operations such as:
let knockout = rect.complement(oval),
overlap = rect.intersect(oval),
footprint = rect.union(oval),
...
Note that the xor
operator is liable to create a path with lines that cross over one another so you’ll get different results when filling it using the "evenodd"
winding rule (as shown above) than with "nonzero"
(the canvas default).
simplify()
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:
let cross = new Path2D("M 10,50 h 100 v 20 h -100 Z M 50,10 h 20 v100 h -20 Z")
let uncrossed = cross.simplify()
The included Image object behaves just like the one in browsers, which is to say that loading images can be verbose, fiddly, and callback-heavy. The loadImage()
utility method wraps image loading in a Promise, allowing for more concise initialization. For instance the following snippets are equivalent:
let img = new Image()
img.onload = function(){
ctx.drawImage(img, 100, 100)
}
img.src = 'https://example.com/icon.png'
let img = await loadImage('https://example.com/icon.png')
ctx.drawImage(img, 100, 100)
In addition to HTTP URLs, both loadImage()
and the Image.src
attribute will also accept data URLs, local file paths, and Buffer objects.
The FontLibrary
is a static class which does not need to be instantiated with new
. Instead you can access the properties and methods on the global FontLibrary
you import from the module and its contents will be shared across all canvases you create.
.families
The .families
property contains a list of family names, merging together all the fonts installed on the system and any fonts that have been added manually through the FontLibrary.use()
method. Any of these names can be passed to FontLibrary.family()
for more information.
family(name)
If the name
argument is the name of a known font family, this method will return an object with information about the available weights and styles. For instance, on my system FontLibrary.family("Avenir Next")
returns:
{
family: 'Avenir Next',
weights: [ 100, 400, 500, 600, 700, 800 ],
widths: [ 'normal' ],
styles: [ 'normal', 'italic' ]
}
Asking for details about an unknown family will return undefined
.
has(familyName)
Returns true
if the family is installed on the system or has been added via FontLibrary.use()
.
use(familyName, [...fontPaths])
The FontLibrary.use()
method allows you to dynamically load local font files and use them with your canvases. By default it will use whatever family name is in the font metadata, but this can be overridden by an alias you provide. Since font-wrangling can be messy, use
can be called in a number of different ways:
// with default family name
FontLibrary.use([
"fonts/Oswald-Regular.ttf",
"fonts/Oswald-SemiBold.ttf",
"fonts/Oswald-Bold.ttf",
])
// with an alias
FontLibrary.use("Grizwald", [
"fonts/Oswald-Regular.ttf",
"fonts/Oswald-SemiBold.ttf",
"fonts/Oswald-Bold.ttf",
])
// with default family name
FontLibrary.use(['fonts/Crimson_Pro/*.ttf'])
// with an alias
FontLibrary.use("Stinson", ['fonts/Crimson_Pro/*.ttf'])
FontLibrary.use({
Nieuwveen: ['fonts/AmstelvarAlpha-VF.ttf', 'fonts/AmstelvarAlphaItalic-VF.ttf'],
Fairway: 'fonts/Raleway/*.ttf'
})
The return value will be either a list or an object (matching the style in which it was called) with an entry describing each font file that was added. For instance, one of the entries from the first example could be:
{
family: 'Grizwald',
weight: 600,
style: 'normal',
width: 'normal',
file: 'fonts/Oswald-SemiBold.ttf'
}
This project is deeply indebted to the work of the Rust Skia project whose Skia bindings provide a safe and idiomatic interface to the mess of C++ that lies underneath.
Many thanks to the node-canvas
developers for their terrific set of unit tests. In the absence of an Acid Test for canvas, these routines were invaluable.
📦 ⟩ [v0.9.22] ⟩ Jun 09, 2021
async
property for details.SaveAs
and the other canvas output functions all accept an optional density
argument which is an integer ≥1 and will upscale the image accordingly. The density can also be passed using the filename
argument by ending the name with an ‘@’ suffix like some-image@2x.png
.outline
argument to true
.toBuffer
, toDataURL
, png
, jpg
, pdf
, and svg
) and file i/o (saveAs
) are now asynchronous and return Promise
objects. The old, synchronous behavior is still available on a canvas-by-canvas basis by setting its async
property to false
.quality
argument accepted by the output methods is now a float in the range 0–1 rather than an integer from 0–100. This is consistent with the encoderOptions arg in the spec. Quality now defaults to 0.92 (again, as per the spec) rather than lossless.measureText
was reporting zero when asked to measure a string that was entirely made of whitespace. This is still the case for ‘blank‘ lines when textWrap
is set to true
but in the default, single-line mode the metrics will now report the width of the whitespace.Context2D
s now use an external Typesetter
struct to manage layout and rendering.FAQs
A GPU-accelerated Canvas Graphics API for Node
The npm package skia-canvas receives a total of 7,623 weekly downloads. As such, skia-canvas popularity was classified as popular.
We found that skia-canvas demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
PyPI now supports digital attestations, enhancing security and trust by allowing package maintainers to verify the authenticity of Python packages.
Security News
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.