diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 775bcba5..506d7caf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: | @@ -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: | @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d24b5f3c..5e9c32bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 + diff --git a/Cargo.toml b/Cargo.toml index 9eb78f6b..7b16155a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/README.md b/README.md index 884cfb1c..0d5e1be0 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ The Canvas object is a stand-in for the HTML `` 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] ⚡ | @@ -222,7 +222,7 @@ The Canvas object is a stand-in for the HTML `` 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 @@ -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: @@ -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. diff --git a/lib/index.d.ts b/lib/index.d.ts index 3da8a508..24a096a5 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -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) */ @@ -79,6 +79,7 @@ export class Canvas { get svg(): Promise get jpg(): Promise get png(): Promise + get webp(): Promise } // @@ -360,4 +361,4 @@ export interface App{ quit(): void } -export const App: App \ No newline at end of file +export const App: App diff --git a/lib/index.js b/lib/index.js index 9b654daf..a82f9a3f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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){ diff --git a/lib/io.js b/lib/io.js index 484d5e2e..286b9dea 100644 --- a/lib/io.js +++ b/lib/io.js @@ -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"}, }) } diff --git a/src/context/page.rs b/src/context/page.rs index 95f258c8..94fadc1e 100644 --- a/src/context/page.rs +++ b/src/context/page.rs @@ -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 }; diff --git a/test/assets/image/format.webp b/test/assets/image/format.webp new file mode 100644 index 00000000..9d3d9a5a Binary files /dev/null and b/test/assets/image/format.webp differ diff --git a/test/assets/rose.webp b/test/assets/rose.webp new file mode 100644 index 00000000..8d1ea5c1 Binary files /dev/null and b/test/assets/rose.webp differ diff --git a/test/canvas.test.js b/test/canvas.test.js index 1315dab6..b23d382f 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -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(`{ } }) + 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`), @@ -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`) diff --git a/test/media.test.js b/test/media.test.js index e4097d9d..e1173f61 100644 --- a/test/media.test.js +++ b/test/media.test.js @@ -150,6 +150,11 @@ describe("Image", () => { img.src = FORMAT + '.ico' expect(img).toMatchObject(PARSED) }) + + test("WEBP", () => { + img.src = FORMAT + '.webp' + expect(img).toMatchObject(PARSED) + }) }) }) diff --git a/test/visual/tests.js b/test/visual/tests.js index c63ed56b..6b1a0e0b 100644 --- a/test/visual/tests.js +++ b/test/visual/tests.js @@ -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 () {