Skip to content

Commit

Permalink
Adds WEBP format as available input and output types, with doc & test…
Browse files Browse the repository at this point in the history
…s. (#177)

* Adds WEBP format as available input and output types, with doc & tests.

* downgrade python to work around bug in depot_tools

- adding the WEBP feature means not being able to use prebuilt bindings, revealing a compilation bug
- `skia_bindings` wasn't building on macOS runner because its python3 is new enough to have removed the `pipes` module the skia build tools are expecting to still be there
- presumably the next rust-skia milestone update will have a fix for this

* separate linux & windows build recipes

- linux needs extra feature flags for woff support

---------

Co-authored-by: Christian Swinehart <drafting@samizdat.co>
  • Loading branch information
mpaperno and samizdatco authored Oct 28, 2024
1 parent d3c95f8 commit f6e8b80
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 17 deletions.
11 changes: 9 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
run: |
cd skia-canvas
npm ci --ignore-scripts
npm run build -- --release --features vulkan,window,skia-safe/embed-freetype
npm run build -- --release --features vulkan,window,skia-safe/embed-freetype,skia-safe/freetype-woff2
- name: Package module
run: |
Expand Down Expand Up @@ -101,7 +101,7 @@ jobs:
run: |
cd skia-canvas
npm ci --ignore-scripts
npm run build -- --release --features vulkan,window,skia-safe/embed-freetype
npm run build -- --release --features vulkan,window,skia-safe/embed-freetype,skia-safe/freetype-woff2
- name: Package module
run: |
Expand Down Expand Up @@ -138,6 +138,13 @@ jobs:
with:
toolchain: stable

# temporarily necessary while skia_bindings 0.78.2 is looking for the `pipes` module which
# was renamed in Python 3.13 (can hopefully be removed at the next release…)
- name: Downgrade Python (depot_tools workaround)
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Build module
env:
MACOSX_DEPLOYMENT_TARGET: 10.13
Expand Down
25 changes: 21 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,34 @@ jobs:
with:
toolchain: stable

- name: Build for Metal
# temporarily necessary while skia_bindings 0.78.2 is looking for the `pipes` module which
# was renamed in Python 3.13 (can hopefully be removed at the next release…)
- name: Downgrade Python (depot_tools workaround)
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Build for Mac
if: ${{ matrix.os == 'macos-latest' }}
run: |
npm ci --ignore-scripts
npm run build -- --release --features metal,window
- name: Build for Vulkan
if: ${{ matrix.os != 'macos-latest' }}
- name: Build for Linux
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
npm ci --ignore-scripts
npm run build -- --release --features vulkan,window,skia-safe/embed-freetype,skia-safe/freetype-woff2
- name: Build for Windows
if: ${{ matrix.os == 'windows-latest' }}
shell: bash
run: |
npm ci --ignore-scripts
npm run build -- --release --features vulkan,window
- name: Run tests
run: npm test
run: |
npm test --verbose
ls -l lib/v6/index.node
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ crossbeam = "0.8.2"
once_cell = "1.13"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
skia-safe = { version = "0.78.2", features = ["textlayout"] }
skia-safe = { version = "0.78.2", features = ["textlayout", "webp"] }
allsorts = { version = "0.15", features = ["flate2_zlib"], default-features = false}

# vulkan
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ The Canvas object is a stand-in for the HTML `<canvas>` element. It defines imag
| Image Dimensions | Rendering Contexts | Output |
| -- | -- | -- |
| [**width**][canvas_width] | [**gpu**][canvas_gpu]| ~~[**async**][canvas_async]~~|
| [**height**][canvas_height] | [**pages**][canvas_pages]| [**pdf**, **png**, **svg**, **jpg**][shorthands]|
| [**height**][canvas_height] | [**pages**][canvas_pages]| [**pdf**, **png**, **svg**, **jpg**, **webp**][shorthands]|
| | [getContext()][getContext] | [saveAs()][saveAs] / [saveAsSync()][saveAs]|
| | [newPage()][newPage]| [toBuffer()][toBuffer] / [toBufferSync()][toBuffer]|
| | | [toDataURL()][toDataURL_ext] / [toDataURLSync()][toDataURL_ext]|
Expand All @@ -222,7 +222,7 @@ The Canvas object is a stand-in for the HTML `<canvas>` element. It defines imag
[newPage]: #newpagewidth-height
[toDataURL_mdn]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
[toDataURL_ext]: #todataurlformat-page-matte-density-quality-outline
[shorthands]: #pdf-svg-jpg-and-png
[shorthands]: #pdf-svg-jpg-webp-and-png

#### Creating new `Canvas` objects

Expand All @@ -239,7 +239,7 @@ When the canvas renders images and writes them to disk, it does so in a backgrou
- [`saveAs()`][saveAs]
- [`toBuffer()`][toBuffer]
- [`toDataURL()`][toDataURL_ext]
- [`.pdf`, `.svg`, `.jpg`, and `.png`][shorthands]
- [`.pdf`, `.svg`, `.jpg`, `.webp`, and `.png`][shorthands]


In cases where this is not the desired behavior, you can use the synchronous equivalents for the primary export functions. They accept identical arguments to their async versions but block execution and return their values synchronously rather than wrapped in Promises. Also note that the [shorthand properties][shorthands] do not have synchronous versions:
Expand Down Expand Up @@ -277,7 +277,7 @@ The `.gpu` attribute allows you to control whether rendering occurs on the graph

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`, `.webp`, and `.png`

These properties are syntactic sugar for calling the `toBuffer()` method. Each returns a [Promise][Promise] that resolves to 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.

Expand Down
5 changes: 3 additions & 2 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class Image extends globalThis.Image {
// Canvas
//

export type ExportFormat = "png" | "jpg" | "jpeg" | "pdf" | "svg";
export type ExportFormat = "png" | "jpg" | "jpeg" | "webp" | "pdf" | "svg";

export interface RenderOptions {
/** Page to export: Defaults to 1 (i.e., first page) */
Expand Down Expand Up @@ -79,6 +79,7 @@ export class Canvas {
get svg(): Promise<Buffer>
get jpg(): Promise<Buffer>
get png(): Promise<Buffer>
get webp(): Promise<Buffer>
}

//
Expand Down Expand Up @@ -360,4 +361,4 @@ export interface App{
quit(): void
}

export const App: App
export const App: App
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ class Canvas extends RustClass{
get jpg(){ return this.toBuffer("jpg") }
get pdf(){ return this.toBuffer("pdf") }
get svg(){ return this.toBuffer("svg") }
get webp(){ return this.toBuffer("webp") }

get async(){ return this.prop('async') }
set async(flag){
Expand Down
6 changes: 3 additions & 3 deletions lib/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ class Format{
toMime: this.toMime.bind(this),
fromMime: this.fromMime.bind(this),
expected: isWeb ? `"png", "jpg", or "webp"`
: `"png", "jpg", "pdf", or "svg"`,
: `"png", "jpg", "webp", "pdf", or "svg"`,
formats: isWeb ? {png, jpg, jpeg, webp}
: {png, jpg, jpeg, pdf, svg},
: {png, jpg, jpeg, webp, pdf, svg},
mimes: isWeb ? {[png]: "png", [jpg]: "jpg", [webp]: "webp"}
: {[png]: "png", [jpg]: "jpg", [pdf]: "pdf", [svg]: "svg"},
: {[png]: "png", [jpg]: "jpg", [webp]: "webp", [pdf]: "pdf", [svg]: "svg"},
})
}

Expand Down
3 changes: 2 additions & 1 deletion src/context/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ impl Page{
let img_dims = self.bounds.size();
let img_format = match format {
"jpg" | "jpeg" => Some(EncodedImageFormat::JPEG),
"png" => Some(EncodedImageFormat::PNG),
"png" => Some(EncodedImageFormat::PNG),
"webp" => Some(EncodedImageFormat::WEBP),
_ => None
};

Expand Down
Binary file added test/assets/image/format.webp
Binary file not shown.
Binary file added test/assets/rose.webp
Binary file not shown.
32 changes: 32 additions & 0 deletions test/canvas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ const BLACK = [0,0,0,255],
MAGIC = {
jpg: Buffer.from([0xFF, 0xD8, 0xFF]),
png: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
webp: Buffer.from([0x52, 0x49, 0x46, 0x46]),
pdf: Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d]),
svg: Buffer.from(`<?xml version`, 'utf-8')
},
MIME = {
png: "image/png",
jpg: "image/jpeg",
webp: "image/webp",
pdf: "application/pdf",
svg: "image/svg+xml"
};
Expand Down Expand Up @@ -183,6 +185,21 @@ describe("Canvas", ()=>{
}
})

test("WEBPs", async ()=>{
await Promise.all([
canvas.saveAs(`${TMP}/output1.webp`),
canvas.saveAs(`${TMP}/output2.WEBP`),
canvas.saveAs(`${TMP}/output3`, {format:'webp'}),
canvas.saveAs(`${TMP}/output4.svg`, {format:'webp'}),
])

let magic = MAGIC.webp
for (let path of findTmp(`*`)){
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`),
Expand Down Expand Up @@ -353,6 +370,21 @@ describe("Canvas", ()=>{
}
})

test("WEBPs", async ()=>{
await Promise.all([
canvas.saveAsSync(`${TMP}/output1.webp`),
canvas.saveAsSync(`${TMP}/output2.WEBP`),
canvas.saveAsSync(`${TMP}/output3`, {format:'webp'}),
canvas.saveAsSync(`${TMP}/output4.svg`, {format:'webp'}),
])

let magic = MAGIC.webp
for (let path of findTmp(`*`)){
let header = fs.readFileSync(path).slice(0, magic.length)
expect(header.equals(magic)).toBe(true)
}
})

test("SVGs", ()=>{
canvas.saveAsSync(`${TMP}/output1.svg`)
canvas.saveAsSync(`${TMP}/output2.SVG`)
Expand Down
5 changes: 5 additions & 0 deletions test/media.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ describe("Image", () => {
img.src = FORMAT + '.ico'
expect(img).toMatchObject(PARSED)
})

test("WEBP", () => {
img.src = FORMAT + '.webp'
expect(img).toMatchObject(PARSED)
})
})
})

Expand Down
10 changes: 10 additions & 0 deletions test/visual/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2155,6 +2155,16 @@ tests['drawImage(img) grayscale JPEG'] = function (ctx, done) {
img.src = imageSrc('pentagon-grayscale.jpg')
}

tests['drawImage(img) webp'] = function (ctx, done) {
var img = new Image()
img.onload = function () {
ctx.drawImage(img, 0, 0, 200, 200)
done(null)
}
img.onerror = done
img.src = imageSrc('rose.webp')
}

// tests['drawImage(img) svg'] = function (ctx, done) {
// var img = new Image()
// img.onload = function () {
Expand Down

0 comments on commit f6e8b80

Please sign in to comment.