diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 1034f6461..10c0a04ab 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,25 +1,17 @@ - - - - - ## Issue or Feature +- [ ] If this is an issue with installation, I have read the [troubleshooting guide](https://github.com/Automattic/node-canvas/issues/1511). + ## Steps to Reproduce ```js var Canvas = require('canvas'); -var canvas = new Canvas(200, 200); +var canvas = Canvas.createCanvas(200, 200); var ctx = canvas.getContext('2d'); // etc. ``` ## Your Environment -* Version of node-canvas (e.g. 1.4.0): -* Environment (e.g. node 4.2.0 on Mac OS X 10.8): \ No newline at end of file +* Version of node-canvas (output of `npm list canvas` or `yarn list canvas`): +* Environment (e.g. node 20.9.0 on macOS 14.1.1): diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..c308e167e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +Thanks for contributing! + +- [ ] Have you updated CHANGELOG.md? diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..963d21721 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,89 @@ +name: Test +on: + push: + paths-ignore: + - ".github/workflows/prebuild.yaml" + pull_request: + paths-ignore: + - ".github/workflows/prebuild.yaml" + +jobs: + Linux: + name: Test on Linux + runs-on: ubuntu-latest + strategy: + matrix: + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] + steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: actions/checkout@v4 + - name: Install Dependencies + run: | + sudo apt update + sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev + - name: Install + run: npm install --build-from-source + - name: Test + run: npm test + + Windows: + name: Test on Windows + runs-on: windows-2025 + strategy: + matrix: + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] + steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: actions/checkout@v4 + - name: Install Dependencies + run: | + Invoke-WebRequest "https://ftp.gnome.org/pub/GNOME/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" + Expand-Archive gtk.zip -DestinationPath "C:\GTK" + Invoke-WebRequest "https://downloads.sourceforge.net/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4-vc64.exe" -OutFile "libjpeg.exe" -UserAgent NativeHost + .\libjpeg.exe /S + winget install --accept-source-agreements --id=Microsoft.VCRedist.2010.x64 -e + npm install -g node-gyp@8 + npm prefix -g | % {npm config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"} + - name: Install + run: npm install --build-from-source + - name: Test + run: npm test + + macOS: + name: Test on macOS + runs-on: macos-15 + strategy: + matrix: + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] + steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: actions/checkout@v4 + - name: Install Dependencies + run: | + brew update + brew install python-setuptools pkg-config cairo pango libpng jpeg giflib librsvg + - name: Install + run: npm install --build-from-source + - name: Test + run: npm test + + Lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-node@v4 + with: + node-version: 20.9.0 + - uses: actions/checkout@v4 + - name: Install + run: npm install --ignore-scripts + - name: Lint + run: npm run lint + - name: Lint Types + run: npm run tsd diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml new file mode 100644 index 000000000..88b6288bf --- /dev/null +++ b/.github/workflows/prebuild.yaml @@ -0,0 +1,13 @@ +# This is a dummy file so that this workflow shows up in the Actions tab. +# Prebuilds are actually run using the prebuilds branch. + +name: Make Prebuilds +on: workflow_dispatch + +jobs: + Linux: + name: Nothing + runs-on: ubuntu-latest + steps: + - name: Nothing + run: echo "Nothing to do here" diff --git a/.gitignore b/.gitignore index 130894492..4fd0b5eda 100644 --- a/.gitignore +++ b/.gitignore @@ -4,13 +4,18 @@ build test/images/*.png examples/*.png examples/*.jpg +examples/*.pdf testing out.png out.pdf out.svg .pomo node_modules +package-lock.json # Vim cruft *.swp *.un~ +npm-debug.log + +.idea diff --git a/.npmignore b/.npmignore deleted file mode 100644 index e91e1490a..000000000 --- a/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -testing -build -benchmarks -examples -support -test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3a25554e9..000000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: node_js -node_js: - - '6' - - '4' - - '0.12' - - '0.10' -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - libcairo2-dev - - libjpeg8-dev - - libpango1.0-dev - - libgif-dev - - g++-4.9 -env: - - CXX=g++-4.9 -before_install: - - npm explore npm -g -- npm install node-gyp@latest -sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..59fad2f10 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1133 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) and this +project adheres to [Semantic Versioning](http://semver.org/). + +(Unreleased) +================== +### Changed +### Added +### Fixed +* Fix error message HTTP response status code in image src setter +* `roundRect()` shape incorrect when radii were large relative to rectangle size (#2400) +* Reject loadImage when src is null or invalid (#2304) + +3.2.0 +================== +### Added +* Added `ctx.lang` to set the ISO language code for text + +3.1.2 +================== +### Fixed +* Fix crash when setting width/height on PDF, SVG canvas (#2520) + +3.1.1 +================== +### Fixed +* Fix a crash when SVGs without width or height are loaded (#2486) +* Fix fetching prebuilds during installation on certain newer versions of Node (#2497) +* Fixed issue with fillText that was breaking subsequent fillText calls (#2171) +* Fix svg rendering when the image is resized (#2498) +* Fix measureText with direction rtl textAlign start/end +* Fix a crash in Node 24, due to external memory API change (#2514) + +3.1.0 +================== +### Changed +* Replaced `simple-get ` with ` Node.js builtin` `fetch` (#2309) +* `ctx.font` has a new C++ parser and is 2x-400x faster. Please file an issue if you experience different results, as caching has been removed. +* The restriction of registering fonts before a canvas is created has been removed. You can now register a font as late as right before the `fillText` call ([#1921](https://github.com/Automattic/node-canvas/issues/1921)) + +### Added +* Support for accessibility and links in PDFs +* `ctx.direction` is implemented: `'rtl'` or `'ltr'` set the base direction of text +* `ctx.textAlign` `'start'` and `'end'` are now `'right'` and `'left'` when `ctx.direction === 'rtl'` + +### Fixed +* Fix a crash in `getImageData` when the rectangle is entirely outside the canvas. ([#2024](https://github.com/Automattic/node-canvas/issues/2024)) +* Fix `getImageData` cropping the resulting `ImageData` when the given rectangle is partly outside the canvas. ([#1849](https://github.com/Automattic/node-canvas/issues/1849)) + +3.0.1 +================== +### Fixed +* Fixed accidental depenency on ambient DOM types + +3.0.0 +================== + +This release notably changes to using N-API. 🎉 + +### Breaking +* Dropped support for Node.js 16.x and below. +### Changed +* Migrated to N-API (by way of node-addon-api) and removed libuv and v8 dependencies +* Change from node-pre-gyp to prebuild-install +* Defer the initialization of the `op` variable to the `default` switch case to avoid a compiler warning. (#2229) +* Use a `default` switch case with a null statement if some enum values aren't suppsed to be handled, this avoids a compiler warning. (#2229) +* Migrate from librsvg's deprecated `rsvg_handle_get_dimensions()` and `rsvg_handle_render_cairo()` functions to the new `rsvg_handle_get_intrinsic_size_in_pixels()` and `rsvg_handle_render_document()` respectively. (#2229) +* Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. (#2229) +* Remove unused private field `backend` in the `Backend` class. (#2229) +* Add Node.js v20 to CI. (#2237) +* Replaced `dtslint` with `tsd` (#2313) +* Changed PNG consts to static properties of Canvas class +* Reverted improved font matching on Linux (#1572) because it doesn't work if fonts are installed. If you experience degraded font selection, please file an issue and use v3.0.0-rc3 in the meantime. + +### Added +* Added string tags to support class detection +* Throw Cairo errors in canvas.toBuffer() +### Fixed +* Fix a case of use-after-free. (#2229) +* Fix usage of garbage value by filling the allocated memory entirely with zeros if it's not modified. (#2229) +* Fix a potential memory leak. (#2229) +* Fix the wrong type of setTransform +* Fix the improper parsing of rgb functions issue. (#2300) +* Fix issue related to improper parsing of leading and trailing whitespaces in CSS color. (#2301) +* RGB functions should support real numbers now instead of just integers. (#2339) +* Allow alternate or properly escaped quotes *within* font-family names +* Fix TextMetrics type to include alphabeticBaseline, emHeightAscent, and emHeightDescent properties +* Fix class properties should have defaults as standard js classes (#2390) +* Fixed Exif orientation in JPEG files being ignored (#1670) +* Align DOMMatrix/DOMPoint to spec by adding missing methods + +2.11.2 +================== +### Fixed +* Building on Windows in CI (and maybe other Windows configurations?) (#2216) + +2.11.1 +================== +### Fixed +* Add missing property `canvas` to the `CanvasRenderingContext2D` type +* Fixed glyph positions getting rounded, resulting text having a slight `letter-spacing` effect +* Fixed `ctx.font` not being restored correctly after `ctx.restore()` (#1946) + +2.11.0 +================== +### Fixed +* Replace triple-slash directive in types with own types to avoid polluting TS modules with globals ([#1656](https://github.com/Automattic/node-canvas/issues/1656)) + +2.10.2 +================== +### Fixed +* Fix `Assertion failed: (object->InternalFieldCount() > 0), function Unwrap, file nan_object_wrap.h, line 32.` ([#2025](https://github.com/Automattic/node-canvas/issues/2025)) +* `textBaseline` and `textAlign` were not saved/restored by `save()`/`restore()`. ([#1936](https://github.com/Automattic/node-canvas/issues/2029)) +* Update nan to v2.17.0 to ensure Node.js v18+ support. +### Changed +* Improve performance and memory usage of `save()`/`restore()`. +* `save()`/`restore()` no longer have a maximum depth (previously 64 states). + +2.10.1 +================== +### Fixed +* Fix `actualBoundingBoxLeft` and `actualBoundingBoxRight` when `textAlign='center'` or `'right'` ([#1909](https://github.com/Automattic/node-canvas/issues/1909)) +* Fix `rgba(r,g,b,0)` with alpha to 0 should parse as transparent, not opaque. ([#2110](https://github.com/Automattic/node-canvas/pull/2110)) + +2.10.0 +================== +### Added +* Export `pangoVersion` +* [`ctx.roundRect()`](https://developer.chrome.com/blog/canvas2d/#round-rect) +### Fixed +* `rgba(r,g,b)` with no alpha should parse as opaque, not transparent. ([#2029](https://github.com/Automattic/node-canvas/issues/2029)) +* Typo in `PngConfig.filters` types. ([#2072](https://github.com/Automattic/node-canvas/issues/2072)) +* `createPattern()` always used "repeat" mode; now supports "repeat-x" and "repeat-y". ([#2066](https://github.com/Automattic/node-canvas/issues/2066)) +* Crashes and hangs when using non-finite values in `context.arc()`. ([#2055](https://github.com/Automattic/node-canvas/issues/2055)) +* Incorrect `context.arc()` geometry logic for full ellipses. ([#1808](https://github.com/Automattic/node-canvas/issues/1808), ([#1736](https://github.com/Automattic/node-canvas/issues/1736))) +* Added missing `deregisterAllFonts` to the Typescript declaration file ([#2096](https://github.com/Automattic/node-canvas/pull/2096)) +* Add `User-Agent` header when requesting remote images ([#2099](https://github.com/Automattic/node-canvas/issues/2099)) + +2.9.3 +================== +### Fixed +* Wrong fonts used when calling `registerFont` multiple times with the same family name ([#2041](https://github.com/Automattic/node-canvas/issues/2041)) + +2.9.2 +================== +### Fixed +* All exports now work when Canvas is used in ES Modules (ESM). ([#2047](https://github.com/Automattic/node-canvas/pull/2047)) +* `npm rebuild` will now re-fetch prebuilt binaries to avoid `NODE_MODULE_VERSION` mismatch errors. ([#1982](https://github.com/Automattic/node-canvas/pull/1982)) + +2.9.1 +================== +### Fixed +* Stringify CanvasGradient, CanvasPattern and ImageData like browsers do. (#1639, #1646) +* Add missing include for `toupper`. +* Throw an error instead of crashing the process if `getImageData` or `putImageData` is called on a PDF or SVG canvas (#1853) +* Compatibility with Typescript 4.6 +* Near-perfect font matching on Linux (#1572) +* Fix multi-byte font path support on Windows. +* Allow rebuild of this library + +2.9.0 +================== +### Changed +* Refactor functions to classes. +* Changed `DOMPoint()` constructor to check for parameter nullability. +* Changed `DOMMatrix.js` to use string literals for non-special cases. +* Remove semicolons from Dommatrix.js. +* Update nan to v2.15.0 to ensure Node.js v14+ support. +* Clean up inf/nan macros and slightly speed up argument checking. +### Added +* Added `deregisterAllFonts` method to free up memory and reduce font conflicts. +### Fixed +* Support Apple M1 Homebrew install that puts canvas install library files in `/opt/homebrew/lib` + +2.8.0 +================== +### Changed +* Upgrade dtslint +* Upgrade node-pre-gyp to 1.0.0. Note that if you are using special node-pre-gyp + features like `node_pre_gyp_accessKeyId`, you may need to make changes to your + installation procedure. See https://github.com/mapbox/node-pre-gyp/blob/master/CHANGELOG.md#100. +* Add Node.js v16 to CI. +* The C++ class method `nBytes()` now returns a size_t. (Because this is a C++ + method only, this is not considered a breaking change.) +### Added +* Add support for `inverse()` and `invertSelf()` to `DOMMatrix` (#1648) +* Add support for `context.getTransform()` ([#1769](https://github.com/Automattic/node-canvas/pull/1769)) +* Add support for `context.setTransform(dommatrix)` ([#1769](https://github.com/Automattic/node-canvas/pull/1769)) +### Fixed +* Fix `actualBoundingBoxLeft` and `actualBoundingBoxRight` returned by `measureText` to be the ink rect ([#1776](https://github.com/Automattic/node-canvas/pull/1776), fixes [#1703](https://github.com/Automattic/node-canvas/issues/1703)). +* Fix Pango logging "expect ugly output" on Windows (#1643) +* Fix benchmark for createPNGStream (#1672) +* Fix dangling reference in BackendOperationNotAvailable exception (#1740) +* Fix always-false comparison warning in Canvas.cc. +* Fix Node.js crash when throwing from an onload or onerror handler. + +2.7.0 +================== +### Changed +* Switch CI to Github Actions. (Adds Windows and macOS builds.) +* Switch prebuilds to GitHub actions in the Automattic/node-canvas repository. + Previously these were in the [node-gfx/node-canvas-prebuilt](https://github.com/node-gfx/node-canvas-prebuilt) + and triggered manually. +* Speed up `fillStyle=` and `strokeStyle=` +### Added +* Export `rsvgVersion`. +* CanvasPattern’s `setTransform` method is no longer missing +### Fixed +* Fix BMP issues. (#1497) +* Update typings to support jpg and addPage on NodeCanvasRenderingContext2D (#1509) +* Fix assertion failure when using Visual Studio Code debugger to inspect Image prototype (#1534) +* Fix signed/unsigned comparison warning introduced in 2.6.0, and function cast warnings with GCC8+ +* Fix to compile without JPEG support (#1593). +* Fix compile errors with cairo +* Fix Image#complete if the image failed to load. +* Upgrade node-pre-gyp to v0.15.0 to use latest version of needle to fix error when downloading prebuilds. +* Don't throw if `fillStyle` or `strokeStyle` is set to an object, but that object is not a Gradient or Pattern. (This behavior was non-standard: invalid inputs are supposed to be ignored.) + +2.6.1 +================== +### Fixed +* Ignore `maxWidth` in `fillText` and `strokeText` if it is undefined +* Fix crash (assertion failure) in Node.js 12.x when patterns or gradients are used +* Fix crash (check failure) in Node.js 12.x when using RGB16_565 format. (The + underlying arraybuffer was incorrectly sized.) +* Fix rendering error when applying shadow width line style (lineCap lineJoin lineDash) + +2.6.0 +================== +### Changed +* Allow larger buffers to be returned from `toBuffer('raw')`. +### Added +* Support for various BMP headers and color depths (#1435) +### Fixed +* Fix crash when changing canvas width/height while `fillStyle` or `strokeStyle` + was set to a `CanvasPattern` or `CanvasGradient` (#1357). +* Fix crash when changing width/height of SVG canvases (#1380). +* Fix crash when using `toBuffer('raw')` with large canvases (#1158). +* Clarified meaning of byte ordering for `toBuffer('raw')` in readme. (#1416) +* Fix package.json Typings field to point to Declaration file (#1432) +* Properly check return value from `Set` and `Call`. (#1415) +* Use `Get` version from `Nan` instead of `v8`. (#1415) + +2.5.0 +================== +### Added +* Support redirects when fetching images (using [simple-get](https://github.com/feross/simple-get)) (#1398) +* Support Node.js v12 +### Fixed +* Fix object literal & arrow function syntax usage for IE. + +2.4.1 +================== +### Fixed +* Guard JPEG width/height against maximum supported (#1385) +* Fix electron 5 and node 12 compatibility +* Fix encoding options (quality) parameter in `canvas.toDataURL()` + +2.4.0 +================== +### Added +* (Actually) added `resolution` option for `canvas.toBuffer("image/png")` and + `canvas.createPNGStream()`. This was documented since 2.0.0 but not working. +* Add typescript definitions. +### Fixed +* PDF metadata (added in 2.3.0) wasn't being set with `canvas.createPDFStream()` +* Fix custom "inspect" function deprecation warnings (#1326) + +2.3.1 +================== +### Fixed +* Fix `canvas.toBuffer()` for JPEGs (#1350) + +2.3.0 +================== +### Added +* Add support for multiple PDF page sizes +* Add support for embedding document metadata in PDFs + +### Fixed +* Don't crash when font string is invalid (bug since 2.2.0) (#1328) +* Fix memory leak in `canvas.toBuffer()` (#1202, #1296) +* Fix memory leak in `ctx.font=` (#1202) + +2.2.0 +================== +### Added +* BMP support + +### Fixed +* Reset context on resurface (#1292) +* Support Jest test framework (#1311) + +2.1.0 +================== +### Added +* Warn when building with old, unsupported versions of cairo or libjpeg. + +2.0.0 +================== + +**Upgrading from 1.x** +```js +// (1) The Canvas constructor is no longer the default export from the module. +/* old: */ +const Canvas = require('canvas') +const mycanvas = new Canvas(width, height) +/* new: */ +const { createCanvas, Canvas } = require('canvas') +const mycanvas = createCanvas(width, height) +mycanvas instanceof Canvas // true + +/* old: */ +const Canvas = require('canvas') +const myimg = new Canvas.Image() +/* new: */ +const { Image } = require('canvas') +const myimg = new Image() + +// (2) The quality argument for canvas.createJPEGStream/canvas.jpegStream now +// goes from 0 to 1 instead of from 0 to 100: +canvas.createJPEGStream({ quality: 50 }) // old +canvas.createJPEGStream({ quality: 0.5 }) // new + +// (3) The ZLIB compression level and PNG filter options for canvas.toBuffer are +// now named instead of positional arguments: +canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE) // old +canvas.toBuffer(undefined, { compressionLevel: 3, filters: canvas.PNG_FILTER_NONE }) // new +// or specify the mime type explicitly: +canvas.toBuffer('image/png', { compressionLevel: 3, filters: canvas.PNG_FILTER_NONE }) // new + +// (4) #2 also applies for canvas.pngStream, although these arguments were not +// documented: +canvas.pngStream(3, canvas.PNG_FILTER_NONE) // old +canvas.pngStream({ compressionLevel: 3, filters: canvas.PNG_FILTER_NONE }) // new + +// (5) canvas.syncPNGStream() and canvas.syncJPEGStream() have been removed: +canvas.syncPNGStream() // old +canvas.createSyncPNGStream() // old +canvas.createPNGStream() // new + +canvas.syncJPEGStream() // old +canvas.createSyncJPEGStream() // old +canvas.createJPEGStream() // new + +// (6) Context2d.filter has been renamed to context2d.quality to avoid a +// conflict with the new standard 'filter' property. +context.filter = 'best' // old +context.quality = 'best' // new +``` + +### Breaking + * Drop support for Node.js <6.x + * Remove sync stream functions (bc53059). Note that most streams are still + synchronous (run in the main thread); this change just removed `syncPNGStream` + and `syncJPEGStream`. + * Pango is now *required* on all platforms (7716ae4). + * Make the `quality` argument for JPEG output go from 0 to 1 to match HTML spec. + * Make the `compressionLevel` and `filters` arguments for `canvas.toBuffer()` + named instead of positional. Same for `canvas.pngStream()`, although these + arguments were not documented. + * See also: *Correct some of the `globalCompositeOperator` types* under + **Fixed**. These changes were bug-fixes, but will break existing code relying + on the incorrect types. + * Rename `context2d.filter` to `context2d.quality` to avoid a conflict with the + new standard 'filter' property. Note that the standard 'filter' property is + not yet implemented. + +### Fixed + * Fix build with SVG support enabled (#1123) + * Prevent segfaults caused by loading invalid fonts (#1105) + * Fix memory leak in font loading + * Port has_lib.sh to javascript (#872) + * Correctly sample the edge of images when scaling (#1084) + * Detect CentOS libjpeg path (b180ea5) + * Improve measureText accuracy (2bbfec5) + * Fix memory leak when image callbacks reference the image (1f4b646) + * Fix putImageData(data, negative, negative) (2102e25) + * Fix SVG recognition when loading from buffer (77749e6) + * Re-rasterize SVG when drawing to a context and dimensions changed (79bf232) + * Prevent JPEG errors from crashing process (#1124) + * Improve handling of invalid arguments (#1129) + * Fix repeating patterns when drawing a canvas to itself (#1136) + * Prevent segfaults caused by creating a too large canvas + * Fix parse-font regex to allow for whitespaces. + * Allow assigning non-string values to fillStyle and strokeStyle + * Fix drawing zero-width and zero-height images. + * Fix DEP0005 deprecation warning + * Don't assume `data:` URIs assigned to `img.src` are always base64-encoded + * Fix formatting of color strings (e.g. `ctx.fillStyle`) on 32-bit platforms + * Explicitly export symbols for the C++ API + * Named CSS colors should match case-insensitive + * Correct some of the `globalCompositeOperator` types to match the spec: + * "hsl-hue" is now "hue" + * "hsl-saturation" is now "saturation" + * "hsl-color" is now "color" + * "hsl-luminosity" is now "luminosity" + * "darker" is now "darken" + * "dest" is now "destination" + * "add" is removed (but is the same as "lighter") + * "source" is now "copy" + * Provide better, Node.js core-style coded errors for failed sys calls. (For + example, provide an error with code 'ENOENT' if setting `img.src` to a path + that does not exist.) + * Support reading CMYK, YCCK JPEGs. + * Hide `Image.prototype.source` + * Fix behavior of maxWidth (#1088) + * Fix behavior of textAlignment with maxWidth (#1253) + +### Added + * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) + * Support `canvas.getContext("2d", {alpha: boolean})` and + `canvas.getContext("2d", {pixelFormat: "..."})` + * Support indexed PNG encoding. + * Support `currentTransform` (d6714ee) + * Export `CanvasGradient` (6a4c0ab) + * Support #RGBA , #RRGGBBAA hex colors (10a82ec) + * Support maxWidth arg for fill/strokeText (175b40d) + * Support image.naturalWidth/Height (a5915f8) + * Render SVG img elements when librsvg is available (1baf00e) + * Support ellipse method (4d4a726) + * Browser-compatible API (6a29a23) + * Support for jpeg on Windows (42e9a74) + * Support for backends (1a6dffe) + * Support for `canvas.toBuffer("image/jpeg")` + * Unified configuration options for `canvas.toBuffer()`, `canvas.pngStream()` + and `canvas.jpegStream()` + * ~~Added `resolution` option for `canvas.toBuffer("image/png")` and + `canvas.createPNGStream()`~~ this was not working + * Support for `canvas.toDataURI("image/jpeg")` (sync) + * Support for `img.src = ` to match browsers + * Support reading data URL on `img.src` + * Readme: add dependencies command for OpenBSD + * Throw error if calling jpegStream when canvas was not built with JPEG support + * Emit error if trying to load GIF, SVG or JPEG image when canvas was not built + with support for that format + +1.6.x (unreleased) +================== +### Fixed + * Make setLineDash able to handle full zeroed dashes (b8cf1d7) + * Fix reading fillStyle after setting it from gradient to color (a84b2bc) + +### Added + * Support for pattern repeat and no-repeat (#1066) + * Support for context globalAlpha for gradients and patterns (#1064) + +1.6.9 / 2017-12-20 +================== +### Fixed + * Fix some instances of crashes (7c9ec58, 8b792c3) + * Fix node 0.x compatibility (dca33f7) + +1.6.8 / 2017-12-12 +================== +### Fixed + * Faster, more compliant parseFont (4625efa, 37cd969) + +1.6.7 / 2017-09-08 +================== +### Fixed + * Minimal backport of #985 (rotated text baselines) (c19edb8) + +1.6.6 / 2017-05-03 +================== +### Fixed + * Use .node extension for requiring native module so webpack works (1b05599) + * Correct text baseline calculation (#1037) + +1.6.5 / 2017-03-18 +================== +### Changed + * Parse font using parse-css-font and units-css (d316416) + +1.6.4 / 2017-02-26 +================== +### Fixed + * Make sure Canvas#toDataURL is always async if callback is passed (8586d72) + +1.6.3 / 2017-02-14 +================== +### Fixed + * Fix isnan() and isinf() on clang (5941e13) + +1.6.2 / 2016-10-30 +================== +### Fixed + * Fix deprecation warnings (c264879) + * Bump nan (e4aea20) + +1.6.1 / 2016-10-23 +================== + +### Fixed + * Make has_lib.sh work on BSD OSes (1727d66) + +1.6.0 / 2016-10-16 +================== + + * Support canvas.getBuffer('raw') (#819) + +1.5.0 / 2016-09-11 +================== + + * Crude PDF stream implementation (#781) + * Update CI settings (#797) + * Reduce some of the install warnings (#794) + * Fix lineDash browser tests never finishing (#793) + * Add issue template (#791) + +1.4.0 / 2016-06-03 +================== + + * Add support for evenodd fill rule (#762) + +1.3.17 / 2016-06-03 +=================== + + * Removing redundant duplicate calls (#769) + * Cleanup examples (#776) + * Fix CanvasRenderingContext2D class name (#777) + +1.3.16 / 2016-05-29 +=================== + + * Fix leak of data when streaming JPEG (#774) + +1.3.15 / 2016-05-09 +=================== + + * Fix segfault in putImageData (#750) + +1.3.14 / 2016-05-05 +=================== + + * Clamp JPEG buffer size (#739) + +1.3.13 / 2016-05-01 +=================== + + * Bumb NAN version (#759) + +1.3.12 / 2016-03-01 +=================== + + * Expose freetype version (#718) + * Require new in constructor (#717) + +1.3.11 / 2016-03-01 +=================== + + * Properly clamp quality in toDataURL (#728) + * Strict mode (#719) + +1.3.10 / 2016-02-07 +=================== + + * Fix segfault on node 0.10.x (#712) + +1.3.9 / 2016-01-27 +================== + + * Allow to unbind onload/onerror callback handlers (#706) + +1.3.8 / 2016-01-22 +================== + + * Cleanup build scripts and fix pangocairo detection (#701) + +1.3.7 / 2016-01-13 +================== + + * Don't unbind onload/onerror callbacks after invoking them (#615) + +1.3.6 / 2016-01-06 +================== + + * Allow optional arguments in `toDataURL` to be `undefined` and improve `toDataURL`'s spec compliance (#690) + +1.3.5 / 2015-12-07 +================== + + * Add image/jpeg support to `toDataUrl` (#685) + +1.3.4 / 2015-11-21 +================== + + * Upgrade nan to 2.1.0 (#671) + +1.3.3 / 2015-11-21 +================== + + * Fix compilation on Visual Studio 2015 (#670) + +1.3.2 / 2015-11-18 +================== + + * Fix incorrect Y offset and scaling for shadows (#669) + +1.3.1 / 2015-11-09 +================== + + * Wrap std::min calls in paranthesis to prevent macro expansion on windows (#660) + +1.3.0 / 2015-10-26 +================== + + * Expose ImageData constructor and make it more spec-compliant (#569) + +1.2.11 / 2015-10-20 +=================== + + * Implement blur on images (#648) + +1.2.10 / 2015-10-12 +=================== + + * Fix segfault in Canvas#jpegStream (#629) + +1.2.9 / 2015-09-14 +================== + + * Upgrade to Nan 2.x with support for iojs 3.x and Node.js 4.x (#622) + +1.2.8 / 2015-08-30 +================== + + * Clean up the tests (#612) + * Replace CanvasPixelArray with Uint8ClampedArray to be API-compliant (#604) + * Specify travis iojs versions (#611) + +1.2.7 / 2015-07-29 +================== + + * Avoid future reserved keyword (#592) + +1.2.6 / 2015-07-29 +================== + + * Fix the build on windows (#589) + +1.2.5 / 2015-07-28 +================== + + * Another npm release, since 1.2.4 was botched (see #596) + +1.2.4 / 2015-07-23 +================== + + * Point `homepage` and `repository` links to [`github.com/Automattic/node-canvas`][repo] + * Fix Travis builds and Cairo include paths (thanks, Linus Unnebäck!) + +1.2.3 / 2015-05-21 +================== + + * Update TJ Holowaychuk's username in the readme + * Fix segmentation fault in `Image::loadFromBuffer` when buffer is empty + * Optimize getImageData() + * package: add "license" attribute + * package: update "nan" to v1.8.4 + * package: append `.git` to "repository" URL + +1.2.2 / 2015-04-18 +================== + + * Now works on io.js + * Fix 'drawImage' scaling (the dimensions of the region that gets clipped also needs to be scaled). + * Fix bug in StreamPNGSync + +1.2.1 / 2015-02-10 +================== + + * Use non-cairo 1.12 API for shadow blur + +1.2.0 / 2015-01-31 +================== + + * travis: drop support for node v0.6 + * Merge pull request #507 from salzhrani/iojs + * io.js compatibility + * Merge pull request #505 from woodcoder/shadow-blur + * Fix issue with line width not being correct in stroked shadows. + * Add another shadow/transform test. + * Refactor setSourceRGBA to allow the context to be supplied. + * Simple image shadow (no blurring or handling current transforms) based on image's alpha channel. + * Test showing issue #133, that images don't have shadows. + * The +1 on the offset seems to match the browser's output better, but I can't work out why it would be needed (unless it's pixel alignment related). + * Make the shadow radius more accurately match the browser's, making use of sigma scale as used in SKIA: https://github.com/google/skia/blob/master/src/effects/SkBlurMask.cpp#L26. + * Create a new image surface to render blurred shadows to, this means that vector formats like PDF will now render blurs. + * Add recommended calls to flush and dirty buffer, as per http://www.cairographics.org/manual/cairo-Image-Surfaces.html#cairo-image-surface-get-data. + * Add PDF button to test page to easily generate PDF version of the test image. + * Fix to ensure shadowOffset is unaffected by the current transform. + * New test illustrating that canvas implementation doesn't translate the shadowOffset. + * Merge pull request #490 from AllYearbooks/master + * Merge pull request #501 from motiz88/hsl-color + * Code style + attribution. Also removed parseClipped() and commented out wrapInt (now wrap_int). + * Added visual tests for hsl() and hsla() color parsing. + * Fixed handling in hsl/hsla color parser. parseNumber() was erroring out on numbers with long fractional parts. + * hsl/hsla color parsing + rebeccapurple hsl() and hsla() color values are now supported, with corresponding unit tests. Also added rebeccapurple (from CSS Color Level 4) to the named color list. + * float rather than int for drawImage arguments + * with_pango to true and use fontconfig to load fonts + * Merge pull request #399 from nulltask/fix/lighten + * Merge pull request #465 from espadrine/master + * Merge pull request #470 from tonylukasavage/patch-1 + * Add one-liner MacPorts install to docs + * Offer SVG output. + * Readme update: node-gyp. + * Readme: fix subheading size + * Readme: remove Gemnasium badge, use SVG for npm badge + * Readme: add Travis-CI badge + * change operator lighter to lighten + +1.1.6 / 2014-08-01 +================== + + * export canvas.CanvasPixelArray instead of canvas.PixelArray which is undefined + * Glib version test into giflib exists test + * Giflib 5.1 + * install: use an even older version of giflib (v4.1.6) + * install: use an older version of giflib (v4.2.3) + * install: install `giflib` + * install: use more compatible sh syntax + * travis: attempt to run the ./install script before testintg + * travis: test node v0.6, v0.8, v0.10, and v0.11 + * Distinguish between 'add' and 'lighter' + +1.1.5 / 2014-06-26 +================== + + * Readme: remove Contributors section + * Readme: update copyright + * On Windows, copy required DLLs next to ".node" file (#442 @pandell) + * Duplicate "msvc_settings" for "Debug" configuration + * Remove unneeded #include + * Use float constants to prevent double->float conversion warning + * Ignore Visual C++ 2013 warnings (#441 @pandell) + * Add algorithm include to CanvasRenderingContext2d.cc for std::min (#435 @kkoopa) + * Updated NAN to 1.2.0 (#434 @kkoopa) + +1.1.4 / 2014-06-08 +================== + + * Fix compile error with Visual C++ + * Add support for the lineDash API + * Update NAN + * New V8 compatibility + * Correctly limit bounds in PutImageData to prevent segment fault + * Fix segfault when onload and onerror are not function + * Add support for Node 0.11.9 + +1.1.3 / 2014-01-08 +================== + + * Add CAIRO_FORMAT_INVALID + * Readjust the amount of allocated memory + * Fix argument index for filter parameter + * Make has_lib.sh work properly on Debian 64bit + +1.1.2 / 2013-10-31 +================== + + * NAN dep upgrade, full node@<=0.11.8 compatibility + * Use node::MakeCallback() instead of v8::Function::Call() + * Improve nan location discovery + * Fix enabling gif/jpeg options on Ubuntu 13.04 + +1.1.1 / 2013-10-09 +================== + + * add better support for outdated versions of Cairo + +1.1.0 / 2013-08-01 +================== + + * add png compression options + * add jpeg stream progressive mode option + * fix resource leaks on read errors + +1.0.4 / 2013-07-23 +================== + + * 0.11.4+ compatibility using NAN + * fix typo in context2d for imageSmoothingEnabled + +1.0.3 / 2013-06-04 +================== + + * add "nearest" and "bilinear" to patternQuality + * fix fread() retval check (items not bytes) + * removed unneeded private fields + +1.0.2 / 2013-03-22 +================== + + * add Context2d#imageSmoothingEnabled= + +1.0.1 / 2013-02-25 +================== + + * travis: test modern node versions + * change the node-gyp build to use pkg-config + +1.0.0 / 2013-01-16 +================== + + * add conditional pango font support [Julian Viereck] + * add `Canvas#{png,jpeg}Stream()` alias of create* legacy methods + * add support for grayscale JPEGs + * fix: explicitly cast the after work callback function to "uv_after_work_cb" + * fix test server for express 3.x + * fix: call cairo_surface_finish in ~Canvas when pdf + * remove old 0.4.x binding support. Closes #197 + +0.13.1 / 2012-08-20 +================== + + * fix cases where GIF_LIB_VERSION is not defined + * fix auto-detection of optional libraries for OS X + * fix Context2d::SetFont for pango when setting normal weight/style + +0.13.0 / 2012-08-12 +================== + + * add pango support [c-spencer] + * add pango / png / jpeg gyp auto-detection [c-spencer] + * add `.gifVersion` [tootallnate] + * add `.jpegVersion` [tootallnate] + * add moar gyp stuff [tootallnate] + * remove wscript + * fix `closure_destroy()` with cast for `AdjustAmountOfExternalAllocatedMemory()` + +0.12.1 / 2012-06-29 +================== + + * fix jpeg malloc Image issue. Closes #160 [c-spencer] + * Improve Image mode API + * Add clearData method to handle reassignment of src, and clean up mime data memory handling. + * Improve how _data_len is managed and use to adjust memory, hide more of mime API behind cairo version conditional. + * Add optional mime-data tracking to Image. + * Refactor JPEG decoding into decodeJPEGIntoSurface + +0.12.0 / 2012-05-02 +================== + + * Added `textDrawingMode` context property [c-spencer] + * Added additional TextMetrics properties [c-spencer] + +0.11.3 / 2012-04-25 +================== + + * Fixed `Image` memory leak. Closes #150 + * Fixed Context2d::hasShadow() + +0.11.2 / 2012-04-12 +================== + + * Fixed: pdf memory leak, free closure and surface in ~Canvas + +0.11.1 / 2012-04-10 +================== + + * Changed: renamed .nextPage() to .addPage() + +0.11.0 / 2012-04-10 +================== + + * Added quick PDF support + * Added `Canvas#type` getter + * Added ./examples/pdf-images.js + * Added ./examples/multiple-page-pdf.js + * Added ./examples/small-pdf.js + +0.10.3 / 2012-02-27 +================== + + * Fixed quadratic curve starting point for undefined path. Closes #155 + +0.10.2 / 2012-02-06 +================== + + * Fixed: Context2d setters with invalid values ignored + * Changed: replaced seek with `fstat()` + +0.10.1 / 2012-01-31 +================== + + * Added _/opt/local/lib_ to wscript [obarthel] + * Added bounds checking to `rgba_to_string()` [obarthel] + * Fixed cleanup in JPEG Image loading [obarthel] + * Fixed missing CSS color table values [obarthel] + +0.10.0 / 2012-01-18 +================== + + * Added `ctx.createPattern()` [slaskis] + +0.9.0 / 2012-01-13 +================== + + * Added `createJPEGStream()` [Elijah Hamovitz] + +0.8.3 / 2012-01-04 +================== + + * Added support for libjpeg62-dev or libjpeg8-dev [wwlinx] + +0.8.2 / 2011-12-14 +================== + + * Fixed two memory leaks in context2d [Tharit] + * Fixed `make test-server` + +0.8.1 / 2011-10-31 +================== + + * Added 0.5.x support [TooTallNate] + * Fixed `measureText().width`. Closes #126 + +0.8.0 / 2011-10-28 +================== + + * Added data uri support. Closes #49 + +0.7.3 / 2011-09-14 +================== + + * Added better lineTo() / moveTo() exception messages + +0.7.2 / 2011-08-30 +================== + + * Changed: prefix some private methods with _ + +0.7.1 / 2011-08-25 +================== + + * Added better image format detection + * Added libpath options to waf configuration; this was necessary to correctly detect gif and jpeg support on FreeBSD + +0.7.0 / 2011-07-12 +================== + + * Added GIF support [Brian McKinney] + +0.6.0 / 2011-06-04 +================== + + * Added `Image#src=Buffer` support. Closes #91 + * Added `devDependencies` + * Added `source-atop` test + * Added _image-src.js_ example + * Removed `V8::AdjustAmountOfExternalAllocatedMemory()` call from `toBuffer()` + * Fixed v8 memory hint when resizing canvas [atomizer] + +0.5.4 / 2011-04-20 +================== + + * Added; special case of zero-width rectangle [atomizer] + * Fixed; do not clamp arguments to integer values [atomizer] + * Fixed; preserve current path during `fillRect()` and `strokeRect()` [atomizer] + * Fixed; `restorePath()`: clear current path before appending [atomizer] + +0.5.3 / 2011-04-11 +================== + + * Clamp image bounds in `PixelArray::PixelArray()` [Marcello Bastea-Forte] + +0.5.2 / 2011-04-09 +================== + + * Changed; make `PNGStream` a real `Stream` [Marcello Bastea-Forte] + +0.5.1 / 2011-03-16 +================== + + * Fixed (kinda) `img.src=` error handling + * Fixed; move closure.h down for malloc ref. Closes #80 + +0.5.0 / 2011-03-14 +================== + + * Added several more operators (color-dodge, color-burn, difference, etc) + * Performance; no longer re-allocating `closure->data` for each png write + * Fixed freeing of `Context2d` states + * Fixed text alignment / baseline [Olaf] + * Fixed HandleScopes [Olaf] + * Fixed small misc memory leaks + * Fixed `Buffer` usage for node 0.4.x + +0.4.3 / 2011-01-11 +================== + + * Fixed font family dereferencing. Closes #72 + * Fixed; stripping of quotes from font-family before applying + * Fixed duplicate textAlign getter + * Removed sans-serif default of _Arial_ + +0.4.2 / 2010-12-28 +================== + + * Fixed font size growing issue after successive calls. Closes #70 + +0.4.1 / 2010-12-18 +================== + + * Fixed; toString() first argument of `{fill,stroke}Text()`. Closes #68 + +0.4.0 / 2010-12-12 +================== + + * Added `drawImage()` with `Canvas` instance support. Closes #67 + +0.3.3 / 2010-11-30 +================== + + * Added `CanvasRenderingContext2d#patternQuality` accessor, accepting _fast_, _good_, and _best_ + * Fixed; pre-multiply `putImageData()` components + * Fixed; `PixelArray` data is not premultiplied + +0.3.2 / 2010-11-26 +================== + + * Added --profile option to config + * Fixed `eio_custom` segfault(s). Closes #46 + * Fixed two named colors. Closes #62 [thanks noonat] + * Fixed a few warnings + * Fixed; freeing data in `Image::loadJPEG()` on failure + * Fixed; include _jpeglib_ only when __HAVE_JPEG__ + * Fixed; using `strstr()` instead of `strnstr()` + +0.3.1 / 2010-11-24 +================== + + * Fixed; `Image` loading is sync until race-condition is resolved + * Fixed; `Image::loadJPEG()` return status based on errno + +0.3.0 / 2010-11-24 +================== + + * Added arcTo(). Closes #11 + * Added c color parser, _./examples/ray.js_ is now twice as fast + * Fixed `putImageData()` bug messing up rgba channels + +0.2.1 / 2010-11-19 +================== + + * Added image _resize_ example + * Fixed canvas resizing via `{width,height}=`. Closes #57 + * Fixed `Canvas#getContext()`, caching the CanvasRenderingContext + * Fixed async image loading (test server still messed) + +0.2.0 / 2010-11-18 +================== + + * Added jpeg `Image` support (when libjpeg is available) + * Added _hsl_ / _hsla_ color support. [Tom Carden] + +0.1.0 / 2010-11-17 +================== + + * Added `Image` + * Added `ImageData` + * Added `PixelArray` + * Added `CanvasRenderingContext2d#drawImage()` + * Added `CanvasRenderingContext2d#getImageData()` + * Added `CanvasRenderingContext2d#createImageData()` + * Added kraken blur benchmark example + * Added several new tests + * Fixed instanceof checks for many c++ methods + * Fixed test runner in firefox [Don Park] + +0.0.8 / 2010-11-12 +================== + + * Added `CanvasRenderingContext2d#drawImage()` + * Fixed `free()` call missing stdlib + * Fixed Image#{width,height} initialization to 0 + * Fixed; load image on non-LOADING state + +0.0.7 / 2010-11-12 +================== + + * Fixed _lighter_ for older versions of cairo + +0.0.6 / 2010-11-12 +================== + + * Added `Image` + * Added conditional support for cairo 1.10.0 operators + +0.0.5 / 2010-11-10 +================== + + * Added custom port support to _test/server.js_ + * Added more global composite operator support + * Added `Context2d#antialias=` + * Added _voronoi_ example + * Added -D__NDEBUG__ to default build + * Added __BUFFER_DATA__ macro for backwards compat buffer data access [Don Park] + * Fixed getter bug preventing patterns from being returned via `fillStyle` etc + + * Fixed; __CAIRO_STATUS_NO_MEMORY___ on failed {re,m}alloc() + * Fixed; free `Canvas::ToBuffer()` closure data + +0.0.4 / 2010-11-09 +================== + + * Bump to fix npm engine cache bug... + +0.0.3 / 2010-11-09 +================== + + * Added async `toDataURL()` support + * Added async `toBuffer()` support + * Removed buffer utils + +0.0.2 / 2010-11-08 +================== + + * Added shadow support (faster/better gaussian blur to come) + * Added node v0.3 support [Don Park] + * Added -O3 to build + * Removed `Canvas#savePNG()` use `Canvas#createPNGStream()` + +0.0.1 / 2010-11-04 +================== + + * Initial release + +[repo]: https://github.com/Automattic/node-canvas diff --git a/History.md b/History.md deleted file mode 100644 index 70c707d72..000000000 --- a/History.md +++ /dev/null @@ -1,634 +0,0 @@ -1.6.0 / 2016-10-16 -================== - - * Support canvas.getBuffer('raw') (#819) - -1.5.0 / 2016-09-11 -================== - - * Crude PDF stream implementation (#781) - * Update CI settings (#797) - * Reduce some of the install warnings (#794) - * Fix lineDash browser tests never finishing (#793) - * Add issue template (#791) - -1.4.0 / 2016-06-03 -================== - - * Add support for evenodd fill rule (#762) - -1.3.17 / 2016-06-03 -=================== - - * Removing redundant duplicate calls (#769) - * Cleanup examples (#776) - * Fix CanvasRenderingContext2D class name (#777) - -1.3.16 / 2016-05-29 -=================== - - * Fix leak of data when streaming JPEG (#774) - -1.3.15 / 2016-05-09 -=================== - - * Fix segfault in putImageData (#750) - -1.3.14 / 2016-05-05 -=================== - - * Clamp JPEG buffer size (#739) - -1.3.13 / 2016-05-01 -=================== - - * Bumb NAN version (#759) - -1.3.12 / 2016-03-01 -=================== - - * Expose freetype version (#718) - * Require new in constructor (#717) - -1.3.11 / 2016-03-01 -=================== - - * Properly clamp quality in toDataURL (#728) - * Strict mode (#719) - -1.3.10 / 2016-02-07 -=================== - - * Fix segfault on node 0.10.x (#712) - -1.3.9 / 2016-01-27 -================== - - * Allow to unbind onload/onerror callback handlers (#706) - -1.3.8 / 2016-01-22 -================== - - * Cleanup build scripts and fix pangocairo detection (#701) - -1.3.7 / 2016-01-13 -================== - - * Don't unbind onload/onerror callbacks after invoking them (#615) - -1.3.6 / 2016-01-06 -================== - - * Allow optional arguments in `toDataURL` to be `undefined` and improve `toDataURL`'s spec compliance (#690) - -1.3.5 / 2015-12-07 -================== - - * Add image/jpeg support to `toDataUrl` (#685) - -1.3.4 / 2015-11-21 -================== - - * Upgrade nan to 2.1.0 (#671) - -1.3.3 / 2015-11-21 -================== - - * Fix compilation on Visual Studio 2015 (#670) - -1.3.2 / 2015-11-18 -================== - - * Fix incorrect Y offset and scaling for shadows (#669) - -1.3.1 / 2015-11-09 -================== - - * Wrap std::min calls in paranthesis to prevent macro expansion on windows (#660) - -1.3.0 / 2015-10-26 -================== - - * Expose ImageData constructor and make it more spec-compliant (#569) - -1.2.11 / 2015-10-20 -=================== - - * Implement blur on images (#648) - -1.2.10 / 2015-10-12 -=================== - - * Fix segfault in Canvas#jpegStream (#629) - -1.2.9 / 2015-09-14 -================== - - * Upgrade to Nan 2.x with support for iojs 3.x and Node.js 4.x (#622) - -1.2.8 / 2015-08-30 -================== - - * Clean up the tests (#612) - * Replace CanvasPixelArray with Uint8ClampedArray to be API-compliant (#604) - * Specify travis iojs versions (#611) - -1.2.7 / 2015-07-29 -================== - - * Avoid future reserved keyword (#592) - -1.2.6 / 2015-07-29 -================== - - * Fix the build on windows (#589) - -1.2.5 / 2015-07-28 -================== - - * Another npm release, since 1.2.4 was botched (see #596) - -1.2.4 / 2015-07-23 -================== - - * Point `homepage` and `repository` links to [`github.com/Automattic/node-canvas`][repo] - * Fix Travis builds and Cairo include paths (thanks, Linus Unnebäck!) - -1.2.3 / 2015-05-21 -================== - - * Update TJ Holowaychuk's username in the readme - * Fix segmentation fault in `Image::loadFromBuffer` when buffer is empty - * Optimize getImageData() - * package: add "license" attribute - * package: update "nan" to v1.8.4 - * package: append `.git` to "repository" URL - -1.2.2 / 2015-04-18 -================== - - * Now works on io.js - * Fix 'drawImage' scaling (the dimensions of the region that gets clipped also needs to be scaled). - * Fix bug in StreamPNGSync - -1.2.1 / 2015-02-10 -================== - - * Use non-cairo 1.12 API for shadow blur - -1.2.0 / 2015-01-31 -================== - - * travis: drop support for node v0.6 - * Merge pull request #507 from salzhrani/iojs - * io.js compatibility - * Merge pull request #505 from woodcoder/shadow-blur - * Fix issue with line width not being correct in stroked shadows. - * Add another shadow/transform test. - * Refactor setSourceRGBA to allow the context to be supplied. - * Simple image shadow (no blurring or handling current transforms) based on image's alpha channel. - * Test showing issue #133, that images don't have shadows. - * The +1 on the offset seems to match the browser's output better, but I can't work out why it would be needed (unless it's pixel alignment related). - * Make the shadow radius more accurately match the browser's, making use of sigma scale as used in SKIA: https://github.com/google/skia/blob/master/src/effects/SkBlurMask.cpp#L26. - * Create a new image surface to render blurred shadows to, this means that vector formats like PDF will now render blurs. - * Add recommended calls to flush and dirty buffer, as per http://www.cairographics.org/manual/cairo-Image-Surfaces.html#cairo-image-surface-get-data. - * Add PDF button to test page to easily generate PDF version of the test image. - * Fix to ensure shadowOffset is unaffected by the current transform. - * New test illustrating that canvas implementation doesn't translate the shadowOffset. - * Merge pull request #490 from AllYearbooks/master - * Merge pull request #501 from motiz88/hsl-color - * Code style + attribution. Also removed parseClipped() and commented out wrapInt (now wrap_int). - * Added visual tests for hsl() and hsla() color parsing. - * Fixed handling in hsl/hsla color parser. parseNumber() was erroring out on numbers with long fractional parts. - * hsl/hsla color parsing + rebeccapurple hsl() and hsla() color values are now supported, with corresponding unit tests. Also added rebeccapurple (from CSS Color Level 4) to the named color list. - * float rather than int for drawImage arguments - * with_pango to true and use fontconfig to load fonts - * Merge pull request #399 from nulltask/fix/lighten - * Merge pull request #465 from espadrine/master - * Merge pull request #470 from tonylukasavage/patch-1 - * Add one-liner MacPorts install to docs - * Offer SVG output. - * Readme update: node-gyp. - * Readme: fix subheading size - * Readme: remove Gemnasium badge, use SVG for npm badge - * Readme: add Travis-CI badge - * change operator lighter to lighten - -1.1.6 / 2014-08-01 -================== - - * export canvas.CanvasPixelArray instead of canvas.PixelArray which is undefined - * Glib version test into giflib exists test - * Giflib 5.1 - * install: use an even older version of giflib (v4.1.6) - * install: use an older version of giflib (v4.2.3) - * install: install `giflib` - * install: use more compatible sh syntax - * travis: attempt to run the ./install script before testintg - * travis: test node v0.6, v0.8, v0.10, and v0.11 - * Distinguish between 'add' and 'lighter' - -1.1.5 / 2014-06-26 -================== - - * Readme: remove Contributors section - * Readme: update copyright - * On Windows, copy required DLLs next to ".node" file (#442 @pandell) - * Duplicate "msvc_settings" for "Debug" configuration - * Remove unneeded #include - * Use float constants to prevent double->float conversion warning - * Ignore Visual C++ 2013 warnings (#441 @pandell) - * Add algorithm include to CanvasRenderingContext2d.cc for std::min (#435 @kkoopa) - * Updated NAN to 1.2.0 (#434 @kkoopa) - -1.1.4 / 2014-06-08 -================== - - * Fix compile error with Visual C++ - * Add support for the lineDash API - * Update NAN - * New V8 compatibility - * Correctly limit bounds in PutImageData to prevent segment fault - * Fix segfault when onload and onerror are not function - * Add support for Node 0.11.9 - -1.1.3 / 2014-01-08 -================== - - * Add CAIRO_FORMAT_INVALID - * Readjust the amount of allocated memory - * Fix argument index for filter parameter - * Make has_lib.sh work properly on Debian 64bit - -1.1.2 / 2013-10-31 -================== - - * NAN dep upgrade, full node@<=0.11.8 compatibility - * Use node::MakeCallback() instead of v8::Function::Call() - * Improve nan location discovery - * Fix enabling gif/jpeg options on Ubuntu 13.04 - -1.1.1 / 2013-10-09 -================== - - * add better support for outdated versions of Cairo - -1.1.0 / 2013-08-01 -================== - - * add png compression options - * add jpeg stream progressive mode option - * fix resource leaks on read errors - -1.0.4 / 2013-07-23 -================== - - * 0.11.4+ compatibility using NAN - * fix typo in context2d for imageSmoothingEnabled - -1.0.3 / 2013-06-04 -================== - - * add "nearest" and "bilinear" to patternQuality - * fix fread() retval check (items not bytes) - * removed unneeded private fields - -1.0.2 / 2013-03-22 -================== - - * add Context2d#imageSmoothingEnabled= - -1.0.1 / 2013-02-25 -================== - - * travis: test modern node versions - * change the node-gyp build to use pkg-config - -1.0.0 / 2013-01-16 -================== - - * add conditional pango font support [Julian Viereck] - * add `Canvas#{png,jpeg}Stream()` alias of create* legacy methods - * add support for grayscale JPEGs - * fix: explicitly cast the after work callback function to "uv_after_work_cb" - * fix test server for express 3.x - * fix: call cairo_surface_finish in ~Canvas when pdf - * remove old 0.4.x binding support. Closes #197 - -0.13.1 / 2012-08-20 -================== - - * fix cases where GIF_LIB_VERSION is not defined - * fix auto-detection of optional libraries for OS X - * fix Context2d::SetFont for pango when setting normal weight/style - -0.13.0 / 2012-08-12 -================== - - * add pango support [c-spencer] - * add pango / png / jpeg gyp auto-detection [c-spencer] - * add `.gifVersion` [tootallnate] - * add `.jpegVersion` [tootallnate] - * add moar gyp stuff [tootallnate] - * remove wscript - * fix `closure_destroy()` with cast for `AdjustAmountOfExternalAllocatedMemory()` - -0.12.1 / 2012-06-29 -================== - - * fix jpeg malloc Image issue. Closes #160 [c-spencer] - * Improve Image mode API - * Add clearData method to handle reassignment of src, and clean up mime data memory handling. - * Improve how _data_len is managed and use to adjust memory, hide more of mime API behind cairo version conditional. - * Add optional mime-data tracking to Image. - * Refactor JPEG decoding into decodeJPEGIntoSurface - -0.12.0 / 2012-05-02 -================== - - * Added `textDrawingMode` context property [c-spencer] - * Added additional TextMetrics properties [c-spencer] - -0.11.3 / 2012-04-25 -================== - - * Fixed `Image` memory leak. Closes #150 - * Fixed Context2d::hasShadow() - -0.11.2 / 2012-04-12 -================== - - * Fixed: pdf memory leak, free closure and surface in ~Canvas - -0.11.1 / 2012-04-10 -================== - - * Changed: renamed .nextPage() to .addPage() - -0.11.0 / 2012-04-10 -================== - - * Added quick PDF support - * Added `Canvas#type` getter - * Added ./examples/pdf-images.js - * Added ./examples/multiple-page-pdf.js - * Added ./examples/small-pdf.js - -0.10.3 / 2012-02-27 -================== - - * Fixed quadratic curve starting point for undefined path. Closes #155 - -0.10.2 / 2012-02-06 -================== - - * Fixed: Context2d setters with invalid values ignored - * Changed: replaced seek with `fstat()` - -0.10.1 / 2012-01-31 -================== - - * Added _/opt/local/lib_ to wscript [obarthel] - * Added bounds checking to `rgba_to_string()` [obarthel] - * Fixed cleanup in JPEG Image loading [obarthel] - * Fixed missing CSS color table values [obarthel] - -0.10.0 / 2012-01-18 -================== - - * Added `ctx.createPattern()` [slaskis] - -0.9.0 / 2012-01-13 -================== - - * Added `createJPEGStream()` [Elijah Hamovitz] - -0.8.3 / 2012-01-04 -================== - - * Added support for libjpeg62-dev or libjpeg8-dev [wwlinx] - -0.8.2 / 2011-12-14 -================== - - * Fixed two memory leaks in context2d [Tharit] - * Fixed `make test-server` - -0.8.1 / 2011-10-31 -================== - - * Added 0.5.x support [TooTallNate] - * Fixed `measureText().width`. Closes #126 - -0.8.0 / 2011-10-28 -================== - - * Added data uri support. Closes #49 - -0.7.3 / 2011-09-14 -================== - - * Added better lineTo() / moveTo() exception messages - -0.7.2 / 2011-08-30 -================== - - * Changed: prefix some private methods with _ - -0.7.1 / 2011-08-25 -================== - - * Added better image format detection - * Added libpath options to waf configuration; this was necessary to correctly detect gif and jpeg support on FreeBSD - -0.7.0 / 2011-07-12 -================== - - * Added GIF support [Brian McKinney] - -0.6.0 / 2011-06-04 -================== - - * Added `Image#src=Buffer` support. Closes #91 - * Added `devDependencies` - * Added `source-atop` test - * Added _image-src.js_ example - * Removed `V8::AdjustAmountOfExternalAllocatedMemory()` call from `toBuffer()` - * Fixed v8 memory hint when resizing canvas [atomizer] - -0.5.4 / 2011-04-20 -================== - - * Added; special case of zero-width rectangle [atomizer] - * Fixed; do not clamp arguments to integer values [atomizer] - * Fixed; preserve current path during `fillRect()` and `strokeRect()` [atomizer] - * Fixed; `restorePath()`: clear current path before appending [atomizer] - -0.5.3 / 2011-04-11 -================== - - * Clamp image bounds in `PixelArray::PixelArray()` [Marcello Bastea-Forte] - -0.5.2 / 2011-04-09 -================== - - * Changed; make `PNGStream` a real `Stream` [Marcello Bastea-Forte] - -0.5.1 / 2011-03-16 -================== - - * Fixed (kinda) `img.src=` error handling - * Fixed; move closure.h down for malloc ref. Closes #80 - -0.5.0 / 2011-03-14 -================== - - * Added several more operators (color-dodge, color-burn, difference, etc) - * Performance; no longer re-allocating `closure->data` for each png write - * Fixed freeing of `Context2d` states - * Fixed text alignment / baseline [Olaf] - * Fixed HandleScopes [Olaf] - * Fixed small misc memory leaks - * Fixed `Buffer` usage for node 0.4.x - -0.4.3 / 2011-01-11 -================== - - * Fixed font family dereferencing. Closes #72 - * Fixed; stripping of quotes from font-family before applying - * Fixed duplicate textAlign getter - * Removed sans-serif default of _Arial_ - -0.4.2 / 2010-12-28 -================== - - * Fixed font size growing issue after successive calls. Closes #70 - -0.4.1 / 2010-12-18 -================== - - * Fixed; toString() first argument of `{fill,stroke}Text()`. Closes #68 - -0.4.0 / 2010-12-12 -================== - - * Added `drawImage()` with `Canvas` instance support. Closes #67 - -0.3.3 / 2010-11-30 -================== - - * Added `CanvasRenderingContext2d#patternQuality` accessor, accepting _fast_, _good_, and _best_ - * Fixed; pre-multiply `putImageData()` components - * Fixed; `PixelArray` data is not premultiplied - -0.3.2 / 2010-11-26 -================== - - * Added --profile option to config - * Fixed `eio_custom` segfault(s). Closes #46 - * Fixed two named colors. Closes #62 [thanks noonat] - * Fixed a few warnings - * Fixed; freeing data in `Image::loadJPEG()` on failure - * Fixed; include _jpeglib_ only when __HAVE_JPEG__ - * Fixed; using `strstr()` instead of `strnstr()` - -0.3.1 / 2010-11-24 -================== - - * Fixed; `Image` loading is sync until race-condition is resolved - * Fixed; `Image::loadJPEG()` return status based on errno - -0.3.0 / 2010-11-24 -================== - - * Added arcTo(). Closes #11 - * Added c color parser, _./examples/ray.js_ is now twice as fast - * Fixed `putImageData()` bug messing up rgba channels - -0.2.1 / 2010-11-19 -================== - - * Added image _resize_ example - * Fixed canvas resizing via `{width,height}=`. Closes #57 - * Fixed `Canvas#getContext()`, caching the CanvasRenderingContext - * Fixed async image loading (test server still messed) - -0.2.0 / 2010-11-18 -================== - - * Added jpeg `Image` support (when libjpeg is available) - * Added _hsl_ / _hsla_ color support. [Tom Carden] - -0.1.0 / 2010-11-17 -================== - - * Added `Image` - * Added `ImageData` - * Added `PixelArray` - * Added `CanvasRenderingContext2d#drawImage()` - * Added `CanvasRenderingContext2d#getImageData()` - * Added `CanvasRenderingContext2d#createImageData()` - * Added kraken blur benchmark example - * Added several new tests - * Fixed instanceof checks for many c++ methods - * Fixed test runner in firefox [Don Park] - -0.0.8 / 2010-11-12 -================== - - * Added `CanvasRenderingContext2d#drawImage()` - * Fixed `free()` call missing stdlib - * Fixed Image#{width,height} initialization to 0 - * Fixed; load image on non-LOADING state - -0.0.7 / 2010-11-12 -================== - - * Fixed _lighter_ for older versions of cairo - -0.0.6 / 2010-11-12 -================== - - * Added `Image` - * Added conditional support for cairo 1.10.0 operators - -0.0.5 / 2010-11-10 -================== - - * Added custom port support to _test/server.js_ - * Added more global composite operator support - * Added `Context2d#antialias=` - * Added _voronoi_ example - * Added -D__NDEBUG__ to default build - * Added __BUFFER_DATA__ macro for backwards compat buffer data access [Don Park] - * Fixed getter bug preventing patterns from being returned via `fillStyle` etc - - * Fixed; __CAIRO_STATUS_NO_MEMORY___ on failed {re,m}alloc() - * Fixed; free `Canvas::ToBuffer()` closure data - -0.0.4 / 2010-11-09 -================== - - * Bump to fix npm engine cache bug... - -0.0.3 / 2010-11-09 -================== - - * Added async `toDataURL()` support - * Added async `toBuffer()` support - * Removed buffer utils - -0.0.2 / 2010-11-08 -================== - - * Added shadow support (faster/better gaussian blur to come) - * Added node v0.3 support [Don Park] - * Added -O3 to build - * Removed `Canvas#savePNG()` use `Canvas#createPNGStream()` - -0.0.1 / 2010-11-04 -================== - - * Initial release - -[repo]: https://github.com/Automattic/node-canvas diff --git a/Readme.md b/Readme.md index 1e679c6dd..d0429c520 100644 --- a/Readme.md +++ b/Readme.md @@ -1,25 +1,9 @@ # node-canvas ------ - -## This is the documentation for the unreleased version 2.0 - -**For the current version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x)** - ------ - -### Canvas graphics API backed by Cairo -[![Build Status](https://travis-ci.org/Automattic/node-canvas.svg?branch=master)](https://travis-ci.org/Automattic/node-canvas) +![Test](https://github.com/Automattic/node-canvas/workflows/Test/badge.svg) [![NPM version](https://badge.fury.io/js/canvas.svg)](http://badge.fury.io/js/canvas) - node-canvas is a [Cairo](http://cairographics.org/) backed Canvas implementation for [NodeJS](http://nodejs.org). - -## Authors - - - TJ Holowaychuk ([tj](http://github.com/tj)) - - Nathan Rajlich ([TooTallNate](http://github.com/TooTallNate)) - - Rod Vagg ([rvagg](http://github.com/rvagg)) - - Juriy Zaytsev ([kangax](http://github.com/kangax)) +node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation for [Node.js](http://nodejs.org). ## Installation @@ -27,342 +11,647 @@ $ npm install canvas ``` -Unless previously installed you'll _need_ __Cairo__ and __Pango__. For system-specific installation view the [Wiki](https://github.com/Automattic/node-canvas/wiki/_pages). +By default, pre-built binaries will be downloaded if you're on one of the following platforms: +- macOS x86/64 +- macOS aarch64 (aka Apple silicon) +- Linux x86/64 (glibc only) +- Windows x86/64 + +If you want to build from source, use `npm install --build-from-source` and see the **Compiling** section below. + +The minimum version of Node.js required is **18.12.0**. -Currently the minimum version of node required is __0.10.0__ +### Compiling -You can quickly install the dependencies by using the command for your OS: +If you don't have a supported OS or processor architecture, or you use `--build-from-source`, the module will be compiled on your system. This requires several dependencies, including Cairo and Pango. + +For detailed installation information, see the [wiki](https://github.com/Automattic/node-canvas/wiki/_pages). One-line installation instructions for common OSes are below. Note that libgif/giflib, librsvg and libjpeg are optional and only required if you need GIF, SVG and JPEG support, respectively. Cairo v1.10.0 or later is required. OS | Command ----- | ----- -OS X | `brew install pkg-config cairo pango libpng jpeg giflib` -Ubuntu | `sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++` -Fedora | `sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel` +macOS | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman python-setuptools` +Ubuntu | `sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev` +Fedora | `sudo yum install gcc-c++ cairo-devel pango-devel libjpeg-turbo-devel giflib-devel` Solaris | `pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto` -Windows | [Instructions on our wiki](https://github.com/Automattic/node-canvas/wiki/Installation---Windows) - -**El Capitan users:** If you have recently updated to El Capitan and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). +OpenBSD | `doas pkg_add cairo pango png jpeg giflib` +Windows | See the [wiki](https://github.com/Automattic/node-canvas/wiki/Installation:-Windows) +Others | See the [wiki](https://github.com/Automattic/node-canvas/wiki) -## Screencasts +**Mac OS X v10.11+:** If you have recently updated to Mac OS X v10.11+ and are experiencing trouble when compiling, run the following command: `xcode-select --install`. Read more about the problem [on Stack Overflow](http://stackoverflow.com/a/32929012/148072). +If you have xcode 10.0 or higher installed, in order to build from source you need NPM 6.4.1 or higher. - - [Introduction](http://screenr.com/CTk) - -## Example +## Quick Example ```javascript -var Canvas = require('canvas') - , Image = Canvas.Image - , canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); - -ctx.font = '30px Impact'; -ctx.rotate(.1); -ctx.fillText("Awesome!", 50, 100); - -var te = ctx.measureText('Awesome!'); -ctx.strokeStyle = 'rgba(0,0,0,0.5)'; -ctx.beginPath(); -ctx.lineTo(50, 102); -ctx.lineTo(50 + te.width, 102); -ctx.stroke(); - -console.log(''); +const { createCanvas, loadImage } = require('canvas') +const canvas = createCanvas(200, 200) +const ctx = canvas.getContext('2d') + +// Write "Awesome!" +ctx.font = '30px Impact' +ctx.rotate(0.1) +ctx.fillText('Awesome!', 50, 100) + +// Draw line under text +var text = ctx.measureText('Awesome!') +ctx.strokeStyle = 'rgba(0,0,0,0.5)' +ctx.beginPath() +ctx.lineTo(50, 102) +ctx.lineTo(50 + text.width, 102) +ctx.stroke() + +// Draw cat with lime helmet +loadImage('examples/images/lime-cat.jpg').then((image) => { + ctx.drawImage(image, 50, 0, 70, 70) + + console.log('') +}) ``` -## Non-Standard API +## Upgrading from 1.x to 2.x - node-canvas extends the canvas API to provide interfacing with node, for example streaming PNG data, converting to a `Buffer` instance, etc. Among the interfacing API, in some cases the drawing API has been extended for SSJS image manipulation / creation usage, however keep in mind these additions may fail to render properly within browsers. +See the [changelog](https://github.com/Automattic/node-canvas/blob/master/CHANGELOG.md) for a guide to upgrading from 1.x to 2.x. -### Image#src=Buffer +For version 1.x documentation, see [the v1.x branch](https://github.com/Automattic/node-canvas/tree/v1.x). - node-canvas adds `Image#src=Buffer` support, allowing you to read images from disc, redis, etc and apply them via `ctx.drawImage()`. Below we draw scaled down squid png by reading it from the disk with node's I/O. +## Documentation -```javascript -fs.readFile(__dirname + '/images/squid.png', function(err, squid){ - if (err) throw err; - img = new Image; - img.src = squid; - ctx.drawImage(img, 0, 0, img.width / 4, img.height / 4); -}); +This project is an implementation of the Web Canvas API and implements that API as closely as possible. For API documentation, please visit [Mozilla Web Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API). (See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status) for the current API compliance.) All utility methods and non-standard APIs are documented below. + +### Utility methods + +* [createCanvas()](#createcanvas) +* [createImageData()](#createimagedata) +* [loadImage()](#loadimage) +* [registerFont()](#registerfont) +* [deregisterAllFonts()](#deregisterAllFonts) + + +### Non-standard APIs + +* [Image#src](#imagesrc) +* [Image#dataMode](#imagedatamode) +* [Canvas#toBuffer()](#canvastobuffer) +* [Canvas#createPNGStream()](#canvascreatepngstream) +* [Canvas#createJPEGStream()](#canvascreatejpegstream) +* [Canvas#createPDFStream()](#canvascreatepdfstream) +* [Canvas#toDataURL()](#canvastodataurl) +* [CanvasRenderingContext2D#patternQuality](#canvasrenderingcontext2dpatternquality) +* [CanvasRenderingContext2D#quality](#canvasrenderingcontext2dquality) +* [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode) +* [CanvasRenderingContext2D#globalCompositeOperation = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperation--saturate) +* [CanvasRenderingContext2D#antialias](#canvasrenderingcontext2dantialias) + +### createCanvas() + +> ```ts +> createCanvas(width: number, height: number, type?: 'PDF'|'SVG') => Canvas +> ``` + +Creates a Canvas instance. This method works in both Node.js and Web browsers, where there is no Canvas constructor. (See `browser.js` for the implementation that runs in browsers.) + +```js +const { createCanvas } = require('canvas') +const mycanvas = createCanvas(200, 200) +const myPDFcanvas = createCanvas(600, 800, 'pdf') // see "PDF Support" section ``` - Below is an example of a canvas drawing it-self as the source several time: +### createImageData() -```javascript -var img = new Image; -img.src = canvas.toBuffer(); -ctx.drawImage(img, 0, 0, 50, 50); -ctx.drawImage(img, 50, 0, 50, 50); -ctx.drawImage(img, 100, 0, 50, 50); +> ```ts +> createImageData(width: number, height: number) => ImageData +> createImageData(data: Uint8ClampedArray, width: number, height?: number) => ImageData +> // for alternative pixel formats: +> createImageData(data: Uint16Array, width: number, height?: number) => ImageData +> ``` + +Creates an ImageData instance. This method works in both Node.js and Web browsers. + +```js +const { createImageData } = require('canvas') +const width = 20, height = 20 +const arraySize = width * height * 4 +const mydata = createImageData(new Uint8ClampedArray(arraySize), width) ``` -### Image#dataMode +### loadImage() -node-canvas adds `Image#dataMode` support, which can be used to opt-in to mime data tracking of images (currently only JPEGs). +> ```ts +> loadImage() => Promise +> ``` -When mime data is tracked, in PDF mode JPEGs can be embedded directly into the output, rather than being re-encoded into PNG. This can drastically reduce filesize, and speed up rendering. +Convenience method for loading images. This method works in both Node.js and Web browsers. -```javascript -var img = new Image; -img.dataMode = Image.MODE_IMAGE; // Only image data tracked -img.dataMode = Image.MODE_MIME; // Only mime data tracked -img.dataMode = Image.MODE_MIME | Image.MODE_IMAGE; // Both are tracked +```js +const { loadImage } = require('canvas') +const myimg = loadImage('http://server.com/image.png') + +myimg.then(() => { + // do something with image +}).catch(err => { + console.log('oh no!', err) +}) + +// or with async/await: +const myimg = await loadImage('http://server.com/image.png') +// do something with image ``` -If image data is not tracked, and the Image is drawn to an image rather than a PDF canvas, the output will be junk. Enabling mime data tracking has no benefits (only a slow down) unless you are generating a PDF. +### registerFont() -### Canvas#pngStream() +> ```ts +> registerFont(path: string, { family: string, weight?: string, style?: string }) => void +> ``` - To create a `PNGStream` simply call `canvas.pngStream()`, and the stream will start to emit _data_ events, finally emitting _end_ when finished. If an exception occurs the _error_ event is emitted. +To use a font file that is not installed as a system font, use `registerFont()` to register the font with Canvas. -```javascript -var fs = require('fs') - , out = fs.createWriteStream(__dirname + '/text.png') - , stream = canvas.pngStream(); +```js +const { registerFont, createCanvas } = require('canvas') +registerFont('comicsans.ttf', { family: 'Comic Sans' }) -stream.on('data', function(chunk){ - out.write(chunk); -}); +const canvas = createCanvas(500, 500) +const ctx = canvas.getContext('2d') -stream.on('end', function(){ - console.log('saved png'); -}); +ctx.font = '12px "Comic Sans"' +ctx.fillText('Everyone hates this font :(', 250, 10) ``` -Currently _only_ sync streaming is supported, however we plan on supporting async streaming as well (of course :) ). Until then the `Canvas#toBuffer(callback)` alternative is async utilizing `eio_custom()`. +The second argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight`, and `style` are optional and default to `'normal'`. + +### deregisterAllFonts() + +> ```ts +> deregisterAllFonts() => void +> ``` + +Use `deregisterAllFonts` to unregister all fonts that have been previously registered. This method is useful when you want to remove all registered fonts, such as when using the canvas in tests + +```ts +const { registerFont, createCanvas, deregisterAllFonts } = require('canvas') + +describe('text rendering', () => { + afterEach(() => { + deregisterAllFonts(); + }) + it('should render text with Comic Sans', () => { + registerFont('comicsans.ttf', { family: 'Comic Sans' }) -### Canvas#jpegStream() and Canvas#syncJPEGStream() + const canvas = createCanvas(500, 500) + const ctx = canvas.getContext('2d') + + ctx.font = '12px "Comic Sans"' + ctx.fillText('Everyone loves this font :)', 250, 10) + + // assertScreenshot() + }) +}) +``` + +### Image#src -You can likewise create a `JPEGStream` by calling `canvas.jpegStream()` with -some optional parameters; functionality is otherwise identical to -`pngStream()`. See `examples/crop.js` for an example. +> ```ts +> img.src: string|Buffer +> ``` -_Note: At the moment, `jpegStream()` is the same as `syncJPEGStream()`, both -are synchronous_ +As in browsers, `img.src` can be set to a `data:` URI or a remote URL. In addition, node-canvas allows setting `src` to a local file path or `Buffer` instance. ```javascript -var stream = canvas.jpegStream({ - bufsize: 4096 // output buffer size in bytes, default: 4096 - , quality: 75 // JPEG quality (0-100) default: 75 - , progressive: false // true for progressive compression, default: false -}); +const { Image } = require('canvas') + +// From a buffer: +fs.readFile('images/squid.png', (err, squid) => { + if (err) throw err + const img = new Image() + img.onload = () => ctx.drawImage(img, 0, 0) + img.onerror = err => { throw err } + img.src = squid +}) + +// From a local file path: +const img = new Image() +img.onload = () => ctx.drawImage(img, 0, 0) +img.onerror = err => { throw err } +img.src = 'images/squid.png' + +// From a remote URL: +img.src = 'http://picsum.photos/200/300' +// ... as above + +// From a `data:` URI: +img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' +// ... as above ``` -### Canvas#toBuffer() +*Note: In some cases, `img.src=` is currently synchronous. However, you should always use `img.onload` and `img.onerror`, as we intend to make `img.src=` always asynchronous as it is in browsers. See https://github.com/Automattic/node-canvas/issues/1007.* + +### Image#dataMode + +> ```ts +> img.dataMode: number +> ``` -A call to `Canvas#toBuffer()` will return a node `Buffer` instance containing image data. +Applies to JPEG images drawn to PDF canvases only. + +Setting `img.dataMode = Image.MODE_MIME` or `Image.MODE_MIME|Image.MODE_IMAGE` enables MIME data tracking of images. When MIME data is tracked, PDF canvases can embed JPEGs directly into the output, rather than re-encoding into PNG. This can drastically reduce filesize and speed up rendering. ```javascript -// PNG Buffer, default settings -var buf = canvas.toBuffer(); +const { Image, createCanvas } = require('canvas') +const canvas = createCanvas(w, h, 'pdf') +const img = new Image() +img.dataMode = Image.MODE_IMAGE // Only image data tracked +img.dataMode = Image.MODE_MIME // Only mime data tracked +img.dataMode = Image.MODE_MIME | Image.MODE_IMAGE // Both are tracked +``` -// PNG Buffer, zlib compression level 3 (from 0-9), faster but bigger -var buf2 = canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE); +If working with a non-PDF canvas, image data *must* be tracked; otherwise the output will be junk. + +Enabling mime data tracking has no benefits (only a slow down) unless you are generating a PDF. + +### Canvas#toBuffer() + +> ```ts +> canvas.toBuffer((err: Error|null, result: Buffer) => void, mimeType?: string, config?: any) => void +> canvas.toBuffer(mimeType?: string, config?: any) => Buffer +> ``` + +Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the image contained in the canvas. + +* **callback** If provided, the buffer will be provided in the callback instead of being returned by the function. Invoked with an error as the first argument if encoding failed, or the resulting buffer as the second argument if it succeeded. Not supported for mimeType `raw` or for PDF or SVG canvases. +* **mimeType** A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support), `raw` (unencoded data in BGRA order on little-endian (most) systems, ARGB on big-endian systems; top-to-bottom), `application/pdf` (for PDF canvases) and `image/svg+xml` (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas. +* **config** + * For `image/jpeg`, an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional. + + * For `image/png`, an object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only), the the background palette index (indexed PNGs only) and/or the resolution (ppi): `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional. + + Note that the PNG format encodes the resolution in pixels per meter, so if you specify `96`, the file will encode 3780 ppm (~96.01 ppi). The resolution is undefined by default to match common browser behavior. + + * For `application/pdf`, an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. All properties are optional and default to `undefined`, except for `creationDate`, which defaults to the current date. *Adding metadata requires Cairo 1.16.0 or later.* + + For a description of these properties, see page 550 of [PDF 32000-1:2008](https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf). + + Note that there is no standard separator for `keywords`. A space is recommended because it is in common use by other applications, and Cairo will enclose the list of keywords in quotes if a comma or semicolon is used. + +**Return value** + +If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html). If a callback is provided, none. + +#### Examples + +```js +// Default: buf contains a PNG-encoded image +const buf = canvas.toBuffer() -// ARGB32 Buffer, native-endian -var buf3 = canvas.toBuffer('raw'); -var stride = canvas.stride; +// PNG-encoded, zlib compression level 3 for faster compression but bigger files, no filtering +const buf2 = canvas.toBuffer('image/png', { compressionLevel: 3, filters: canvas.PNG_FILTER_NONE }) + +// JPEG-encoded, 50% quality +const buf3 = canvas.toBuffer('image/jpeg', { quality: 0.5 }) + +// Asynchronous PNG +canvas.toBuffer((err, buf) => { + if (err) throw err // encoding failed + // buf is PNG-encoded image +}) + +canvas.toBuffer((err, buf) => { + if (err) throw err // encoding failed + // buf is JPEG-encoded image at 95% quality +}, 'image/jpeg', { quality: 0.95 }) + +// BGRA pixel values, native-endian +const buf4 = canvas.toBuffer('raw') +const { stride, width } = canvas // In memory, this is `canvas.height * canvas.stride` bytes long. -// The top row of pixels, in ARGB order, left-to-right, is: -var topPixelsARGBLeftToRight = buf3.slice(0, canvas.width * 4); -var row3 = buf3.slice(2 * canvas.stride, 2 * canvas.stride + canvas.width * 4); +// The top row of pixels, in BGRA order on little-endian hardware, +// left-to-right, is: +const topPixelsBGRALeftToRight = buf4.slice(0, width * 4) +// And the third row is: +const row3 = buf4.slice(2 * stride, 2 * stride + width * 4) + +// SVG and PDF canvases +const myCanvas = createCanvas(w, h, 'pdf') +myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas +// With optional metadata: +myCanvas.toBuffer('application/pdf', { + title: 'my picture', + keywords: 'node.js demo cairo', + creationDate: new Date() +}) ``` -### Canvas#toBuffer() async +### Canvas#createPNGStream() -Optionally we may pass a callback function to `Canvas#toBuffer()`, and this process will be performed asynchronously, and will `callback(err, buf)`. +> ```ts +> canvas.createPNGStream(config?: any) => ReadableStream +> ``` + +Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits PNG-encoded data. + +* `config` An object specifying the ZLIB compression level (between 0 and 9), the compression filter(s), the palette (indexed PNGs only) and/or the background palette index (indexed PNGs only): `{compressionLevel: 6, filters: canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0, resolution: undefined}`. All properties are optional. + +#### Examples ```javascript -canvas.toBuffer(function(err, buf){ +const fs = require('fs') +const out = fs.createWriteStream(__dirname + '/test.png') +const stream = canvas.createPNGStream() +stream.pipe(out) +out.on('finish', () => console.log('The PNG file was created.')) +``` -}); +To encode indexed PNGs from canvases with `pixelFormat: 'A8'` or `'A1'`, provide an options object: + +```js +const palette = new Uint8ClampedArray([ + //r g b a + 0, 50, 50, 255, // index 1 + 10, 90, 90, 255, // index 2 + 127, 127, 255, 255 + // ... +]) +canvas.createPNGStream({ + palette: palette, + backgroundIndex: 0 // optional, defaults to 0 +}) ``` -### Canvas#toDataURL() sync and async +### Canvas#createJPEGStream() + +> ```ts +> canvas.createJPEGStream(config?: any) => ReadableStream +> ``` + +Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits JPEG-encoded data. -The following syntax patterns are supported: +*Note: At the moment, `createJPEGStream()` is synchronous under the hood. That is, it runs in the main thread, not in the libuv threadpool.* + +* `config` an object specifying the quality (0 to 1), if progressive compression should be used and/or if chroma subsampling should be used: `{quality: 0.75, progressive: false, chromaSubsampling: true}`. All properties are optional. + +#### Examples ```javascript -var dataUrl = canvas.toDataURL(); // defaults to PNG -var dataUrl = canvas.toDataURL('image/png'); -canvas.toDataURL(function(err, png){ }); // defaults to PNG -canvas.toDataURL('image/png', function(err, png){ }); -canvas.toDataURL('image/jpeg', function(err, jpeg){ }); // sync JPEG is not supported -canvas.toDataURL('image/jpeg', {opts...}, function(err, jpeg){ }); // see Canvas#jpegStream for valid options -canvas.toDataURL('image/jpeg', quality, function(err, jpeg){ }); // spec-following; quality from 0 to 1 +const fs = require('fs') +const out = fs.createWriteStream(__dirname + '/test.jpeg') +const stream = canvas.createJPEGStream() +stream.pipe(out) +out.on('finish', () => console.log('The JPEG file was created.')) + +// Disable 2x2 chromaSubsampling for deeper colors and use a higher quality +const stream = canvas.createJPEGStream({ + quality: 0.95, + chromaSubsampling: false +}) ``` -### Canvas.registerFont for bundled fonts +### Canvas#createPDFStream() -It can be useful to use a custom font file if you are distributing code that uses node-canvas and a specific font. Or perhaps you are using it to do automated tests and you want the renderings to be the same across operating systems regardless of what fonts are installed. +> ```ts +> canvas.createPDFStream(config?: any) => ReadableStream +> ``` -To do that, you should use `Canvas.registerFont`. +* `config` an object specifying optional document metadata: `{title: string, author: string, subject: string, keywords: string, creator: string, creationDate: Date, modDate: Date}`. See `toBuffer()` for more information. *Adding metadata requires Cairo 1.16.0 or later.* -**You need to call it before the Canvas is created** +Applies to PDF canvases only. Creates a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) that emits the encoded PDF. `canvas.toBuffer()` also produces an encoded PDF, but `createPDFStream()` can be used to reduce memory usage. -```javascript -Canvas.registerFont('comicsans.ttf', {family: 'Comic Sans'}); +### Canvas#toDataURL() -var canvas = new Canvas(500, 500), - ctx = canvas.getContext('2d'); +This is a standard API, but several non-standard calls are supported. The full list of supported calls is: -ctx.font = '12px "Comic Sans"'; -ctx.fillText(250, 10, 'Everyone hates this font :('); +```js +dataUrl = canvas.toDataURL() // defaults to PNG +dataUrl = canvas.toDataURL('image/png') +dataUrl = canvas.toDataURL('image/jpeg') +dataUrl = canvas.toDataURL('image/jpeg', quality) // quality from 0 to 1 +canvas.toDataURL((err, png) => { }) // defaults to PNG +canvas.toDataURL('image/png', (err, png) => { }) +canvas.toDataURL('image/jpeg', (err, jpeg) => { }) // sync JPEG is not supported +canvas.toDataURL('image/jpeg', {...opts}, (err, jpeg) => { }) // see Canvas#createJPEGStream for valid options +canvas.toDataURL('image/jpeg', quality, (err, jpeg) => { }) // spec-following; quality from 0 to 1 ``` -The second argument is an object with properties that resemble the CSS properties that are specified in `@font-face` rules. You must specify at least `family`. `weight`, and `style` are optional (and default to "normal"). - ### CanvasRenderingContext2D#patternQuality -Given one of the values below will alter pattern (gradients, images, etc) render quality, defaults to _good_. +> ```ts +> context.patternQuality: 'fast'|'good'|'best'|'nearest'|'bilinear' +> ``` + +Defaults to `'good'`. Affects pattern (gradient, image, etc.) rendering quality. + +### CanvasRenderingContext2D#quality + +> ```ts +> context.quality: 'fast'|'good'|'best'|'nearest'|'bilinear' +> ``` - - fast - - good - - best - - nearest - - bilinear +Defaults to `'good'`. Like `patternQuality`, but applies to transformations affecting more than just patterns. ### CanvasRenderingContext2D#textDrawingMode -Can be either `path` or `glyph`. Using `glyph` is much faster than `path` for drawing, and when using a PDF context will embed the text natively, so will be selectable and lower filesize. The downside is that cairo does not have any subpixel precision for `glyph`, so this will be noticeably lower quality for text positioning in cases such as rotated text. Also, strokeText in `glyph` will act the same as fillText, except using the stroke style for the fill. +> ```ts +> context.textDrawingMode: 'path'|'glyph' +> ``` -Defaults to _path_. +Defaults to `'path'`. The effect depends on the canvas type: -This property is tracked as part of the canvas state in save/restore. +* **Standard (image)** `glyph` and `path` both result in rasterized text. Glyph mode is faster than `path`, but may result in lower-quality text, especially when rotated or translated. + +* **PDF** `glyph` will embed text instead of paths into the PDF. This is faster to encode, faster to open with PDF viewers, yields a smaller file size and makes the text selectable. The subset of the font needed to render the glyphs will be embedded in the PDF. This is usually the mode you want to use with PDF canvases. -### CanvasRenderingContext2D#filter +* **SVG** `glyph` does *not* cause `` elements to be produced as one might expect ([cairo bug](https://gitlab.freedesktop.org/cairo/cairo/issues/253)). Rather, `glyph` will create a `` section with a `` for each glyph, then those glyphs be reused via `` elements. `path` mode creates a `` element for each text string. `glyph` mode is faster and yields a smaller file size. -Like `patternQuality`, but applies to transformations effecting more than just patterns. Defaults to _good_. +In `glyph` mode, `ctx.strokeText()` and `ctx.fillText()` behave the same (aside from using the stroke and fill style, respectively). - - fast - - good - - best - - nearest - - bilinear +This property is tracked as part of the canvas state in save/restore. -### Global Composite Operations +### CanvasRenderingContext2D#globalCompositeOperation = 'saturate' -In addition to those specified and commonly implemented by browsers, the following have been added: +In addition to all of the standard global composite operations defined by the Canvas specification, the ['saturate'](https://www.cairographics.org/operators/#saturate) operation is also available. - - multiply - - screen - - overlay - - hard-light - - soft-light - - hsl-hue - - hsl-saturation - - hsl-color - - hsl-luminosity +### CanvasRenderingContext2D#antialias -## Anti-Aliasing +> ```ts +> context.antialias: 'default'|'none'|'gray'|'subpixel' +> ``` - Set anti-aliasing mode +Sets the anti-aliasing mode. - - default - - none - - gray - - subpixel +## PDF Output Support - For example: +node-canvas can create PDF documents instead of images. The canvas type must be set when creating the canvas as follows: -```javascript -ctx.antialias = 'none'; +```js +const canvas = createCanvas(200, 500, 'pdf') ``` -## PDF Support +An additional method `.addPage()` is then available to create multiple page PDFs: + +```js +// On first page +ctx.font = '22px Helvetica' +ctx.fillText('Hello World', 50, 80) + +ctx.addPage() +// Now on second page +ctx.font = '22px Helvetica' +ctx.fillText('Hello World 2', 50, 80) + +canvas.toBuffer() // returns a PDF file +canvas.createPDFStream() // returns a ReadableStream that emits a PDF +// With optional document metadata (requires Cairo 1.16.0): +canvas.toBuffer('application/pdf', { + title: 'my picture', + keywords: 'node.js demo cairo', + creationDate: new Date() +}) +``` - Basic PDF support was added in 0.11.0. Make sure to install cairo with `--enable-pdf=yes` for the PDF backend. node-canvas must know that it is creating - a PDF on initialization, using the "pdf" string: +It is also possible to create pages with different sizes by passing `width` and `height` to the `.addPage()` method: ```js -var canvas = new Canvas(200, 500, 'pdf'); +ctx.font = '22px Helvetica' +ctx.fillText('Hello World', 50, 80) +ctx.addPage(400, 800) + +ctx.fillText('Hello World 2', 50, 80) ``` - An additional method `.addPage()` is then available to create - multiple page PDFs: +It is possible to add hyperlinks using `.beginTag()` and `.endTag()`: ```js -ctx.font = '22px Helvetica'; -ctx.fillText('Hello World', 50, 80); -ctx.addPage(); +ctx.beginTag('Link', "uri='https://google.com'") +ctx.font = '22px Helvetica' +ctx.fillText('Hello World', 50, 80) +ctx.endTag('Link') +``` -ctx.font = '22px Helvetica'; -ctx.fillText('Hello World 2', 50, 80); -ctx.addPage(); +Or with a defined rectangle: -ctx.font = '22px Helvetica'; -ctx.fillText('Hello World 3', 50, 80); -ctx.addPage(); +```js +ctx.beginTag('Link', "uri='https://google.com' rect=[50 80 100 20]") +ctx.endTag('Link') ``` -## SVG support +Note that the syntax for attributes is unique to Cairo. See [cairo_tag_begin](https://www.cairographics.org/manual/cairo-Tags-and-Links.html#cairo-tag-begin) for the full documentation. + +You can create areas on the canvas using the "cairo.dest" tag, and then link to them using the "Link" tag with the `dest=` attribute. You can also define PDF structure for accessibility by using tag names like "P", "H1", and "TABLE". The standard tags are defined in §14.8.4 of the [PDF 1.7](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf) specification. - Just like PDF support, make sure to install cairo with `--enable-svg=yes`. - You also need to tell node-canvas that it is working on SVG upon its initialization: +See also: + +* [Image#dataMode](#imagedatamode) for embedding JPEGs in PDFs +* [Canvas#createPDFStream()](#canvascreatepdfstream) for creating PDF streams +* [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode) + for embedding text instead of paths + +## SVG Output Support + +node-canvas can create SVG documents instead of images. The canvas type must be set when creating the canvas as follows: ```js -var canvas = new Canvas(200, 500, 'svg'); +const canvas = createCanvas(200, 500, 'svg') // Use the normal primitives. -fs.writeFile('out.svg', canvas.toBuffer()); +fs.writeFileSync('out.svg', canvas.toBuffer()) ``` -## Benchmarks +## SVG Image Support - Although node-canvas is extremely new, and we have not even begun optimization yet it is already quite fast. For benchmarks vs other node canvas implementations view this [gist](https://gist.github.com/664922), or update the submodules and run `$ make benchmark` yourself. +If librsvg is available when node-canvas is installed, node-canvas can render SVG images to your canvas context. This currently works by rasterizing the SVG image (i.e. drawing an SVG image to an SVG canvas will not preserve the SVG data). -## Contribute +```js +const img = new Image() +img.onload = () => ctx.drawImage(img, 0, 0) +img.onerror = err => { throw err } +img.src = './example.svg' +``` - Want to contribute to node-canvas? patches for features, bug fixes, documentation, examples and others are certainly welcome. Take a look at the [issue queue](https://github.com/Automattic/node-canvas/issues) for existing issues. +## Image pixel formats (experimental) -## Examples +node-canvas has experimental support for additional pixel formats, roughly following the [Canvas color space proposal](https://github.com/WICG/canvas-color-space/blob/master/CanvasColorSpaceProposal.md). + +```js +const canvas = createCanvas(200, 200) +const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) +``` + +By default, canvases are created in the `RGBA32` format, which corresponds to the native HTML Canvas behavior. Each pixel is 32 bits. The JavaScript APIs that involve pixel data (`getImageData`, `putImageData`) store the colors in the order {red, green, blue, alpha} without alpha pre-multiplication. (The C++ API stores the colors in the order {alpha, red, green, blue} in native-[endian](https://en.wikipedia.org/wiki/Endianness) ordering, with alpha pre-multiplication.) + +These additional pixel formats have experimental support: + +* `RGB24` Like `RGBA32`, but the 8 alpha bits are always opaque. This format is always used if the `alpha` context attribute is set to false (i.e. `canvas.getContext('2d', {alpha: false})`). This format can be faster than `RGBA32` because transparency does not need to be calculated. +* `A8` Each pixel is 8 bits. This format can either be used for creating grayscale images (treating each byte as an alpha value), or for creating indexed PNGs (treating each byte as a palette index) (see [the example using alpha values with `fillStyle`](examples/indexed-png-alpha.js) and [the example using `imageData`](examples/indexed-png-image-data.js)). +* `RGB16_565` Each pixel is 16 bits, with red in the upper 5 bits, green in the middle 6 bits, and blue in the lower 5 bits, in native platform endianness. Some hardware devices and frame buffers use this format. Note that PNG does not support this format; when creating a PNG, the image will be converted to 24-bit RGB. This format is thus suboptimal for generating PNGs. `ImageData` instances for this mode use a `Uint16Array` instead of a `Uint8ClampedArray`. +* `A1` Each pixel is 1 bit, and pixels are packed together into 32-bit quantities. The ordering of the bits matches the endianness of the + platform: on a little-endian machine, the first pixel is the least-significant bit. This format can be used for creating single-color images. *Support for this format is incomplete, see note below.* +* `RGB30` Each pixel is 30 bits, with red in the upper 10, green in the middle 10, and blue in the lower 10. (Requires Cairo 1.12 or later.) *Support for this format is incomplete, see note below.* + +Notes and caveats: + +* Using a non-default format can affect the behavior of APIs that involve pixel data: + + * `context2d.createImageData` The size of the array returned depends on the number of bit per pixel for the underlying image data format, per the above descriptions. + * `context2d.getImageData` The format of the array returned depends on the underlying image mode, per the above descriptions. Be aware of platform endianness, which can be determined using node.js's [`os.endianness()`](https://nodejs.org/api/os.html#os_os_endianness) + function. + * `context2d.putImageData` As above. - Examples are placed in _./examples_, be sure to check them out! most produce a png image of the same name, and others such as _live-clock.js_ launch an http server to be viewed in the browser. +* `A1` and `RGB30` do not yet support `getImageData` or `putImageData`. Have a use case and/or opinion on working with these formats? Open an issue and let us know! (See #935.) + +* `A1`, `A8`, `RGB30` and `RGB16_565` with shadow blurs may crash or not render properly. + +* The `ImageData(width, height)` and `ImageData(Uint8ClampedArray, width)` constructors assume 4 bytes per pixel. To create an `ImageData` instance with a different number of bytes per pixel, use `new ImageData(new Uint8ClampedArray(size), width, height)` or `new ImageData(new Uint16ClampedArray(size), width, height)`. ## Testing -If you have not previously, init git submodules: +First make sure you've built the latest version. Get all the deps you need (see [compiling](#compiling) above), and run: - $ git submodule update --init +``` +npm install --build-from-source +``` -Install the node modules: +For visual tests: `npm run test-server` and point your browser to http://localhost:4000. - $ npm install +For unit tests: `npm run test`. -Build node-canvas: +## Benchmarks - $ node-gyp rebuild +Benchmarks live in the `benchmarks` directory. -Unit tests: +## Examples - $ make test +Examples line in the `examples` directory. Most produce a png image of the same name, and others such as *live-clock.js* launch an HTTP server to be viewed in the browser. -Visual tests: +## Original Authors - $ make test-server + - TJ Holowaychuk ([tj](http://github.com/tj)) + - Nathan Rajlich ([TooTallNate](http://github.com/TooTallNate)) + - Rod Vagg ([rvagg](http://github.com/rvagg)) + - Juriy Zaytsev ([kangax](http://github.com/kangax)) ## License +### node-canvas + (The MIT License) Copyright (c) 2010 LearnBoost, and contributors <dev@learnboost.com> Copyright (c) 2014 Automattic, Inc and contributors <dev@automattic.com> -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +### BMP parser + +See [license](src/bmp/LICENSE.md) diff --git a/benchmarks/run.js b/benchmarks/run.js index 6a3f06154..5a5c9d507 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -4,16 +4,16 @@ * milliseconds to complete. */ -var Canvas = require('../') -var canvas = new Canvas(200, 200) -var largeCanvas = new Canvas(1000, 1000) -var ctx = canvas.getContext('2d') +const { createCanvas } = require('../') +const canvas = createCanvas(200, 200) +const largeCanvas = createCanvas(1000, 1000) +const ctx = canvas.getContext('2d') -var initialTimes = 10 -var minDurationMs = 2000 +const initialTimes = 10 +const minDurationMs = 2000 -var queue = [] -var running = false +const queue = [] +let running = false function bm (label, fn) { queue.push({ label: label, fn: fn }) @@ -28,11 +28,11 @@ function next () { function run (benchmark, n, start) { running = true - var originalN = n - var fn = benchmark.fn + const originalN = n + const fn = benchmark.fn if (fn.length) { // async - var pending = n + let pending = n while (n--) { fn(function () { @@ -46,12 +46,12 @@ function run (benchmark, n, start) { } function done (benchmark, times, start, isAsync) { - var duration = Date.now() - start + const duration = Date.now() - start if (duration < minDurationMs) { run(benchmark, times * 2, Date.now()) } else { - var opsSec = times / duration * 1000 + const opsSec = times / duration * 1000 if (isAsync) { console.log(' - \x1b[33m%s\x1b[0m %s ops/sec (%s times, async)', benchmark.label, opsSec.toLocaleString(), times) } else { @@ -64,6 +64,40 @@ function done (benchmark, times, start, isAsync) { // node-canvas +function fontName () { + return String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) + + String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) + + String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) + + String.fromCharCode(0x61 + Math.floor(Math.random() * 26)) +} + +bm('font setter', function () { + ctx.font = `12px ${fontName()}` + ctx.font = `400 6px ${fontName()}` + ctx.font = `1px ${fontName()}` + ctx.font = `normal normal bold 12cm ${fontName()}` + ctx.font = `italic 9mm ${fontName}, "Times New Roman", "Apple Color Emoji", "Comic Sans"` + ctx.font = `small-caps oblique 44px/44px ${fontName()}, "The Quick Brown", "Fox Jumped", "Over", "The", "Lazy Dog"` +}) + +bm('save/restore', function () { + for (let i = 0; i < 1000; i++) { + const max = i & 15 + for (let j = 0; j < max; ++j) { + ctx.save() + } + for (let j = 0; j < max; ++j) { + ctx.restore() + } + } +}) + +bm('fillStyle= name', function () { + for (let i = 0; i < 10000; i++) { + ctx.fillStyle = '#fefefe' + } +}) + bm('lineTo()', function () { ctx.lineTo(0, 50) }) @@ -91,7 +125,7 @@ bm('strokeRect()', function () { }) bm('linear gradients', function () { - var lingrad = ctx.createLinearGradient(0, 50, 0, 95) + const lingrad = ctx.createLinearGradient(0, 50, 0, 95) lingrad.addColorStop(0.5, '#000') lingrad.addColorStop(1, 'rgba(0,0,0,0)') ctx.fillStyle = lingrad @@ -134,11 +168,11 @@ bm('moveTo() / arc() / stroke()', function () { ctx.beginPath() ctx.arc(75, 75, 50, 0, Math.PI * 2, true) // Outer circle ctx.moveTo(110, 75) - ctx.arc(75, 75, 35, 0, Math.PI, false) // Mouth + ctx.arc(75, 75, 35, 0, Math.PI, false) // Mouth ctx.moveTo(65, 65) - ctx.arc(60, 65, 5, 0, Math.PI * 2, true) // Left eye + ctx.arc(60, 65, 5, 0, Math.PI * 2, true) // Left eye ctx.moveTo(95, 65) - ctx.arc(90, 65, 5, 0, Math.PI * 2, true) // Right eye + ctx.arc(90, 65, 5, 0, Math.PI * 2, true) // Right eye ctx.stroke() }) @@ -151,7 +185,7 @@ bm('getImageData(0,0,100,100)', function () { }) bm('PNGStream 200x200', function (done) { - var stream = canvas.createSyncPNGStream() + const stream = canvas.createPNGStream() stream.on('data', function (chunk) { // whatever }) diff --git a/binding.gyp b/binding.gyp old mode 100755 new mode 100644 index 4f56e1a4c..bf647f7d1 --- a/binding.gyp +++ b/binding.gyp @@ -2,14 +2,27 @@ 'conditions': [ ['OS=="win"', { 'variables': { - 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle + 'GTK_Root%': 'C:/GTK', # Set the location of GTK all-in-one bundle 'with_jpeg%': 'false', - 'with_gif%': 'false' + 'with_gif%': 'false', + 'with_rsvg%': 'false', + 'variables': { # Nest jpeg_root to evaluate it before with_jpeg + 'jpeg_root%': ' Font Information and copy the Family Name -Canvas.registerFont(fontFile('Pfennig.ttf'), {family: 'pfennigFont'}) -Canvas.registerFont(fontFile('PfennigBold.ttf'), {family: 'pfennigFont', weight: 'bold'}) -Canvas.registerFont(fontFile('PfennigItalic.ttf'), {family: 'pfennigFont', style: 'italic'}) -Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), {family: 'pfennigFont', weight: 'bold', style: 'italic'}) +Canvas.registerFont(fontFile('Pfennig.ttf'), { family: 'pfennigFont' }) +Canvas.registerFont(fontFile('PfennigBold.ttf'), { family: 'pfennigFont', weight: 'bold' }) +Canvas.registerFont(fontFile('PfennigItalic.ttf'), { family: 'pfennigFont', style: 'italic' }) +Canvas.registerFont(fontFile('PfennigBoldItalic.ttf'), { family: 'pfennigFont', weight: 'bold', style: 'italic' }) -var canvas = new Canvas(320, 320) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(320, 320) +const ctx = canvas.getContext('2d') ctx.font = 'normal normal 50px Helvetica' ctx.fillText('Quo Vaids?', 0, 70) ctx.font = 'bold 50px pfennigFont' -ctx.fillText('Quo Vaids?', 0, 140) +ctx.fillText('Quo Vaids?', 0, 140, 100) ctx.font = 'italic 50px pfennigFont' ctx.fillText('Quo Vaids?', 0, 210) diff --git a/examples/globalAlpha.js b/examples/globalAlpha.js index 2af849ebf..e618c0b10 100644 --- a/examples/globalAlpha.js +++ b/examples/globalAlpha.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = new Canvas(150, 150) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(150, 150) +const ctx = canvas.getContext('2d') ctx.fillStyle = '#FD0' ctx.fillRect(0, 0, 75, 75) @@ -23,7 +23,7 @@ ctx.fillStyle = '#FFF' ctx.globalAlpha = 0.2 // Draw semi transparent circles -for (var i = 0; i < 7; i++) { +for (let i = 0; i < 7; i++) { ctx.beginPath() ctx.arc(75, 75, 10 + 10 * i, 0, Math.PI * 2, true) ctx.fill() diff --git a/examples/gradients.js b/examples/gradients.js index 3a6ff8116..f504258c2 100644 --- a/examples/gradients.js +++ b/examples/gradients.js @@ -1,18 +1,18 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = new Canvas(320, 320) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(320, 320) +const ctx = canvas.getContext('2d') // Create gradients -var lingrad = ctx.createLinearGradient(0, 0, 0, 150) +const lingrad = ctx.createLinearGradient(0, 0, 0, 150) lingrad.addColorStop(0, '#00ABEB') lingrad.addColorStop(0.5, '#fff') lingrad.addColorStop(0.5, '#26C000') lingrad.addColorStop(1, '#fff') -var lingrad2 = ctx.createLinearGradient(0, 50, 0, 95) +const lingrad2 = ctx.createLinearGradient(0, 50, 0, 95) lingrad2.addColorStop(0.5, '#000') lingrad2.addColorStop(1, 'rgba(0,0,0,0)') diff --git a/examples/grayscale-image.js b/examples/grayscale-image.js index 1ee12b153..ce3ffa06c 100644 --- a/examples/grayscale-image.js +++ b/examples/grayscale-image.js @@ -1,14 +1,18 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var Image = Canvas.Image -var canvas = new Canvas(288, 288) -var ctx = canvas.getContext('2d') +const Image = Canvas.Image +const canvas = Canvas.createCanvas(288, 288) +const ctx = canvas.getContext('2d') -var img = new Image() -img.src = fs.readFileSync(path.join(__dirname, 'images', 'grayscaleImage.jpg')) +const img = new Image() +img.onload = () => { + ctx.drawImage(img, 0, 0) + canvas.createJPEGStream().pipe(fs.createWriteStream(path.join(__dirname, 'passedThroughGrayscale.jpg'))) +} +img.onerror = err => { + throw err +} -ctx.drawImage(img, 0, 0) - -canvas.createJPEGStream().pipe(fs.createWriteStream(path.join(__dirname, 'passedThroughGrayscale.jpg'))) +img.src = path.join(__dirname, 'images', 'grayscaleImage.jpg') diff --git a/examples/image-caption-overlay.js b/examples/image-caption-overlay.js new file mode 100644 index 000000000..e156b42b8 --- /dev/null +++ b/examples/image-caption-overlay.js @@ -0,0 +1,87 @@ +import { createWriteStream } from 'fs' +import pify from 'pify' +import imageSizeOf from 'image-size' +import { createCanvas, loadImage, Image } from 'canvas' + +const imageSizeOfP = pify(imageSizeOf) + +function createImageFromBuffer (buffer) { + const image = new Image() + image.src = buffer + + return image +} + +function createCaptionOverlay ({ + text, + width, + height, + font = 'Arial', + fontSize = 48, + captionHeight = 120, + decorateCaptionTextFillStyle = null, + decorateCaptionFillStyle = null, + offsetX = 0, + offsetY = 0 +}) { + const canvas = createCanvas(width, height) + const ctx = canvas.getContext('2d') + + const createGradient = (first, second) => { + const grd = ctx.createLinearGradient(width, captionY, width, height) + grd.addColorStop(0, first) + grd.addColorStop(1, second) + + return grd + } + + // Hold computed caption position + const captionX = offsetX + const captionY = offsetY + height - captionHeight + const captionTextX = captionX + (width / 2) + const captionTextY = captionY + (captionHeight / 2) + + // Fill caption rect + ctx.fillStyle = decorateCaptionFillStyle + ? decorateCaptionFillStyle(ctx) + : createGradient('rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0.45)') + ctx.fillRect(captionX, captionY, width, captionHeight) + + // Fill caption text + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + ctx.font = `${fontSize}px ${font}` + ctx.fillStyle = decorateCaptionTextFillStyle + ? decorateCaptionTextFillStyle(ctx) + : 'white' + ctx.fillText(text, captionTextX, captionTextY) + + return createImageFromBuffer(canvas.toBuffer()) +} + +(async () => { + try { + const source = 'images/lime-cat.jpg' + const { width, height } = await imageSizeOfP(source) + const canvas = createCanvas(width, height) + const ctx = canvas.getContext('2d') + + // Draw base image + const image = await loadImage(source) + ctx.drawImage(image, 0, 0) + + // Draw caption overlay + const overlay = await createCaptionOverlay({ + text: 'Hello!', + width, + height + }) + ctx.drawImage(overlay, 0, 0) + + // Output to `.png` file + canvas.createPNGStream().pipe(createWriteStream('foo.png')) + } catch (err) { + console.log(err) + process.exit(1) + } +})() diff --git a/examples/image-src-svg.js b/examples/image-src-svg.js new file mode 100644 index 000000000..88a5365ed --- /dev/null +++ b/examples/image-src-svg.js @@ -0,0 +1,18 @@ +const fs = require('fs') +const path = require('path') +const Canvas = require('..') + +const canvas = Canvas.createCanvas(500, 500) +const ctx = canvas.getContext('2d') +ctx.fillStyle = 'white' +ctx.fillRect(0, 0, 500, 500) + +Canvas.loadImage(path.join(__dirname, 'images', 'small-svg.svg')) + .then(image => { + image.width *= 1.5 + image.height *= 1.5 + ctx.drawImage(image, canvas.width / 2 - image.width / 2, canvas.height / 2 - image.height / 2) + + canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'image-src-svg.png'))) + }) + .catch(e => console.error(e)) diff --git a/examples/image-src-url.js b/examples/image-src-url.js new file mode 100644 index 000000000..fcad291df --- /dev/null +++ b/examples/image-src-url.js @@ -0,0 +1,18 @@ +const fs = require('fs') +const path = require('path') +const Canvas = require('..') + +const Image = Canvas.Image +const canvas = Canvas.createCanvas(200, 300) +const ctx = canvas.getContext('2d') + +const img = new Image() +img.onload = () => { + ctx.drawImage(img, 0, 0) + canvas.createPNGStream() + .pipe(fs.createWriteStream(path.join(__dirname, 'image-src-url.png'))) +} +img.onerror = err => { + console.log(err) +} +img.src = 'http://picsum.photos/200/300' diff --git a/examples/image-src.js b/examples/image-src.js index 51ed4b351..704b5d39b 100644 --- a/examples/image-src.js +++ b/examples/image-src.js @@ -1,10 +1,10 @@ -var fs = require('fs') -var path = require(path) -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var Image = Canvas.Image -var canvas = new Canvas(200, 200) -var ctx = canvas.getContext('2d') +const Image = Canvas.Image +const canvas = Canvas.createCanvas(200, 200) +const ctx = canvas.getContext('2d') ctx.fillRect(0, 0, 150, 150) ctx.save() @@ -23,14 +23,19 @@ ctx.fillRect(45, 45, 60, 60) ctx.restore() ctx.fillRect(60, 60, 30, 30) -var img = new Image() -img.src = canvas.toBuffer() -ctx.drawImage(img, 0, 0, 50, 50) -ctx.drawImage(img, 50, 0, 50, 50) -ctx.drawImage(img, 100, 0, 50, 50) - -img = new Image() -img.src = fs.readFileSync(path.join(__dirname, 'images', 'squid.png')) -ctx.drawImage(img, 30, 50, img.width / 4, img.height / 4) - -canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'image-src.png'))) +const img = new Image() +img.onerror = err => { throw err } +img.onload = () => { + img.src = canvas.toBuffer() + ctx.drawImage(img, 0, 0, 50, 50) + ctx.drawImage(img, 50, 0, 50, 50) + ctx.drawImage(img, 100, 0, 50, 50) + + const img2 = new Image() + img2.onload = () => { + ctx.drawImage(img2, 30, 50, img2.width / 4, img2.height / 4) + canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'image-src.png'))) + } + img2.onerror = err => { throw err } + img2.src = path.join(__dirname, 'images', 'squid.png') +} diff --git a/examples/images/small-svg.svg b/examples/images/small-svg.svg new file mode 100755 index 000000000..abf449bce --- /dev/null +++ b/examples/images/small-svg.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/indexed-png-alpha.js b/examples/indexed-png-alpha.js new file mode 100644 index 000000000..79a8a5d41 --- /dev/null +++ b/examples/indexed-png-alpha.js @@ -0,0 +1,34 @@ +const Canvas = require('..') +const fs = require('fs') +const path = require('path') +const canvas = Canvas.createCanvas(200, 200) +const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) + +// Matches the "fillStyle" browser test, made by using alpha fillStyle value +const palette = new Uint8ClampedArray(37 * 4) +let i, j +let k = 0 +// First value is opaque white: +palette[k++] = 255 +palette[k++] = 255 +palette[k++] = 255 +palette[k++] = 255 +for (i = 0; i < 6; i++) { + for (j = 0; j < 6; j++) { + palette[k++] = Math.floor(255 - 42.5 * i) + palette[k++] = Math.floor(255 - 42.5 * j) + palette[k++] = 0 + palette[k++] = 255 + } +} +for (i = 0; i < 6; i++) { + for (j = 0; j < 6; j++) { + const index = i * 6 + j + 1.5 // 0.5 to bias rounding + const fraction = index / 255 + ctx.fillStyle = 'rgba(0,0,0,' + fraction + ')' + ctx.fillRect(j * 25, i * 25, 25, 25) + } +} + +canvas.createPNGStream({ palette: palette }) + .pipe(fs.createWriteStream(path.join(__dirname, 'indexed2.png'))) diff --git a/examples/indexed-png-image-data.js b/examples/indexed-png-image-data.js new file mode 100644 index 000000000..253835f0e --- /dev/null +++ b/examples/indexed-png-image-data.js @@ -0,0 +1,39 @@ +const Canvas = require('..') +const fs = require('fs') +const path = require('path') +const canvas = Canvas.createCanvas(200, 200) +const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) + +// Matches the "fillStyle" browser test, made by manipulating imageData +const palette = new Uint8ClampedArray(37 * 4) +let k = 0 +let i, j +// First value is opaque white: +palette[k++] = 255 +palette[k++] = 255 +palette[k++] = 255 +palette[k++] = 255 +for (i = 0; i < 6; i++) { + for (j = 0; j < 6; j++) { + palette[k++] = Math.floor(255 - 42.5 * i) + palette[k++] = Math.floor(255 - 42.5 * j) + palette[k++] = 0 + palette[k++] = 255 + } +} +const idata = ctx.getImageData(0, 0, 200, 200) +for (i = 0; i < 6; i++) { + for (j = 0; j < 6; j++) { + const index = j * 6 + i + // fill rect: + for (let xr = j * 25; xr < j * 25 + 25; xr++) { + for (let yr = i * 25; yr < i * 25 + 25; yr++) { + idata.data[xr * 200 + yr] = index + 1 + } + } + } +} +ctx.putImageData(idata, 0, 0) + +canvas.createPNGStream({ palette: palette }) + .pipe(fs.createWriteStream(path.join(__dirname, 'indexed.png'))) diff --git a/examples/kraken.js b/examples/kraken.js index 21d278c57..ed2f25e8c 100644 --- a/examples/kraken.js +++ b/examples/kraken.js @@ -1,26 +1,28 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var Image = Canvas.Image -var canvas = new Canvas(400, 267) -var ctx = canvas.getContext('2d') +const Image = Canvas.Image +const canvas = Canvas.createCanvas(400, 267) +const ctx = canvas.getContext('2d') -var img = new Image() +const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0) } +img.onerror = err => { throw err } + img.src = path.join(__dirname, 'images', 'squid.png') -var sigma = 10 // radius -var kernel, kernelSize, kernelSum +const sigma = 10 // radius +let kernel, kernelSize, kernelSum function buildKernel () { - var i, j, g - var ss = sigma * sigma - var factor = 2 * Math.PI * ss + let i, j, g + const ss = sigma * sigma + const factor = 2 * Math.PI * ss kernel = [[]] @@ -50,17 +52,17 @@ function buildKernel () { } function blurTest () { - var x, y, i, j - var r, g, b, a + let x, y, i, j + let r, g, b, a console.log('... running') - var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height) - var data = imgData.data - var width = imgData.width - var height = imgData.height + const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height) + const data = imgData.data + const width = imgData.width + const height = imgData.height - var startTime = (new Date()).getTime() + const startTime = (new Date()).getTime() for (y = 0; y < height; ++y) { for (x = 0; x < width; ++x) { @@ -89,7 +91,7 @@ function blurTest () { } } - var finishTime = Date.now() - startTime + const finishTime = Date.now() - startTime for (i = 0; i < data.length; i++) { imgData.data[i] = data[i] } diff --git a/examples/live-clock.js b/examples/live-clock.js index 08bf25526..365680b84 100644 --- a/examples/live-clock.js +++ b/examples/live-clock.js @@ -1,10 +1,10 @@ -var http = require('http') -var Canvas = require('..') +const http = require('http') +const Canvas = require('..') -var clock = require('./clock') +const clock = require('./clock') -var canvas = new Canvas(320, 320) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(320, 320) +const ctx = canvas.getContext('2d') http.createServer(function (req, res) { clock(ctx) diff --git a/examples/multi-page-pdf.js b/examples/multi-page-pdf.js index cd27f2f1d..46b8237e7 100644 --- a/examples/multi-page-pdf.js +++ b/examples/multi-page-pdf.js @@ -1,10 +1,10 @@ -var fs = require('fs') -var Canvas = require('..') +const fs = require('fs') +const Canvas = require('..') -var canvas = new Canvas(500, 500, 'pdf') -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(500, 500, 'pdf') +const ctx = canvas.getContext('2d') -var x, y +let x, y function reset () { x = 50 @@ -29,7 +29,7 @@ ctx.addPage() reset() h1('Page #2') p('This is the second page') -ctx.addPage() +ctx.addPage(250, 250) // create a page with half the size of the canvas reset() h1('Page #3') diff --git a/examples/pango-glyphs.js b/examples/pango-glyphs.js index 695fe1731..eddbbdb4a 100644 --- a/examples/pango-glyphs.js +++ b/examples/pango-glyphs.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = new Canvas(400, 100) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(400, 100) +const ctx = canvas.getContext('2d') ctx.globalAlpha = 1 ctx.font = 'normal 16px Impact' diff --git a/examples/pdf-images.js b/examples/pdf-images.js index 69e59e3e4..2753dcb58 100644 --- a/examples/pdf-images.js +++ b/examples/pdf-images.js @@ -1,11 +1,10 @@ -var fs = require('fs') -var Canvas = require('..') +const fs = require('fs') +const { Image, createCanvas } = require('..') -var Image = Canvas.Image -var canvas = new Canvas(500, 500, 'pdf') -var ctx = canvas.getContext('2d') +const canvas = createCanvas(500, 500, 'pdf') +const ctx = canvas.getContext('2d') -var x, y +let x, y function reset () { x = 50 @@ -23,7 +22,7 @@ function p (str) { } function img (src) { - var img = new Image() + const img = new Image() img.src = src ctx.drawImage(img, x, (y += 20)) y += img.height @@ -43,7 +42,16 @@ img('examples/images/lime-cat.jpg') p('Figure 1.1 - Lime cat is awesome') ctx.addPage() -fs.writeFile('out.pdf', canvas.toBuffer(), function (err) { +const buff = canvas.toBuffer('application/pdf', { + title: 'Squid and Cat!', + author: 'Octocat', + subject: 'An example PDF made with node-canvas', + keywords: 'node.js squid cat lime', + creator: 'my app', + modDate: new Date() +}) + +fs.writeFile('out.pdf', buff, function (err) { if (err) throw err console.log('created out.pdf') diff --git a/examples/pdf-link.js b/examples/pdf-link.js new file mode 100644 index 000000000..f6e40291b --- /dev/null +++ b/examples/pdf-link.js @@ -0,0 +1,20 @@ +const fs = require('fs') +const path = require('path') +const Canvas = require('..') + +const canvas = Canvas.createCanvas(400, 300, 'pdf') +const ctx = canvas.getContext('2d') + +ctx.beginTag('Link', 'uri=\'https://google.com\'') +ctx.font = '22px Helvetica' +ctx.fillText('Text link to Google', 110, 50) +ctx.endTag('Link') + +ctx.fillText('Rect link to node-canvas below!', 40, 180) + +ctx.beginTag('Link', 'uri=\'https://github.com/Automattic/node-canvas\' rect=[0 200 400 100]') +ctx.endTag('Link') + +fs.writeFile(path.join(__dirname, 'pdf-link.pdf'), canvas.toBuffer(), function (err) { + if (err) throw err +}) diff --git "a/examples/pfennigFont/pfennigMultiByte\360\237\232\200.ttf" "b/examples/pfennigFont/pfennigMultiByte\360\237\232\200.ttf" new file mode 100644 index 000000000..bbbe4636d Binary files /dev/null and "b/examples/pfennigFont/pfennigMultiByte\360\237\232\200.ttf" differ diff --git a/examples/ray.js b/examples/ray.js index a191034f2..c8a8e5326 100644 --- a/examples/ray.js +++ b/examples/ray.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = new Canvas(243 * 4, 243) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(243 * 4, 243) +const ctx = canvas.getContext('2d') function render (level) { ctx.fillStyle = getPointColour(122, 122) @@ -12,7 +12,7 @@ function render (level) { } function renderLevel (minimumLevel, level, y) { - var x + let x for (x = 0; x < 243 / level; ++x) { drawBlock(x, y, level) @@ -51,26 +51,26 @@ function getPointColour (x, y) { x = x / 121.5 - 1 y = -y / 121.5 + 1 - var x2y2 = x * x + y * y + const x2y2 = x * x + y * y if (x2y2 > 1) { return '#000' } - var root = Math.sqrt(1 - x2y2) - var x3d = x * 0.7071067812 + root / 2 - y / 2 - var y3d = x * 0.7071067812 - root / 2 + y / 2 - var z3d = 0.7071067812 * root + 0.7071067812 * y - var brightness = -x / 2 + root * 0.7071067812 + y / 2 + const root = Math.sqrt(1 - x2y2) + const x3d = x * 0.7071067812 + root / 2 - y / 2 + const y3d = x * 0.7071067812 - root / 2 + y / 2 + const z3d = 0.7071067812 * root + 0.7071067812 * y + let brightness = -x / 2 + root * 0.7071067812 + y / 2 if (brightness < 0) brightness = 0 - var r = Math.round(brightness * 127.5 * (1 - y3d)) - var g = Math.round(brightness * 127.5 * (x3d + 1)) - var b = Math.round(brightness * 127.5 * (z3d + 1)) + const r = Math.round(brightness * 127.5 * (1 - y3d)) + const g = Math.round(brightness * 127.5 * (x3d + 1)) + const b = Math.round(brightness * 127.5 * (z3d + 1)) return 'rgb(' + r + ', ' + g + ', ' + b + ')' } -var start = new Date() +const start = new Date() render(10) ctx.translate(243, 0) @@ -82,4 +82,4 @@ render(1) console.log('Rendered in %s seconds', (new Date() - start) / 1000) -canvas.pngStream().pipe(fs.createWriteStream(path.join(__dirname, 'ray.png'))) +canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'ray.png'))) diff --git a/examples/resize.js b/examples/resize.js index 9703b3240..f13a04451 100644 --- a/examples/resize.js +++ b/examples/resize.js @@ -1,27 +1,27 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var Image = Canvas.Image +const Image = Canvas.Image -var img = new Image() -var start = new Date() +const img = new Image() +const start = new Date() img.onerror = function (err) { throw err } img.onload = function () { - var width = 100 - var height = 100 - var canvas = new Canvas(width, height) - var ctx = canvas.getContext('2d') - var out = fs.createWriteStream(path.join(__dirname, 'resize.png')) + const width = 100 + const height = 100 + const canvas = Canvas.createCanvas(width, height) + const ctx = canvas.getContext('2d') + const out = fs.createWriteStream(path.join(__dirname, 'resize.png')) ctx.imageSmoothingEnabled = true ctx.drawImage(img, 0, 0, width, height) - canvas.pngStream().pipe(out) + canvas.createPNGStream().pipe(out) out.on('finish', function () { console.log('Resized and saved in %dms', new Date() - start) diff --git a/examples/small-pdf.js b/examples/small-pdf.js index 94d25d7bd..e01134976 100644 --- a/examples/small-pdf.js +++ b/examples/small-pdf.js @@ -1,11 +1,11 @@ -var fs = require('fs') -var Canvas = require('..') +const fs = require('fs') +const Canvas = require('..') -var canvas = new Canvas(400, 200, 'pdf') -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(400, 200, 'pdf') +const ctx = canvas.getContext('2d') -var y = 80 -var x = 50 +let y = 80 +let x = 50 ctx.font = '22px Helvetica' ctx.fillText('node-canvas pdf', x, y) diff --git a/examples/small-svg.js b/examples/small-svg.js index 992830c31..97799687e 100644 --- a/examples/small-svg.js +++ b/examples/small-svg.js @@ -1,11 +1,11 @@ -var fs = require('fs') -var Canvas = require('..') +const fs = require('fs') +const Canvas = require('..') -var canvas = new Canvas(400, 200, 'svg') -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(400, 200, 'svg') +const ctx = canvas.getContext('2d') -var y = 80 -var x = 50 +let y = 80 +let x = 50 ctx.font = '22px Helvetica' ctx.fillText('node-canvas SVG', x, y) diff --git a/examples/spark.js b/examples/spark.js index c6aafa960..a369473f4 100644 --- a/examples/spark.js +++ b/examples/spark.js @@ -1,25 +1,25 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = new Canvas(40, 15) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(40, 15) +const ctx = canvas.getContext('2d') function spark (ctx, data) { - var len = data.length - var pad = 1 - var width = ctx.canvas.width - var height = ctx.canvas.height - var barWidth = width / len - var max = Math.max.apply(null, data) + const len = data.length + const pad = 1 + const width = ctx.canvas.width + const height = ctx.canvas.height + const barWidth = width / len + const max = Math.max.apply(null, data) ctx.fillStyle = 'rgba(0,0,255,0.5)' ctx.strokeStyle = 'red' ctx.lineWidth = 1 data.forEach(function (n, i) { - var x = i * barWidth + pad - var y = height * (n / max) + const x = i * barWidth + pad + const y = height * (n / max) ctx.lineTo(x, height - y) ctx.fillRect(x, height, barWidth - pad, -y) diff --git a/examples/state.js b/examples/state.js index a8518382e..ce8eeb6d8 100644 --- a/examples/state.js +++ b/examples/state.js @@ -1,25 +1,25 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = new Canvas(150, 150) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(150, 150) +const ctx = canvas.getContext('2d') -ctx.fillRect(0, 0, 150, 150) // Draw a rectangle with default settings -ctx.save() // Save the default state +ctx.fillRect(0, 0, 150, 150) // Draw a rectangle with default settings +ctx.save() // Save the default state -ctx.fillStyle = '#09F' // Make changes to the settings +ctx.fillStyle = '#09F' // Make changes to the settings ctx.fillRect(15, 15, 120, 120) // Draw a rectangle with new settings -ctx.save() // Save the current state -ctx.fillStyle = '#FFF' // Make changes to the settings +ctx.save() // Save the current state +ctx.fillStyle = '#FFF' // Make changes to the settings ctx.globalAlpha = 0.5 -ctx.fillRect(30, 30, 90, 90) // Draw a rectangle with new settings +ctx.fillRect(30, 30, 90, 90) // Draw a rectangle with new settings -ctx.restore() // Restore previous state -ctx.fillRect(45, 45, 60, 60) // Draw a rectangle with restored settings +ctx.restore() // Restore previous state +ctx.fillRect(45, 45, 60, 60) // Draw a rectangle with restored settings -ctx.restore() // Restore original state -ctx.fillRect(60, 60, 30, 30) // Draw a rectangle with restored settings +ctx.restore() // Restore original state +ctx.fillRect(60, 60, 30, 30) // Draw a rectangle with restored settings canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'state.png'))) diff --git a/examples/text.js b/examples/text.js index 112741db4..04fbcdb8b 100644 --- a/examples/text.js +++ b/examples/text.js @@ -1,9 +1,9 @@ -var fs = require('fs') -var path = require('path') -var Canvas = require('..') +const fs = require('fs') +const path = require('path') +const Canvas = require('..') -var canvas = new Canvas(200, 200) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(200, 200) +const ctx = canvas.getContext('2d') ctx.globalAlpha = 0.2 @@ -30,7 +30,7 @@ ctx.strokeText('Wahoo', 50, 100) ctx.fillStyle = '#000' ctx.fillText('Wahoo', 49, 99) -var m = ctx.measureText('Wahoo') +const m = ctx.measureText('Wahoo') ctx.strokeStyle = '#f00' diff --git a/examples/voronoi.js b/examples/voronoi.js index 567d02b8a..07eecdfbc 100644 --- a/examples/voronoi.js +++ b/examples/voronoi.js @@ -1,25 +1,25 @@ -var http = require('http') -var Canvas = require('..') +const http = require('http') +const Canvas = require('..') -var canvas = new Canvas(1920, 1200) -var ctx = canvas.getContext('2d') +const canvas = Canvas.createCanvas(1920, 1200) +const ctx = canvas.getContext('2d') -var voronoiFactory = require('./rhill-voronoi-core-min') +const voronoiFactory = require('./rhill-voronoi-core-min') http.createServer(function (req, res) { - var x, y, v, iHalfedge + let x, y, v, iHalfedge - var voronoi = voronoiFactory() - var start = new Date() - var bbox = { xl: 0, xr: canvas.width, yt: 0, yb: canvas.height } + const voronoi = voronoiFactory() + const start = new Date() + const bbox = { xl: 0, xr: canvas.width, yt: 0, yb: canvas.height } - for (var i = 0; i < 340; i++) { + for (let i = 0; i < 340; i++) { x = Math.random() * canvas.width y = Math.random() * canvas.height voronoi.addSites([{ x: x, y: y }]) } - var diagram = voronoi.compute(bbox) + const diagram = voronoi.compute(bbox) ctx.beginPath() ctx.rect(0, 0, canvas.width, canvas.height) @@ -31,24 +31,24 @@ http.createServer(function (req, res) { ctx.strokeStyle = 'rgba(255,255,255,0.5)' ctx.lineWidth = 4 // edges - var edges = diagram.edges - var nEdges = edges.length + const edges = diagram.edges + const nEdges = edges.length - var sites = diagram.sites - var nSites = sites.length - for (var iSite = nSites - 1; iSite >= 0; iSite -= 1) { - var site = sites[iSite] + const sites = diagram.sites + const nSites = sites.length + for (let iSite = nSites - 1; iSite >= 0; iSite -= 1) { + const site = sites[iSite] ctx.rect(site.x - 0.5, site.y - 0.5, 1, 1) - var cell = diagram.cells[diagram.sites[iSite].id] + const cell = diagram.cells[diagram.sites[iSite].id] if (cell !== undefined) { - var halfedges = cell.halfedges - var nHalfedges = halfedges.length + const halfedges = cell.halfedges + const nHalfedges = halfedges.length if (nHalfedges < 3) return - var minx = canvas.width - var miny = canvas.height - var maxx = 0 - var maxy = 0 + let minx = canvas.width + let miny = canvas.height + let maxx = 0 + let maxy = 0 v = halfedges[0].getStartpoint() ctx.beginPath() @@ -63,35 +63,35 @@ http.createServer(function (req, res) { if (v.y > maxy) maxy = v.y } - var midx = (maxx + minx) / 2 - var midy = (maxy + miny) / 2 - var R = 0 + let midx = (maxx + minx) / 2 + let midy = (maxy + miny) / 2 + let R = 0 for (iHalfedge = 0; iHalfedge < nHalfedges; iHalfedge++) { v = halfedges[iHalfedge].getEndpoint() - var dx = v.x - site.x - var dy = v.y - site.y - var newR = Math.sqrt(dx * dx + dy * dy) + const dx = v.x - site.x + const dy = v.y - site.y + const newR = Math.sqrt(dx * dx + dy * dy) if (newR > R) R = newR } midx = site.x midy = site.y - var radgrad = ctx.createRadialGradient(midx + R * 0.3, midy - R * 0.3, 0, midx, midy, R) + const radgrad = ctx.createRadialGradient(midx + R * 0.3, midy - R * 0.3, 0, midx, midy, R) radgrad.addColorStop(0, '#09760b') radgrad.addColorStop(1.0, 'black') ctx.fillStyle = radgrad ctx.fill() - var radgrad2 = ctx.createRadialGradient(midx - R * 0.5, midy + R * 0.5, R * 0.1, midx, midy, R) + const radgrad2 = ctx.createRadialGradient(midx - R * 0.5, midy + R * 0.5, R * 0.1, midx, midy, R) radgrad2.addColorStop(0, 'rgba(255,255,255,0.5)') radgrad2.addColorStop(0.04, 'rgba(255,255,255,0.3)') radgrad2.addColorStop(0.05, 'rgba(255,255,255,0)') ctx.fillStyle = radgrad2 ctx.fill() - var lingrad = ctx.createLinearGradient(minx, site.y, minx + 100, site.y - 20) + const lingrad = ctx.createLinearGradient(minx, site.y, minx + 100, site.y - 20) lingrad.addColorStop(0.0, 'rgba(255,255,255,0.5)') lingrad.addColorStop(0.2, 'rgba(255,255,255,0.2)') lingrad.addColorStop(1.0, 'rgba(255,255,255,0)') @@ -101,11 +101,11 @@ http.createServer(function (req, res) { } if (nEdges) { - var edge + let edge ctx.beginPath() - for (var iEdge = nEdges - 1; iEdge >= 0; iEdge -= 1) { + for (let iEdge = nEdges - 1; iEdge >= 0; iEdge -= 1) { edge = edges[iEdge] v = edge.va ctx.moveTo(v.x, v.y) @@ -119,7 +119,7 @@ http.createServer(function (req, res) { canvas.toBuffer(function (err, buf) { if (err) throw err - var duration = new Date() - start + const duration = new Date() - start console.log('Rendered in %dms', duration) res.writeHead(200, { diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 000000000..27ab0c341 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,507 @@ +// TypeScript Version: 3.0 + +import { Readable } from 'stream' + +export interface PngConfig { + /** Specifies the ZLIB compression level. Defaults to 6. */ + compressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 + /** + * Any bitwise combination of `PNG_FILTER_NONE`, `PNG_FILTER_SUB`, + * `PNG_FILTER_UP`, `PNG_FILTER_AVG` and `PNG_FILTER_PATETH`; or one of + * `PNG_ALL_FILTERS` or `PNG_NO_FILTERS` (all are properties of the canvas + * instance). These specify which filters *may* be used by libpng. During + * encoding, libpng will select the best filter from this list of allowed + * filters. Defaults to `canvas.PNG_ALL_FILTERS`. + */ + filters?: number + /** + * _For creating indexed PNGs._ The palette of colors. Entries should be in + * RGBA order. + */ + palette?: Uint8ClampedArray + /** + * _For creating indexed PNGs._ The index of the background color. Defaults + * to 0. + */ + backgroundIndex?: number + /** pixels per inch */ + resolution?: number +} + +export interface JpegConfig { + /** Specifies the quality, between 0 and 1. Defaults to 0.75. */ + quality?: number + /** Enables progressive encoding. Defaults to `false`. */ + progressive?: boolean + /** Enables 2x2 chroma subsampling. Defaults to `true`. */ + chromaSubsampling?: boolean +} + +export interface PdfConfig { + title?: string + author?: string + subject?: string + keywords?: string + creator?: string + creationDate?: Date + modDate?: Date +} + +export interface NodeCanvasRenderingContext2DSettings { + alpha?: boolean + pixelFormat?: 'RGBA32' | 'RGB24' | 'A8' | 'RGB16_565' | 'A1' | 'RGB30' +} + +export class Canvas { + width: number + height: number + + /** _Non standard._ The type of the canvas. */ + readonly type: 'image'|'pdf'|'svg' + + /** _Non standard._ Getter. The stride used by the canvas. */ + readonly stride: number; + + /** Constant used in PNG encoding methods. */ + static readonly PNG_NO_FILTERS: number + /** Constant used in PNG encoding methods. */ + static readonly PNG_ALL_FILTERS: number + /** Constant used in PNG encoding methods. */ + static readonly PNG_FILTER_NONE: number + /** Constant used in PNG encoding methods. */ + static readonly PNG_FILTER_SUB: number + /** Constant used in PNG encoding methods. */ + static readonly PNG_FILTER_UP: number + /** Constant used in PNG encoding methods. */ + static readonly PNG_FILTER_AVG: number + /** Constant used in PNG encoding methods. */ + static readonly PNG_FILTER_PAETH: number + + constructor(width: number, height: number, type?: 'image'|'pdf'|'svg') + + getContext(contextId: '2d', contextAttributes?: NodeCanvasRenderingContext2DSettings): CanvasRenderingContext2D + + /** + * For image canvases, encodes the canvas as a PNG. For PDF canvases, + * encodes the canvas as a PDF. For SVG canvases, encodes the canvas as an + * SVG. + */ + toBuffer(cb: (err: Error|null, result: Buffer) => void): void + toBuffer(cb: (err: Error|null, result: Buffer) => void, mimeType: 'image/png', config?: PngConfig): void + toBuffer(cb: (err: Error|null, result: Buffer) => void, mimeType: 'image/jpeg', config?: JpegConfig): void + + /** + * For image canvases, encodes the canvas as a PNG. For PDF canvases, + * encodes the canvas as a PDF. For SVG canvases, encodes the canvas as an + * SVG. + */ + toBuffer(): Buffer + toBuffer(mimeType: 'image/png', config?: PngConfig): Buffer + toBuffer(mimeType: 'image/jpeg', config?: JpegConfig): Buffer + toBuffer(mimeType: 'application/pdf', config?: PdfConfig): Buffer + + /** + * Returns the unencoded pixel data, top-to-bottom. On little-endian (most) + * systems, the array will be ordered BGRA; on big-endian systems, it will + * be ARGB. + */ + toBuffer(mimeType: 'raw'): Buffer + + createPNGStream(config?: PngConfig): PNGStream + createJPEGStream(config?: JpegConfig): JPEGStream + createPDFStream(config?: PdfConfig): PDFStream + + /** Defaults to PNG image. */ + toDataURL(): string + toDataURL(mimeType: 'image/png'): string + toDataURL(mimeType: 'image/jpeg', quality?: number): string + /** _Non-standard._ Defaults to PNG image. */ + toDataURL(cb: (err: Error|null, result: string) => void): void + /** _Non-standard._ */ + toDataURL(mimeType: 'image/png', cb: (err: Error|null, result: string) => void): void + /** _Non-standard._ */ + toDataURL(mimeType: 'image/jpeg', cb: (err: Error|null, result: string) => void): void + /** _Non-standard._ */ + toDataURL(mimeType: 'image/jpeg', config: JpegConfig, cb: (err: Error|null, result: string) => void): void + /** _Non-standard._ */ + toDataURL(mimeType: 'image/jpeg', quality: number, cb: (err: Error|null, result: string) => void): void +} + +export interface TextMetrics { + readonly alphabeticBaseline: number; + readonly actualBoundingBoxAscent: number; + readonly actualBoundingBoxDescent: number; + readonly actualBoundingBoxLeft: number; + readonly actualBoundingBoxRight: number; + readonly emHeightAscent: number; + readonly emHeightDescent: number; + readonly fontBoundingBoxAscent: number; + readonly fontBoundingBoxDescent: number; + readonly width: number; +} + +export type CanvasFillRule = 'evenodd' | 'nonzero'; + +export type GlobalCompositeOperation = + | 'clear' + | 'copy' + | 'destination' + | 'source-over' + | 'destination-over' + | 'source-in' + | 'destination-in' + | 'source-out' + | 'destination-out' + | 'source-atop' + | 'destination-atop' + | 'xor' + | 'lighter' + | 'normal' + | 'multiply' + | 'screen' + | 'overlay' + | 'darken' + | 'lighten' + | 'color-dodge' + | 'color-burn' + | 'hard-light' + | 'soft-light' + | 'difference' + | 'exclusion' + | 'hue' + | 'saturation' + | 'color' + | 'luminosity' + | 'saturate'; + +export type CanvasLineCap = 'butt' | 'round' | 'square'; + +export type CanvasLineJoin = 'bevel' | 'miter' | 'round'; + +export type CanvasTextBaseline = 'alphabetic' | 'bottom' | 'hanging' | 'ideographic' | 'middle' | 'top'; + +export type CanvasTextAlign = 'center' | 'end' | 'left' | 'right' | 'start'; + +export class CanvasRenderingContext2D { + drawImage(image: Canvas|Image, dx: number, dy: number): void + drawImage(image: Canvas|Image, dx: number, dy: number, dw: number, dh: number): void + drawImage(image: Canvas|Image, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void + putImageData(imagedata: ImageData, dx: number, dy: number): void; + putImageData(imagedata: ImageData, dx: number, dy: number, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number): void; + getImageData(sx: number, sy: number, sw: number, sh: number): ImageData; + createImageData(sw: number, sh: number): ImageData; + createImageData(imagedata: ImageData): ImageData; + /** + * For PDF canvases, adds another page. If width and/or height are omitted, + * the canvas's initial size is used. + */ + addPage(width?: number, height?: number): void + save(): void; + restore(): void; + rotate(angle: number): void; + translate(x: number, y: number): void; + transform(a: number, b: number, c: number, d: number, e: number, f: number): void; + getTransform(): DOMMatrix; + resetTransform(): void; + setTransform(transform?: DOMMatrix): void; + setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void; + isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean; + scale(x: number, y: number): void; + clip(fillRule?: CanvasFillRule): void; + fill(fillRule?: CanvasFillRule): void; + stroke(): void; + fillText(text: string, x: number, y: number, maxWidth?: number): void; + strokeText(text: string, x: number, y: number, maxWidth?: number): void; + fillRect(x: number, y: number, w: number, h: number): void; + strokeRect(x: number, y: number, w: number, h: number): void; + clearRect(x: number, y: number, w: number, h: number): void; + rect(x: number, y: number, w: number, h: number): void; + roundRect(x: number, y: number, w: number, h: number, radii?: number | number[]): void; + measureText(text: string): TextMetrics; + moveTo(x: number, y: number): void; + lineTo(x: number, y: number): void; + bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void; + quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void; + beginPath(): void; + closePath(): void; + arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void; + arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void; + ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void; + setLineDash(segments: number[]): void; + getLineDash(): number[]; + createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): CanvasPattern + createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient; + createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient; + beginTag(tagName: string, attributes?: string): void; + endTag(tagName: string): void; + /** + * _Non-standard_. Defaults to 'good'. Affects pattern (gradient, image, + * etc.) rendering quality. + */ + patternQuality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear' + imageSmoothingEnabled: boolean; + globalCompositeOperation: GlobalCompositeOperation; + globalAlpha: number; + shadowColor: string; + miterLimit: number; + lineWidth: number; + lineCap: CanvasLineCap; + lineJoin: CanvasLineJoin; + lineDashOffset: number; + shadowOffsetX: number; + shadowOffsetY: number; + shadowBlur: number; + /** _Non-standard_. Sets the antialiasing mode. */ + antialias: 'default' | 'gray' | 'none' | 'subpixel' + /** + * Defaults to 'path'. The effect depends on the canvas type: + * + * * **Standard (image)** `'glyph'` and `'path'` both result in rasterized + * text. Glyph mode is faster than path, but may result in lower-quality + * text, especially when rotated or translated. + * + * * **PDF** `'glyph'` will embed text instead of paths into the PDF. This + * is faster to encode, faster to open with PDF viewers, yields a smaller + * file size and makes the text selectable. The subset of the font needed + * to render the glyphs will be embedded in the PDF. This is usually the + * mode you want to use with PDF canvases. + * + * * **SVG** glyph does not cause `` elements to be produced as one + * might expect ([cairo bug](https://gitlab.freedesktop.org/cairo/cairo/issues/253)). + * Rather, glyph will create a `` section with a `` for each + * glyph, then those glyphs be reused via `` elements. `'path'` mode + * creates a `` element for each text string. glyph mode is faster + * and yields a smaller file size. + * + * In glyph mode, `ctx.strokeText()` and `ctx.fillText()` behave the same + * (aside from using the stroke and fill style, respectively). + */ + textDrawingMode: 'path' | 'glyph' + /** + * _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to + * transformations affecting more than just patterns. + */ + quality: 'fast' | 'good' | 'best' | 'nearest' | 'bilinear' + /** Returns or sets a `DOMMatrix` for the current transformation matrix. */ + currentTransform: DOMMatrix + fillStyle: string | CanvasGradient | CanvasPattern; + strokeStyle: string | CanvasGradient | CanvasPattern; + font: string; + textBaseline: CanvasTextBaseline; + textAlign: CanvasTextAlign; + canvas: Canvas; + direction: 'ltr' | 'rtl'; + lang: string; +} + +export class CanvasGradient { + addColorStop(offset: number, color: string): void; +} + +export class CanvasPattern { + setTransform(transform?: DOMMatrix): void; +} + +// This does not extend HTMLImageElement because there are dozens of inherited +// methods and properties that we do not provide. +export class Image { + /** Track image data */ + static readonly MODE_IMAGE: number + /** Track MIME data */ + static readonly MODE_MIME: number + + /** + * The URL, `data:` URI or local file path of the image to be loaded, or a + * Buffer instance containing an encoded image. + */ + src: string | Buffer + /** Retrieves whether the object is fully loaded. */ + readonly complete: boolean + /** Sets or retrieves the height of the image. */ + height: number + /** Sets or retrieves the width of the image. */ + width: number + + /** The original height of the image resource before sizing. */ + readonly naturalHeight: number + /** The original width of the image resource before sizing. */ + readonly naturalWidth: number + /** + * Applies to JPEG images drawn to PDF canvases only. Setting + * `img.dataMode = Image.MODE_MIME` or `Image.MODE_MIME|Image.MODE_IMAGE` + * enables image MIME data tracking. When MIME data is tracked, PDF canvases + * can embed JPEGs directly into the output, rather than re-encoding into + * PNG. This can drastically reduce filesize and speed up rendering. + */ + dataMode: number + + onload: (() => void) | null; + onerror: ((err: Error) => void) | null; +} + +/** + * Creates a Canvas instance. This function works in both Node.js and Web + * browsers, where there is no Canvas constructor. + * @param type Optionally specify to create a PDF or SVG canvas. Defaults to an + * image canvas. + */ +export function createCanvas(width: number, height: number, type?: 'pdf'|'svg'): Canvas + +/** + * Creates an ImageData instance. This function works in both Node.js and Web + * browsers. + * @param data An array containing the pixel representation of the image. + * @param height If omitted, the height is calculated based on the array's size + * and `width`. + */ +export function createImageData(data: Uint8ClampedArray, width: number, height?: number): ImageData +/** + * _Non-standard._ Creates an ImageData instance for an alternative pixel + * format, such as RGB16_565 + * @param data An array containing the pixel representation of the image. + * @param height If omitted, the height is calculated based on the array's size + * and `width`. + */ +export function createImageData(data: Uint16Array, width: number, height?: number): ImageData +/** + * Creates an ImageData instance. This function works in both Node.js and Web + * browsers. + */ +export function createImageData(width: number, height: number): ImageData + +/** + * Convenience function for loading an image with a Promise interface. This + * function works in both Node.js and Web browsers; however, the `src` must be + * a string in Web browsers (it can only be a Buffer in Node.js). + * @param src URL, `data: ` URI or (Node.js only) a local file path or Buffer + * instance. + */ +export function loadImage(src: string|Buffer, options?: any): Promise + +/** + * Registers a font that is not installed as a system font. This must be used + * before creating Canvas instances. + * @param path Path to local font file. + * @param fontFace Description of the font face, corresponding to CSS properties + * used in `@font-face` rules. + */ +export function registerFont(path: string, fontFace: {family: string, weight?: string, style?: string}): void + +/** + * Unloads all fonts + */ +export function deregisterAllFonts(): void; + +/** This class must not be constructed directly; use `canvas.createPNGStream()`. */ +export class PNGStream extends Readable {} +/** This class must not be constructed directly; use `canvas.createJPEGStream()`. */ +export class JPEGStream extends Readable {} +/** This class must not be constructed directly; use `canvas.createPDFStream()`. */ +export class PDFStream extends Readable {} + +// TODO: this is wrong. See matrixTransform in lib/DOMMatrix.js +type DOMMatrixInit = DOMMatrix | string | number[]; + +interface DOMPointInit { + w?: number; + x?: number; + y?: number; + z?: number; +} + +export class DOMPoint { + w: number; + x: number; + y: number; + z: number; + matrixTransform(matrix?: DOMMatrixInit): DOMPoint; + toJSON(): any; + static fromPoint(other?: DOMPointInit): DOMPoint; +} + +export class DOMMatrix { + constructor(init?: string | number[]); + toString(): string; + multiply(other?: DOMMatrix): DOMMatrix; + multiplySelf(other?: DOMMatrix): DOMMatrix; + preMultiplySelf(other?: DOMMatrix): DOMMatrix; + translate(tx?: number, ty?: number, tz?: number): DOMMatrix; + translateSelf(tx?: number, ty?: number, tz?: number): DOMMatrix; + scale(scaleX?: number, scaleY?: number, scaleZ?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + scale3d(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + scale3dSelf(scale?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + scaleSelf(scaleX?: number, scaleY?: number, scaleZ?: number, originX?: number, originY?: number, originZ?: number): DOMMatrix; + /** + * @deprecated + */ + scaleNonUniform(scaleX?: number, scaleY?: number): DOMMatrix; + rotateFromVector(x?: number, y?: number): DOMMatrix; + rotateFromVectorSelf(x?: number, y?: number): DOMMatrix; + rotate(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix; + rotateSelf(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix; + rotateAxisAngle(x?: number, y?: number, z?: number, angle?: number): DOMMatrix; + rotateAxisAngleSelf(x?: number, y?: number, z?: number, angle?: number): DOMMatrix; + skewX(sx?: number): DOMMatrix; + skewXSelf(sx?: number): DOMMatrix; + skewY(sy?: number): DOMMatrix; + skewYSelf(sy?: number): DOMMatrix; + flipX(): DOMMatrix; + flipY(): DOMMatrix; + inverse(): DOMMatrix; + invertSelf(): DOMMatrix; + setMatrixValue(transformList: string): DOMMatrix; + transformPoint(point?: DOMPoint): DOMPoint; + toJSON(): any; + toFloat32Array(): Float32Array; + toFloat64Array(): Float64Array; + readonly is2D: boolean; + readonly isIdentity: boolean; + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; + m11: number; + m12: number; + m13: number; + m14: number; + m21: number; + m22: number; + m23: number; + m24: number; + m31: number; + m32: number; + m33: number; + m34: number; + m41: number; + m42: number; + m43: number; + m44: number; + static fromMatrix(other: DOMMatrix): DOMMatrix; + static fromFloat32Array(a: Float32Array): DOMMatrix; + static fromFloat64Array(a: Float64Array): DOMMatrix; +} + +export class ImageData { + constructor(sw: number, sh: number); + constructor(data: Uint8ClampedArray, sw: number, sh?: number); + readonly data: Uint8ClampedArray; + readonly height: number; + readonly width: number; +} + +// Not documented: backends + +/** Library version. */ +export const version: string +/** Cairo version. */ +export const cairoVersion: string +/** jpeglib version, if built with JPEG support. */ +export const jpegVersion: string | undefined +/** giflib version, if built with GIF support. */ +export const gifVersion: string | undefined +/** freetype version. */ +export const freetypeVersion: string +/** rsvg version. */ +export const rsvgVersion: string | undefined diff --git a/index.js b/index.js index a006b6107..adde4da12 100644 --- a/index.js +++ b/index.js @@ -1 +1,94 @@ -module.exports = require('./lib/canvas'); \ No newline at end of file +const Canvas = require('./lib/canvas') +const Image = require('./lib/image') +const CanvasRenderingContext2D = require('./lib/context2d') +const CanvasPattern = require('./lib/pattern') +const packageJson = require('./package.json') +const bindings = require('./lib/bindings') +const fs = require('fs') +const PNGStream = require('./lib/pngstream') +const PDFStream = require('./lib/pdfstream') +const JPEGStream = require('./lib/jpegstream') +const { DOMPoint, DOMMatrix } = require('./lib/DOMMatrix') + +bindings.setDOMMatrix(DOMMatrix) + +function createCanvas (width, height, type) { + return new Canvas(width, height, type) +} + +function createImageData (array, width, height) { + return new bindings.ImageData(array, width, height) +} + +function loadImage (src) { + return new Promise((resolve, reject) => { + const image = new Image() + + function cleanup () { + image.onload = null + image.onerror = null + } + + image.onload = () => { cleanup(); resolve(image) } + image.onerror = (err) => { cleanup(); reject(err) } + + image.src = src + }) +} + +/** + * Resolve paths for registerFont. Must be called *before* creating a Canvas + * instance. + * @param src {string} Path to font file. + * @param fontFace {{family: string, weight?: string, style?: string}} Object + * specifying font information. `weight` and `style` default to `"normal"`. + */ +function registerFont (src, fontFace) { + // TODO this doesn't need to be on Canvas; it should just be a static method + // of `bindings`. + return Canvas._registerFont(fs.realpathSync(src), fontFace) +} + +/** + * Unload all fonts from pango to free up memory + */ +function deregisterAllFonts () { + return Canvas._deregisterAllFonts() +} + +exports.Canvas = Canvas +exports.Context2d = CanvasRenderingContext2D // Legacy/compat export +exports.CanvasRenderingContext2D = CanvasRenderingContext2D +exports.CanvasGradient = bindings.CanvasGradient +exports.CanvasPattern = CanvasPattern +exports.Image = Image +exports.ImageData = bindings.ImageData +exports.PNGStream = PNGStream +exports.PDFStream = PDFStream +exports.JPEGStream = JPEGStream +exports.DOMMatrix = DOMMatrix +exports.DOMPoint = DOMPoint + +exports.registerFont = registerFont +exports.deregisterAllFonts = deregisterAllFonts + +exports.createCanvas = createCanvas +exports.createImageData = createImageData +exports.loadImage = loadImage + +exports.backends = bindings.Backends + +/** Library version. */ +exports.version = packageJson.version +/** Cairo version. */ +exports.cairoVersion = bindings.cairoVersion +/** jpeglib version. */ +exports.jpegVersion = bindings.jpegVersion +/** gif_lib version. */ +exports.gifVersion = bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined +/** freetype version. */ +exports.freetypeVersion = bindings.freetypeVersion +/** rsvg version. */ +exports.rsvgVersion = bindings.rsvgVersion +/** pango version. */ +exports.pangoVersion = bindings.pangoVersion diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 000000000..f898f2d58 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,53 @@ +import { expectAssignable, expectType } from 'tsd' +import * as path from 'path' +import { Readable } from 'stream' + +import * as Canvas from './index' + +Canvas.registerFont(path.join(__dirname, '../pfennigFont/Pfennig.ttf'), {family: 'pfennigFont'}) + +Canvas.createCanvas(5, 10) +Canvas.createCanvas(200, 200, 'pdf') +Canvas.createCanvas(150, 150, 'svg') + +const canv = Canvas.createCanvas(10, 10) +const ctx = canv.getContext('2d') +canv.getContext('2d', {alpha: false}) + +// LHS is ImageData, not Canvas.ImageData +const id = ctx.getImageData(0, 0, 10, 10) +expectType(id.height) +expectType(id.width) + +ctx.currentTransform = ctx.getTransform() + +ctx.quality = 'best' +ctx.textDrawingMode = 'glyph' + +const grad = ctx.createLinearGradient(0, 1, 2, 3) +expectType(grad) +grad.addColorStop(0.1, 'red') + +const dm = new Canvas.DOMMatrix([1, 2, 3, 4, 5, 6]) +expectType(dm.a) + +expectType(canv.toBuffer()) +expectType(canv.toBuffer('application/pdf')) +canv.toBuffer((err, data) => {}, 'image/png', {filters: Canvas.Canvas.PNG_ALL_FILTERS}) +expectAssignable(canv.createJPEGStream({ quality: 0.5 })) +expectAssignable(canv.createPDFStream({ author: 'octocat' })) +canv.toDataURL() + +const img = new Canvas.Image() +img.src = Buffer.alloc(0) +img.dataMode = Canvas.Image.MODE_IMAGE | Canvas.Image.MODE_MIME +img.onload = () => {} +img.onload = null + +const id2 = Canvas.createImageData(new Uint16Array(4), 1) +expectType(id2) +ctx.putImageData(id2, 0, 0) + +ctx.drawImage(canv, 0, 0) + +Canvas.deregisterAllFonts() diff --git a/lib/DOMMatrix.js b/lib/DOMMatrix.js new file mode 100644 index 000000000..97015adcf --- /dev/null +++ b/lib/DOMMatrix.js @@ -0,0 +1,678 @@ +'use strict' + +const util = require('util') + +// DOMMatrix per https://drafts.fxtf.org/geometry/#DOMMatrix + +class DOMPoint { + constructor (x, y, z, w) { + if (typeof x === 'object' && x !== null) { + w = x.w + z = x.z + y = x.y + x = x.x + } + this.x = typeof x === 'number' ? x : 0 + this.y = typeof y === 'number' ? y : 0 + this.z = typeof z === 'number' ? z : 0 + this.w = typeof w === 'number' ? w : 1 + } + + matrixTransform(init) { + // TODO: this next line is wrong. matrixTransform is supposed to only take + // an object with the DOMMatrix properties called DOMMatrixInit + const m = init instanceof DOMMatrix ? init : new DOMMatrix(init) + return m.transformPoint(this) + } + + toJSON() { + return { + x: this.x, + y: this.y, + z: this.z, + w: this.w + } + } + + static fromPoint(other) { + return new this(other.x, other.y, other.z, other.w) + } +} + +// Constants to index into _values (col-major) +const M11 = 0; const M12 = 1; const M13 = 2; const M14 = 3 +const M21 = 4; const M22 = 5; const M23 = 6; const M24 = 7 +const M31 = 8; const M32 = 9; const M33 = 10; const M34 = 11 +const M41 = 12; const M42 = 13; const M43 = 14; const M44 = 15 + +const DEGREE_PER_RAD = 180 / Math.PI +const RAD_PER_DEGREE = Math.PI / 180 + +function parseMatrix (init) { + let parsed = init.replace('matrix(', '') + parsed = parsed.split(',', 7) // 6 + 1 to handle too many params + if (parsed.length !== 6) throw new Error(`Failed to parse ${init}`) + parsed = parsed.map(parseFloat) + return [ + parsed[0], parsed[1], 0, 0, + parsed[2], parsed[3], 0, 0, + 0, 0, 1, 0, + parsed[4], parsed[5], 0, 1 + ] +} + +function parseMatrix3d (init) { + let parsed = init.replace('matrix3d(', '') + parsed = parsed.split(',', 17) // 16 + 1 to handle too many params + if (parsed.length !== 16) throw new Error(`Failed to parse ${init}`) + return parsed.map(parseFloat) +} + +function parseTransform (tform) { + const type = tform.split('(', 1)[0] + switch (type) { + case 'matrix': + return parseMatrix(tform) + case 'matrix3d': + return parseMatrix3d(tform) + // TODO This is supposed to support any CSS transform value. + default: + throw new Error(`${type} parsing not implemented`) + } +} + +class DOMMatrix { + constructor (init) { + this._is2D = true + this._values = new Float64Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]) + + let i + + if (typeof init === 'string') { // parse CSS transformList + if (init === '') return // default identity matrix + const tforms = init.split(/\)\s+/, 20).map(parseTransform) + if (tforms.length === 0) return + init = tforms[0] + for (i = 1; i < tforms.length; i++) init = multiply(tforms[i], init) + } + + i = 0 + if (init && init.length === 6) { + setNumber2D(this, M11, init[i++]) + setNumber2D(this, M12, init[i++]) + setNumber2D(this, M21, init[i++]) + setNumber2D(this, M22, init[i++]) + setNumber2D(this, M41, init[i++]) + setNumber2D(this, M42, init[i++]) + } else if (init && init.length === 16) { + setNumber2D(this, M11, init[i++]) + setNumber2D(this, M12, init[i++]) + setNumber3D(this, M13, init[i++]) + setNumber3D(this, M14, init[i++]) + setNumber2D(this, M21, init[i++]) + setNumber2D(this, M22, init[i++]) + setNumber3D(this, M23, init[i++]) + setNumber3D(this, M24, init[i++]) + setNumber3D(this, M31, init[i++]) + setNumber3D(this, M32, init[i++]) + setNumber3D(this, M33, init[i++]) + setNumber3D(this, M34, init[i++]) + setNumber2D(this, M41, init[i++]) + setNumber2D(this, M42, init[i++]) + setNumber3D(this, M43, init[i++]) + setNumber3D(this, M44, init[i]) + } else if (init !== undefined) { + throw new TypeError('Expected string or array.') + } + } + + toString () { + return this.is2D + ? `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})` + : `matrix3d(${this._values.join(', ')})` + } + + multiply (other) { + return newInstance(this._values).multiplySelf(other) + } + + multiplySelf (other) { + this._values = multiply(other._values, this._values) + if (!other.is2D) this._is2D = false + return this + } + + preMultiplySelf (other) { + this._values = multiply(this._values, other._values) + if (!other.is2D) this._is2D = false + return this + } + + translate (tx, ty, tz) { + return newInstance(this._values).translateSelf(tx, ty, tz) + } + + translateSelf (tx, ty, tz) { + if (typeof tx !== 'number') tx = 0 + if (typeof ty !== 'number') ty = 0 + if (typeof tz !== 'number') tz = 0 + this._values = multiply([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + tx, ty, tz, 1 + ], this._values) + if (tz !== 0) this._is2D = false + return this + } + + scale (scaleX, scaleY, scaleZ, originX, originY, originZ) { + return newInstance(this._values).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) + } + + scale3d (scale, originX, originY, originZ) { + return newInstance(this._values).scale3dSelf(scale, originX, originY, originZ) + } + + scale3dSelf (scale, originX, originY, originZ) { + return this.scaleSelf(scale, scale, scale, originX, originY, originZ) + } + + /** + * @deprecated + */ + scaleNonUniform(scaleX, scaleY) { + return this.scale(scaleX, scaleY) + } + + scaleSelf (scaleX, scaleY, scaleZ, originX, originY, originZ) { + // Not redundant with translate's checks because we need to negate the values later. + if (typeof originX !== 'number') originX = 0 + if (typeof originY !== 'number') originY = 0 + if (typeof originZ !== 'number') originZ = 0 + this.translateSelf(originX, originY, originZ) + if (typeof scaleX !== 'number') scaleX = 1 + if (typeof scaleY !== 'number') scaleY = scaleX + if (typeof scaleZ !== 'number') scaleZ = 1 + this._values = multiply([ + scaleX, 0, 0, 0, + 0, scaleY, 0, 0, + 0, 0, scaleZ, 0, + 0, 0, 0, 1 + ], this._values) + this.translateSelf(-originX, -originY, -originZ) + if (scaleZ !== 1 || originZ !== 0) this._is2D = false + return this + } + + rotateFromVector (x, y) { + return newInstance(this._values).rotateFromVectorSelf(x, y) + } + + rotateFromVectorSelf (x, y) { + if (typeof x !== 'number') x = 0 + if (typeof y !== 'number') y = 0 + const theta = (x === 0 && y === 0) ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD + return this.rotateSelf(theta) + } + + rotate (rotX, rotY, rotZ) { + return newInstance(this._values).rotateSelf(rotX, rotY, rotZ) + } + + rotateSelf (rotX, rotY, rotZ) { + if (rotY === undefined && rotZ === undefined) { + rotZ = rotX + rotX = rotY = 0 + } + if (typeof rotY !== 'number') rotY = 0 + if (typeof rotZ !== 'number') rotZ = 0 + if (rotX !== 0 || rotY !== 0) this._is2D = false + rotX *= RAD_PER_DEGREE + rotY *= RAD_PER_DEGREE + rotZ *= RAD_PER_DEGREE + let c, s + c = Math.cos(rotZ) + s = Math.sin(rotZ) + this._values = multiply([ + c, s, 0, 0, + -s, c, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values) + c = Math.cos(rotY) + s = Math.sin(rotY) + this._values = multiply([ + c, 0, -s, 0, + 0, 1, 0, 0, + s, 0, c, 0, + 0, 0, 0, 1 + ], this._values) + c = Math.cos(rotX) + s = Math.sin(rotX) + this._values = multiply([ + 1, 0, 0, 0, + 0, c, s, 0, + 0, -s, c, 0, + 0, 0, 0, 1 + ], this._values) + return this + } + + rotateAxisAngle (x, y, z, angle) { + return newInstance(this._values).rotateAxisAngleSelf(x, y, z, angle) + } + + rotateAxisAngleSelf (x, y, z, angle) { + if (typeof x !== 'number') x = 0 + if (typeof y !== 'number') y = 0 + if (typeof z !== 'number') z = 0 + // Normalize axis + const length = Math.sqrt(x * x + y * y + z * z) + if (length === 0) return this + if (length !== 1) { + x /= length + y /= length + z /= length + } + angle *= RAD_PER_DEGREE + const c = Math.cos(angle) + const s = Math.sin(angle) + const t = 1 - c + const tx = t * x + const ty = t * y + // NB: This is the generic transform. If the axis is a major axis, there are + // faster transforms. + this._values = multiply([ + tx * x + c, tx * y + s * z, tx * z - s * y, 0, + tx * y - s * z, ty * y + c, ty * z + s * x, 0, + tx * z + s * y, ty * z - s * x, t * z * z + c, 0, + 0, 0, 0, 1 + ], this._values) + if (x !== 0 || y !== 0) this._is2D = false + return this + } + + skewX (sx) { + return newInstance(this._values).skewXSelf(sx) + } + + skewXSelf (sx) { + if (typeof sx !== 'number') return this + const t = Math.tan(sx * RAD_PER_DEGREE) + this._values = multiply([ + 1, 0, 0, 0, + t, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values) + return this + } + + skewY (sy) { + return newInstance(this._values).skewYSelf(sy) + } + + skewYSelf (sy) { + if (typeof sy !== 'number') return this + const t = Math.tan(sy * RAD_PER_DEGREE) + this._values = multiply([ + 1, t, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values) + return this + } + + flipX () { + return newInstance(multiply([ + -1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values)) + } + + flipY () { + return newInstance(multiply([ + 1, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], this._values)) + } + + inverse () { + return newInstance(this._values).invertSelf() + } + + invertSelf () { + const m = this._values + const inv = m.map(v => 0) + + inv[0] = m[5] * m[10] * m[15] - + m[5] * m[11] * m[14] - + m[9] * m[6] * m[15] + + m[9] * m[7] * m[14] + + m[13] * m[6] * m[11] - + m[13] * m[7] * m[10] + + inv[4] = -m[4] * m[10] * m[15] + + m[4] * m[11] * m[14] + + m[8] * m[6] * m[15] - + m[8] * m[7] * m[14] - + m[12] * m[6] * m[11] + + m[12] * m[7] * m[10] + + inv[8] = m[4] * m[9] * m[15] - + m[4] * m[11] * m[13] - + m[8] * m[5] * m[15] + + m[8] * m[7] * m[13] + + m[12] * m[5] * m[11] - + m[12] * m[7] * m[9] + + inv[12] = -m[4] * m[9] * m[14] + + m[4] * m[10] * m[13] + + m[8] * m[5] * m[14] - + m[8] * m[6] * m[13] - + m[12] * m[5] * m[10] + + m[12] * m[6] * m[9] + + // If the determinant is zero, this matrix cannot be inverted, and all + // values should be set to NaN, with the is2D flag set to false. + + const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12] + + if (det === 0) { + this._values = m.map(v => NaN) + this._is2D = false + return this + } + + inv[1] = -m[1] * m[10] * m[15] + + m[1] * m[11] * m[14] + + m[9] * m[2] * m[15] - + m[9] * m[3] * m[14] - + m[13] * m[2] * m[11] + + m[13] * m[3] * m[10] + + inv[5] = m[0] * m[10] * m[15] - + m[0] * m[11] * m[14] - + m[8] * m[2] * m[15] + + m[8] * m[3] * m[14] + + m[12] * m[2] * m[11] - + m[12] * m[3] * m[10] + + inv[9] = -m[0] * m[9] * m[15] + + m[0] * m[11] * m[13] + + m[8] * m[1] * m[15] - + m[8] * m[3] * m[13] - + m[12] * m[1] * m[11] + + m[12] * m[3] * m[9] + + inv[13] = m[0] * m[9] * m[14] - + m[0] * m[10] * m[13] - + m[8] * m[1] * m[14] + + m[8] * m[2] * m[13] + + m[12] * m[1] * m[10] - + m[12] * m[2] * m[9] + + inv[2] = m[1] * m[6] * m[15] - + m[1] * m[7] * m[14] - + m[5] * m[2] * m[15] + + m[5] * m[3] * m[14] + + m[13] * m[2] * m[7] - + m[13] * m[3] * m[6] + + inv[6] = -m[0] * m[6] * m[15] + + m[0] * m[7] * m[14] + + m[4] * m[2] * m[15] - + m[4] * m[3] * m[14] - + m[12] * m[2] * m[7] + + m[12] * m[3] * m[6] + + inv[10] = m[0] * m[5] * m[15] - + m[0] * m[7] * m[13] - + m[4] * m[1] * m[15] + + m[4] * m[3] * m[13] + + m[12] * m[1] * m[7] - + m[12] * m[3] * m[5] + + inv[14] = -m[0] * m[5] * m[14] + + m[0] * m[6] * m[13] + + m[4] * m[1] * m[14] - + m[4] * m[2] * m[13] - + m[12] * m[1] * m[6] + + m[12] * m[2] * m[5] + + inv[3] = -m[1] * m[6] * m[11] + + m[1] * m[7] * m[10] + + m[5] * m[2] * m[11] - + m[5] * m[3] * m[10] - + m[9] * m[2] * m[7] + + m[9] * m[3] * m[6] + + inv[7] = m[0] * m[6] * m[11] - + m[0] * m[7] * m[10] - + m[4] * m[2] * m[11] + + m[4] * m[3] * m[10] + + m[8] * m[2] * m[7] - + m[8] * m[3] * m[6] + + inv[11] = -m[0] * m[5] * m[11] + + m[0] * m[7] * m[9] + + m[4] * m[1] * m[11] - + m[4] * m[3] * m[9] - + m[8] * m[1] * m[7] + + m[8] * m[3] * m[5] + + inv[15] = m[0] * m[5] * m[10] - + m[0] * m[6] * m[9] - + m[4] * m[1] * m[10] + + m[4] * m[2] * m[9] + + m[8] * m[1] * m[6] - + m[8] * m[2] * m[5] + + inv.forEach((v, i) => { inv[i] = v / det }) + this._values = inv + return this + } + + setMatrixValue (transformList) { + const temp = new DOMMatrix(transformList) + this._values = temp._values + this._is2D = temp._is2D + return this + } + + transformPoint (point) { + point = new DOMPoint(point) + const x = point.x + const y = point.y + const z = point.z + const w = point.w + const values = this._values + const nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w + const ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w + const nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w + const nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w + return new DOMPoint(nx, ny, nz, nw) + } + + toFloat32Array () { + return Float32Array.from(this._values) + } + + toFloat64Array () { + return this._values.slice(0) + } + + static fromMatrix (init) { + if (!(init instanceof DOMMatrix)) throw new TypeError('Expected DOMMatrix') + return new DOMMatrix(init._values) + } + + static fromFloat32Array (init) { + if (!(init instanceof Float32Array)) throw new TypeError('Expected Float32Array') + return new DOMMatrix(init) + } + + static fromFloat64Array (init) { + if (!(init instanceof Float64Array)) throw new TypeError('Expected Float64Array') + return new DOMMatrix(init) + } + + [util.inspect.custom || 'inspect'] (depth, options) { + if (depth < 0) return '[DOMMatrix]' + + return `DOMMatrix [ + a: ${this.a} + b: ${this.b} + c: ${this.c} + d: ${this.d} + e: ${this.e} + f: ${this.f} + m11: ${this.m11} + m12: ${this.m12} + m13: ${this.m13} + m14: ${this.m14} + m21: ${this.m21} + m22: ${this.m22} + m23: ${this.m23} + m23: ${this.m23} + m31: ${this.m31} + m32: ${this.m32} + m33: ${this.m33} + m34: ${this.m34} + m41: ${this.m41} + m42: ${this.m42} + m43: ${this.m43} + m44: ${this.m44} + is2D: ${this.is2D} + isIdentity: ${this.isIdentity} ]` + } +} + +/** + * Checks that `value` is a number and sets the value. + */ +function setNumber2D (receiver, index, value) { + if (typeof value !== 'number') throw new TypeError('Expected number') + return (receiver._values[index] = value) +} + +/** + * Checks that `value` is a number, sets `_is2D = false` if necessary and sets + * the value. + */ +function setNumber3D (receiver, index, value) { + if (typeof value !== 'number') throw new TypeError('Expected number') + if (index === M33 || index === M44) { + if (value !== 1) receiver._is2D = false + } else if (value !== 0) receiver._is2D = false + return (receiver._values[index] = value) +} + +Object.defineProperties(DOMMatrix.prototype, { + m11: { get () { return this._values[M11] }, set (v) { return setNumber2D(this, M11, v) } }, + m12: { get () { return this._values[M12] }, set (v) { return setNumber2D(this, M12, v) } }, + m13: { get () { return this._values[M13] }, set (v) { return setNumber3D(this, M13, v) } }, + m14: { get () { return this._values[M14] }, set (v) { return setNumber3D(this, M14, v) } }, + m21: { get () { return this._values[M21] }, set (v) { return setNumber2D(this, M21, v) } }, + m22: { get () { return this._values[M22] }, set (v) { return setNumber2D(this, M22, v) } }, + m23: { get () { return this._values[M23] }, set (v) { return setNumber3D(this, M23, v) } }, + m24: { get () { return this._values[M24] }, set (v) { return setNumber3D(this, M24, v) } }, + m31: { get () { return this._values[M31] }, set (v) { return setNumber3D(this, M31, v) } }, + m32: { get () { return this._values[M32] }, set (v) { return setNumber3D(this, M32, v) } }, + m33: { get () { return this._values[M33] }, set (v) { return setNumber3D(this, M33, v) } }, + m34: { get () { return this._values[M34] }, set (v) { return setNumber3D(this, M34, v) } }, + m41: { get () { return this._values[M41] }, set (v) { return setNumber2D(this, M41, v) } }, + m42: { get () { return this._values[M42] }, set (v) { return setNumber2D(this, M42, v) } }, + m43: { get () { return this._values[M43] }, set (v) { return setNumber3D(this, M43, v) } }, + m44: { get () { return this._values[M44] }, set (v) { return setNumber3D(this, M44, v) } }, + + a: { get () { return this.m11 }, set (v) { return (this.m11 = v) } }, + b: { get () { return this.m12 }, set (v) { return (this.m12 = v) } }, + c: { get () { return this.m21 }, set (v) { return (this.m21 = v) } }, + d: { get () { return this.m22 }, set (v) { return (this.m22 = v) } }, + e: { get () { return this.m41 }, set (v) { return (this.m41 = v) } }, + f: { get () { return this.m42 }, set (v) { return (this.m42 = v) } }, + + is2D: { get () { return this._is2D } }, // read-only + + isIdentity: { + get () { + const values = this._values + return (values[M11] === 1 && values[M12] === 0 && values[M13] === 0 && values[M14] === 0 && + values[M21] === 0 && values[M22] === 1 && values[M23] === 0 && values[M24] === 0 && + values[M31] === 0 && values[M32] === 0 && values[M33] === 1 && values[M34] === 0 && + values[M41] === 0 && values[M42] === 0 && values[M43] === 0 && values[M44] === 1) + } + }, + + toJSON: { + value() { + return { + a: this.a, + b: this.b, + c: this.c, + d: this.d, + e: this.e, + f: this.f, + m11: this.m11, + m12: this.m12, + m13: this.m13, + m14: this.m14, + m21: this.m21, + m22: this.m22, + m23: this.m23, + m23: this.m23, + m31: this.m31, + m32: this.m32, + m33: this.m33, + m34: this.m34, + m41: this.m41, + m42: this.m42, + m43: this.m43, + m44: this.m44, + is2D: this.is2D, + isIdentity: this.isIdentity, + } + } + } +}) + +/** + * Instantiates a DOMMatrix, bypassing the constructor. + * @param {Float64Array} values Value to assign to `_values`. This is assigned + * without copying (okay because all usages are followed by a multiply). + */ +function newInstance (values) { + const instance = Object.create(DOMMatrix.prototype) + instance.constructor = DOMMatrix + instance._is2D = true + instance._values = values + return instance +} + +function multiply (A, B) { + const dest = new Float64Array(16) + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + let sum = 0 + for (let k = 0; k < 4; k++) { + sum += A[i * 4 + k] * B[k * 4 + j] + } + dest[i * 4 + j] = sum + } + } + return dest +} + +module.exports = { DOMMatrix, DOMPoint } diff --git a/lib/bindings.js b/lib/bindings.js index c5c95b522..40cef3c69 100644 --- a/lib/bindings.js +++ b/lib/bindings.js @@ -1,3 +1,43 @@ -'use strict'; +'use strict' -module.exports = require('../build/Release/canvas'); +const bindings = require('../build/Release/canvas.node') + +module.exports = bindings + +Object.defineProperty(bindings.Canvas.prototype, Symbol.toStringTag, { + value: 'HTMLCanvasElement', + configurable: true +}) + +Object.defineProperty(bindings.Image.prototype, Symbol.toStringTag, { + value: 'HTMLImageElement', + configurable: true +}) + +bindings.ImageData.prototype.toString = function () { + return '[object ImageData]' +} + +Object.defineProperty(bindings.ImageData.prototype, Symbol.toStringTag, { + value: 'ImageData', + configurable: true +}) + +bindings.CanvasGradient.prototype.toString = function () { + return '[object CanvasGradient]' +} + +Object.defineProperty(bindings.CanvasGradient.prototype, Symbol.toStringTag, { + value: 'CanvasGradient', + configurable: true +}) + +Object.defineProperty(bindings.CanvasPattern.prototype, Symbol.toStringTag, { + value: 'CanvasPattern', + configurable: true +}) + +Object.defineProperty(bindings.CanvasRenderingContext2d.prototype, Symbol.toStringTag, { + value: 'CanvasRenderingContext2d', + configurable: true +}) diff --git a/lib/canvas.js b/lib/canvas.js index 8272c9b86..03fa1a959 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas @@ -6,216 +6,45 @@ * MIT Licensed */ -/** - * Module dependencies. - */ - -var canvas = require('./bindings') - , Canvas = canvas.Canvas - , Image = canvas.Image - , cairoVersion = canvas.cairoVersion - , Context2d = require('./context2d') - , PNGStream = require('./pngstream') - , PDFStream = require('./pdfstream') - , JPEGStream = require('./jpegstream') - , fs = require('fs') - , packageJson = require("../package.json") - , FORMATS = ['image/png', 'image/jpeg']; - -/** - * Export `Canvas` as the module. - */ - -var Canvas = exports = module.exports = Canvas; - -/** - * Library version. - */ - -exports.version = packageJson.version; - -/** - * Cairo version. - */ - -exports.cairoVersion = cairoVersion; +const bindings = require('./bindings') +const Canvas = module.exports = bindings.Canvas +const Context2d = require('./context2d') +const PNGStream = require('./pngstream') +const PDFStream = require('./pdfstream') +const JPEGStream = require('./jpegstream') +const FORMATS = ['image/png', 'image/jpeg'] +const util = require('util') -/** - * jpeglib version. - */ - -if (canvas.jpegVersion) { - exports.jpegVersion = canvas.jpegVersion; +// TODO || is for Node.js pre-v6.6.0 +Canvas.prototype[util.inspect.custom || 'inspect'] = function () { + return `[Canvas ${this.width}x${this.height}]` } -/** - * gif_lib version. - */ - -if (canvas.gifVersion) { - exports.gifVersion = canvas.gifVersion.replace(/[^.\d]/g, ''); -} - -/** - * freetype version. - */ - -if (canvas.freetypeVersion) { - exports.freetypeVersion = canvas.freetypeVersion; -} - -/** - * Expose constructors. - */ - -exports.Context2d = Context2d; -exports.PNGStream = PNGStream; -exports.PDFStream = PDFStream; -exports.JPEGStream = JPEGStream; -exports.Image = Image; -exports.ImageData = canvas.ImageData; - -/** - * Resolve paths for registerFont - */ - -Canvas.registerFont = function(src, fontFace){ - return Canvas._registerFont(fs.realpathSync(src), fontFace); -}; - -/** - * Context2d implementation. - */ - -require('./context2d'); - -/** - * Image implementation. - */ - -require('./image'); - -/** - * Inspect canvas. - * - * @return {String} - * @api public - */ - -Canvas.prototype.inspect = function(){ - return '[Canvas ' + this.width + 'x' + this.height + ']'; -}; - -/** - * Get a context object. - * - * @param {String} contextId - * @return {Context2d} - * @api public - */ - -Canvas.prototype.getContext = function(contextId){ - if ('2d' == contextId) { - var ctx = this._context2d || (this._context2d = new Context2d(this)); - this.context = ctx; - ctx.canvas = this; - return ctx; +Canvas.prototype.getContext = function (contextType, contextAttributes) { + if (contextType == '2d') { + const ctx = this._context2d || (this._context2d = new Context2d(this, contextAttributes)) + this.context = ctx + ctx.canvas = this + return ctx } -}; - -/** - * Create a `PNGStream` for `this` canvas. - * - * @return {PNGStream} - * @api public - */ +} Canvas.prototype.pngStream = -Canvas.prototype.createPNGStream = function(){ - return new PNGStream(this); -}; - -/** - * Create a synchronous `PNGStream` for `this` canvas. - * - * @return {PNGStream} - * @api public - */ - -Canvas.prototype.syncPNGStream = -Canvas.prototype.createSyncPNGStream = function(){ - return new PNGStream(this, true); -}; - -/** - * Create a `PDFStream` for `this` canvas. - * - * @return {PDFStream} - * @api public - */ +Canvas.prototype.createPNGStream = function (options) { + return new PNGStream(this, options) +} Canvas.prototype.pdfStream = -Canvas.prototype.createPDFStream = function(){ - return new PDFStream(this); -}; - -/** - * Create a synchronous `PDFStream` for `this` canvas. - * - * @return {PDFStream} - * @api public - */ - -Canvas.prototype.syncPDFStream = -Canvas.prototype.createSyncPDFStream = function(){ - return new PDFStream(this, true); -}; - -/** - * Create a `JPEGStream` for `this` canvas. - * - * @param {Object} options - * @return {JPEGStream} - * @api public - */ +Canvas.prototype.createPDFStream = function (options) { + return new PDFStream(this, options) +} Canvas.prototype.jpegStream = -Canvas.prototype.createJPEGStream = function(options){ - return this.createSyncJPEGStream(options); -}; - -/** - * Create a synchronous `JPEGStream` for `this` canvas. - * - * @param {Object} options - * @return {JPEGStream} - * @api public - */ - -Canvas.prototype.syncJPEGStream = -Canvas.prototype.createSyncJPEGStream = function(options){ - options = options || {}; - // Don't allow the buffer size to exceed the size of the canvas (#674) - var maxBufSize = this.width * this.height * 4; - var clampedBufSize = Math.min(options.bufsize || 4096, maxBufSize); - return new JPEGStream(this, { - bufsize: clampedBufSize - , quality: options.quality || 75 - , progressive: options.progressive || false - }); -}; - -/** - * Return a data url. Pass a function for async support (required for "image/jpeg"). - * - * @param {String} type, optional, one of "image/png" or "image/jpeg", defaults to "image/png" - * @param {Object|Number} encoderOptions, optional, options for jpeg compression (see documentation for Canvas#jpegStream) or the JPEG encoding quality from 0 to 1. - * @param {Function} fn, optional, callback for asynchronous operation. Required for type "image/jpeg". - * @return {String} data URL if synchronous (callback omitted) - * @api public - */ +Canvas.prototype.createJPEGStream = function (options) { + return new JPEGStream(this, options) +} -Canvas.prototype.toDataURL = function(a1, a2, a3){ +Canvas.prototype.toDataURL = function (a1, a2, a3) { // valid arg patterns (args -> [type, opts, fn]): // [] -> ['image/png', null, null] // [qual] -> ['image/png', null, null] @@ -230,67 +59,55 @@ Canvas.prototype.toDataURL = function(a1, a2, a3){ // ['image/jpeg', opts, fn] -> ['image/jpeg', opts, fn] // ['image/jpeg', qual, fn] -> ['image/jpeg', {quality: qual}, fn] // ['image/jpeg', undefined, fn] -> ['image/jpeg', null, fn] + // ['image/jpeg'] -> ['image/jpeg', null, fn] + // ['image/jpeg', opts] -> ['image/jpeg', opts, fn] + // ['image/jpeg', qual] -> ['image/jpeg', {quality: qual}, fn] - if (this.width === 0 || this.height === 0) { - // Per spec, if the bitmap has no pixels, return this string: - return "data:,"; - } + let type = 'image/png' + let opts = {} + let fn - var type = 'image/png'; - var opts = {}; - var fn; - - if ('function' === typeof a1) { - fn = a1; + if (typeof a1 === 'function') { + fn = a1 } else { - if ('string' === typeof a1 && FORMATS.indexOf(a1.toLowerCase()) !== -1) { - type = a1.toLowerCase(); + if (typeof a1 === 'string' && FORMATS.includes(a1.toLowerCase())) { + type = a1.toLowerCase() } - if ('function' === typeof a2) { - fn = a2; + if (typeof a2 === 'function') { + fn = a2 } else { - if ('object' === typeof a2) { - opts = a2; - } else if ('number' === typeof a2) { - opts = {quality: Math.max(0, Math.min(1, a2)) * 100}; + if (typeof a2 === 'object') { + opts = a2 + } else if (typeof a2 === 'number') { + opts = { quality: Math.max(0, Math.min(1, a2)) } } - if ('function' === typeof a3) { - fn = a3; + if (typeof a3 === 'function') { + fn = a3 } else if (undefined !== a3) { - throw new TypeError(typeof a3 + ' is not a function'); + throw new TypeError(`${typeof a3} is not a function`) } } } - if ('image/png' === type) { + if (this.width === 0 || this.height === 0) { + // Per spec, if the bitmap has no pixels, return this string: + const str = 'data:,' if (fn) { - this.toBuffer(function(err, buf){ - if (err) return fn(err); - fn(null, 'data:image/png;base64,' + buf.toString('base64')); - }); + setTimeout(() => fn(null, str)) + return } else { - return 'data:image/png;base64,' + this.toBuffer().toString('base64'); - } - - } else if ('image/jpeg' === type) { - if (undefined === fn) { - throw new Error('Missing required callback function for format "image/jpeg"'); + return str } + } - var stream = this.jpegStream(opts); - // note that jpegStream is synchronous - var buffers = []; - stream.on('data', function (chunk) { - buffers.push(chunk); - }); - stream.on('error', function (err) { - fn(err); - }); - stream.on('end', function() { - var result = 'data:image/jpeg;base64,' + Buffer.concat(buffers).toString('base64'); - fn(null, result); - }); + if (fn) { + this.toBuffer((err, buf) => { + if (err) return fn(err) + fn(null, `data:${type};base64,${buf.toString('base64')}`) + }, type, opts) + } else { + return `data:${type};base64,${this.toBuffer(type, opts).toString('base64')}` } -}; +} diff --git a/lib/context2d.js b/lib/context2d.js index 3ecb37922..103ec6325 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas - Context2d @@ -6,345 +6,6 @@ * MIT Licensed */ -/** - * Module dependencies. - */ - -var canvas = require('./bindings') - , Context2d = canvas.CanvasRenderingContext2d - , CanvasGradient = canvas.CanvasGradient - , CanvasPattern = canvas.CanvasPattern - , ImageData = canvas.ImageData; - -/** - * Export `Context2d` as the module. - */ - -var Context2d = exports = module.exports = Context2d; - -/** - * Cache color string RGBA values. - */ - -var cache = {}; - -/** - * Text baselines. - */ - -var baselines = ['alphabetic', 'top', 'bottom', 'middle', 'ideographic', 'hanging']; - -/** - * Font RegExp helpers. - */ - -var weights = 'normal|bold|bolder|lighter|[1-9]00' - , styles = 'normal|italic|oblique' - , units = 'px|pt|pc|in|cm|mm|%' - , string = '\'([^\']+)\'|"([^"]+)"|[\\w-]+'; - -/** - * Font parser RegExp; - */ - -var fontre = new RegExp('^ *' - + '(?:(' + weights + ') *)?' - + '(?:(' + styles + ') *)?' - + '([\\d\\.]+)(' + units + ') *' - + '((?:' + string + ')( *, *(?:' + string + '))*)' - ); - -/** - * Parse font `str`. - * - * @param {String} str - * @return {Object} - * @api private - */ - -var parseFont = exports.parseFont = function(str){ - var font = {} - , captures = fontre.exec(str); - - // Invalid - if (!captures) return; - - // Cached - if (cache[str]) return cache[str]; - - // Populate font object - font.weight = captures[1] || 'normal'; - font.style = captures[2] || 'normal'; - font.size = parseFloat(captures[3]); - font.unit = captures[4]; - font.family = captures[5].replace(/["']/g, '').split(',').map(function (family) { - return family.trim(); - }).join(','); - - // TODO: dpi - // TODO: remaining unit conversion - switch (font.unit) { - case 'pt': - font.size /= .75; - break; - case 'in': - font.size *= 96; - break; - case 'mm': - font.size *= 96.0 / 25.4; - break; - case 'cm': - font.size *= 96.0 / 2.54; - break; - } - - return cache[str] = font; -}; - -/** - * Enable or disable image smoothing. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('imageSmoothingEnabled', function(val){ - this._imageSmoothing = !! val; - this.patternQuality = val ? 'best' : 'fast'; -}); - -/** - * Get image smoothing value. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('imageSmoothingEnabled', function(val){ - return !! this._imageSmoothing; -}); - -/** - * Create a pattern from `Image` or `Canvas`. - * - * @param {Image|Canvas} image - * @param {String} repetition - * @return {CanvasPattern} - * @api public - */ - -Context2d.prototype.createPattern = function(image, repetition){ - // TODO Use repetition (currently always 'repeat') - return new CanvasPattern(image); -}; - -/** - * Create a linear gradient at the given point `(x0, y0)` and `(x1, y1)`. - * - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} x1 - * @param {Number} y1 - * @return {CanvasGradient} - * @api public - */ - -Context2d.prototype.createLinearGradient = function(x0, y0, x1, y1){ - return new CanvasGradient(x0, y0, x1, y1); -}; - -/** - * Create a radial gradient at the given point `(x0, y0)` and `(x1, y1)` - * and radius `r0` and `r1`. - * - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} r0 - * @param {Number} x1 - * @param {Number} y1 - * @param {Number} r1 - * @return {CanvasGradient} - * @api public - */ - -Context2d.prototype.createRadialGradient = function(x0, y0, r0, x1, y1, r1){ - return new CanvasGradient(x0, y0, r0, x1, y1, r1); -}; - -/** - * Reset transform matrix to identity, then apply the given args. - * - * @param {...} - * @api public - */ - -Context2d.prototype.setTransform = function(){ - this.resetTransform(); - this.transform.apply(this, arguments); -}; - -/** - * Set the fill style with the given css color string. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('fillStyle', function(val){ - if (!val) return; - if ('CanvasGradient' == val.constructor.name - || 'CanvasPattern' == val.constructor.name) { - this.lastFillStyle = val; - this._setFillPattern(val); - } else if ('string' == typeof val) { - this._setFillColor(val); - } -}); - -/** - * Get previous fill style. - * - * @return {CanvasGradient|String} - * @api public - */ - -Context2d.prototype.__defineGetter__('fillStyle', function(){ - return this.lastFillStyle || this.fillColor; -}); - -/** - * Set the stroke style with the given css color string. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('strokeStyle', function(val){ - if (!val) return; - if ('CanvasGradient' == val.constructor.name - || 'CanvasPattern' == val.constructor.name) { - this.lastStrokeStyle = val; - this._setStrokePattern(val); - } else if ('string' == typeof val) { - this._setStrokeColor(val); - } -}); - -/** - * Get previous stroke style. - * - * @return {CanvasGradient|String} - * @api public - */ - -Context2d.prototype.__defineGetter__('strokeStyle', function(){ - return this.lastStrokeStyle || this.strokeColor; -}); - -/** - * Set font. - * - * @see exports.parseFont() - * @api public - */ - -Context2d.prototype.__defineSetter__('font', function(val){ - if (!val) return; - if ('string' == typeof val) { - var font; - if (font = parseFont(val)) { - this.lastFontString = val; - this._setFont( - font.weight - , font.style - , font.size - , font.unit - , font.family); - } - } -}); - -/** - * Get the current font. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('font', function(){ - return this.lastFontString || '10px sans-serif'; -}); - -/** - * Set text baseline. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('textBaseline', function(val){ - if (!val) return; - var n = baselines.indexOf(val); - if (~n) { - this.lastBaseline = val; - this._setTextBaseline(n); - } -}); - -/** - * Get the current baseline setting. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('textBaseline', function(){ - return this.lastBaseline || 'alphabetic'; -}); - -/** - * Set text alignment. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('textAlign', function(val){ - switch (val) { - case 'center': - this._setTextAlignment(0); - this.lastTextAlignment = val; - break; - case 'left': - case 'start': - this._setTextAlignment(-1); - this.lastTextAlignment = val; - break; - case 'right': - case 'end': - this._setTextAlignment(1); - this.lastTextAlignment = val; - break; - } -}); - -/** - * Get the current font. - * - * @see exports.parseFont() - * @api public - */ - -Context2d.prototype.__defineGetter__('textAlign', function(){ - return this.lastTextAlignment || 'start'; -}); - -/** - * Create `ImageData` with the given dimensions or - * `ImageData` instance for dimensions. - * - * @param {Number|ImageData} width - * @param {Number} height - * @return {ImageData} - * @api public - */ +const bindings = require('./bindings') -Context2d.prototype.createImageData = function(width, height){ - if ('ImageData' == width.constructor.name) { - height = width.height; - width = width.width; - } - return new ImageData(new Uint8ClampedArray(width * height * 4), width, height); -}; +module.exports = bindings.CanvasRenderingContext2d diff --git a/lib/image.js b/lib/image.js index 932ad9012..a6c81ba83 100644 --- a/lib/image.js +++ b/lib/image.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas - Image @@ -10,52 +10,88 @@ * Module dependencies. */ -var Canvas = require('./bindings') - , Image = Canvas.Image; +const bindings = require('./bindings') +const Image = module.exports = bindings.Image +const util = require('util') -/** - * Src setter. - * - * - convert data uri to `Buffer` - * - * @param {String|Buffer} val filename, buffer, data uri - * @api public - */ +const { GetSource, SetSource } = bindings -Image.prototype.__defineSetter__('src', function(val){ - if ('string' == typeof val && 0 == val.indexOf('data:')) { - val = val.slice(val.indexOf(',') + 1); - this.source = new Buffer(val, 'base64'); - } else { - this.source = val; - } -}); +Object.defineProperty(Image.prototype, 'src', { + /** + * src setter. Valid values: + * * `data:` URI + * * Local file path + * * HTTP or HTTPS URL + * * Buffer containing image data (i.e. not a `data:` URI stored in a Buffer) + * + * @param {String|Buffer} val filename, buffer, data URI, URL + * @api public + */ + set (val) { + if (typeof val === 'string') { + if (/^\s*data:/.test(val)) { // data: URI + const commaI = val.indexOf(',') + // 'base64' must come before the comma + const isBase64 = val.lastIndexOf('base64', commaI) !== -1 + const content = val.slice(commaI + 1) + setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val) + } else if (/^\s*https?:\/\//.test(val)) { // remote URL + const onerror = err => { + if (typeof this.onerror === 'function') { + this.onerror(err) + } else { + throw err + } + } -/** - * Src getter. - * - * TODO: return buffer - * - * @api public - */ + fetch(val, { + method: 'GET', + headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36' } + }) + .then(res => { + if (!res.ok) { + throw new Error(`Server responded with ${res.status}`) + } + return res.arrayBuffer() + }) + .then(data => { + setSource(this, Buffer.from(data)) + }) + .catch(onerror) + } else { // local file path assumed + setSource(this, val) + } + } else if (Buffer.isBuffer(val)) { + setSource(this, val) + } else { + const err = new Error("Invalid image source") + if (typeof this.onerror === 'function') this.onerror(err) + else throw err + } + }, -Image.prototype.__defineGetter__('src', function(){ - return this.source; -}); + get () { + // TODO https://github.com/Automattic/node-canvas/issues/118 + return getSource(this) + }, -/** - * Inspect image. - * - * TODO: indicate that the .src was a buffer, data uri etc - * - * @return {String} - * @api public - */ + configurable: true +}) + +// TODO || is for Node.js pre-v6.6.0 +Image.prototype[util.inspect.custom || 'inspect'] = function () { + return '[Image' + + (this.complete ? ':' + this.width + 'x' + this.height : '') + + (this.src ? ' ' + this.src : '') + + (this.complete ? ' complete' : '') + + ']' +} + +function getSource (img) { + return img._originalSource || GetSource.call(img) +} -Image.prototype.inspect = function(){ - return '[Image' - + (this.complete ? ':' + this.width + 'x' + this.height : '') - + (this.src ? ' ' + this.src : '') - + (this.complete ? ' complete' : '') - + ']'; -}; +function setSource (img, src, origSrc) { + SetSource.call(img, src) + img._originalSource = origSrc +} diff --git a/lib/jpegstream.js b/lib/jpegstream.js index 24ca6f396..701d2f870 100644 --- a/lib/jpegstream.js +++ b/lib/jpegstream.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas - JPEGStream @@ -6,57 +6,36 @@ * MIT Licensed */ -/** - * Module dependencies. - */ +const { Readable } = require('stream') +function noop () {} -var Stream = require('stream').Stream; - -/** - * Initialize a `JPEGStream` with the given `canvas`. - * - * "data" events are emitted with `Buffer` chunks, once complete the - * "end" event is emitted. The following example will stream to a file - * named "./my.jpeg". - * - * var out = fs.createWriteStream(__dirname + '/my.jpeg') - * , stream = canvas.createJPEGStream(); - * - * stream.pipe(out); - * - * @param {Canvas} canvas - * @param {Boolean} sync - * @api public - */ +class JPEGStream extends Readable { + constructor (canvas, options) { + super() + + if (canvas.streamJPEGSync === undefined) { + throw new Error('node-canvas was built without JPEG support.') + } + + this.options = options + this.canvas = canvas + } -var JPEGStream = module.exports = function JPEGStream(canvas, options, sync) { - var self = this - , method = sync - ? 'streamJPEGSync' - : 'streamJPEG'; - this.options = options; - this.sync = sync; - this.canvas = canvas; - this.readable = true; - // TODO: implement async - if ('streamJPEG' == method) method = 'streamJPEGSync'; - process.nextTick(function(){ - canvas[method](options.bufsize, options.quality, options.progressive, function(err, chunk){ + _read () { + // For now we're not controlling the c++ code's data emission, so we only + // call canvas.streamJPEGSync once and let it emit data at will. + this._read = noop + + this.canvas.streamJPEGSync(this.options, (err, chunk) => { if (err) { - self.emit('error', err); - self.readable = false; + this.emit('error', err) } else if (chunk) { - self.emit('data', chunk); + this.push(chunk) } else { - self.emit('end'); - self.readable = false; + this.push(null) } - }); - }); + }) + } }; -/** - * Inherit from `EventEmitter`. - */ - -JPEGStream.prototype.__proto__ = Stream.prototype; +module.exports = JPEGStream diff --git a/lib/pattern.js b/lib/pattern.js new file mode 100644 index 000000000..fe5bbc300 --- /dev/null +++ b/lib/pattern.js @@ -0,0 +1,15 @@ +'use strict' + +/*! + * Canvas - CanvasPattern + * Copyright (c) 2010 LearnBoost + * MIT Licensed + */ + +const bindings = require('./bindings') + +module.exports = bindings.CanvasPattern + +bindings.CanvasPattern.prototype.toString = function () { + return '[object CanvasPattern]' +} diff --git a/lib/pdfstream.js b/lib/pdfstream.js index 92560ccbc..8643af75b 100644 --- a/lib/pdfstream.js +++ b/lib/pdfstream.js @@ -1,59 +1,35 @@ -'use strict'; +'use strict' /*! * Canvas - PDFStream */ -/** - * Module dependencies. - */ +const { Readable } = require('stream') +function noop () {} -var Stream = require('stream').Stream; +class PDFStream extends Readable { + constructor (canvas, options) { + super() -/** - * Initialize a `PDFStream` with the given `canvas`. - * - * "data" events are emitted with `Buffer` chunks, once complete the - * "end" event is emitted. The following example will stream to a file - * named "./my.pdf". - * - * var out = fs.createWriteStream(__dirname + '/my.pdf') - * , stream = canvas.createPDFStream(); - * - * stream.pipe(out); - * - * @param {Canvas} canvas - * @param {Boolean} sync - * @api public - */ + this.canvas = canvas + this.options = options + } + + _read () { + // For now we're not controlling the c++ code's data emission, so we only + // call canvas.streamPDFSync once and let it emit data at will. + this._read = noop -var PDFStream = module.exports = function PDFStream(canvas, sync) { - var self = this - , method = sync - ? 'streamPDFSync' - : 'streamPDF'; - this.sync = sync; - this.canvas = canvas; - this.readable = true; - // TODO: implement async - if ('streamPDF' == method) method = 'streamPDFSync'; - process.nextTick(function(){ - canvas[method](function(err, chunk, len){ + this.canvas.streamPDFSync((err, chunk, len) => { if (err) { - self.emit('error', err); - self.readable = false; + this.emit('error', err) } else if (len) { - self.emit('data', chunk, len); + this.push(chunk) } else { - self.emit('end'); - self.readable = false; + this.push(null) } - }); - }); -}; - -/** - * Inherit from `EventEmitter`. - */ + }, this.options) + } +} -PDFStream.prototype.__proto__ = Stream.prototype; +module.exports = PDFStream diff --git a/lib/pngstream.js b/lib/pngstream.js index 8a538d03a..db8fdb465 100644 --- a/lib/pngstream.js +++ b/lib/pngstream.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /*! * Canvas - PNGStream @@ -6,56 +6,37 @@ * MIT Licensed */ -/** - * Module dependencies. - */ +const { Readable } = require('stream') +function noop () {} -var Stream = require('stream').Stream; +class PNGStream extends Readable { + constructor (canvas, options) { + super() -/** - * Initialize a `PNGStream` with the given `canvas`. - * - * "data" events are emitted with `Buffer` chunks, once complete the - * "end" event is emitted. The following example will stream to a file - * named "./my.png". - * - * var out = fs.createWriteStream(__dirname + '/my.png') - * , stream = canvas.createPNGStream(); - * - * stream.pipe(out); - * - * @param {Canvas} canvas - * @param {Boolean} sync - * @api public - */ + if (options && + options.palette instanceof Uint8ClampedArray && + options.palette.length % 4 !== 0) { + throw new Error('Palette length must be a multiple of 4.') + } + this.canvas = canvas + this.options = options || {} + } + + _read () { + // For now we're not controlling the c++ code's data emission, so we only + // call canvas.streamPNGSync once and let it emit data at will. + this._read = noop -var PNGStream = module.exports = function PNGStream(canvas, sync) { - var self = this - , method = sync - ? 'streamPNGSync' - : 'streamPNG'; - this.sync = sync; - this.canvas = canvas; - this.readable = true; - // TODO: implement async - if ('streamPNG' == method) method = 'streamPNGSync'; - process.nextTick(function(){ - canvas[method](function(err, chunk, len){ + this.canvas.streamPNGSync((err, chunk, len) => { if (err) { - self.emit('error', err); - self.readable = false; + this.emit('error', err) } else if (len) { - self.emit('data', chunk, len); + this.push(chunk) } else { - self.emit('end'); - self.readable = false; + this.push(null) } - }); - }); -}; - -/** - * Inherit from `EventEmitter`. - */ + }, this.options) + } +} -PNGStream.prototype.__proto__ = Stream.prototype; +module.exports = PNGStream diff --git a/package.json b/package.json index 2ce52209a..3dd7e1329 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "1.6.0", + "version": "3.2.0", "author": "TJ Holowaychuk ", + "main": "index.js", + "browser": "browser.js", + "types": "index.d.ts", "contributors": [ "Nathan Rajlich ", "Rod Vagg ", @@ -23,22 +26,46 @@ "scripts": { "prebenchmark": "node-gyp build", "benchmark": "node benchmarks/run.js", - "pretest": "node-gyp build", - "test": "standard examples/*.js test/server.js test/public/*.js benchmark/run.js && mocha test/*.test.js", + "lint": "standard examples/*.js test/server.js test/public/*.js benchmarks/run.js lib/context2d.js util/has_lib.js browser.js index.js", + "test": "mocha test/*.test.js", "pretest-server": "node-gyp build", - "test-server": "node test/server.js" + "test-server": "node test/server.js", + "generate-wpt": "node ./test/wpt/generate.js", + "test-wpt": "mocha test/wpt/generated/*.js", + "install": "prebuild-install -r napi || node-gyp rebuild", + "tsd": "tsd" }, + "files": [ + "binding.gyp", + "browser.js", + "index.d.ts", + "index.js", + "lib/", + "src/", + "util/" + ], "dependencies": { - "nan": "^2.4.0" + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" }, "devDependencies": { - "express": "^4.14.0", - "mocha": "^3.1.2", - "standard": "^8.5.0" + "@types/node": "^10.12.18", + "assert-rejects": "^1.0.0", + "express": "^4.16.3", + "js-yaml": "^4.1.0", + "mocha": "^5.2.0", + "pixelmatch": "^4.0.2", + "standard": "^12.0.1", + "tsd": "^0.29.0", + "typescript": "^4.2.2" }, "engines": { - "node": ">=0.10.0" + "node": "^18.12.0 || >= 20.9.0" + }, + "binary": { + "napi_versions": [ + 7 + ] }, - "main": "./lib/canvas.js", "license": "MIT" } diff --git a/src/Backends.cc b/src/Backends.cc new file mode 100644 index 000000000..3a557669c --- /dev/null +++ b/src/Backends.cc @@ -0,0 +1,18 @@ +#include "Backends.h" + +#include "backend/ImageBackend.h" +#include "backend/PdfBackend.h" +#include "backend/SvgBackend.h" + +using namespace Napi; + +void +Backends::Initialize(Napi::Env env, Napi::Object exports) { + Napi::Object obj = Napi::Object::New(env); + + ImageBackend::Initialize(obj); + PdfBackend::Initialize(obj); + SvgBackend::Initialize(obj); + + exports.Set("Backends", obj); +} diff --git a/src/Backends.h b/src/Backends.h new file mode 100644 index 000000000..66a1c1db8 --- /dev/null +++ b/src/Backends.h @@ -0,0 +1,9 @@ +#pragma once + +#include "backend/Backend.h" +#include + +class Backends : public Napi::ObjectWrap { + public: + static void Initialize(Napi::Env env, Napi::Object exports); +}; diff --git a/src/Canvas.cc b/src/Canvas.cc index 89c1e5950..bc790add5 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -1,134 +1,186 @@ -// -// Canvas.cc -// // Copyright (c) 2010 LearnBoost -// +#include "Canvas.h" +#include "InstanceData.h" +#include // std::min #include -#include -#include -#include -#include -#include #include #include - -#include "Canvas.h" -#include "PNG.h" #include "CanvasRenderingContext2d.h" #include "closure.h" +#include +#include +#include +#include +#include "PNG.h" #include "register_font.h" +#include +#include +#include +#include +#include "Util.h" +#include +#include "node_buffer.h" +#include "FontParser.h" #ifdef HAVE_JPEG #include "JPEGStream.h" #endif +#include "backend/ImageBackend.h" +#include "backend/PdfBackend.h" +#include "backend/SvgBackend.h" + #define GENERIC_FACE_ERROR \ "The second argument to registerFont is required, and should be an object " \ "with at least a family (string) and optionally weight (string/number) " \ "and style (string)." -Nan::Persistent Canvas::constructor; +using namespace std; + +std::vector Canvas::font_face_list; + +// Increases each time a font is (de)registered +int Canvas::fontSerial = 1; /* * Initialize Canvas. */ void -Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; +Canvas::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + InstanceData* data = env.GetInstanceData(); // Constructor - Local ctor = Nan::New(Canvas::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("Canvas").ToLocalChecked()); - - // Prototype - Local proto = ctor->PrototypeTemplate(); - Nan::SetPrototypeMethod(ctor, "toBuffer", ToBuffer); - Nan::SetPrototypeMethod(ctor, "streamPNGSync", StreamPNGSync); - Nan::SetPrototypeMethod(ctor, "streamPDFSync", StreamPDFSync); + Napi::Function ctor = DefineClass(env, "Canvas", { + InstanceMethod<&Canvas::ToBuffer>("toBuffer", napi_default_method), + InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync", napi_default_method), + InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync", napi_default_method), #ifdef HAVE_JPEG - Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); + InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync", napi_default_method), #endif - Nan::SetAccessor(proto, Nan::New("type").ToLocalChecked(), GetType); - Nan::SetAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); - - Nan::SetTemplate(proto, "PNG_NO_FILTERS", Nan::New(PNG_NO_FILTERS)); - Nan::SetTemplate(proto, "PNG_FILTER_NONE", Nan::New(PNG_FILTER_NONE)); - Nan::SetTemplate(proto, "PNG_FILTER_SUB", Nan::New(PNG_FILTER_SUB)); - Nan::SetTemplate(proto, "PNG_FILTER_UP", Nan::New(PNG_FILTER_UP)); - Nan::SetTemplate(proto, "PNG_FILTER_AVG", Nan::New(PNG_FILTER_AVG)); - Nan::SetTemplate(proto, "PNG_FILTER_PAETH", Nan::New(PNG_FILTER_PAETH)); - Nan::SetTemplate(proto, "PNG_ALL_FILTERS", Nan::New(PNG_ALL_FILTERS)); - - // Class methods - Nan::SetMethod(ctor, "_registerFont", RegisterFont); - - Nan::Set(target, Nan::New("Canvas").ToLocalChecked(), ctor->GetFunction()); + InstanceAccessor<&Canvas::GetType>("type", napi_default_jsproperty), + InstanceAccessor<&Canvas::GetStride>("stride", napi_default_jsproperty), + InstanceAccessor<&Canvas::GetWidth, &Canvas::SetWidth>("width", napi_default_jsproperty), + InstanceAccessor<&Canvas::GetHeight, &Canvas::SetHeight>("height", napi_default_jsproperty), + StaticValue("PNG_NO_FILTERS", Napi::Number::New(env, PNG_NO_FILTERS), napi_default_jsproperty), + StaticValue("PNG_FILTER_NONE", Napi::Number::New(env, PNG_FILTER_NONE), napi_default_jsproperty), + StaticValue("PNG_FILTER_SUB", Napi::Number::New(env, PNG_FILTER_SUB), napi_default_jsproperty), + StaticValue("PNG_FILTER_UP", Napi::Number::New(env, PNG_FILTER_UP), napi_default_jsproperty), + StaticValue("PNG_FILTER_AVG", Napi::Number::New(env, PNG_FILTER_AVG), napi_default_jsproperty), + StaticValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH), napi_default_jsproperty), + StaticValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS), napi_default_jsproperty), + StaticMethod<&Canvas::RegisterFont>("_registerFont", napi_default_method), + StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method), + StaticMethod<&Canvas::ParseFont>("parseFont", napi_default_method) + }); + + data->CanvasCtor = Napi::Persistent(ctor); + exports.Set("Canvas", ctor); } /* * Initialize a Canvas with the given width and height. */ -NAN_METHOD(Canvas::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); +Canvas::Canvas(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + InstanceData* data = env.GetInstanceData(); + ctor = Napi::Persistent(data->CanvasCtor.Value()); + Backend* backend = NULL; + Napi::Object jsBackend; + + if (info[0].IsNumber()) { + Napi::Number width = info[0].As(); + Napi::Number height = Napi::Number::New(env, 0); + + if (info[1].IsNumber()) height = info[1].As(); + + if (info[2].IsString()) { + std::string str = info[2].As(); + if (str == "pdf") { + Napi::Maybe instance = data->PdfBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = PdfBackend::Unwrap(jsBackend = instance.Unwrap()); + } else if (str == "svg") { + Napi::Maybe instance = data->SvgBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = SvgBackend::Unwrap(jsBackend = instance.Unwrap()); + } else { + Napi::Maybe instance = data->ImageBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = ImageBackend::Unwrap(jsBackend = instance.Unwrap()); + } + } else { + Napi::Maybe instance = data->ImageBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = ImageBackend::Unwrap(jsBackend = instance.Unwrap()); + } + } else if (info[0].IsObject()) { + jsBackend = info[0].As(); + if (jsBackend.InstanceOf(data->ImageBackendCtor.Value()).UnwrapOr(false)) { + backend = ImageBackend::Unwrap(jsBackend); + } else if (jsBackend.InstanceOf(data->PdfBackendCtor.Value()).UnwrapOr(false)) { + backend = PdfBackend::Unwrap(jsBackend); + } else if (jsBackend.InstanceOf(data->SvgBackendCtor.Value()).UnwrapOr(false)) { + backend = SvgBackend::Unwrap(jsBackend); + } else { + Napi::TypeError::New(env, "Invalid arguments").ThrowAsJavaScriptException(); + return; + } + } else { + Napi::Number width = Napi::Number::New(env, 0); + Napi::Number height = Napi::Number::New(env, 0); + Napi::Maybe instance = data->ImageBackendCtor.New({ width, height }); + if (instance.IsJust()) backend = ImageBackend::Unwrap(jsBackend = instance.Unwrap()); + } + + backend->setCanvas(this); + + if (!backend->isSurfaceValid()) { + Napi::Error::New(env, backend->getError()).ThrowAsJavaScriptException(); + return; } - int width = 0, height = 0; - canvas_type_t type = CANVAS_TYPE_IMAGE; - if (info[0]->IsNumber()) width = info[0]->Uint32Value(); - if (info[1]->IsNumber()) height = info[1]->Uint32Value(); - if (info[2]->IsString()) type = !strcmp("pdf", *String::Utf8Value(info[2])) - ? CANVAS_TYPE_PDF - : !strcmp("svg", *String::Utf8Value(info[2])) - ? CANVAS_TYPE_SVG - : CANVAS_TYPE_IMAGE; - Canvas *canvas = new Canvas(width, height, type); - canvas->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + // Note: the backend gets destroyed when the jsBackend is GC'd. The cleaner + // way would be to only store the jsBackend and unwrap it when the c++ ref is + // needed, but that's slower and a burden. The _backend might be null if we + // returned early, but since an exception was thrown it gets destroyed soon. + _backend = backend; + _jsBackend = Napi::Persistent(jsBackend); } /* * Get type string. */ -NAN_GETTER(Canvas::GetType) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->isPDF() ? "pdf" : canvas->isSVG() ? "svg" : "image").ToLocalChecked()); +Napi::Value +Canvas::GetType(const Napi::CallbackInfo& info) { + return Napi::String::New(env, backend()->getName()); } /* * Get stride. */ -NAN_GETTER(Canvas::GetStride) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->stride())); +Napi::Value +Canvas::GetStride(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, stride()); } /* * Get width. */ -NAN_GETTER(Canvas::GetWidth) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->width)); +Napi::Value +Canvas::GetWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, getWidth()); } /* * Set width. */ -NAN_SETTER(Canvas::SetWidth) { - if (value->IsNumber()) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - canvas->width = value->Uint32Value(); - canvas->resurface(info.This()); +void +Canvas::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + backend()->setWidth(value.As().Uint32Value()); + resurface(info.This().As()); } } @@ -136,240 +188,323 @@ NAN_SETTER(Canvas::SetWidth) { * Get height. */ -NAN_GETTER(Canvas::GetHeight) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->height)); +Napi::Value +Canvas::GetHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, getHeight()); } /* * Set height. */ -NAN_SETTER(Canvas::SetHeight) { - if (value->IsNumber()) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - canvas->height = value->Uint32Value(); - canvas->resurface(info.This()); +void +Canvas::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + backend()->setHeight(value.As().Uint32Value()); + resurface(info.This().As()); } } /* - * Canvas::ToBuffer callback. + * EIO toBuffer callback. */ -static cairo_status_t -toBuffer(void *c, const uint8_t *data, unsigned len) { - closure_t *closure = (closure_t *) c; - - if (closure->len + len > closure->max_len) { - uint8_t *data; - unsigned max = closure->max_len; - - do { - max *= 2; - } while (closure->len + len > max); - - data = (uint8_t *) realloc(closure->data, max); - if (!data) return CAIRO_STATUS_NO_MEMORY; - closure->data = data; - closure->max_len = max; - } - - memcpy(closure->data + closure->len, data, len); - closure->len += len; +void +Canvas::ToPngBufferAsync(Closure* base) { + PngClosure* closure = static_cast(base); - return CAIRO_STATUS_SUCCESS; + closure->status = canvas_write_to_png_stream( + closure->canvas->surface(), + PngClosure::writeVec, + closure); } -/* - * EIO toBuffer callback. - */ - -#if NODE_VERSION_AT_LEAST(0, 6, 0) -void -Canvas::ToBufferAsync(uv_work_t *req) { -#elif NODE_VERSION_AT_LEAST(0, 5, 4) +#ifdef HAVE_JPEG void -Canvas::EIO_ToBuffer(eio_req *req) { -#else -int -Canvas::EIO_ToBuffer(eio_req *req) { +Canvas::ToJpegBufferAsync(Closure* base) { + JpegClosure* closure = static_cast(base); + write_to_jpeg_buffer(closure->canvas->surface(), closure); +} #endif - closure_t *closure = (closure_t *) req->data; - closure->status = canvas_write_to_png_stream( - closure->canvas->surface() - , toBuffer - , closure); +static void +parsePNGArgs(Napi::Value arg, PngClosure& pngargs) { + if (arg.IsObject()) { + Napi::Object obj = arg.As(); + Napi::Value cLevel; -#if !NODE_VERSION_AT_LEAST(0, 5, 4) - return 0; -#endif + if (obj.Get("compressionLevel").UnwrapTo(&cLevel) && cLevel.IsNumber()) { + uint32_t val = cLevel.As().Uint32Value(); + // See quote below from spec section 4.12.5.5. + if (val <= 9) pngargs.compressionLevel = val; + } + + Napi::Value rez; + if (obj.Get("resolution").UnwrapTo(&rez) && rez.IsNumber()) { + uint32_t val = rez.As().Uint32Value(); + if (val > 0) pngargs.resolution = val; + } + + Napi::Value filters; + if (obj.Get("filters").UnwrapTo(&filters) && filters.IsNumber()) { + pngargs.filters = filters.As().Uint32Value(); + } + + Napi::Value palette; + if (obj.Get("palette").UnwrapTo(&palette) && palette.IsTypedArray()) { + Napi::TypedArray palette_ta = palette.As(); + if (palette_ta.TypedArrayType() == napi_uint8_clamped_array) { + pngargs.nPaletteColors = palette_ta.ElementLength(); + if (pngargs.nPaletteColors % 4 != 0) { + throw "Palette length must be a multiple of 4."; + } + pngargs.palette = palette_ta.As().Data(); + pngargs.nPaletteColors /= 4; + // Optional background color index: + Napi::Value backgroundIndexVal; + if (obj.Get("backgroundIndex").UnwrapTo(&backgroundIndexVal) && backgroundIndexVal.IsNumber()) { + pngargs.backgroundIndex = backgroundIndexVal.As().Uint32Value(); + } + } + } + } } -/* - * EIO after toBuffer callback. - */ +#ifdef HAVE_JPEG +static void parseJPEGArgs(Napi::Value arg, JpegClosure& jpegargs) { + // "If Type(quality) is not Number, or if quality is outside that range, the + // user agent must use its default quality value, as if the quality argument + // had not been given." - 4.12.5.5 + if (arg.IsObject()) { + Napi::Object obj = arg.As(); + + Napi::Value qual; + if (obj.Get("quality").UnwrapTo(&qual) && qual.IsNumber()) { + double quality = qual.As().DoubleValue(); + if (quality >= 0.0 && quality <= 1.0) { + jpegargs.quality = static_cast(100.0 * quality); + } + } -#if NODE_VERSION_AT_LEAST(0, 6, 0) -void -Canvas::ToBufferAsyncAfter(uv_work_t *req) { -#else -int -Canvas::EIO_AfterToBuffer(eio_req *req) { -#endif + Napi::Value chroma; + if (obj.Get("chromaSubsampling").UnwrapTo(&chroma)) { + if (chroma.IsBoolean()) { + bool subsample = chroma.As().Value(); + jpegargs.chromaSubsampling = subsample ? 2 : 1; + } else if (chroma.IsNumber()) { + jpegargs.chromaSubsampling = chroma.As().Uint32Value(); + } + } - Nan::HandleScope scope; - closure_t *closure = (closure_t *) req->data; -#if NODE_VERSION_AT_LEAST(0, 6, 0) - delete req; -#else - ev_unref(EV_DEFAULT_UC); + Napi::Value progressive; + if (obj.Get("progressive").UnwrapTo(&progressive) && progressive.IsBoolean()) { + jpegargs.progressive = progressive.As().Value(); + } + } +} #endif - if (closure->status) { - Local argv[1] = { Canvas::Error(closure->status) }; - closure->pfn->Call(1, argv); - } else { - Local buf = Nan::CopyBuffer((char*)closure->data, closure->len).ToLocalChecked(); - memcpy(Buffer::Data(buf), closure->data, closure->len); - Local argv[2] = { Nan::Null(), buf }; - closure->pfn->Call(2, argv); +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + +static inline void setPdfMetaStr(cairo_surface_t* surf, Napi::Object opts, + cairo_pdf_metadata_t t, const char* propName) { + Napi::Value propValue; + if (opts.Get(propName).UnwrapTo(&propValue) && propValue.IsString()) { + // (copies char data) + cairo_pdf_surface_set_metadata(surf, t, propValue.As().Utf8Value().c_str()); } +} - closure->canvas->Unref(); - delete closure->pfn; - closure_destroy(closure); - free(closure); +static inline void setPdfMetaDate(cairo_surface_t* surf, Napi::Object opts, + cairo_pdf_metadata_t t, const char* propName) { + Napi::Value propValue; + if (opts.Get(propName).UnwrapTo(&propValue) && propValue.IsDate()) { + auto date = static_cast(propValue.As().ValueOf() / 1000); // ms -> s + char buf[sizeof "2011-10-08T07:07:09Z"]; + strftime(buf, sizeof buf, "%FT%TZ", gmtime(&date)); + cairo_pdf_surface_set_metadata(surf, t, buf); + } +} -#if !NODE_VERSION_AT_LEAST(0, 6, 0) - return 0; -#endif +static void setPdfMetadata(Canvas* canvas, Napi::Object opts) { + cairo_surface_t* surf = canvas->surface(); + + setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_TITLE, "title"); + setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_AUTHOR, "author"); + setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_SUBJECT, "subject"); + setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_KEYWORDS, "keywords"); + setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_CREATOR, "creator"); + setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_CREATE_DATE, "creationDate"); + setPdfMetaDate(surf, opts, CAIRO_PDF_METADATA_MOD_DATE, "modDate"); } +#endif // CAIRO 16+ + /* - * Convert PNG data to a node::Buffer, async when a - * callback function is passed. + * Converts/encodes data to a Buffer. Async when a callback function is passed. + + * PDF canvases: + (any) => Buffer + ("application/pdf", config) => Buffer + + * SVG canvases: + (any) => Buffer + + * ARGB data: + ("raw") => Buffer + + * PNG-encoded + () => Buffer + (undefined|"image/png", {compressionLevel?: number, filter?: number}) => Buffer + ((err: null|Error, buffer) => any) + ((err: null|Error, buffer) => any, undefined|"image/png", {compressionLevel?: number, filter?: number}) + + * JPEG-encoded + ("image/jpeg") => Buffer + ("image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) => Buffer + ((err: null|Error, buffer) => any, "image/jpeg") + ((err: null|Error, buffer) => any, "image/jpeg", {quality?: number, progressive?: Boolean, chromaSubsampling?: Boolean|number}) */ -NAN_METHOD(Canvas::ToBuffer) { +Napi::Value +Canvas::ToBuffer(const Napi::CallbackInfo& info) { cairo_status_t status; - uint32_t compression_level = 6; - uint32_t filter = PNG_ALL_FILTERS; - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - // TODO: async / move this out - if (canvas->isPDF() || canvas->isSVG()) { - cairo_surface_finish(canvas->surface()); - closure_t *closure = (closure_t *) canvas->closure(); + // Vector canvases, sync only + const std::string name = backend()->getName(); + if (name == "pdf" || name == "svg") { + // mime type may be present, but it's not checked + PdfSvgClosure* closure; + if (name == "pdf") { + closure = static_cast(backend())->closure(); +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + if (info[1].IsObject()) { // toBuffer("application/pdf", config) + setPdfMetadata(this, info[1].As()); + } +#endif // CAIRO 16+ + } else { + closure = static_cast(backend())->closure(); + } + + cairo_surface_t *surf = surface(); + cairo_surface_finish(surf); - Local buf = Nan::CopyBuffer((char*) closure->data, closure->len).ToLocalChecked(); - info.GetReturnValue().Set(buf); - return; + cairo_status_t status = cairo_surface_status(surf); + if (status != CAIRO_STATUS_SUCCESS) { + Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException(); + return env.Undefined(); + } + + return Napi::Buffer::Copy(env, &closure->vec[0], closure->vec.size()); } - if (info.Length() >= 1 && info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { - // Return raw ARGB data -- just a memcpy() - cairo_surface_t *surface = canvas->surface(); + // Raw ARGB data -- just a memcpy() + if (info[0].StrictEquals(Napi::String::New(env, "raw"))) { + cairo_surface_t *surface = this->surface(); cairo_surface_flush(surface); - const unsigned char *data = cairo_image_surface_get_data(surface); - Local buf = Nan::CopyBuffer(reinterpret_cast(data), canvas->nBytes()).ToLocalChecked(); - info.GetReturnValue().Set(buf); - return; + if (nBytes() > node::Buffer::kMaxLength) { + Napi::Error::New(env, "Data exceeds maximum buffer length.").ThrowAsJavaScriptException(); + return env.Undefined(); + } + return Napi::Buffer::Copy(env, cairo_image_surface_get_data(surface), nBytes()); } - if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) { - if (!info[1]->IsUndefined()) { - bool good = true; - if (info[1]->IsNumber()) { - compression_level = info[1]->Uint32Value(); - } else if (info[1]->IsString()) { - if (info[1]->StrictEquals(Nan::New("0").ToLocalChecked())) { - compression_level = 0; - } else { - uint32_t tmp = info[1]->Uint32Value(); - if (tmp == 0) { - good = false; - } else { - compression_level = tmp; - } - } - } else { - good = false; - } - - if (good) { - if (compression_level > 9) { - return Nan::ThrowRangeError("Allowed compression levels lie in the range [0, 9]."); - } - } else { - return Nan::ThrowTypeError("Compression level must be a number."); - } - } + // Sync PNG, default + if (info[0].IsUndefined() || info[0].StrictEquals(Napi::String::New(env, "image/png"))) { + try { + PngClosure closure(this); + parsePNGArgs(info[1], closure); + if (closure.nPaletteColors == 0xFFFFFFFF) { + Napi::Error::New(env, "Palette length must be a multiple of 4.").ThrowAsJavaScriptException(); + return env.Undefined(); + } - if (!info[2]->IsUndefined()) { - if (info[2]->IsUint32()) { - filter = info[2]->Uint32Value(); - } else { - return Nan::ThrowTypeError("Invalid filter value."); + status = canvas_write_to_png_stream(surface(), PngClosure::writeVec, &closure); + + if (!env.IsExceptionPending()) { + if (status) { + throw status; // TODO: throw in js? + } else { + // TODO it's possible to avoid this copy + return Napi::Buffer::Copy(env, &closure.vec[0], closure.vec.size()); + } } + } catch (cairo_status_t ex) { + CairoError(ex).ThrowAsJavaScriptException(); + } catch (const char* ex) { + Napi::Error::New(env, ex).ThrowAsJavaScriptException(); } - } - // Async - if (info[0]->IsFunction()) { - closure_t *closure = (closure_t *) malloc(sizeof(closure_t)); - status = closure_init(closure, canvas, compression_level, filter); + return env.Undefined(); + } - // ensure closure is ok - if (status) { - closure_destroy(closure); - free(closure); - return Nan::ThrowError(Canvas::Error(status)); + // Async PNG + if (info[0].IsFunction() && + (info[1].IsUndefined() || info[1].StrictEquals(Napi::String::New(env, "image/png")))) { + + PngClosure* closure; + try { + closure = new PngClosure(this); + parsePNGArgs(info[2], *closure); + } catch (cairo_status_t ex) { + CairoError(ex).ThrowAsJavaScriptException(); + return env.Undefined(); + } catch (const char* ex) { + Napi::Error::New(env, ex).ThrowAsJavaScriptException(); + return env.Undefined(); } - // TODO: only one callback fn in closure - canvas->Ref(); - closure->pfn = new Nan::Callback(info[0].As()); - -#if NODE_VERSION_AT_LEAST(0, 6, 0) - uv_work_t* req = new uv_work_t; - req->data = closure; - uv_queue_work(uv_default_loop(), req, ToBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); -#else - eio_custom(EIO_ToBuffer, EIO_PRI_DEFAULT, EIO_AfterToBuffer, closure); - ev_ref(EV_DEFAULT_UC); -#endif + Ref(); + closure->cb = Napi::Persistent(info[0].As()); - return; - // Sync - } else { - closure_t closure; - status = closure_init(&closure, canvas, compression_level, filter); + // Make sure the surface exists since we won't have an isolate context in the async block: + surface(); + EncodingWorker* worker = new EncodingWorker(env); + worker->Init(&ToPngBufferAsync, closure); + worker->Queue(); - // ensure closure is ok - if (status) { - closure_destroy(&closure); - return Nan::ThrowError(Canvas::Error(status)); + return env.Undefined(); + } + +#ifdef HAVE_JPEG + // Sync JPEG + Napi::Value jpegStr = Napi::String::New(env, "image/jpeg"); + if (info[0].StrictEquals(jpegStr)) { + try { + JpegClosure closure(this); + parseJPEGArgs(info[1], closure); + + write_to_jpeg_buffer(surface(), &closure); + + if (!env.IsExceptionPending()) { + // TODO it's possible to avoid this copy. + return Napi::Buffer::Copy(env, &closure.vec[0], closure.vec.size()); + } + } catch (cairo_status_t ex) { + CairoError(ex).ThrowAsJavaScriptException(); + return env.Undefined(); } + return env.Undefined(); + } - Nan::TryCatch try_catch; - status = canvas_write_to_png_stream(canvas->surface(), toBuffer, &closure); + // Async JPEG + if (info[0].IsFunction() && info[1].StrictEquals(jpegStr)) { + JpegClosure* closure = new JpegClosure(this); + parseJPEGArgs(info[2], *closure); - if (try_catch.HasCaught()) { - closure_destroy(&closure); - try_catch.ReThrow(); - return; - } else if (status) { - closure_destroy(&closure); - return Nan::ThrowError(Canvas::Error(status)); - } else { - Local buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked(); - closure_destroy(&closure); - info.GetReturnValue().Set(buf); - return; - } + Ref(); + closure->cb = Napi::Persistent(info[0].As()); + + // Make sure the surface exists since we won't have an isolate context in the async block: + surface(); + EncodingWorker* worker = new EncodingWorker(env); + worker->Init(&ToJpegBufferAsync, closure); + worker->Queue(); + return env.Undefined(); } +#endif + + return env.Undefined(); } /* @@ -378,98 +513,49 @@ NAN_METHOD(Canvas::ToBuffer) { static cairo_status_t streamPNG(void *c, const uint8_t *data, unsigned len) { - Nan::HandleScope scope; - closure_t *closure = (closure_t *) c; - Local buf = Nan::CopyBuffer((char *)data, len).ToLocalChecked(); - Local argv[3] = { - Nan::Null() - , buf - , Nan::New(len) }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)closure->fn, 3, argv); + PngClosure* closure = (PngClosure*) c; + Napi::Env env = closure->canvas->env; + Napi::HandleScope scope(env); + Napi::AsyncContext async(env, "canvas:StreamPNG"); + Napi::Value buf = Napi::Buffer::Copy(env, data, len); + closure->cb.MakeCallback(env.Global(), { env.Null(), buf, Napi::Number::New(env, len) }, async); return CAIRO_STATUS_SUCCESS; } /* - * Stream PNG data synchronously. + * Stream PNG data synchronously. TODO async + * StreamPngSync(this, options: {palette?: Uint8ClampedArray, backgroundIndex?: uint32, compressionLevel: uint32, filters: uint32}) */ -NAN_METHOD(Canvas::StreamPNGSync) { - uint32_t compression_level = 6; - uint32_t filter = PNG_ALL_FILTERS; - // TODO: async as well - if (!info[0]->IsFunction()) - return Nan::ThrowTypeError("callback function required"); - - if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) { - if (!info[1]->IsUndefined()) { - bool good = true; - if (info[1]->IsNumber()) { - compression_level = info[1]->Uint32Value(); - } else if (info[1]->IsString()) { - if (info[1]->StrictEquals(Nan::New("0").ToLocalChecked())) { - compression_level = 0; - } else { - uint32_t tmp = info[1]->Uint32Value(); - if (tmp == 0) { - good = false; - } else { - compression_level = tmp; - } - } - } else { - good = false; - } - - if (good) { - if (compression_level > 9) { - return Nan::ThrowRangeError("Allowed compression levels lie in the range [0, 9]."); - } - } else { - return Nan::ThrowTypeError("Compression level must be a number."); - } - } - - if (!info[2]->IsUndefined()) { - if (info[2]->IsUint32()) { - filter = info[2]->Uint32Value(); - } else { - return Nan::ThrowTypeError("Invalid filter value."); - } - } +void +Canvas::StreamPNGSync(const Napi::CallbackInfo& info) { + if (!info[0].IsFunction()) { + Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException(); + return; } + PngClosure closure(this); + parsePNGArgs(info[1], closure); - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - closure_t closure; - closure.fn = Local::Cast(info[0]); - closure.compression_level = compression_level; - closure.filter = filter; - - Nan::TryCatch try_catch; + closure.cb = Napi::Persistent(info[0].As()); - cairo_status_t status = canvas_write_to_png_stream(canvas->surface(), streamPNG, &closure); + cairo_status_t status = canvas_write_to_png_stream(surface(), streamPNG, &closure); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - return; - } else if (status) { - Local argv[1] = { Canvas::Error(status) }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)closure.fn, 1, argv); - } else { - Local argv[3] = { - Nan::Null() - , Nan::Null() - , Nan::New(0) }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)closure.fn, 1, argv); + if (!env.IsExceptionPending()) { + if (status) { + closure.cb.Call(env.Global(), { CairoError(status).Value() }); + } else { + closure.cb.Call(env.Global(), { env.Null(), env.Null(), Napi::Number::New(env, 0) }); + } } - return; } -/* - * Canvas::StreamPDF FreeCallback - */ -void stream_pdf_free(char *, void *) {} +struct PdfStreamInfo { + Napi::Function fn; + uint32_t len; + uint8_t* data; +}; /* * Canvas::StreamPDF callback. @@ -477,29 +563,29 @@ void stream_pdf_free(char *, void *) {} static cairo_status_t streamPDF(void *c, const uint8_t *data, unsigned len) { - Nan::HandleScope scope; - closure_t *closure = static_cast(c); - Local buf = Nan::NewBuffer(const_cast(reinterpret_cast(data)), len, stream_pdf_free, 0).ToLocalChecked(); - Local argv[3] = { - Nan::Null() - , buf - , Nan::New(len) }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), closure->fn, 3, argv); + PdfStreamInfo* streaminfo = static_cast(c); + Napi::Env env = streaminfo->fn.Env(); + Napi::HandleScope scope(env); + Napi::AsyncContext async(env, "canvas:StreamPDF"); + // TODO this is technically wrong, we're returning a pointer to the data in a + // vector in a class with automatic storage duration. If the canvas goes out + // of scope while we're in the handler, a use-after-free could happen. + Napi::Value buf = Napi::Buffer::New(env, (uint8_t *)(data), len); + streaminfo->fn.MakeCallback(env.Global(), { env.Null(), buf, Napi::Number::New(env, len) }, async); return CAIRO_STATUS_SUCCESS; } -cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, void *closure) { - closure_t *pdf_closure = static_cast(closure); - size_t whole_chunks = pdf_closure->len / PAGE_SIZE; - size_t remainder = pdf_closure->len - whole_chunks * PAGE_SIZE; +cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PdfStreamInfo* streaminfo) { + size_t whole_chunks = streaminfo->len / PAGE_SIZE; + size_t remainder = streaminfo->len - whole_chunks * PAGE_SIZE; for (size_t i = 0; i < whole_chunks; ++i) { - write_func(pdf_closure, &pdf_closure->data[i * PAGE_SIZE], PAGE_SIZE); + write_func(streaminfo, &streaminfo->data[i * PAGE_SIZE], PAGE_SIZE); } if (remainder) { - write_func(pdf_closure, &pdf_closure->data[whole_chunks * PAGE_SIZE], remainder); + write_func(streaminfo, &streaminfo->data[whole_chunks * PAGE_SIZE], remainder); } return CAIRO_STATUS_SUCCESS; @@ -509,37 +595,41 @@ cairo_status_t canvas_write_to_pdf_stream(cairo_surface_t *surface, cairo_write_ * Stream PDF data synchronously. */ -NAN_METHOD(Canvas::StreamPDFSync) { - if (!info[0]->IsFunction()) - return Nan::ThrowTypeError("callback function required"); - - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.Holder()); +void +Canvas::StreamPDFSync(const Napi::CallbackInfo& info) { + if (!info[0].IsFunction()) { + Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException(); + return; + } - if (!canvas->isPDF()) - return Nan::ThrowTypeError("wrong canvas type"); + if (backend()->getName() != "pdf") { + Napi::TypeError::New(env, "wrong canvas type").ThrowAsJavaScriptException(); + return; + } - cairo_surface_finish(canvas->surface()); +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + if (info[1].IsObject()) { + setPdfMetadata(this, info[1].As()); + } +#endif - closure_t closure; - closure.data = static_cast(canvas->closure())->data; - closure.len = static_cast(canvas->closure())->len; - closure.fn = info[0].As(); + cairo_surface_finish(surface()); - Nan::TryCatch try_catch; + PdfSvgClosure* closure = static_cast(backend())->closure(); + Napi::Function fn = info[0].As(); + PdfStreamInfo streaminfo; + streaminfo.fn = fn; + streaminfo.data = &closure->vec[0]; + streaminfo.len = closure->vec.size(); - cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &closure); + cairo_status_t status = canvas_write_to_pdf_stream(surface(), streamPDF, &streaminfo); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } else if (status) { - Local error = Canvas::Error(status); - Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), 1, &error); - } else { - Local argv[3] = { - Nan::Null() - , Nan::Null() - , Nan::New(0) }; - Nan::Call(closure.fn, Nan::GetCurrentContext()->Global(), 3, argv); + if (!env.IsExceptionPending()) { + if (status) { + fn.Call(env.Global(), { CairoError(status).Value() }); + } else { + fn.Call(env.Global(), { env.Null(), env.Null(), Napi::Number::New(env, 0) }); + } } } @@ -548,162 +638,158 @@ NAN_METHOD(Canvas::StreamPDFSync) { */ #ifdef HAVE_JPEG +static uint32_t getSafeBufSize(Canvas* canvas) { + // Don't allow the buffer size to exceed the size of the canvas (#674) + // TODO not sure if this is really correct, but it fixed #674 + return (std::min)(canvas->getWidth() * canvas->getHeight() * 4, static_cast(PAGE_SIZE)); +} -NAN_METHOD(Canvas::StreamJPEGSync) { - // TODO: async as well - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("buffer size required"); - if (!info[1]->IsNumber()) - return Nan::ThrowTypeError("quality setting required"); - if (!info[2]->IsBoolean()) - return Nan::ThrowTypeError("progressive setting required"); - if (!info[3]->IsFunction()) - return Nan::ThrowTypeError("callback function required"); - - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - closure_t closure; - closure.fn = Local::Cast(info[3]); +void +Canvas::StreamJPEGSync(const Napi::CallbackInfo& info) { + if (!info[1].IsFunction()) { + Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException(); + return; + } - Nan::TryCatch try_catch; - write_to_jpeg_stream(canvas->surface(), info[0]->NumberValue(), info[1]->NumberValue(), info[2]->BooleanValue(), &closure); + JpegClosure closure(this); + parseJPEGArgs(info[0], closure); + closure.cb = Napi::Persistent(info[1].As()); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } - return; + uint32_t bufsize = getSafeBufSize(this); + write_to_jpeg_stream(surface(), bufsize, &closure); } - #endif char * -str_value(Local val, const char *fallback, bool can_be_number) { - if (val->IsString() || (can_be_number && val->IsNumber())) { - return g_strdup(*String::Utf8Value(val)); - } else if (fallback) { - return g_strdup(fallback); - } else { - return NULL; +str_value(Napi::Maybe maybe, const char *fallback, bool can_be_number) { + Napi::Value val; + if (maybe.UnwrapTo(&val)) { + if (val.IsString() || (can_be_number && val.IsNumber())) { + Napi::String strVal; + if (val.ToString().UnwrapTo(&strVal)) return strdup(strVal.Utf8Value().c_str()); + } else if (fallback) { + return strdup(fallback); + } } + + return NULL; } -NAN_METHOD(Canvas::RegisterFont) { - if (!info[0]->IsString()) { - return Nan::ThrowError("Wrong argument type"); - } else if (!info[1]->IsObject()) { - return Nan::ThrowError(GENERIC_FACE_ERROR); +void +Canvas::RegisterFont(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (!info[0].IsString()) { + Napi::Error::New(env, "Wrong argument type").ThrowAsJavaScriptException(); + return; + } else if (!info[1].IsObject()) { + Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException(); + return; } - String::Utf8Value filePath(info[0]); - PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *) *filePath); + std::string filePath = info[0].As(); + PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *)(filePath.c_str())); - if (!sys_desc) return Nan::ThrowError("Could not parse font file"); + if (!sys_desc) { + Napi::Error::New(env, "Could not parse font file").ThrowAsJavaScriptException(); + return; + } PangoFontDescription *user_desc = pango_font_description_new(); // now check the attrs, there are many ways to be wrong - Local js_user_desc = info[1]->ToObject(); - Local family_prop = Nan::New("family").ToLocalChecked(); - Local weight_prop = Nan::New("weight").ToLocalChecked(); - Local style_prop = Nan::New("style").ToLocalChecked(); + Napi::Object js_user_desc = info[1].As(); - char *family = str_value(js_user_desc->Get(family_prop), NULL, false); - char *weight = str_value(js_user_desc->Get(weight_prop), "normal", true); - char *style = str_value(js_user_desc->Get(style_prop), "normal", false); + // TODO: use FontParser on these values just like the FontFace API works + char *family = str_value(js_user_desc.Get("family"), NULL, false); + char *weight = str_value(js_user_desc.Get("weight"), "normal", true); + char *style = str_value(js_user_desc.Get("style"), "normal", false); if (family && weight && style) { pango_font_description_set_weight(user_desc, Canvas::GetWeightFromCSSString(weight)); pango_font_description_set_style(user_desc, Canvas::GetStyleFromCSSString(style)); pango_font_description_set_family(user_desc, family); - std::vector::iterator it = _font_face_list.begin(); - FontFace *already_registered = NULL; - - for (; it != _font_face_list.end() && !already_registered; ++it) { - if (pango_font_description_equal(it->sys_desc, sys_desc)) { - already_registered = &(*it); - } - } + auto found = std::find_if(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) { + return pango_font_description_equal(f.sys_desc, sys_desc); + }); - if (already_registered) { - pango_font_description_free(already_registered->user_desc); - already_registered->user_desc = user_desc; - } else if (register_font((unsigned char *) *filePath)) { + if (found != font_face_list.end()) { + pango_font_description_free(found->user_desc); + found->user_desc = user_desc; + } else if (register_font((unsigned char *) filePath.c_str())) { FontFace face; face.user_desc = user_desc; face.sys_desc = sys_desc; - _font_face_list.push_back(face); + strncpy((char *)face.file_path, (char *) filePath.c_str(), 1023); + font_face_list.push_back(face); } else { pango_font_description_free(user_desc); - Nan::ThrowError("Could not load font to the system's font host"); + Napi::Error::New(env, "Could not load font to the system's font host").ThrowAsJavaScriptException(); + } } else { pango_font_description_free(user_desc); - Nan::ThrowError(GENERIC_FACE_ERROR); + if (!env.IsExceptionPending()) { + Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException(); + } } - g_free(family); - g_free(weight); - g_free(style); + free(family); + free(weight); + free(style); + fontSerial++; } -/* - * Initialize cairo surface. - */ - -Canvas::Canvas(int w, int h, canvas_type_t t): Nan::ObjectWrap() { - type = t; - width = w; - height = h; - _surface = NULL; - _closure = NULL; - - if (CANVAS_TYPE_PDF == t) { - _closure = malloc(sizeof(closure_t)); - assert(_closure); - cairo_status_t status = closure_init((closure_t *) _closure, this, 0, PNG_NO_FILTERS); - assert(status == CAIRO_STATUS_SUCCESS); - _surface = cairo_pdf_surface_create_for_stream(toBuffer, _closure, w, h); - } else if (CANVAS_TYPE_SVG == t) { - _closure = malloc(sizeof(closure_t)); - assert(_closure); - cairo_status_t status = closure_init((closure_t *) _closure, this, 0, PNG_NO_FILTERS); - assert(status == CAIRO_STATUS_SUCCESS); - _surface = cairo_svg_surface_create_for_stream(toBuffer, _closure, w, h); - } else { - _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); - assert(_surface); - Nan::AdjustExternalMemory(nBytes()); - } +void +Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + // Unload all fonts from pango to free up memory + bool success = true; + + std::for_each(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) { + if (!deregister_font( (unsigned char *)f.file_path )) success = false; + pango_font_description_free(f.user_desc); + pango_font_description_free(f.sys_desc); + }); + + font_face_list.clear(); + fontSerial++; + if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException(); } /* - * Destroy cairo surface. + * Do not use! This is only exported for testing */ +Napi::Value +Canvas::ParseFont(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); -Canvas::~Canvas() { - switch (type) { - case CANVAS_TYPE_PDF: - case CANVAS_TYPE_SVG: - cairo_surface_finish(_surface); - closure_destroy((closure_t *) _closure); - free(_closure); - cairo_surface_destroy(_surface); - break; - case CANVAS_TYPE_IMAGE: - int oldNBytes = nBytes(); - cairo_surface_destroy(_surface); - Nan::AdjustExternalMemory(-oldNBytes); - break; + if (info.Length() != 1) return env.Undefined(); + + Napi::String str; + if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined(); + + bool ok; + auto props = FontParser::parse(str, &ok); + if (!ok) return env.Undefined(); + + Napi::Object obj = Napi::Object::New(env); + obj.Set("size", Napi::Number::New(env, props.fontSize)); + Napi::Array families = Napi::Array::New(env); + obj.Set("families", families); + + unsigned int index = 0; + + for (auto& family : props.fontFamily) { + families[index++] = Napi::String::New(env, family); } -} -std::vector -_init_font_face_list() { - std::vector x; - return x; -} + obj.Set("weight", Napi::Number::New(env, props.fontWeight)); + obj.Set("variant", Napi::Number::New(env, static_cast(props.fontVariant))); + obj.Set("style", Napi::Number::New(env, static_cast(props.fontStyle))); -std::vector Canvas::_font_face_list = _init_font_face_list(); + return obj; +} /* * Get a PangoStyle from a CSS string (like "italic") @@ -766,40 +852,47 @@ Canvas::GetWeightFromCSSString(const char *weight) { PangoFontDescription * Canvas::ResolveFontDescription(const PangoFontDescription *desc) { - FontFace best; - PangoFontDescription *ret = NULL; - // One of the user-specified families could map to multiple SFNT family names // if someone registered two different fonts under the same family name. // https://drafts.csswg.org/css-fonts-3/#font-style-matching - char **families = g_strsplit(pango_font_description_get_family(desc), ",", -1); - GString *resolved_families = g_string_new(""); - - for (int i = 0; families[i]; ++i) { - GString *renamed_families = g_string_new(""); - std::vector::iterator it = _font_face_list.begin(); - - for (; it != _font_face_list.end(); ++it) { - if (g_ascii_strcasecmp(families[i], pango_font_description_get_family(it->user_desc)) == 0) { - if (renamed_families->len) g_string_append(renamed_families, ","); - g_string_append(renamed_families, pango_font_description_get_family(it->sys_desc)); - - if (i == 0 && (best.user_desc == NULL || pango_font_description_better_match(desc, best.user_desc, it->user_desc))) { - best = *it; + FontFace best; + istringstream families(pango_font_description_get_family(desc)); + unordered_set seen_families; + string resolved_families; + bool first = true; + + for (string family; getline(families, family, ','); ) { + string renamed_families; + for (auto& ff : font_face_list) { + string pangofamily = string(pango_font_description_get_family(ff.user_desc)); + if (streq_casein(family, pangofamily)) { + const char* sys_desc_family_name = pango_font_description_get_family(ff.sys_desc); + bool unseen = seen_families.find(sys_desc_family_name) == seen_families.end(); + bool better = best.user_desc == nullptr || pango_font_description_better_match(desc, best.user_desc, ff.user_desc); + + // Avoid sending duplicate SFNT font names due to a bug in Pango for macOS: + // https://bugzilla.gnome.org/show_bug.cgi?id=762873 + if (unseen) { + seen_families.insert(sys_desc_family_name); + + if (better) { + renamed_families = string(sys_desc_family_name) + (renamed_families.size() ? "," : "") + renamed_families; + } else { + renamed_families = renamed_families + (renamed_families.size() ? "," : "") + sys_desc_family_name; + } } + + if (first && better) best = ff; } } - if (resolved_families->len) g_string_append(resolved_families, ","); - g_string_append(resolved_families, renamed_families->len ? renamed_families->str : families[i]); - g_string_free(renamed_families, true); + if (resolved_families.size()) resolved_families += ','; + resolved_families += renamed_families.size() ? renamed_families : family; + first = false; } - ret = pango_font_description_copy(best.sys_desc ? best.sys_desc : desc); - pango_font_description_set_family_static(ret, resolved_families->str); - - g_strfreev(families); - g_string_free(resolved_families, false); + PangoFontDescription* ret = pango_font_description_copy(best.sys_desc ? best.sys_desc : desc); + pango_font_description_set_family(ret, resolved_families.c_str()); return ret; } @@ -809,54 +902,38 @@ Canvas::ResolveFontDescription(const PangoFontDescription *desc) { */ void -Canvas::resurface(Local canvas) { - Nan::HandleScope scope; - Local context; - switch (type) { - case CANVAS_TYPE_PDF: - cairo_pdf_surface_set_size(_surface, width, height); - break; - case CANVAS_TYPE_SVG: - // Re-surface - cairo_surface_finish(_surface); - closure_destroy((closure_t *) _closure); - cairo_surface_destroy(_surface); - closure_init((closure_t *) _closure, this, 0, PNG_NO_FILTERS); - _surface = cairo_svg_surface_create_for_stream(toBuffer, _closure, width, height); - - // Reset context - context = canvas->Get(Nan::New("context").ToLocalChecked()); - if (!context->IsUndefined()) { - Context2d *context2d = Nan::ObjectWrap::Unwrap(context->ToObject()); - cairo_t *prev = context2d->context(); - context2d->setContext(cairo_create(surface())); - cairo_destroy(prev); - } - break; - case CANVAS_TYPE_IMAGE: - // Re-surface - size_t oldNBytes = nBytes(); - cairo_surface_destroy(_surface); - _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); - Nan::AdjustExternalMemory(nBytes() - oldNBytes); - - // Reset context - context = canvas->Get(Nan::New("context").ToLocalChecked()); - if (!context->IsUndefined()) { - Context2d *context2d = Nan::ObjectWrap::Unwrap(context->ToObject()); - cairo_t *prev = context2d->context(); - context2d->setContext(cairo_create(surface())); - cairo_destroy(prev); - } - break; +Canvas::resurface(Napi::Object This) { + Napi::HandleScope scope(env); + Napi::Value context; + + if (This.Get("context").UnwrapTo(&context) && context.IsObject()) { + backend()->destroySurface(); + backend()->ensureSurface(); + // Reset context + Context2d *context2d = Context2d::Unwrap(context.As()); + cairo_t *prev = context2d->context(); + context2d->setContext(createCairoContext()); + context2d->resetState(); + cairo_destroy(prev); } } +/** + * Wrapper around cairo_create() + * (do not call cairo_create directly, call this instead) + */ +cairo_t* +Canvas::createCairoContext() { + cairo_t* ret = cairo_create(surface()); + cairo_set_line_width(ret, 1); // Cairo defaults to 2 + return ret; +} + /* * Construct an Error from the given cairo status. */ -Local -Canvas::Error(cairo_status_t status) { - return Exception::Error(Nan::New(cairo_status_to_string(status)).ToLocalChecked()); +Napi::Error +Canvas::CairoError(cairo_status_t status) { + return Napi::Error::New(env, cairo_status_to_string(status)); } diff --git a/src/Canvas.h b/src/Canvas.h index 9411a1863..7d0bc9d6d 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -1,44 +1,17 @@ - -// -// Canvas.h -// // Copyright (c) 2010 LearnBoost -// -#ifndef __NODE_CANVAS_H__ -#define __NODE_CANVAS_H__ +#pragma once + +struct Closure; -#include -#include -#include -#include +#include "backend/Backend.h" +#include "closure.h" +#include +#include "dll_visibility.h" +#include #include #include -#include -#include - - -using namespace node; -using namespace v8; - -/* - * Maxmimum states per context. - * TODO: remove/resize - */ - -#ifndef CANVAS_MAX_STATES -#define CANVAS_MAX_STATES 64 -#endif - -/* - * Canvas types. - */ - -typedef enum { - CANVAS_TYPE_IMAGE, - CANVAS_TYPE_PDF, - CANVAS_TYPE_SVG -} canvas_type_t; +#include /* * FontFace describes a font file in terms of one PangoFontDescription that @@ -46,66 +19,83 @@ typedef enum { */ class FontFace { public: - PangoFontDescription *sys_desc = NULL; - PangoFontDescription *user_desc = NULL; + PangoFontDescription *sys_desc = nullptr; + PangoFontDescription *user_desc = nullptr; + unsigned char file_path[1024]; +}; + +enum text_baseline_t : uint8_t { + TEXT_BASELINE_ALPHABETIC = 0, + TEXT_BASELINE_TOP = 1, + TEXT_BASELINE_BOTTOM = 2, + TEXT_BASELINE_MIDDLE = 3, + TEXT_BASELINE_IDEOGRAPHIC = 4, + TEXT_BASELINE_HANGING = 5 +}; + +enum text_align_t : int8_t { + TEXT_ALIGNMENT_LEFT = -1, + TEXT_ALIGNMENT_CENTER = 0, + TEXT_ALIGNMENT_RIGHT = 1, + TEXT_ALIGNMENT_START = -2, + TEXT_ALIGNMENT_END = 2 +}; + +enum canvas_draw_mode_t : uint8_t { + TEXT_DRAW_PATHS, + TEXT_DRAW_GLYPHS }; /* * Canvas. */ -class Canvas: public Nan::ObjectWrap { +class Canvas : public Napi::ObjectWrap { public: - int width; - int height; - canvas_type_t type; - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_METHOD(ToBuffer); - static NAN_GETTER(GetType); - static NAN_GETTER(GetStride); - static NAN_GETTER(GetWidth); - static NAN_GETTER(GetHeight); - static NAN_SETTER(SetWidth); - static NAN_SETTER(SetHeight); - static NAN_METHOD(StreamPNGSync); - static NAN_METHOD(StreamPDFSync); - static NAN_METHOD(StreamJPEGSync); - static NAN_METHOD(RegisterFont); - static Local Error(cairo_status_t status); -#if NODE_VERSION_AT_LEAST(0, 6, 0) - static void ToBufferAsync(uv_work_t *req); - static void ToBufferAsyncAfter(uv_work_t *req); -#else - static -#if NODE_VERSION_AT_LEAST(0, 5, 4) - void -#else - int -#endif - EIO_ToBuffer(eio_req *req); - static int EIO_AfterToBuffer(eio_req *req); -#endif + Canvas(const Napi::CallbackInfo& info); + static void Initialize(Napi::Env& env, Napi::Object& target); + + Napi::Value ToBuffer(const Napi::CallbackInfo& info); + Napi::Value GetType(const Napi::CallbackInfo& info); + Napi::Value GetStride(const Napi::CallbackInfo& info); + Napi::Value GetWidth(const Napi::CallbackInfo& info); + Napi::Value GetHeight(const Napi::CallbackInfo& info); + void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value); + void StreamPNGSync(const Napi::CallbackInfo& info); + void StreamPDFSync(const Napi::CallbackInfo& info); + void StreamJPEGSync(const Napi::CallbackInfo& info); + static void RegisterFont(const Napi::CallbackInfo& info); + static void DeregisterAllFonts(const Napi::CallbackInfo& info); + static Napi::Value ParseFont(const Napi::CallbackInfo& info); + Napi::Error CairoError(cairo_status_t status); + static void ToPngBufferAsync(Closure* closure); + static void ToJpegBufferAsync(Closure* closure); static PangoWeight GetWeightFromCSSString(const char *weight); static PangoStyle GetStyleFromCSSString(const char *style); static PangoFontDescription *ResolveFontDescription(const PangoFontDescription *desc); - inline bool isPDF(){ return CANVAS_TYPE_PDF == type; } - inline bool isSVG(){ return CANVAS_TYPE_SVG == type; } - inline cairo_surface_t *surface(){ return _surface; } - inline void *closure(){ return _closure; } - inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } - inline int stride(){ return cairo_image_surface_get_stride(_surface); } - inline int nBytes(){ return height * stride(); } - Canvas(int width, int height, canvas_type_t type); - void resurface(Local canvas); + DLL_PUBLIC inline Backend* backend() { return _backend; } + DLL_PUBLIC inline cairo_surface_t* surface(){ return backend()->ensureSurface(); } + cairo_t* createCairoContext(); + + DLL_PUBLIC inline uint8_t *data(){ return cairo_image_surface_get_data(surface()); } + DLL_PUBLIC inline int stride(){ return cairo_image_surface_get_stride(surface()); } + DLL_PUBLIC inline std::size_t nBytes(){ + return static_cast(backend()->getHeight()) * stride(); + } + + DLL_PUBLIC inline int getWidth() { return backend()->getWidth(); } + DLL_PUBLIC inline int getHeight() { return backend()->getHeight(); } + + void resurface(Napi::Object This); + + Napi::Env env; + static int fontSerial; private: - ~Canvas(); - cairo_surface_t *_surface; - void *_closure; - static std::vector _font_face_list; + Backend* _backend; + Napi::ObjectReference _jsBackend; + Napi::FunctionReference ctor; + static std::vector font_face_list; }; - -#endif diff --git a/src/CanvasError.h b/src/CanvasError.h new file mode 100644 index 000000000..535d153fa --- /dev/null +++ b/src/CanvasError.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +class CanvasError { + public: + std::string message; + std::string syscall; + std::string path; + int cerrno = 0; + void set(const char* iMessage = NULL, const char* iSyscall = NULL, int iErrno = 0, const char* iPath = NULL) { + if (iMessage) message.assign(iMessage); + if (iSyscall) syscall.assign(iSyscall); + cerrno = iErrno; + if (iPath) path.assign(iPath); + } + void reset() { + message.clear(); + syscall.clear(); + path.clear(); + cerrno = 0; + } + bool empty() { + return cerrno == 0 && message.empty(); + } + Napi::Error toError(Napi::Env env) { + if (cerrno) { + Napi::Error err = Napi::Error::New(env, strerror(cerrno)); + if (!syscall.empty()) err.Value().Set("syscall", syscall); + if (!path.empty()) err.Value().Set("path", path); + return err; + } else { + return Napi::Error::New(env, message); + } + } +}; diff --git a/src/CanvasGradient.cc b/src/CanvasGradient.cc index 8063b6dad..ceb0e5054 100644 --- a/src/CanvasGradient.cc +++ b/src/CanvasGradient.cc @@ -1,122 +1,113 @@ - -// -// Gradient.cc -// // Copyright (c) 2010 LearnBoost -// -#include "color.h" -#include "Canvas.h" #include "CanvasGradient.h" +#include "InstanceData.h" -Nan::Persistent Gradient::constructor; +#include "Canvas.h" +#include "color.h" + +using namespace Napi; /* * Initialize CanvasGradient. */ void -Gradient::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; - - // Constructor - Local ctor = Nan::New(Gradient::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("CanvasGradient").ToLocalChecked()); - - // Prototype - Nan::SetPrototypeMethod(ctor, "addColorStop", AddColorStop); - Nan::Set(target, Nan::New("CanvasGradient").ToLocalChecked(), ctor->GetFunction()); +Gradient::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + InstanceData* data = env.GetInstanceData(); + + Napi::Function ctor = DefineClass(env, "CanvasGradient", { + InstanceMethod<&Gradient::AddColorStop>("addColorStop", napi_default_method) + }); + + exports.Set("CanvasGradient", ctor); + data->CanvasGradientCtor = Napi::Persistent(ctor); } /* * Initialize a new CanvasGradient. */ -NAN_METHOD(Gradient::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - +Gradient::Gradient(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { // Linear - if (4 == info.Length()) { - Gradient *grad = new Gradient( - info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue()); - grad->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + if ( + 4 == info.Length() && + info[0].IsNumber() && + info[1].IsNumber() && + info[2].IsNumber() && + info[3].IsNumber() + ) { + double x0 = info[0].As().DoubleValue(); + double y0 = info[1].As().DoubleValue(); + double x1 = info[2].As().DoubleValue(); + double y1 = info[3].As().DoubleValue(); + _pattern = cairo_pattern_create_linear(x0, y0, x1, y1); return; } // Radial - if (6 == info.Length()) { - Gradient *grad = new Gradient( - info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue() - , info[4]->NumberValue() - , info[5]->NumberValue()); - grad->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + if ( + 6 == info.Length() && + info[0].IsNumber() && + info[1].IsNumber() && + info[2].IsNumber() && + info[3].IsNumber() && + info[4].IsNumber() && + info[5].IsNumber() + ) { + double x0 = info[0].As().DoubleValue(); + double y0 = info[1].As().DoubleValue(); + double r0 = info[2].As().DoubleValue(); + double x1 = info[3].As().DoubleValue(); + double y1 = info[4].As().DoubleValue(); + double r1 = info[5].As().DoubleValue(); + _pattern = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); return; } - return Nan::ThrowTypeError("invalid arguments"); + Napi::TypeError::New(env, "invalid arguments").ThrowAsJavaScriptException(); } /* * Add color stop. */ -NAN_METHOD(Gradient::AddColorStop) { - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("offset required"); - if (!info[1]->IsString()) - return Nan::ThrowTypeError("color string required"); +void +Gradient::AddColorStop(const Napi::CallbackInfo& info) { + if (!info[0].IsNumber()) { + Napi::TypeError::New(env, "offset required").ThrowAsJavaScriptException(); + return; + } + + if (!info[1].IsString()) { + Napi::TypeError::New(env, "color string required").ThrowAsJavaScriptException(); + return; + } - Gradient *grad = Nan::ObjectWrap::Unwrap(info.This()); short ok; - String::Utf8Value str(info[1]); - uint32_t rgba = rgba_from_string(*str, &ok); + std::string str = info[1].As(); + uint32_t rgba = rgba_from_string(str.c_str(), &ok); if (ok) { rgba_t color = rgba_create(rgba); cairo_pattern_add_color_stop_rgba( - grad->pattern() - , info[0]->NumberValue() + _pattern + , info[0].As().DoubleValue() , color.r , color.g , color.b , color.a); } else { - return Nan::ThrowTypeError("parse color failed"); + Napi::TypeError::New(env, "parse color failed").ThrowAsJavaScriptException(); } } -/* - * Initialize linear gradient. - */ - -Gradient::Gradient(double x0, double y0, double x1, double y1) { - _pattern = cairo_pattern_create_linear(x0, y0, x1, y1); -} - -/* - * Initialize radial gradient. - */ - -Gradient::Gradient(double x0, double y0, double r0, double x1, double y1, double r1) { - _pattern = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); -} /* * Destroy the pattern. */ Gradient::~Gradient() { - cairo_pattern_destroy(_pattern); + if (_pattern) cairo_pattern_destroy(_pattern); } diff --git a/src/CanvasGradient.h b/src/CanvasGradient.h index 2459eb60d..103e80748 100644 --- a/src/CanvasGradient.h +++ b/src/CanvasGradient.h @@ -1,28 +1,20 @@ - -// -// CanvasGradient.h -// // Copyright (c) 2010 LearnBoost -// -#ifndef __NODE_GRADIENT_H__ -#define __NODE_GRADIENT_H__ +#pragma once -#include "Canvas.h" +#include +#include -class Gradient: public Nan::ObjectWrap { +class Gradient : public Napi::ObjectWrap { public: - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_METHOD(AddColorStop); - Gradient(double x0, double y0, double x1, double y1); - Gradient(double x0, double y0, double r0, double x1, double y1, double r1); + static void Initialize(Napi::Env& env, Napi::Object& target); + Gradient(const Napi::CallbackInfo& info); + void AddColorStop(const Napi::CallbackInfo& info); inline cairo_pattern_t *pattern(){ return _pattern; } + ~Gradient(); + + Napi::Env env; private: - ~Gradient(); cairo_pattern_t *_pattern; }; - -#endif diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index ed64f9ae7..ec30b6f09 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -1,80 +1,123 @@ - -// -// Pattern.cc -// // Copyright (c) 2010 LearnBoost -// + +#include "CanvasPattern.h" #include "Canvas.h" #include "Image.h" -#include "CanvasPattern.h" +#include "InstanceData.h" + +using namespace Napi; -Nan::Persistent Pattern::constructor; +const cairo_user_data_key_t *pattern_repeat_key; /* * Initialize CanvasPattern. */ void -Pattern::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; +Pattern::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + InstanceData* data = env.GetInstanceData(); // Constructor - Local ctor = Nan::New(Pattern::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("CanvasPattern").ToLocalChecked()); - - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("CanvasPattern").ToLocalChecked()); + Napi::Function ctor = DefineClass(env, "CanvasPattern", { + InstanceMethod<&Pattern::setTransform>("setTransform", napi_default_method) + }); // Prototype - Nan::Set(target, Nan::New("CanvasPattern").ToLocalChecked(), ctor->GetFunction()); + exports.Set("CanvasPattern", ctor); + data->CanvasPatternCtor = Napi::Persistent(ctor); } /* * Initialize a new CanvasPattern. */ -NAN_METHOD(Pattern::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); +Pattern::Pattern(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info.Env()) { + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); + return; } + Napi::Object obj = info[0].As(); + InstanceData* data = env.GetInstanceData(); cairo_surface_t *surface; - Local obj = info[0]->ToObject(); - // Image - if (Nan::New(Image::constructor)->HasInstance(obj)) { - Image *img = Nan::ObjectWrap::Unwrap(obj); + if (obj.InstanceOf(data->ImageCtor.Value()).UnwrapOr(false)) { + Image *img = Image::Unwrap(obj); if (!img->isComplete()) { - return Nan::ThrowError("Image given has not completed loading"); + Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException(); + return; } surface = img->surface(); // Canvas - } else if (Nan::New(Canvas::constructor)->HasInstance(obj)) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); + } else if (obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) { + Canvas *canvas = Canvas::Unwrap(obj); surface = canvas->surface(); - // Invalid } else { - return Nan::ThrowTypeError("Image or Canvas expected"); + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); + } + return; } + _pattern = cairo_pattern_create_for_surface(surface); - Pattern *pattern = new Pattern(surface); - pattern->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); -} + if (info[1].IsString()) { + if ("no-repeat" == info[1].As().Utf8Value()) { + _repeat = NO_REPEAT; + } else if ("repeat-x" == info[1].As().Utf8Value()) { + _repeat = REPEAT_X; + } else if ("repeat-y" == info[1].As().Utf8Value()) { + _repeat = REPEAT_Y; + } + } + cairo_pattern_set_user_data(_pattern, pattern_repeat_key, &_repeat, NULL); +} /* - * Initialize linear gradient. + * Set the pattern-space to user-space transform. */ +void +Pattern::setTransform(const Napi::CallbackInfo& info) { + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); + return; + } -Pattern::Pattern(cairo_surface_t *surface) { - _pattern = cairo_pattern_create_for_surface(surface); + Napi::Object mat = info[0].As(); + + InstanceData* data = env.GetInstanceData(); + if (!mat.InstanceOf(data->DOMMatrixCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); + } + return; + } + + Napi::Value one = Napi::Number::New(env, 1); + Napi::Value zero = Napi::Number::New(env, 0); + + cairo_matrix_t matrix; + cairo_matrix_init(&matrix, + mat.Get("a").UnwrapOr(one).As().DoubleValue(), + mat.Get("b").UnwrapOr(zero).As().DoubleValue(), + mat.Get("c").UnwrapOr(zero).As().DoubleValue(), + mat.Get("d").UnwrapOr(one).As().DoubleValue(), + mat.Get("e").UnwrapOr(zero).As().DoubleValue(), + mat.Get("f").UnwrapOr(zero).As().DoubleValue() + ); + + cairo_matrix_invert(&matrix); + cairo_pattern_set_matrix(_pattern, &matrix); +} + +repeat_type_t Pattern::get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern) { + void *ud = cairo_pattern_get_user_data(pattern, pattern_repeat_key); + return *reinterpret_cast(ud); } /* @@ -82,5 +125,5 @@ Pattern::Pattern(cairo_surface_t *surface) { */ Pattern::~Pattern() { - cairo_pattern_destroy(_pattern); + if (_pattern) cairo_pattern_destroy(_pattern); } diff --git a/src/CanvasPattern.h b/src/CanvasPattern.h index 102c74f7a..1f768e03b 100644 --- a/src/CanvasPattern.h +++ b/src/CanvasPattern.h @@ -1,27 +1,33 @@ - -// -// CanvasPattern.h -// // Copyright (c) 2011 LearnBoost -// -#ifndef __NODE_PATTERN_H__ -#define __NODE_PATTERN_H__ +#pragma once + +#include +#include + +/* + * Canvas types. + */ + +typedef enum { + NO_REPEAT, // match CAIRO_EXTEND_NONE + REPEAT, // match CAIRO_EXTEND_REPEAT + REPEAT_X, // needs custom processing + REPEAT_Y // needs custom processing +} repeat_type_t; -#include "Canvas.h" +extern const cairo_user_data_key_t *pattern_repeat_key; -class Pattern: public Nan::ObjectWrap { +class Pattern : public Napi::ObjectWrap { public: - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - Pattern(cairo_surface_t *surface); + Pattern(const Napi::CallbackInfo& info); + static void Initialize(Napi::Env& env, Napi::Object& target); + void setTransform(const Napi::CallbackInfo& info); + static repeat_type_t get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern); inline cairo_pattern_t *pattern(){ return _pattern; } - - private: ~Pattern(); - // TODO REPEAT/REPEAT_X/REPEAT_Y + Napi::Env env; + private: cairo_pattern_t *_pattern; + repeat_type_t _repeat = REPEAT; }; - -#endif diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc old mode 100755 new mode 100644 index f3f32d50d..3f52c1fdd --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1,63 +1,40 @@ - -// -// CanvasRenderingContext2d.cc -// // Copyright (c) 2010 LearnBoost -// -#include -#include -#include -#include -#include +#include "CanvasRenderingContext2d.h" +#include +#include "backend/ImageBackend.h" +#include #include "Canvas.h" -#include "Point.h" -#include "Image.h" -#include "ImageData.h" -#include "CanvasRenderingContext2d.h" #include "CanvasGradient.h" #include "CanvasPattern.h" - -// Windows doesn't support the C99 names for these -#ifdef _MSC_VER -#define isnan(x) _isnan(x) -#define isinf(x) (!_finite(x)) -#endif - -#ifndef isnan -#define isnan(x) std::isnan(x) -#define isinf(x) std::isinf(x) -#endif - -Nan::Persistent Context2d::constructor; +#include "InstanceData.h" +#include "FontParser.h" +#include +#include +#include "Image.h" +#include "ImageData.h" +#include +#include +#include "Point.h" +#include +#include "Util.h" +#include /* * Rectangle arg assertions. */ #define RECT_ARGS \ - if (!info[0]->IsNumber() \ - ||!info[1]->IsNumber() \ - ||!info[2]->IsNumber() \ - ||!info[3]->IsNumber()) return; \ - double x = info[0]->NumberValue(); \ - double y = info[1]->NumberValue(); \ - double width = info[2]->NumberValue(); \ - double height = info[3]->NumberValue(); + double args[4]; \ + if(!checkArgs(info, args, 4)) \ + return; \ + double x = args[0]; \ + double y = args[1]; \ + double width = args[2]; \ + double height = args[3]; -/* - * Text baselines. - */ - -enum { - TEXT_BASELINE_ALPHABETIC - , TEXT_BASELINE_TOP - , TEXT_BASELINE_BOTTOM - , TEXT_BASELINE_MIDDLE - , TEXT_BASELINE_IDEOGRAPHIC - , TEXT_BASELINE_HANGING -}; +constexpr double twoPi = M_PI * 2.; /* * Simple helper macro for a rather verbose function call. @@ -66,109 +43,199 @@ enum { #define PANGO_LAYOUT_GET_METRICS(LAYOUT) pango_context_get_metrics( \ pango_layout_get_context(LAYOUT), \ pango_layout_get_font_description(LAYOUT), \ - pango_context_get_language(pango_layout_get_context(LAYOUT))) + pango_language_from_string(state->lang.c_str())) + +inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){ + Napi::Env env = info.Env(); + int argsEnd = std::min(9, offset + argsNum); + bool areArgsValid = true; + + napi_value argv[9]; + size_t argc = 9; + napi_get_cb_info(env, static_cast(info), &argc, argv, nullptr, nullptr); + + for (int i = offset; i < argsEnd; i++) { + napi_valuetype type; + double val = 0; + + napi_typeof(env, argv[i], &type); + if (type == napi_number) { + // fast path + napi_get_value_double(env, argv[i], &val); + } else { + napi_value num; + if (napi_coerce_to_number(env, argv[i], &num) == napi_ok) { + napi_get_value_double(env, num, &val); + } + } + + if (areArgsValid) { + if (!std::isfinite(val)) { + // We should continue the loop instead of returning immediately + // See https://html.spec.whatwg.org/multipage/canvas.html + + areArgsValid = false; + continue; + } + + args[i - offset] = val; + } + } + + return areArgsValid; +} /* * Initialize Context2d. */ void -Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; - - // Constructor - Local ctor = Nan::New(Context2d::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("CanvasRenderingContext2D").ToLocalChecked()); - - // Prototype - Local proto = ctor->PrototypeTemplate(); - Nan::SetPrototypeMethod(ctor, "drawImage", DrawImage); - Nan::SetPrototypeMethod(ctor, "putImageData", PutImageData); - Nan::SetPrototypeMethod(ctor, "getImageData", GetImageData); - Nan::SetPrototypeMethod(ctor, "addPage", AddPage); - Nan::SetPrototypeMethod(ctor, "save", Save); - Nan::SetPrototypeMethod(ctor, "restore", Restore); - Nan::SetPrototypeMethod(ctor, "rotate", Rotate); - Nan::SetPrototypeMethod(ctor, "translate", Translate); - Nan::SetPrototypeMethod(ctor, "transform", Transform); - Nan::SetPrototypeMethod(ctor, "resetTransform", ResetTransform); - Nan::SetPrototypeMethod(ctor, "isPointInPath", IsPointInPath); - Nan::SetPrototypeMethod(ctor, "scale", Scale); - Nan::SetPrototypeMethod(ctor, "clip", Clip); - Nan::SetPrototypeMethod(ctor, "fill", Fill); - Nan::SetPrototypeMethod(ctor, "stroke", Stroke); - Nan::SetPrototypeMethod(ctor, "fillText", FillText); - Nan::SetPrototypeMethod(ctor, "strokeText", StrokeText); - Nan::SetPrototypeMethod(ctor, "fillRect", FillRect); - Nan::SetPrototypeMethod(ctor, "strokeRect", StrokeRect); - Nan::SetPrototypeMethod(ctor, "clearRect", ClearRect); - Nan::SetPrototypeMethod(ctor, "rect", Rect); - Nan::SetPrototypeMethod(ctor, "measureText", MeasureText); - Nan::SetPrototypeMethod(ctor, "moveTo", MoveTo); - Nan::SetPrototypeMethod(ctor, "lineTo", LineTo); - Nan::SetPrototypeMethod(ctor, "bezierCurveTo", BezierCurveTo); - Nan::SetPrototypeMethod(ctor, "quadraticCurveTo", QuadraticCurveTo); - Nan::SetPrototypeMethod(ctor, "beginPath", BeginPath); - Nan::SetPrototypeMethod(ctor, "closePath", ClosePath); - Nan::SetPrototypeMethod(ctor, "arc", Arc); - Nan::SetPrototypeMethod(ctor, "arcTo", ArcTo); - Nan::SetPrototypeMethod(ctor, "setLineDash", SetLineDash); - Nan::SetPrototypeMethod(ctor, "getLineDash", GetLineDash); - Nan::SetPrototypeMethod(ctor, "_setFont", SetFont); - Nan::SetPrototypeMethod(ctor, "_setFillColor", SetFillColor); - Nan::SetPrototypeMethod(ctor, "_setStrokeColor", SetStrokeColor); - Nan::SetPrototypeMethod(ctor, "_setFillPattern", SetFillPattern); - Nan::SetPrototypeMethod(ctor, "_setStrokePattern", SetStrokePattern); - Nan::SetPrototypeMethod(ctor, "_setTextBaseline", SetTextBaseline); - Nan::SetPrototypeMethod(ctor, "_setTextAlignment", SetTextAlignment); - Nan::SetAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality); - Nan::SetAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation); - Nan::SetAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha); - Nan::SetAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor); - Nan::SetAccessor(proto, Nan::New("fillColor").ToLocalChecked(), GetFillColor); - Nan::SetAccessor(proto, Nan::New("strokeColor").ToLocalChecked(), GetStrokeColor); - Nan::SetAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit); - Nan::SetAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth); - Nan::SetAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap); - Nan::SetAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin); - Nan::SetAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset); - Nan::SetAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX); - Nan::SetAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY); - Nan::SetAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur); - Nan::SetAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias); - Nan::SetAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode); - Nan::SetAccessor(proto, Nan::New("filter").ToLocalChecked(), GetFilter, SetFilter); - Nan::Set(target, Nan::New("CanvasRenderingContext2d").ToLocalChecked(), ctor->GetFunction()); +Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + InstanceData* data = env.GetInstanceData(); + + Napi::Function ctor = DefineClass(env, "CanvasRenderingContext2D", { + InstanceMethod<&Context2d::DrawImage>("drawImage", napi_default_method), + InstanceMethod<&Context2d::PutImageData>("putImageData", napi_default_method), + InstanceMethod<&Context2d::GetImageData>("getImageData", napi_default_method), + InstanceMethod<&Context2d::CreateImageData>("createImageData", napi_default_method), + InstanceMethod<&Context2d::AddPage>("addPage", napi_default_method), + InstanceMethod<&Context2d::Save>("save", napi_default_method), + InstanceMethod<&Context2d::Restore>("restore", napi_default_method), + InstanceMethod<&Context2d::Rotate>("rotate", napi_default_method), + InstanceMethod<&Context2d::Translate>("translate", napi_default_method), + InstanceMethod<&Context2d::Transform>("transform", napi_default_method), + InstanceMethod<&Context2d::GetTransform>("getTransform", napi_default_method), + InstanceMethod<&Context2d::ResetTransform>("resetTransform", napi_default_method), + InstanceMethod<&Context2d::SetTransform>("setTransform", napi_default_method), + InstanceMethod<&Context2d::IsPointInPath>("isPointInPath", napi_default_method), + InstanceMethod<&Context2d::Scale>("scale", napi_default_method), + InstanceMethod<&Context2d::Clip>("clip", napi_default_method), + InstanceMethod<&Context2d::Fill>("fill", napi_default_method), + InstanceMethod<&Context2d::Stroke>("stroke", napi_default_method), + InstanceMethod<&Context2d::FillText>("fillText", napi_default_method), + InstanceMethod<&Context2d::StrokeText>("strokeText", napi_default_method), + InstanceMethod<&Context2d::FillRect>("fillRect", napi_default_method), + InstanceMethod<&Context2d::StrokeRect>("strokeRect", napi_default_method), + InstanceMethod<&Context2d::ClearRect>("clearRect", napi_default_method), + InstanceMethod<&Context2d::Rect>("rect", napi_default_method), + InstanceMethod<&Context2d::RoundRect>("roundRect", napi_default_method), + InstanceMethod<&Context2d::MeasureText>("measureText", napi_default_method), + InstanceMethod<&Context2d::MoveTo>("moveTo", napi_default_method), + InstanceMethod<&Context2d::LineTo>("lineTo", napi_default_method), + InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo", napi_default_method), + InstanceMethod<&Context2d::QuadraticCurveTo>("quadraticCurveTo", napi_default_method), + InstanceMethod<&Context2d::BeginPath>("beginPath", napi_default_method), + InstanceMethod<&Context2d::ClosePath>("closePath", napi_default_method), + InstanceMethod<&Context2d::Arc>("arc", napi_default_method), + InstanceMethod<&Context2d::ArcTo>("arcTo", napi_default_method), + InstanceMethod<&Context2d::Ellipse>("ellipse", napi_default_method), + InstanceMethod<&Context2d::SetLineDash>("setLineDash", napi_default_method), + InstanceMethod<&Context2d::GetLineDash>("getLineDash", napi_default_method), + InstanceMethod<&Context2d::CreatePattern>("createPattern", napi_default_method), + InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient", napi_default_method), + InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient", napi_default_method), + #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + InstanceMethod<&Context2d::BeginTag>("beginTag", napi_default_method), + InstanceMethod<&Context2d::EndTag>("endTag", napi_default_method), + #endif + InstanceAccessor<&Context2d::GetFormat>("pixelFormat", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetGlobalCompositeOperation, &Context2d::SetGlobalCompositeOperation>("globalCompositeOperation", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetGlobalAlpha, &Context2d::SetGlobalAlpha>("globalAlpha", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetShadowColor, &Context2d::SetShadowColor>("shadowColor", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetMiterLimit, &Context2d::SetMiterLimit>("miterLimit", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLineWidth, &Context2d::SetLineWidth>("lineWidth", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLineCap, &Context2d::SetLineCap>("lineCap", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLineJoin, &Context2d::SetLineJoin>("lineJoin", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLineDashOffset, &Context2d::SetLineDashOffset>("lineDashOffset", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetShadowOffsetX, &Context2d::SetShadowOffsetX>("shadowOffsetX", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLanguage, &Context2d::SetLanguage>("lang", napi_default_jsproperty) + }); + + exports.Set("CanvasRenderingContext2d", ctor); + data->Context2dCtor = Napi::Persistent(ctor); } /* * Create a cairo context. */ -Context2d::Context2d(Canvas *canvas) { - _canvas = canvas; - _context = cairo_create(canvas->surface()); +Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + InstanceData* data = env.GetInstanceData(); + + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException(); + return; + } + + Napi::Object obj = info[0].As(); + if (!obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException(); + } + return; + } + + _canvas = Canvas::Unwrap(obj); + + bool isImageBackend = _canvas->backend()->getName() == "image"; + if (isImageBackend) { + cairo_format_t format = ImageBackend::DEFAULT_FORMAT; + + if (info[1].IsObject()) { + Napi::Object ctxAttributes = info[1].As(); + Napi::Value pixelFormat; + + if (ctxAttributes.Get("pixelFormat").UnwrapTo(&pixelFormat) && pixelFormat.IsString()) { + std::string utf8PixelFormat = pixelFormat.As(); + if (utf8PixelFormat == "RGBA32") format = CAIRO_FORMAT_ARGB32; + else if (utf8PixelFormat == "RGB24") format = CAIRO_FORMAT_RGB24; + else if (utf8PixelFormat == "A8") format = CAIRO_FORMAT_A8; + else if (utf8PixelFormat == "RGB16_565") format = CAIRO_FORMAT_RGB16_565; + else if (utf8PixelFormat == "A1") format = CAIRO_FORMAT_A1; +#ifdef CAIRO_FORMAT_RGB30 + else if (utf8PixelFormat == "RGB30") format = CAIRO_FORMAT_RGB30; +#endif + } + + // alpha: false forces use of RGB24 + Napi::Value alpha; + + if (ctxAttributes.Get("alpha").UnwrapTo(&alpha) && alpha.IsBoolean() && !alpha.As().Value()) { + format = CAIRO_FORMAT_RGB24; + } + } + + static_cast(_canvas->backend())->setFormat(format); + } + + _context = _canvas->createCairoContext(); _layout = pango_cairo_create_layout(_context); - cairo_set_line_width(_context, 1); - state = states[stateno = 0] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); - state->shadowBlur = 0; - state->shadowOffsetX = state->shadowOffsetY = 0; - state->globalAlpha = 1; - state->textAlignment = -1; - state->fillPattern = state->strokePattern = NULL; - state->fillGradient = state->strokeGradient = NULL; - state->textBaseline = TEXT_BASELINE_ALPHABETIC; - rgba_t transparent = { 0,0,0,1 }; - rgba_t transparent_black = { 0,0,0,0 }; - state->fill = transparent; - state->stroke = transparent; - state->shadow = transparent_black; - state->patternQuality = CAIRO_FILTER_GOOD; - state->textDrawingMode = TEXT_DRAW_PATHS; - state->fontDescription = pango_font_description_from_string("sans serif"); - pango_font_description_set_absolute_size(state->fontDescription, 10 * PANGO_SCALE); + + // As of January 2023, Pango rounds glyph positions which renders text wider + // or narrower than the browser. See #2184 for more information +#if PANGO_VERSION_CHECK(1, 44, 0) + pango_context_set_round_glyph_positions(pango_layout_get_context(_layout), FALSE); +#endif + + pango_layout_set_auto_dir(_layout, FALSE); + + states.emplace(); + state = &states.top(); pango_layout_set_font_description(_layout, state->fontDescription); } @@ -177,12 +244,25 @@ Context2d::Context2d(Canvas *canvas) { */ Context2d::~Context2d() { - while(stateno >= 0) { - pango_font_description_free(states[stateno]->fontDescription); - free(states[stateno--]); - } - g_object_unref(_layout); - cairo_destroy(_context); + if (_layout) g_object_unref(_layout); + if (_context) cairo_destroy(_context); + _resetPersistentHandles(); +} + +/* + * Reset canvas state. + */ + +void Context2d::resetState() { + states.pop(); + states.emplace(); + pango_layout_set_font_description(_layout, state->fontDescription); + _resetPersistentHandles(); +} + +void Context2d::_resetPersistentHandles() { + _fillStyle.Reset(); + _strokeStyle.Reset(); } /* @@ -191,13 +271,9 @@ Context2d::~Context2d() { void Context2d::save() { - if (stateno < CANVAS_MAX_STATES) { - cairo_save(_context); - states[++stateno] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); - memcpy(states[stateno], state, sizeof(canvas_state_t)); - states[stateno]->fontDescription = pango_font_description_copy(states[stateno-1]->fontDescription); - state = states[stateno]; - } + cairo_save(_context); + states.emplace(states.top()); + state = &states.top(); } /* @@ -206,12 +282,10 @@ Context2d::save() { void Context2d::restore() { - if (stateno > 0) { + if (states.size() > 1) { cairo_restore(_context); - pango_font_description_free(states[stateno]->fontDescription); - free(states[stateno]); - states[stateno] = NULL; - state = states[--stateno]; + states.pop(); + state = &states.top(); pango_layout_set_font_description(_layout, state->fontDescription); } } @@ -237,16 +311,75 @@ Context2d::restorePath() { cairo_path_destroy(_path); } +/* + * Create temporary surface for gradient or pattern transparency + */ +cairo_pattern_t* +create_transparent_gradient(cairo_pattern_t *source, float alpha) { + double x0; + double y0; + double x1; + double y1; + double r0; + double r1; + int count; + int i; + double offset; + double r; + double g; + double b; + double a; + cairo_pattern_t *newGradient; + cairo_pattern_type_t type = cairo_pattern_get_type(source); + cairo_pattern_get_color_stop_count(source, &count); + if (type == CAIRO_PATTERN_TYPE_LINEAR) { + cairo_pattern_get_linear_points (source, &x0, &y0, &x1, &y1); + newGradient = cairo_pattern_create_linear(x0, y0, x1, y1); + } else if (type == CAIRO_PATTERN_TYPE_RADIAL) { + cairo_pattern_get_radial_circles(source, &x0, &y0, &r0, &x1, &y1, &r1); + newGradient = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); + } else { + return NULL; + } + for ( i = 0; i < count; i++ ) { + cairo_pattern_get_color_stop_rgba(source, i, &offset, &r, &g, &b, &a); + cairo_pattern_add_color_stop_rgba(newGradient, offset, r, g, b, a * alpha); + } + return newGradient; +} + +cairo_pattern_t* +create_transparent_pattern(cairo_pattern_t *source, float alpha) { + cairo_surface_t *surface; + cairo_pattern_get_surface(source, &surface); + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + cairo_surface_t *mask_surface = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, + width, + height); + cairo_t *mask_context = cairo_create(mask_surface); + if (cairo_status(mask_context) != CAIRO_STATUS_SUCCESS) { + return NULL; + } + cairo_set_source(mask_context, source); + cairo_paint_with_alpha(mask_context, alpha); + cairo_destroy(mask_context); + cairo_pattern_t* newPattern = cairo_pattern_create_for_surface(mask_surface); + cairo_surface_destroy(mask_surface); + return newPattern; +} + /* * Fill and apply shadow. */ void -Context2d::setFillRule(v8::Local value) { +Context2d::setFillRule(Napi::Value value) { cairo_fill_rule_t rule = CAIRO_FILL_RULE_WINDING; - if (value->IsString()) { - String::Utf8Value str(value); - if (std::strcmp(*str, "evenodd") == 0) { + if (value.IsString()) { + std::string str = value.As().Utf8Value(); + if (str == "evenodd") { rule = CAIRO_FILL_RULE_EVEN_ODD; } } @@ -255,17 +388,72 @@ Context2d::setFillRule(v8::Local value) { void Context2d::fill(bool preserve) { + cairo_pattern_t *new_pattern; + bool needsRestore = false; if (state->fillPattern) { - cairo_set_source(_context, state->fillPattern); - cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); - // TODO repeat/repeat-x/repeat-y + if (state->globalAlpha < 1) { + new_pattern = create_transparent_pattern(state->fillPattern, state->globalAlpha); + if (new_pattern == NULL) { + Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException(); + // failed to allocate + return; + } + cairo_set_source(_context, new_pattern); + cairo_pattern_destroy(new_pattern); + } else { + cairo_pattern_set_filter(state->fillPattern, state->patternQuality); + cairo_set_source(_context, state->fillPattern); + } + repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->fillPattern); + if (repeat == NO_REPEAT) { + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE); + } else if (repeat == REPEAT) { + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); + } else { + cairo_save(_context); + cairo_path_t *savedPath = cairo_copy_path(_context); + cairo_surface_t *patternSurface = nullptr; + cairo_pattern_get_surface(cairo_get_source(_context), &patternSurface); + + double width, height; + if (repeat == REPEAT_X) { + double x1, x2; + cairo_path_extents(_context, &x1, nullptr, &x2, nullptr); + width = x2 - x1; + height = cairo_image_surface_get_height(patternSurface); + } else { + double y1, y2; + cairo_path_extents(_context, nullptr, &y1, nullptr, &y2); + width = cairo_image_surface_get_width(patternSurface); + height = y2 - y1; + } + + cairo_new_path(_context); + cairo_rectangle(_context, 0, 0, width, height); + cairo_clip(_context); + cairo_append_path(_context, savedPath); + cairo_path_destroy(savedPath); + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); + needsRestore = true; + } } else if (state->fillGradient) { - cairo_pattern_set_filter(state->fillGradient, state->patternQuality); - cairo_set_source(_context, state->fillGradient); + if (state->globalAlpha < 1) { + new_pattern = create_transparent_gradient(state->fillGradient, state->globalAlpha); + if (new_pattern == NULL) { + Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException(); + // failed to recognize gradient + return; + } + cairo_pattern_set_filter(new_pattern, state->patternQuality); + cairo_set_source(_context, new_pattern); + cairo_pattern_destroy(new_pattern); + } else { + cairo_pattern_set_filter(state->fillGradient, state->patternQuality); + cairo_set_source(_context, state->fillGradient); + } } else { setSourceRGBA(state->fill); } - if (preserve) { hasShadow() ? shadow(cairo_fill_preserve) @@ -275,6 +463,9 @@ Context2d::fill(bool preserve) { ? shadow(cairo_fill) : cairo_fill(_context); } + if (needsRestore) { + cairo_restore(_context); + } } /* @@ -283,12 +474,42 @@ Context2d::fill(bool preserve) { void Context2d::stroke(bool preserve) { + cairo_pattern_t *new_pattern; if (state->strokePattern) { - cairo_set_source(_context, state->strokePattern); - cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); + if (state->globalAlpha < 1) { + new_pattern = create_transparent_pattern(state->strokePattern, state->globalAlpha); + if (new_pattern == NULL) { + Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException(); + // failed to allocate + return; + } + cairo_set_source(_context, new_pattern); + cairo_pattern_destroy(new_pattern); + } else { + cairo_pattern_set_filter(state->strokePattern, state->patternQuality); + cairo_set_source(_context, state->strokePattern); + } + repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->strokePattern); + if (NO_REPEAT == repeat) { + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_NONE); + } else { + cairo_pattern_set_extend(cairo_get_source(_context), CAIRO_EXTEND_REPEAT); + } } else if (state->strokeGradient) { - cairo_pattern_set_filter(state->strokeGradient, state->patternQuality); - cairo_set_source(_context, state->strokeGradient); + if (state->globalAlpha < 1) { + new_pattern = create_transparent_gradient(state->strokeGradient, state->globalAlpha); + if (new_pattern == NULL) { + Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException(); + // failed to recognize gradient + return; + } + cairo_pattern_set_filter(new_pattern, state->patternQuality); + cairo_set_source(_context, new_pattern); + cairo_pattern_destroy(new_pattern); + } else { + cairo_pattern_set_filter(state->strokeGradient, state->patternQuality); + cairo_set_source(_context, state->strokeGradient); + } } else { setSourceRGBA(state->stroke); } @@ -345,6 +566,16 @@ Context2d::shadow(void (fn)(cairo_t *cr)) { cairo_translate(shadow_context, pad-x1, pad-y1); cairo_transform(shadow_context, &path_matrix); + // set lineCap lineJoin lineDash + cairo_set_line_cap(shadow_context, cairo_get_line_cap(_context)); + cairo_set_line_join(shadow_context, cairo_get_line_join(_context)); + + double offset; + int dashes = cairo_get_dash_count(_context); + std::vector a(dashes); + cairo_get_dash(_context, a.data(), &offset); + cairo_set_dash(shadow_context, a.data(), dashes, offset); + // draw the path and blur cairo_set_line_width(shadow_context, cairo_get_line_width(_context)); cairo_new_path(shadow_context); @@ -434,8 +665,8 @@ Context2d::blur(cairo_surface_t *surface, int radius) { // get width, height int width = cairo_image_surface_get_width( surface ); int height = cairo_image_surface_get_height( surface ); - unsigned* precalc = - (unsigned*)malloc(width*height*sizeof(unsigned)); + const unsigned int size = width * height * sizeof(unsigned); + unsigned* precalc = (unsigned*)malloc(size); cairo_surface_flush( surface ); unsigned char* src = cairo_image_surface_get_data( surface ); double mul=1.f/((radius*2)*(radius*2)); @@ -454,6 +685,8 @@ Context2d::blur(cairo_surface_t *surface, int radius) { unsigned char* pix = src; unsigned* pre = precalc; + bool modified = false; + pix += channel; for (y=0;y0) tot+=pre[-width]; if (x>0 && y>0) tot-=pre[-width-1]; *pre++=tot; + if (!modified) modified = true; pix += 4; } } + if (!modified) { + memset(precalc, 0, size); + } + // blur step. pix = src + (int)radius * width * 4 + (int)radius * 4 + channel; for (y=radius;ybackend()->getFormat()) { + case CAIRO_FORMAT_ARGB32: pixelFormatString = "RGBA32"; break; + case CAIRO_FORMAT_RGB24: pixelFormatString = "RGB24"; break; + case CAIRO_FORMAT_A8: pixelFormatString = "A8"; break; + case CAIRO_FORMAT_A1: pixelFormatString = "A1"; break; + case CAIRO_FORMAT_RGB16_565: pixelFormatString = "RGB16_565"; break; +#ifdef CAIRO_FORMAT_RGB30 + case CAIRO_FORMAT_RGB30: pixelFormatString = "RGB30"; break; +#endif + default: return env.Null(); } - - Local obj = info[0]->ToObject(); - if (!Nan::New(Canvas::constructor)->HasInstance(obj)) - return Nan::ThrowTypeError("Canvas expected"); - Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); - Context2d *context = new Context2d(canvas); - context->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + return Napi::String::New(env, pixelFormatString); } /* * Create a new page. */ -NAN_METHOD(Context2d::AddPage) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (!context->canvas()->isPDF()) { - return Nan::ThrowError("only PDF canvases support .nextPage()"); +void +Context2d::AddPage(const Napi::CallbackInfo& info) { + if (canvas()->backend()->getName() != "pdf") { + Napi::Error::New(env, "only PDF canvases support .addPage()").ThrowAsJavaScriptException(); + return; } - cairo_show_page(context->context()); - return; + cairo_show_page(context()); + Napi::Number zero = Napi::Number::New(env, 0); + int width = info[0].ToNumber().UnwrapOr(zero).Int32Value(); + int height = info[1].ToNumber().UnwrapOr(zero).Int32Value(); + if (width < 1) width = canvas()->getWidth(); + if (height < 1) height = canvas()->getHeight(); + cairo_pdf_surface_set_size(canvas()->surface(), width, height); +} + +/* + * Get text direction. + */ +Napi::Value +Context2d::GetDirection(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->direction); +} + +/* + * Set text direction. + */ +void +Context2d::SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string dir = value.As(); + if (dir != "ltr" && dir != "rtl") return; + + state->direction = dir; +} + +/* + * Get language. + */ +Napi::Value +Context2d::GetLanguage(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->lang); +} + +/* + * Set language. + */ +void +Context2d::SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string lang = value.As(); + state->lang = lang; } /* @@ -527,45 +814,52 @@ NAN_METHOD(Context2d::AddPage) { * */ -NAN_METHOD(Context2d::PutImageData) { - if (!info[0]->IsObject()) - return Nan::ThrowTypeError("ImageData expected"); - Local obj = info[0]->ToObject(); - if (!Nan::New(ImageData::constructor)->HasInstance(obj)) - return Nan::ThrowTypeError("ImageData expected"); +void +Context2d::PutImageData(const Napi::CallbackInfo& info) { + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException(); + return; + } + Napi::Object obj = info[0].As(); + InstanceData* data = env.GetInstanceData(); + if (!obj.InstanceOf(data->ImageDataCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException(); + } + return; + } - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - ImageData *imageData = Nan::ObjectWrap::Unwrap(obj); + ImageData *imageData = ImageData::Unwrap(obj); + Napi::Number zero = Napi::Number::New(env, 0); uint8_t *src = imageData->data(); - uint8_t *dst = context->canvas()->data(); + uint8_t *dst = canvas()->data(); - int srcStride = imageData->stride() - , dstStride = context->canvas()->stride(); + int dstStride = canvas()->stride(); + int Bpp = dstStride / canvas()->getWidth(); + int srcStride = Bpp * imageData->width(); int sx = 0 , sy = 0 , sw = 0 , sh = 0 - , dx = info[1]->Int32Value() - , dy = info[2]->Int32Value() + , dx = info[1].ToNumber().UnwrapOr(zero).Int32Value() + , dy = info[2].ToNumber().UnwrapOr(zero).Int32Value() , rows , cols; switch (info.Length()) { // imageData, dx, dy case 3: - // Need to wrap std::min calls using parens to prevent macro expansion on - // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error - cols = (std::min)(imageData->width(), context->canvas()->width - dx); - rows = (std::min)(imageData->height(), context->canvas()->height - dy); + sw = imageData->width(); + sh = imageData->height(); break; // imageData, dx, dy, sx, sy, sw, sh case 7: - sx = info[3]->Int32Value(); - sy = info[4]->Int32Value(); - sw = info[5]->Int32Value(); - sh = info[6]->Int32Value(); + sx = info[3].ToNumber().UnwrapOr(zero).Int32Value(); + sy = info[4].ToNumber().UnwrapOr(zero).Int32Value(); + sw = info[5].ToNumber().UnwrapOr(zero).Int32Value(); + sh = info[6].ToNumber().UnwrapOr(zero).Int32Value(); // fix up negative height, width if (sw < 0) sx += sw, sw = -sw; if (sh < 0) sy += sh, sh = -sh; @@ -578,60 +872,134 @@ NAN_METHOD(Context2d::PutImageData) { // start destination at source offset dx += sx; dy += sy; - // chop off outlying source data - if (dx < 0) sw += dx, sx -= dx, dx = 0; - if (dy < 0) sh += dy, sy -= dy, dy = 0; - // clamp width at canvas size - // Need to wrap std::min calls using parens to prevent macro expansion on - // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error - cols = (std::min)(sw, context->canvas()->width - dx); - rows = (std::min)(sh, context->canvas()->height - dy); break; default: - return Nan::ThrowError("invalid arguments"); + Napi::Error::New(env, "invalid arguments").ThrowAsJavaScriptException(); + return; } + // chop off outlying source data + if (dx < 0) sw += dx, sx -= dx, dx = 0; + if (dy < 0) sh += dy, sy -= dy, dy = 0; + // clamp width at canvas size + // Need to wrap std::min calls using parens to prevent macro expansion on + // windows. See http://stackoverflow.com/questions/5004858/stdmin-gives-error + cols = (std::min)(sw, canvas()->getWidth() - dx); + rows = (std::min)(sh, canvas()->getHeight() - dy); + if (cols <= 0 || rows <= 0) return; - src += sy * srcStride + sx * 4; - dst += dstStride * dy + 4 * dx; - for (int y = 0; y < rows; ++y) { - uint8_t *dstRow = dst; - uint8_t *srcRow = src; - for (int x = 0; x < cols; ++x) { - // rgba - uint8_t r = *srcRow++; - uint8_t g = *srcRow++; - uint8_t b = *srcRow++; - uint8_t a = *srcRow++; - - // argb - // performance optimization: fully transparent/opaque pixels can be - // processed more efficiently. - if (a == 0) { - *dstRow++ = 0; - *dstRow++ = 0; - *dstRow++ = 0; - *dstRow++ = 0; - } else if (a == 255) { + switch (canvas()->backend()->getFormat()) { + case CAIRO_FORMAT_ARGB32: { + src += sy * srcStride + sx * 4; + dst += dstStride * dy + 4 * dx; + for (int y = 0; y < rows; ++y) { + uint8_t *dstRow = dst; + uint8_t *srcRow = src; + for (int x = 0; x < cols; ++x) { + // rgba + uint8_t r = *srcRow++; + uint8_t g = *srcRow++; + uint8_t b = *srcRow++; + uint8_t a = *srcRow++; + + // argb + // performance optimization: fully transparent/opaque pixels can be + // processed more efficiently. + if (a == 0) { + *dstRow++ = 0; + *dstRow++ = 0; + *dstRow++ = 0; + *dstRow++ = 0; + } else if (a == 255) { + *dstRow++ = b; + *dstRow++ = g; + *dstRow++ = r; + *dstRow++ = a; + } else { + float alpha = (float)a / 255; + *dstRow++ = b * alpha; + *dstRow++ = g * alpha; + *dstRow++ = r * alpha; + *dstRow++ = a; + } + } + dst += dstStride; + src += srcStride; + } + break; + } + case CAIRO_FORMAT_RGB24: { + src += sy * srcStride + sx * 4; + dst += dstStride * dy + 4 * dx; + for (int y = 0; y < rows; ++y) { + uint8_t *dstRow = dst; + uint8_t *srcRow = src; + for (int x = 0; x < cols; ++x) { + // rgba + uint8_t r = *srcRow++; + uint8_t g = *srcRow++; + uint8_t b = *srcRow++; + srcRow++; + + // argb *dstRow++ = b; *dstRow++ = g; *dstRow++ = r; - *dstRow++ = a; - } else { - float alpha = (float)a / 255; - *dstRow++ = b * alpha; - *dstRow++ = g * alpha; - *dstRow++ = r * alpha; - *dstRow++ = a; + *dstRow++ = 255; } + dst += dstStride; + src += srcStride; } - dst += dstStride; - src += srcStride; + break; + } + case CAIRO_FORMAT_A8: { + src += sy * srcStride + sx; + dst += dstStride * dy + dx; + if (srcStride == dstStride && cols == dstStride) { + // fast path: strides are the same and doing a full-width put + memcpy(dst, src, cols * rows); + } else { + for (int y = 0; y < rows; ++y) { + memcpy(dst, src, cols); + dst += dstStride; + src += srcStride; + } + } + break; + } + case CAIRO_FORMAT_A1: { + // TODO Should this be totally packed, or maintain a stride divisible by 4? + Napi::Error::New(env, "putImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException(); + + break; + } + case CAIRO_FORMAT_RGB16_565: { + src += sy * srcStride + sx * 2; + dst += dstStride * dy + 2 * dx; + for (int y = 0; y < rows; ++y) { + memcpy(dst, src, cols * 2); + dst += dstStride; + src += srcStride; + } + break; + } +#ifdef CAIRO_FORMAT_RGB30 + case CAIRO_FORMAT_RGB30: { + // TODO + Napi::Error::New(env, "putImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException(); + + break; + } +#endif + default: { + Napi::Error::New(env, "Invalid pixel format or not an image canvas").ThrowAsJavaScriptException(); + return; + } } cairo_surface_mark_dirty_rectangle( - context->canvas()->surface() + canvas()->surface() , dx , dy , cols @@ -645,19 +1013,36 @@ NAN_METHOD(Context2d::PutImageData) { * */ -NAN_METHOD(Context2d::GetImageData) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Canvas *canvas = context->canvas(); +Napi::Value +Context2d::GetImageData(const Napi::CallbackInfo& info) { + Napi::Number zero = Napi::Number::New(env, 0); + Canvas *canvas = this->canvas(); + + int sx = info[0].ToNumber().UnwrapOr(zero).Int32Value(); + int sy = info[1].ToNumber().UnwrapOr(zero).Int32Value(); + int sw = info[2].ToNumber().UnwrapOr(zero).Int32Value(); + int sh = info[3].ToNumber().UnwrapOr(zero).Int32Value(); + + if (!sw) { + Napi::Error::New(env, "IndexSizeError: The source width is 0.").ThrowAsJavaScriptException(); + return env.Undefined(); + } + if (!sh) { + Napi::Error::New(env, "IndexSizeError: The source height is 0.").ThrowAsJavaScriptException(); + return env.Undefined(); + } - int sx = info[0]->Int32Value(); - int sy = info[1]->Int32Value(); - int sw = info[2]->Int32Value(); - int sh = info[3]->Int32Value(); + int width = canvas->getWidth(); + int height = canvas->getHeight(); - if (!sw) - return Nan::ThrowError("IndexSizeError: The source width is 0."); - if (!sh) - return Nan::ThrowError("IndexSizeError: The source height is 0."); + if (!width) { + Napi::TypeError::New(env, "Canvas width is 0").ThrowAsJavaScriptException(); + return env.Undefined(); + } + if (!height) { + Napi::TypeError::New(env, "Canvas height is 0").ThrowAsJavaScriptException(); + return env.Undefined(); + } // WebKit and Firefox have this behavior: // Flip the coordinates so the origin is top/left-most: @@ -670,83 +1055,203 @@ NAN_METHOD(Context2d::GetImageData) { sh = -sh; } - if (sx + sw > canvas->width) sw = canvas->width - sx; - if (sy + sh > canvas->height) sh = canvas->height - sy; + // Width and height to actually copy + int cw = sw; + int ch = sh; + // Offsets in the destination image + int ox = 0; + int oy = 0; - // WebKit/moz functionality. node-canvas used to return in either case. - if (sw <= 0) sw = 1; - if (sh <= 0) sh = 1; + // Clamp the copy width and height if the copy would go outside the image + if (sx + sw > width) cw = width - sx; + if (sy + sh > height) ch = height - sy; - // Non-compliant. "Pixels outside the canvas must be returned as transparent - // black." This instead clips the returned array to the canvas area. + // Clamp the copy origin if the copy would go outside the image if (sx < 0) { - sw += sx; + ox = -sx; + cw += sx; sx = 0; } if (sy < 0) { - sh += sy; + oy = -sy; + ch += sy; sy = 0; } - int size = sw * sh * 4; - int srcStride = canvas->stride(); - int dstStride = sw * 4; + int bpp = srcStride / width; + int size = sw * sh * bpp; + int dstStride = sw * bpp; uint8_t *src = canvas->data(); -#if NODE_MAJOR_VERSION == 0 && NODE_MINOR_VERSION <= 10 - Local global = Context::GetCurrent()->Global(); + Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, size); + Napi::TypedArray dataArray; - Local sizeHandle = Nan::New(size); - Local caargv[] = { sizeHandle }; - Local clampedArray = global->Get(Nan::New("Uint8ClampedArray").ToLocalChecked()).As()->NewInstance(1, caargv); -#else - Local buffer = ArrayBuffer::New(Isolate::GetCurrent(), size); - Local clampedArray = Uint8ClampedArray::New(buffer, 0, size); -#endif + if (canvas->backend()->getFormat() == CAIRO_FORMAT_RGB16_565) { + dataArray = Napi::Uint16Array::New(env, size >> 1, buffer, 0); + } else { + dataArray = Napi::Uint8Array::New(env, size, buffer, 0, napi_uint8_clamped_array); + } - Nan::TypedArrayContents typedArrayContents(clampedArray); - uint8_t* dst = *typedArrayContents; + uint8_t *dst = (uint8_t *)buffer.Data(); + + if (!(cw > 0 && ch > 0)) goto return_empty; + + switch (canvas->backend()->getFormat()) { + case CAIRO_FORMAT_ARGB32: { + dst += oy * dstStride + ox * 4; + // Rearrange alpha (argb -> rgba), undo alpha pre-multiplication, + // and store in big-endian format + for (int y = 0; y < ch; ++y) { + uint32_t *row = (uint32_t *)(src + srcStride * (y + sy)); + for (int x = 0; x < cw; ++x) { + int bx = x * 4; + uint32_t *pixel = row + x + sx; + uint8_t a = *pixel >> 24; + uint8_t r = *pixel >> 16; + uint8_t g = *pixel >> 8; + uint8_t b = *pixel; + dst[bx + 3] = a; + + // Performance optimization: fully transparent/opaque pixels can be + // processed more efficiently. + if (a == 0 || a == 255) { + dst[bx + 0] = r; + dst[bx + 1] = g; + dst[bx + 2] = b; + } else { + // Undo alpha pre-multiplication + float alphaR = (float)255 / a; + dst[bx + 0] = (int)((float)r * alphaR); + dst[bx + 1] = (int)((float)g * alphaR); + dst[bx + 2] = (int)((float)b * alphaR); + } - // Normalize data (argb -> rgba) - for (int y = 0; y < sh; ++y) { + } + dst += dstStride; + } + break; + } + case CAIRO_FORMAT_RGB24: { + dst += oy * dstStride + ox * 4; + // Rearrange alpha (argb -> rgba) and store in big-endian format + for (int y = 0; y < ch; ++y) { uint32_t *row = (uint32_t *)(src + srcStride * (y + sy)); - for (int x = 0; x < sw; ++x) { + for (int x = 0; x < cw; ++x) { int bx = x * 4; uint32_t *pixel = row + x + sx; - uint8_t a = *pixel >> 24; uint8_t r = *pixel >> 16; uint8_t g = *pixel >> 8; uint8_t b = *pixel; - dst[bx + 3] = a; - - // Performance optimization: fully transparent/opaque pixels can be - // processed more efficiently. - if (a == 0 || a == 255) { - dst[bx + 0] = r; - dst[bx + 1] = g; - dst[bx + 2] = b; - } else { - float alpha = (float)a / 255; - dst[bx + 0] = (int)((float)r / alpha); - dst[bx + 1] = (int)((float)g / alpha); - dst[bx + 2] = (int)((float)b / alpha); - } + dst[bx + 0] = r; + dst[bx + 1] = g; + dst[bx + 2] = b; + dst[bx + 3] = 255; } dst += dstStride; + } + break; + } + case CAIRO_FORMAT_A8: { + dst += oy * dstStride + ox; + for (int y = 0; y < ch; ++y) { + uint8_t *row = (uint8_t *)(src + srcStride * (y + sy)); + memcpy(dst, row + sx, cw); + dst += dstStride; + } + break; + } + case CAIRO_FORMAT_A1: { + // TODO Should this be totally packed, or maintain a stride divisible by 4? + Napi::Error::New(env, "getImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException(); + + break; + } + case CAIRO_FORMAT_RGB16_565: { + dst += oy * dstStride + ox * 2; + for (int y = 0; y < ch; ++y) { + uint16_t *row = (uint16_t *)(src + srcStride * (y + sy)); + memcpy(dst, row + sx, cw * 2); + dst += dstStride; + } + break; + } +#ifdef CAIRO_FORMAT_RGB30 + case CAIRO_FORMAT_RGB30: { + // TODO + Napi::Error::New(env, "getImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException(); + + break; + } +#endif + default: { + // Unlikely + Napi::Error::New(env, "Invalid pixel format or not an image canvas").ThrowAsJavaScriptException(); + return env.Null(); + } + } + +return_empty: + Napi::Number swHandle = Napi::Number::New(env, sw); + Napi::Number shHandle = Napi::Number::New(env, sh); + Napi::Function ctor = env.GetInstanceData()->ImageDataCtor.Value(); + Napi::Maybe ret = ctor.New({ dataArray, swHandle, shHandle }); + + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); +} + +/** + * Create `ImageData` with the given dimensions or + * `ImageData` instance for dimensions. + */ + +Napi::Value +Context2d::CreateImageData(const Napi::CallbackInfo& info){ + Canvas *canvas = this->canvas(); + Napi::Number zero = Napi::Number::New(env, 0); + int32_t width, height; + + if (info[0].IsObject()) { + Napi::Object obj = info[0].As(); + width = obj.Get("width").UnwrapOr(zero).ToNumber().UnwrapOr(zero).Int32Value(); + height = obj.Get("height").UnwrapOr(zero).ToNumber().UnwrapOr(zero).Int32Value(); + } else { + width = info[0].ToNumber().UnwrapOr(zero).Int32Value(); + height = info[1].ToNumber().UnwrapOr(zero).Int32Value(); } - const int argc = 3; - Local swHandle = Nan::New(sw); - Local shHandle = Nan::New(sh); - Local argv[argc] = { clampedArray, swHandle, shHandle }; + int stride = canvas->stride(); + double Bpp = static_cast(stride) / canvas->getWidth(); + int nBytes = static_cast(Bpp * width * height + .5); + + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, nBytes); + Napi::Value arr; + + if (canvas->backend()->getFormat() == CAIRO_FORMAT_RGB16_565) + arr = Napi::Uint16Array::New(env, nBytes / 2, ab, 0); + else + arr = Napi::Uint8Array::New(env, nBytes, ab, 0, napi_uint8_clamped_array); + + Napi::Function ctor = env.GetInstanceData()->ImageDataCtor.Value(); + Napi::Maybe ret = ctor.New({ arr, Napi::Number::New(env, width), Napi::Number::New(env, height) }); - Local constructor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(constructor, argc, argv).ToLocalChecked(); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); +} - info.GetReturnValue().Set(instance); +/* + * Take a transform matrix and return its components + * 0: angle, 1: scaleX, 2: scaleY, 3: skewX, 4: translateX, 5: translateY + */ +void decompose_matrix(cairo_matrix_t matrix, double *destination) { + double denom = pow(matrix.xx, 2) + pow(matrix.yx, 2); + destination[0] = atan2(matrix.yx, matrix.xx); + destination[1] = sqrt(denom); + destination[2] = (matrix.xx * matrix.yy - matrix.xy * matrix.yx) / destination[1]; + destination[3] = atan2(matrix.xx * matrix.xy + matrix.yx * matrix.yy, denom); + destination[4] = matrix.x0; + destination[5] = matrix.y0; } /* @@ -758,103 +1263,181 @@ NAN_METHOD(Context2d::GetImageData) { * */ -NAN_METHOD(Context2d::DrawImage) { - if (info.Length() < 3) - return Nan::ThrowTypeError("invalid arguments"); +void +Context2d::DrawImage(const Napi::CallbackInfo& info) { + int infoLen = info.Length(); + + if (infoLen != 3 && infoLen != 5 && infoLen != 9) { + Napi::TypeError::New(env, "Invalid arguments").ThrowAsJavaScriptException(); + return; + } + + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "The first argument must be an object").ThrowAsJavaScriptException(); + return; + } + + double args[8]; + if(!checkArgs(info, args, infoLen - 1, 1)) + return; - float sx = 0 + double sx = 0 , sy = 0 , sw = 0 , sh = 0 - , dx, dy, dw, dh; + , dx = 0 + , dy = 0 + , dw = 0 + , dh = 0 + , source_w = 0 + , source_h = 0; cairo_surface_t *surface; - Local obj = info[0]->ToObject(); + Napi::Object obj = info[0].As(); // Image - if (Nan::New(Image::constructor)->HasInstance(obj)) { - Image *img = Nan::ObjectWrap::Unwrap(obj); + if (obj.InstanceOf(env.GetInstanceData()->ImageCtor.Value()).UnwrapOr(false)) { + Image *img = Image::Unwrap(obj); if (!img->isComplete()) { - return Nan::ThrowError("Image given has not completed loading"); + Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException(); + return; } - sw = img->width; - sh = img->height; + source_w = sw = img->width; + source_h = sh = img->height; surface = img->surface(); // Canvas - } else if (Nan::New(Canvas::constructor)->HasInstance(obj)) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); - sw = canvas->width; - sh = canvas->height; + } else if (obj.InstanceOf(env.GetInstanceData()->CanvasCtor.Value()).UnwrapOr(false)) { + Canvas *canvas = Canvas::Unwrap(obj); + source_w = sw = canvas->getWidth(); + source_h = sh = canvas->getHeight(); surface = canvas->surface(); // Invalid } else { - return Nan::ThrowTypeError("Image or Canvas expected"); + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); + } + return; } - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); // Arguments - switch (info.Length()) { + switch (infoLen) { // img, sx, sy, sw, sh, dx, dy, dw, dh case 9: - sx = info[1]->NumberValue(); - sy = info[2]->NumberValue(); - sw = info[3]->NumberValue(); - sh = info[4]->NumberValue(); - dx = info[5]->NumberValue(); - dy = info[6]->NumberValue(); - dw = info[7]->NumberValue(); - dh = info[8]->NumberValue(); + sx = args[0]; + sy = args[1]; + sw = args[2]; + sh = args[3]; + dx = args[4]; + dy = args[5]; + dw = args[6]; + dh = args[7]; break; // img, dx, dy, dw, dh case 5: - dx = info[1]->NumberValue(); - dy = info[2]->NumberValue(); - dw = info[3]->NumberValue(); - dh = info[4]->NumberValue(); + dx = args[0]; + dy = args[1]; + dw = args[2]; + dh = args[3]; break; // img, dx, dy case 3: - dx = info[1]->NumberValue(); - dy = info[2]->NumberValue(); + dx = args[0]; + dy = args[1]; dw = sw; dh = sh; break; - default: - return Nan::ThrowTypeError("invalid arguments"); } + if (!(sw && sh && dw && dh)) + return; + // Start draw cairo_save(ctx); - // Scale src - float fx = (float) dw / sw; - float fy = (float) dh / sh; - - if (dw != sw || dh != sh) { - cairo_scale(ctx, fx, fy); - dx /= fx; - dy /= fy; - dw /= fx; - dh /= fy; + cairo_matrix_t matrix; + double transforms[6]; + cairo_get_matrix(ctx, &matrix); + decompose_matrix(matrix, transforms); + // extract the scale value from the current transform so that we know how many pixels we + // need for our extra canvas in the drawImage operation. + double current_scale_x = std::abs(transforms[1]); + double current_scale_y = std::abs(transforms[2]); + double extra_dx = 0; + double extra_dy = 0; + double fx = dw / sw * current_scale_x; // transforms[1] is scale on X + double fy = dh / sh * current_scale_y; // transforms[2] is scale on X + bool needScale = dw != sw || dh != sh; + bool needCut = sw != source_w || sh != source_h || sx < 0 || sy < 0; + bool sameCanvas = surface == canvas()->surface(); + bool needsExtraSurface = sameCanvas || needCut || needScale; + cairo_surface_t *surfTemp = NULL; + cairo_t *ctxTemp = NULL; + + if (needsExtraSurface) { + // we want to create the extra surface as small as possible. + // fx and fy are the total scaling we need to apply to sw, sh. + // from sw and sh we want to remove the part that is outside the source_w and soruce_h + double real_w = sw; + double real_h = sh; + double translate_x = 0; + double translate_y = 0; + // if sx or sy are negative, a part of the area represented by sw and sh is empty + // because there are empty pixels, so we cut it out. + // On the other hand if sx or sy are positive, but sw and sh extend outside the real + // source pixels, we cut the area in that case too. + if (sx < 0) { + extra_dx = -sx * fx; + real_w = sw + sx; + } else if (sx + sw > source_w) { + real_w = sw - (sx + sw - source_w); + } + if (sy < 0) { + extra_dy = -sy * fy; + real_h = sh + sy; + } else if (sy + sh > source_h) { + real_h = sh - (sy + sh - source_h); + } + // if after cutting we are still bigger than source pixels, we restrict again + if (real_w > source_w) { + real_w = source_w; + } + if (real_h > source_h) { + real_h = source_h; + } + // TODO: find a way to limit the surfTemp to real_w and real_h if fx and fy are bigger than 1. + // there are no more pixel than the one available in the source, no need to create a bigger surface. + surfTemp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, round(real_w * fx), round(real_h * fy)); + ctxTemp = cairo_create(surfTemp); + cairo_scale(ctxTemp, fx, fy); + if (sx > 0) { + translate_x = sx; + } + if (sy > 0) { + translate_y = sy; + } + cairo_set_source_surface(ctxTemp, surface, -translate_x, -translate_y); + cairo_pattern_set_filter(cairo_get_source(ctxTemp), state->imageSmoothingEnabled ? state->patternQuality : CAIRO_FILTER_NEAREST); + cairo_pattern_set_extend(cairo_get_source(ctxTemp), CAIRO_EXTEND_REFLECT); + cairo_paint_with_alpha(ctxTemp, 1); + surface = surfTemp; } - // apply shadow if there is one - if (context->hasShadow()) { - if(context->state->shadowBlur) { + if (hasShadow()) { + if(state->shadowBlur) { // we need to create a new surface in order to blur - int pad = context->state->shadowBlur * 2; + int pad = state->shadowBlur * 2; cairo_surface_t *shadow_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dw + 2 * pad, dh + 2 * pad); cairo_t *shadow_context = cairo_create(shadow_surface); // mask and blur - context->setSourceRGBA(shadow_context, context->state->shadow); + setSourceRGBA(shadow_context, state->shadow); cairo_mask_surface(shadow_context, surface, pad, pad); - context->blur(shadow_surface, context->state->shadowBlur); + blur(shadow_surface, state->shadowBlur); // paint // @note: ShadowBlur looks different in each browser. This implementation matches chrome as close as possible. @@ -862,52 +1445,62 @@ NAN_METHOD(Context2d::DrawImage) { // implementation, and its not immediately clear why an offset is necessary, but without it, the result // in chrome is different. cairo_set_source_surface(ctx, shadow_surface, - dx - sx + (context->state->shadowOffsetX / fx) - pad + 1.4, - dy - sy + (context->state->shadowOffsetY / fy) - pad + 1.4); + dx + state->shadowOffsetX - pad + 1.4, + dy + state->shadowOffsetY - pad + 1.4); cairo_paint(ctx); - // cleanup cairo_destroy(shadow_context); cairo_surface_destroy(shadow_surface); } else { - context->setSourceRGBA(context->state->shadow); + setSourceRGBA(state->shadow); cairo_mask_surface(ctx, surface, - dx - sx + (context->state->shadowOffsetX / fx), - dy - sy + (context->state->shadowOffsetY / fy)); + dx + (state->shadowOffsetX), + dy + (state->shadowOffsetY)); } } - context->savePath(); - cairo_rectangle(ctx, dx, dy, dw, dh); - cairo_clip(ctx); - context->restorePath(); + double scaled_dx = dx; + double scaled_dy = dy; + if (needsExtraSurface && (current_scale_x != 1 || current_scale_y != 1)) { + // in this case our surface contains already current_scale_x, we need to scale back + cairo_scale(ctx, 1 / current_scale_x, 1 / current_scale_y); + scaled_dx *= current_scale_x; + scaled_dy *= current_scale_y; + } // Paint - cairo_set_source_surface(ctx, surface, dx - sx, dy - sy); - cairo_pattern_set_filter(cairo_get_source(ctx), context->state->patternQuality); - cairo_paint_with_alpha(ctx, context->state->globalAlpha); + cairo_set_source_surface(ctx, surface, scaled_dx + extra_dx, scaled_dy + extra_dy); + cairo_pattern_set_filter(cairo_get_source(ctx), state->imageSmoothingEnabled ? state->patternQuality : CAIRO_FILTER_NEAREST); + cairo_pattern_set_extend(cairo_get_source(ctx), CAIRO_EXTEND_NONE); + cairo_paint_with_alpha(ctx, state->globalAlpha); cairo_restore(ctx); + + if (needsExtraSurface) { + cairo_destroy(ctxTemp); + cairo_surface_destroy(surfTemp); + } } /* * Get global alpha. */ -NAN_GETTER(Context2d::GetGlobalAlpha) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->globalAlpha)); +Napi::Value +Context2d::GetGlobalAlpha(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, state->globalAlpha); } /* * Set global alpha. */ -NAN_SETTER(Context2d::SetGlobalAlpha) { - double n = value->NumberValue(); - if (n >= 0 && n <= 1) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->globalAlpha = n; +void +Context2d::SetGlobalAlpha(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe numberValue = value.ToNumber(); + if (numberValue.IsJust()) { + double n = numberValue.Unwrap().DoubleValue(); + if (n >= 0 && n <= 1) state->globalAlpha = n; } } @@ -915,163 +1508,157 @@ NAN_SETTER(Context2d::SetGlobalAlpha) { * Get global composite operation. */ -NAN_GETTER(Context2d::GetGlobalCompositeOperation) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); +Napi::Value +Context2d::GetGlobalCompositeOperation(const Napi::CallbackInfo& info) { + cairo_t *ctx = context(); - const char *op = "source-over"; + const char *op{}; switch (cairo_get_operator(ctx)) { - case CAIRO_OPERATOR_ATOP: op = "source-atop"; break; + // composite modes: + case CAIRO_OPERATOR_CLEAR: op = "clear"; break; + case CAIRO_OPERATOR_SOURCE: op = "copy"; break; + case CAIRO_OPERATOR_DEST: op = "destination"; break; + case CAIRO_OPERATOR_OVER: op = "source-over"; break; + case CAIRO_OPERATOR_DEST_OVER: op = "destination-over"; break; case CAIRO_OPERATOR_IN: op = "source-in"; break; - case CAIRO_OPERATOR_OUT: op = "source-out"; break; - case CAIRO_OPERATOR_XOR: op = "xor"; break; - case CAIRO_OPERATOR_DEST_ATOP: op = "destination-atop"; break; case CAIRO_OPERATOR_DEST_IN: op = "destination-in"; break; + case CAIRO_OPERATOR_OUT: op = "source-out"; break; case CAIRO_OPERATOR_DEST_OUT: op = "destination-out"; break; - case CAIRO_OPERATOR_DEST_OVER: op = "destination-over"; break; - case CAIRO_OPERATOR_CLEAR: op = "clear"; break; - case CAIRO_OPERATOR_SOURCE: op = "source"; break; - case CAIRO_OPERATOR_DEST: op = "dest"; break; - case CAIRO_OPERATOR_OVER: op = "over"; break; - case CAIRO_OPERATOR_SATURATE: op = "saturate"; break; - // Non-standard - // supported by resent versions of cairo -#if CAIRO_VERSION_MINOR >= 10 - case CAIRO_OPERATOR_LIGHTEN: op = "lighten"; break; - case CAIRO_OPERATOR_ADD: op = "add"; break; - case CAIRO_OPERATOR_DARKEN: op = "darker"; break; + case CAIRO_OPERATOR_ATOP: op = "source-atop"; break; + case CAIRO_OPERATOR_DEST_ATOP: op = "destination-atop"; break; + case CAIRO_OPERATOR_XOR: op = "xor"; break; + case CAIRO_OPERATOR_ADD: op = "lighter"; break; + // blend modes: + // Note: "source-over" and "normal" are synonyms. Chrome and FF both report + // "source-over" after setting gCO to "normal". + // case CAIRO_OPERATOR_OVER: op = "normal"; case CAIRO_OPERATOR_MULTIPLY: op = "multiply"; break; case CAIRO_OPERATOR_SCREEN: op = "screen"; break; case CAIRO_OPERATOR_OVERLAY: op = "overlay"; break; - case CAIRO_OPERATOR_HARD_LIGHT: op = "hard-light"; break; - case CAIRO_OPERATOR_SOFT_LIGHT: op = "soft-light"; break; - case CAIRO_OPERATOR_HSL_HUE: op = "hsl-hue"; break; - case CAIRO_OPERATOR_HSL_SATURATION: op = "hsl-saturation"; break; - case CAIRO_OPERATOR_HSL_COLOR: op = "hsl-color"; break; - case CAIRO_OPERATOR_HSL_LUMINOSITY: op = "hsl-luminosity"; break; + case CAIRO_OPERATOR_DARKEN: op = "darken"; break; + case CAIRO_OPERATOR_LIGHTEN: op = "lighten"; break; case CAIRO_OPERATOR_COLOR_DODGE: op = "color-dodge"; break; case CAIRO_OPERATOR_COLOR_BURN: op = "color-burn"; break; + case CAIRO_OPERATOR_HARD_LIGHT: op = "hard-light"; break; + case CAIRO_OPERATOR_SOFT_LIGHT: op = "soft-light"; break; case CAIRO_OPERATOR_DIFFERENCE: op = "difference"; break; case CAIRO_OPERATOR_EXCLUSION: op = "exclusion"; break; -#else - case CAIRO_OPERATOR_ADD: op = "lighter"; break; -#endif + case CAIRO_OPERATOR_HSL_HUE: op = "hue"; break; + case CAIRO_OPERATOR_HSL_SATURATION: op = "saturation"; break; + case CAIRO_OPERATOR_HSL_COLOR: op = "color"; break; + case CAIRO_OPERATOR_HSL_LUMINOSITY: op = "luminosity"; break; + // non-standard: + case CAIRO_OPERATOR_SATURATE: op = "saturate"; break; + default: op = "source-over"; } - info.GetReturnValue().Set(Nan::New(op).ToLocalChecked()); + return Napi::String::New(env, op); } /* * Set pattern quality. */ -NAN_SETTER(Context2d::SetPatternQuality) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - String::Utf8Value quality(value->ToString()); - if (0 == strcmp("fast", *quality)) { - context->state->patternQuality = CAIRO_FILTER_FAST; - } else if (0 == strcmp("good", *quality)) { - context->state->patternQuality = CAIRO_FILTER_GOOD; - } else if (0 == strcmp("best", *quality)) { - context->state->patternQuality = CAIRO_FILTER_BEST; - } else if (0 == strcmp("nearest", *quality)) { - context->state->patternQuality = CAIRO_FILTER_NEAREST; - } else if (0 == strcmp("bilinear", *quality)) { - context->state->patternQuality = CAIRO_FILTER_BILINEAR; - } +void +Context2d::SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsString()) { + std::string quality = value.As().Utf8Value(); + if (quality == "fast") { + state->patternQuality = CAIRO_FILTER_FAST; + } else if (quality == "good") { + state->patternQuality = CAIRO_FILTER_GOOD; + } else if (quality == "best") { + state->patternQuality = CAIRO_FILTER_BEST; + } else if (quality == "nearest") { + state->patternQuality = CAIRO_FILTER_NEAREST; + } else if (quality == "bilinear") { + state->patternQuality = CAIRO_FILTER_BILINEAR; + } + } } /* * Get pattern quality. */ -NAN_GETTER(Context2d::GetPatternQuality) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetPatternQuality(const Napi::CallbackInfo& info) { const char *quality; - switch (context->state->patternQuality) { + switch (state->patternQuality) { case CAIRO_FILTER_FAST: quality = "fast"; break; case CAIRO_FILTER_BEST: quality = "best"; break; case CAIRO_FILTER_NEAREST: quality = "nearest"; break; case CAIRO_FILTER_BILINEAR: quality = "bilinear"; break; default: quality = "good"; } - info.GetReturnValue().Set(Nan::New(quality).ToLocalChecked()); + return Napi::String::New(env, quality); +} + +/* + * Set ImageSmoothingEnabled value. + */ + +void +Context2d::SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Boolean boolValue; + if (value.ToBoolean().UnwrapTo(&boolValue)) state->imageSmoothingEnabled = boolValue.Value(); +} + +/* + * Get pattern quality. + */ + +Napi::Value +Context2d::GetImageSmoothingEnabled(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(env, state->imageSmoothingEnabled); } /* * Set global composite operation. */ -NAN_SETTER(Context2d::SetGlobalCompositeOperation) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - String::Utf8Value type(value->ToString()); - if (0 == strcmp("xor", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_XOR); - } else if (0 == strcmp("source-atop", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_ATOP); - } else if (0 == strcmp("source-in", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_IN); - } else if (0 == strcmp("source-out", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_OUT); - } else if (0 == strcmp("destination-atop", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST_ATOP); - } else if (0 == strcmp("destination-in", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST_IN); - } else if (0 == strcmp("destination-out", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST_OUT); - } else if (0 == strcmp("destination-over", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST_OVER); - } else if (0 == strcmp("clear", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_CLEAR); - } else if (0 == strcmp("source", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_SOURCE); - } else if (0 == strcmp("dest", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DEST); - } else if (0 == strcmp("saturate", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_SATURATE); - } else if (0 == strcmp("over", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_OVER); - // Non-standard - // supported by resent versions of cairo -#if CAIRO_VERSION_MINOR >= 10 - } else if (0 == strcmp("add", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_ADD); - } else if (0 == strcmp("lighten", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_LIGHTEN); - } else if (0 == strcmp("darker", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DARKEN); - } else if (0 == strcmp("multiply", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_MULTIPLY); - } else if (0 == strcmp("screen", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_SCREEN); - } else if (0 == strcmp("overlay", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_OVERLAY); - } else if (0 == strcmp("hard-light", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HARD_LIGHT); - } else if (0 == strcmp("soft-light", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_SOFT_LIGHT); - } else if (0 == strcmp("hsl-hue", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HSL_HUE); - } else if (0 == strcmp("hsl-saturation", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HSL_SATURATION); - } else if (0 == strcmp("hsl-color", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HSL_COLOR); - } else if (0 == strcmp("hsl-luminosity", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_HSL_LUMINOSITY); - } else if (0 == strcmp("color-dodge", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_COLOR_DODGE); - } else if (0 == strcmp("color-burn", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_COLOR_BURN); - } else if (0 == strcmp("difference", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_DIFFERENCE); - } else if (0 == strcmp("exclusion", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_EXCLUSION); -#endif - } else if (0 == strcmp("lighter", *type)) { - cairo_set_operator(ctx, CAIRO_OPERATOR_ADD); - } else { - cairo_set_operator(ctx, CAIRO_OPERATOR_OVER); +void +Context2d::SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value) { + cairo_t *ctx = this->context(); + Napi::String opStr; + if (value.ToString().UnwrapTo(&opStr)) { // Unlike CSS colors, this *is* case-sensitive + const std::map blendmodes = { + // composite modes: + {"clear", CAIRO_OPERATOR_CLEAR}, + {"copy", CAIRO_OPERATOR_SOURCE}, + {"destination", CAIRO_OPERATOR_DEST}, // this seems to have been omitted from the spec + {"source-over", CAIRO_OPERATOR_OVER}, + {"destination-over", CAIRO_OPERATOR_DEST_OVER}, + {"source-in", CAIRO_OPERATOR_IN}, + {"destination-in", CAIRO_OPERATOR_DEST_IN}, + {"source-out", CAIRO_OPERATOR_OUT}, + {"destination-out", CAIRO_OPERATOR_DEST_OUT}, + {"source-atop", CAIRO_OPERATOR_ATOP}, + {"destination-atop", CAIRO_OPERATOR_DEST_ATOP}, + {"xor", CAIRO_OPERATOR_XOR}, + {"lighter", CAIRO_OPERATOR_ADD}, + // blend modes: + {"normal", CAIRO_OPERATOR_OVER}, + {"multiply", CAIRO_OPERATOR_MULTIPLY}, + {"screen", CAIRO_OPERATOR_SCREEN}, + {"overlay", CAIRO_OPERATOR_OVERLAY}, + {"darken", CAIRO_OPERATOR_DARKEN}, + {"lighten", CAIRO_OPERATOR_LIGHTEN}, + {"color-dodge", CAIRO_OPERATOR_COLOR_DODGE}, + {"color-burn", CAIRO_OPERATOR_COLOR_BURN}, + {"hard-light", CAIRO_OPERATOR_HARD_LIGHT}, + {"soft-light", CAIRO_OPERATOR_SOFT_LIGHT}, + {"difference", CAIRO_OPERATOR_DIFFERENCE}, + {"exclusion", CAIRO_OPERATOR_EXCLUSION}, + {"hue", CAIRO_OPERATOR_HSL_HUE}, + {"saturation", CAIRO_OPERATOR_HSL_SATURATION}, + {"color", CAIRO_OPERATOR_HSL_COLOR}, + {"luminosity", CAIRO_OPERATOR_HSL_LUMINOSITY}, + // non-standard: + {"saturate", CAIRO_OPERATOR_SATURATE} + }; + auto op = blendmodes.find(opStr.Utf8Value()); + if (op != blendmodes.end()) cairo_set_operator(ctx, op->second); } } @@ -1079,56 +1666,61 @@ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { * Get shadow offset x. */ -NAN_GETTER(Context2d::GetShadowOffsetX) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetX)); +Napi::Value +Context2d::GetShadowOffsetX(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, state->shadowOffsetX); } /* * Set shadow offset x. */ -NAN_SETTER(Context2d::SetShadowOffsetX) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadowOffsetX = value->NumberValue(); +void +Context2d::SetShadowOffsetX(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Number numberValue; + if (value.ToNumber().UnwrapTo(&numberValue)) state->shadowOffsetX = numberValue.DoubleValue(); } /* * Get shadow offset y. */ -NAN_GETTER(Context2d::GetShadowOffsetY) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetY)); +Napi::Value +Context2d::GetShadowOffsetY(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, state->shadowOffsetY); } /* * Set shadow offset y. */ -NAN_SETTER(Context2d::SetShadowOffsetY) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadowOffsetY = value->NumberValue(); +void +Context2d::SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Number numberValue; + if (value.ToNumber().UnwrapTo(&numberValue)) state->shadowOffsetY = numberValue.DoubleValue(); } /* * Get shadow blur. */ -NAN_GETTER(Context2d::GetShadowBlur) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->shadowBlur)); +Napi::Value +Context2d::GetShadowBlur(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, state->shadowBlur); } /* * Set shadow blur. */ -NAN_SETTER(Context2d::SetShadowBlur) { - int n = value->NumberValue(); - if (n >= 0) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadowBlur = n; +void +Context2d::SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Number n; + if (value.ToNumber().UnwrapTo(&n)) { + double v = n.DoubleValue(); + if (v >= 0 && v <= std::numeric_limitsshadowBlur)>::max()) { + state->shadowBlur = v; + } } } @@ -1136,69 +1728,76 @@ NAN_SETTER(Context2d::SetShadowBlur) { * Get current antialiasing setting. */ -NAN_GETTER(Context2d::GetAntiAlias) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetAntiAlias(const Napi::CallbackInfo& info) { const char *aa; - switch (cairo_get_antialias(context->context())) { + switch (cairo_get_antialias(context())) { case CAIRO_ANTIALIAS_NONE: aa = "none"; break; case CAIRO_ANTIALIAS_GRAY: aa = "gray"; break; case CAIRO_ANTIALIAS_SUBPIXEL: aa = "subpixel"; break; default: aa = "default"; } - info.GetReturnValue().Set(Nan::New(aa).ToLocalChecked()); + return Napi::String::New(env, aa); } /* * Set antialiasing. */ -NAN_SETTER(Context2d::SetAntiAlias) { - String::Utf8Value str(value->ToString()); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - cairo_antialias_t a; - if (0 == strcmp("none", *str)) { - a = CAIRO_ANTIALIAS_NONE; - } else if (0 == strcmp("default", *str)) { - a = CAIRO_ANTIALIAS_DEFAULT; - } else if (0 == strcmp("gray", *str)) { - a = CAIRO_ANTIALIAS_GRAY; - } else if (0 == strcmp("subpixel", *str)) { - a = CAIRO_ANTIALIAS_SUBPIXEL; - } else { - a = cairo_get_antialias(ctx); +void +Context2d::SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::String stringValue; + + if (value.ToString().UnwrapTo(&stringValue)) { + std::string str = stringValue.Utf8Value(); + cairo_t *ctx = context(); + cairo_antialias_t a; + if (str == "none") { + a = CAIRO_ANTIALIAS_NONE; + } else if (str == "default") { + a = CAIRO_ANTIALIAS_DEFAULT; + } else if (str == "gray") { + a = CAIRO_ANTIALIAS_GRAY; + } else if (str == "subpixel") { + a = CAIRO_ANTIALIAS_SUBPIXEL; + } else { + a = cairo_get_antialias(ctx); + } + cairo_set_antialias(ctx, a); } - cairo_set_antialias(ctx, a); } /* * Get text drawing mode. */ -NAN_GETTER(Context2d::GetTextDrawingMode) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetTextDrawingMode(const Napi::CallbackInfo& info) { const char *mode; - if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { + if (state->textDrawingMode == TEXT_DRAW_PATHS) { mode = "path"; - } else if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { + } else if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { mode = "glyph"; } else { mode = "unknown"; } - info.GetReturnValue().Set(Nan::New(mode).ToLocalChecked()); + return Napi::String::New(env, mode); } /* * Set text drawing mode. */ -NAN_SETTER(Context2d::SetTextDrawingMode) { - String::Utf8Value str(value->ToString()); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (0 == strcmp("path", *str)) { - context->state->textDrawingMode = TEXT_DRAW_PATHS; - } else if (0 == strcmp("glyph", *str)) { - context->state->textDrawingMode = TEXT_DRAW_GLYPHS; +void +Context2d::SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::String stringValue; + if (value.ToString().UnwrapTo(&stringValue)) { + std::string str = stringValue.Utf8Value(); + if (str == "path") { + state->textDrawingMode = TEXT_DRAW_PATHS; + } else if (str == "glyph") { + state->textDrawingMode = TEXT_DRAW_GLYPHS; + } } } @@ -1206,59 +1805,212 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { * Get filter. */ -NAN_GETTER(Context2d::GetFilter) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetQuality(const Napi::CallbackInfo& info) { const char *filter; - switch (cairo_pattern_get_filter(cairo_get_source(context->context()))) { + switch (cairo_pattern_get_filter(cairo_get_source(context()))) { case CAIRO_FILTER_FAST: filter = "fast"; break; case CAIRO_FILTER_BEST: filter = "best"; break; case CAIRO_FILTER_NEAREST: filter = "nearest"; break; case CAIRO_FILTER_BILINEAR: filter = "bilinear"; break; default: filter = "good"; } - info.GetReturnValue().Set(Nan::New(filter).ToLocalChecked()); + return Napi::String::New(env, filter); } /* * Set filter. */ -NAN_SETTER(Context2d::SetFilter) { - String::Utf8Value str(value->ToString()); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_filter_t filter; - if (0 == strcmp("fast", *str)) { - filter = CAIRO_FILTER_FAST; - } else if (0 == strcmp("best", *str)) { - filter = CAIRO_FILTER_BEST; - } else if (0 == strcmp("nearest", *str)) { - filter = CAIRO_FILTER_NEAREST; - } else if (0 == strcmp("bilinear", *str)) { - filter = CAIRO_FILTER_BILINEAR; - } else { - filter = CAIRO_FILTER_GOOD; +void +Context2d::SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::String stringValue; + if (value.ToString().UnwrapTo(&stringValue)) { + std::string str = stringValue.Utf8Value(); + cairo_filter_t filter; + if (str == "fast") { + filter = CAIRO_FILTER_FAST; + } else if (str == "best") { + filter = CAIRO_FILTER_BEST; + } else if (str == "nearest") { + filter = CAIRO_FILTER_NEAREST; + } else if (str == "bilinear") { + filter = CAIRO_FILTER_BILINEAR; + } else { + filter = CAIRO_FILTER_GOOD; + } + cairo_pattern_set_filter(cairo_get_source(context()), filter); + } +} + +/* + * Helper for get current transform matrix + */ + +Napi::Value +Context2d::get_current_transform() { + Napi::Float64Array arr = Napi::Float64Array::New(env, 6); + double *dest = arr.Data(); + cairo_matrix_t matrix; + cairo_get_matrix(context(), &matrix); + dest[0] = matrix.xx; + dest[1] = matrix.yx; + dest[2] = matrix.xy; + dest[3] = matrix.yy; + dest[4] = matrix.x0; + dest[5] = matrix.y0; + Napi::Maybe ret = env.GetInstanceData()->DOMMatrixCtor.Value().New({ arr }); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); +} + +/* + * Helper for get/set transform. + */ + +void parse_matrix_from_object(cairo_matrix_t &matrix, Napi::Object mat) { + Napi::Value zero = Napi::Number::New(mat.Env(), 0); + cairo_matrix_init(&matrix, + mat.Get("a").UnwrapOr(zero).As().DoubleValue(), + mat.Get("b").UnwrapOr(zero).As().DoubleValue(), + mat.Get("c").UnwrapOr(zero).As().DoubleValue(), + mat.Get("d").UnwrapOr(zero).As().DoubleValue(), + mat.Get("e").UnwrapOr(zero).As().DoubleValue(), + mat.Get("f").UnwrapOr(zero).As().DoubleValue() + ); +} + + +/* + * Get current transform. + */ + +Napi::Value +Context2d::GetCurrentTransform(const Napi::CallbackInfo& info) { + return get_current_transform(); +} + +/* + * Set current transform. + */ + +void +Context2d::SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Object mat; + + if (value.ToObject().UnwrapTo(&mat)) { + if (!mat.InstanceOf(env.GetInstanceData()->DOMMatrixCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); + } + return; + } + + cairo_matrix_t matrix; + parse_matrix_from_object(matrix, mat); + + cairo_transform(context(), &matrix); + } +} + +/* + * Get current fill style. + */ + +Napi::Value +Context2d::GetFillStyle(const Napi::CallbackInfo& info) { + Napi::Value style; + + if (_fillStyle.IsEmpty()) + style = _getFillColor(); + else + style = _fillStyle.Value(); + + return style; +} + +/* + * Set current fill style. + */ + +void +Context2d::SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsString()) { + _fillStyle.Reset(); + _setFillColor(value.As()); + } else if (value.IsObject()) { + InstanceData *data = env.GetInstanceData(); + Napi::Object obj = value.As(); + if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) { + _fillStyle.Reset(obj); + Gradient *grad = Gradient::Unwrap(obj); + state->fillGradient = grad->pattern(); + } else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) { + _fillStyle.Reset(obj); + Pattern *pattern = Pattern::Unwrap(obj); + state->fillPattern = pattern->pattern(); + } + } +} + +/* + * Get current stroke style. + */ + +Napi::Value +Context2d::GetStrokeStyle(const Napi::CallbackInfo& info) { + Napi::Value style; + + if (_strokeStyle.IsEmpty()) + style = _getStrokeColor(); + else + style = _strokeStyle.Value(); + + return style; +} + +/* + * Set current stroke style. + */ + +void +Context2d::SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsString()) { + _strokeStyle.Reset(); + _setStrokeColor(value.As()); + } else if (value.IsObject()) { + InstanceData *data = env.GetInstanceData(); + Napi::Object obj = value.As(); + if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) { + _strokeStyle.Reset(obj); + Gradient *grad = Gradient::Unwrap(obj); + state->strokeGradient = grad->pattern(); + } else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) { + _strokeStyle.Reset(value); + Pattern *pattern = Pattern::Unwrap(obj); + state->strokePattern = pattern->pattern(); + } } - cairo_pattern_set_filter(cairo_get_source(context->context()), filter); } /* * Get miter limit. */ -NAN_GETTER(Context2d::GetMiterLimit) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(cairo_get_miter_limit(context->context()))); +Napi::Value +Context2d::GetMiterLimit(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, cairo_get_miter_limit(context())); } /* * Set miter limit. */ -NAN_SETTER(Context2d::SetMiterLimit) { - double n = value->NumberValue(); - if (n > 0) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_set_miter_limit(context->context(), n); +void +Context2d::SetMiterLimit(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe numberValue = value.ToNumber(); + if (numberValue.IsJust()) { + double n = numberValue.Unwrap().DoubleValue(); + if (n > 0) cairo_set_miter_limit(context(), n); } } @@ -1266,20 +2018,23 @@ NAN_SETTER(Context2d::SetMiterLimit) { * Get line width. */ -NAN_GETTER(Context2d::GetLineWidth) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(cairo_get_line_width(context->context()))); +Napi::Value +Context2d::GetLineWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, cairo_get_line_width(context())); } /* * Set line width. */ -NAN_SETTER(Context2d::SetLineWidth) { - double n = value->NumberValue(); - if (n > 0 && n != std::numeric_limits::infinity()) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_set_line_width(context->context(), n); +void +Context2d::SetLineWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe numberValue = value.ToNumber(); + if (numberValue.IsJust()) { + double n = numberValue.Unwrap().DoubleValue(); + if (n > 0 && n != std::numeric_limits::infinity()) { + cairo_set_line_width(context(), n); + } } } @@ -1287,31 +2042,35 @@ NAN_SETTER(Context2d::SetLineWidth) { * Get line join. */ -NAN_GETTER(Context2d::GetLineJoin) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetLineJoin(const Napi::CallbackInfo& info) { const char *join; - switch (cairo_get_line_join(context->context())) { + switch (cairo_get_line_join(context())) { case CAIRO_LINE_JOIN_BEVEL: join = "bevel"; break; case CAIRO_LINE_JOIN_ROUND: join = "round"; break; default: join = "miter"; } - info.GetReturnValue().Set(Nan::New(join).ToLocalChecked()); + return Napi::String::New(env, join); } /* * Set line join. */ -NAN_SETTER(Context2d::SetLineJoin) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - String::Utf8Value type(value->ToString()); - if (0 == strcmp("round", *type)) { - cairo_set_line_join(ctx, CAIRO_LINE_JOIN_ROUND); - } else if (0 == strcmp("bevel", *type)) { - cairo_set_line_join(ctx, CAIRO_LINE_JOIN_BEVEL); - } else { - cairo_set_line_join(ctx, CAIRO_LINE_JOIN_MITER); +void +Context2d::SetLineJoin(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe stringValue = value.ToString(); + cairo_t *ctx = context(); + + if (stringValue.IsJust()) { + std::string type = stringValue.Unwrap().Utf8Value(); + if (type == "round") { + cairo_set_line_join(ctx, CAIRO_LINE_JOIN_ROUND); + } else if (type == "bevel") { + cairo_set_line_join(ctx, CAIRO_LINE_JOIN_BEVEL); + } else { + cairo_set_line_join(ctx, CAIRO_LINE_JOIN_MITER); + } } } @@ -1319,31 +2078,35 @@ NAN_SETTER(Context2d::SetLineJoin) { * Get line cap. */ -NAN_GETTER(Context2d::GetLineCap) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetLineCap(const Napi::CallbackInfo& info) { const char *cap; - switch (cairo_get_line_cap(context->context())) { + switch (cairo_get_line_cap(context())) { case CAIRO_LINE_CAP_ROUND: cap = "round"; break; case CAIRO_LINE_CAP_SQUARE: cap = "square"; break; default: cap = "butt"; } - info.GetReturnValue().Set(Nan::New(cap).ToLocalChecked()); + return Napi::String::New(env, cap); } /* * Set line cap. */ -NAN_SETTER(Context2d::SetLineCap) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - String::Utf8Value type(value->ToString()); - if (0 == strcmp("round", *type)) { - cairo_set_line_cap(ctx, CAIRO_LINE_CAP_ROUND); - } else if (0 == strcmp("square", *type)) { - cairo_set_line_cap(ctx, CAIRO_LINE_CAP_SQUARE); - } else { - cairo_set_line_cap(ctx, CAIRO_LINE_CAP_BUTT); +void +Context2d::SetLineCap(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe stringValue = value.ToString(); + cairo_t *ctx = context(); + + if (stringValue.IsJust()) { + std::string type = stringValue.Unwrap().Utf8Value(); + if (type == "round") { + cairo_set_line_cap(ctx, CAIRO_LINE_CAP_ROUND); + } else if (type == "square") { + cairo_set_line_cap(ctx, CAIRO_LINE_CAP_SQUARE); + } else { + cairo_set_line_cap(ctx, CAIRO_LINE_CAP_BUTT); + } } } @@ -1351,68 +2114,30 @@ NAN_SETTER(Context2d::SetLineCap) { * Check if the given point is within the current path. */ -NAN_METHOD(Context2d::IsPointInPath) { - if (info[0]->IsNumber() && info[1]->IsNumber()) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - double x = info[0]->NumberValue() - , y = info[1]->NumberValue(); - context->setFillRule(info[2]); - info.GetReturnValue().Set(Nan::New(cairo_in_fill(ctx, x, y) || cairo_in_stroke(ctx, x, y))); - return; - } - info.GetReturnValue().Set(Nan::False()); -} - -/* - * Set fill pattern, useV internally for fillStyle= - */ - -NAN_METHOD(Context2d::SetFillPattern) { - Local obj = info[0]->ToObject(); - if (Nan::New(Gradient::constructor)->HasInstance(obj)){ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Gradient *grad = Nan::ObjectWrap::Unwrap(obj); - context->state->fillGradient = grad->pattern(); - } else if(Nan::New(Pattern::constructor)->HasInstance(obj)){ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); - context->state->fillPattern = pattern->pattern(); - } else { - return Nan::ThrowTypeError("Gradient or Pattern expected"); - } -} - -/* - * Set stroke pattern, used internally for strokeStyle= - */ - -NAN_METHOD(Context2d::SetStrokePattern) { - Local obj = info[0]->ToObject(); - if (Nan::New(Gradient::constructor)->HasInstance(obj)){ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Gradient *grad = Nan::ObjectWrap::Unwrap(obj); - context->state->strokeGradient = grad->pattern(); - } else if(Nan::New(Pattern::constructor)->HasInstance(obj)){ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); - context->state->strokePattern = pattern->pattern(); - } else { - return Nan::ThrowTypeError("Gradient or Pattern expected"); +Napi::Value +Context2d::IsPointInPath(const Napi::CallbackInfo& info) { + if (info[0].IsNumber() && info[1].IsNumber()) { + cairo_t *ctx = context(); + double x = info[0].As(), y = info[1].As(); + setFillRule(info[2]); + return Napi::Boolean::New(env, cairo_in_fill(ctx, x, y) || cairo_in_stroke(ctx, x, y)); } + return Napi::Boolean::New(env, false); } /* * Set shadow color. */ -NAN_SETTER(Context2d::SetShadowColor) { +void +Context2d::SetShadowColor(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Maybe stringValue = value.ToString(); short ok; - String::Utf8Value str(value->ToString()); - uint32_t rgba = rgba_from_string(*str, &ok); - if (ok) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->shadow = rgba_create(rgba); + + if (stringValue.IsJust()) { + std::string str = stringValue.Unwrap().Utf8Value(); + uint32_t rgba = rgba_from_string(str.c_str(), &ok); + if (ok) state->shadow = rgba_create(rgba); } } @@ -1420,105 +2145,128 @@ NAN_SETTER(Context2d::SetShadowColor) { * Get shadow color. */ -NAN_GETTER(Context2d::GetShadowColor) { +Napi::Value +Context2d::GetShadowColor(const Napi::CallbackInfo& info) { char buf[64]; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - rgba_to_string(context->state->shadow, buf, sizeof(buf)); - info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); + rgba_to_string(state->shadow, buf, sizeof(buf)); + return Napi::String::New(env, buf); } /* * Set fill color, used internally for fillStyle= */ -NAN_METHOD(Context2d::SetFillColor) { +void +Context2d::_setFillColor(Napi::Value arg) { + Napi::Maybe stringValue = arg.ToString(); short ok; - if (!info[0]->IsString()) return; - String::Utf8Value str(info[0]); - uint32_t rgba = rgba_from_string(*str, &ok); - if (!ok) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->fillPattern = context->state->fillGradient = NULL; - context->state->fill = rgba_create(rgba); + + if (stringValue.IsJust()) { + Napi::String str = stringValue.Unwrap(); + char buf[128] = {0}; + napi_status status = napi_get_value_string_utf8(env, str, buf, sizeof(buf) - 1, nullptr); + if (status != napi_ok) return; + uint32_t rgba = rgba_from_string(buf, &ok); + if (!ok) return; + state->fillPattern = state->fillGradient = NULL; + state->fill = rgba_create(rgba); + } } /* * Get fill color. */ -NAN_GETTER(Context2d::GetFillColor) { +Napi::Value +Context2d::_getFillColor() { char buf[64]; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - rgba_to_string(context->state->fill, buf, sizeof(buf)); - info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); + rgba_to_string(state->fill, buf, sizeof(buf)); + return Napi::String::New(env, buf); } /* * Set stroke color, used internally for strokeStyle= */ -NAN_METHOD(Context2d::SetStrokeColor) { +void +Context2d::_setStrokeColor(Napi::Value arg) { short ok; - if (!info[0]->IsString()) return; - String::Utf8Value str(info[0]); - uint32_t rgba = rgba_from_string(*str, &ok); + std::string str = arg.As(); + uint32_t rgba = rgba_from_string(str.c_str(), &ok); if (!ok) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->strokePattern = context->state->strokeGradient = NULL; - context->state->stroke = rgba_create(rgba); + state->strokePattern = state->strokeGradient = NULL; + state->stroke = rgba_create(rgba); } /* * Get stroke color. */ -NAN_GETTER(Context2d::GetStrokeColor) { +Napi::Value +Context2d::_getStrokeColor() { char buf[64]; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - rgba_to_string(context->state->stroke, buf, sizeof(buf)); - info.GetReturnValue().Set(Nan::New(buf).ToLocalChecked()); + rgba_to_string(state->stroke, buf, sizeof(buf)); + return Napi::String::New(env, buf); +} + +Napi::Value +Context2d::CreatePattern(const Napi::CallbackInfo& info) { + Napi::Function ctor = env.GetInstanceData()->CanvasPatternCtor.Value(); + Napi::Maybe ret = ctor.New({ info[0], info[1] }); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); +} + +Napi::Value +Context2d::CreateLinearGradient(const Napi::CallbackInfo& info) { + Napi::Function ctor = env.GetInstanceData()->CanvasGradientCtor.Value(); + Napi::Maybe ret = ctor.New({ info[0], info[1], info[2], info[3] }); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); + +} + +Napi::Value +Context2d::CreateRadialGradient(const Napi::CallbackInfo& info) { + Napi::Function ctor = env.GetInstanceData()->CanvasGradientCtor.Value(); + Napi::Maybe ret = ctor.New({ info[0], info[1], info[2], info[3], info[4], info[5] }); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); } /* * Bezier curve. */ -NAN_METHOD(Context2d::BezierCurveTo) { - if (!info[0]->IsNumber() - ||!info[1]->IsNumber() - ||!info[2]->IsNumber() - ||!info[3]->IsNumber() - ||!info[4]->IsNumber() - ||!info[5]->IsNumber()) return; +void +Context2d::BezierCurveTo(const Napi::CallbackInfo& info) { + double args[6]; + if(!checkArgs(info, args, 6)) + return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_curve_to(context->context() - , info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue() - , info[4]->NumberValue() - , info[5]->NumberValue()); + cairo_curve_to(context() + , args[0] + , args[1] + , args[2] + , args[3] + , args[4] + , args[5]); } /* * Quadratic curve approximation from libsvg-cairo. */ -NAN_METHOD(Context2d::QuadraticCurveTo) { - if (!info[0]->IsNumber() - ||!info[1]->IsNumber() - ||!info[2]->IsNumber() - ||!info[3]->IsNumber()) return; +void +Context2d::QuadraticCurveTo(const Napi::CallbackInfo& info) { + double args[4]; + if(!checkArgs(info, args, 4)) + return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); double x, y - , x1 = info[0]->NumberValue() - , y1 = info[1]->NumberValue() - , x2 = info[2]->NumberValue() - , y2 = info[3]->NumberValue(); + , x1 = args[0] + , y1 = args[1] + , x2 = args[2] + , y2 = args[3]; cairo_get_current_point(ctx, &x, &y); @@ -1538,105 +2286,151 @@ NAN_METHOD(Context2d::QuadraticCurveTo) { * Save state. */ -NAN_METHOD(Context2d::Save) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->save(); +void +Context2d::Save(const Napi::CallbackInfo& info) { + save(); } /* * Restore state. */ -NAN_METHOD(Context2d::Restore) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->restore(); +void +Context2d::Restore(const Napi::CallbackInfo& info) { + restore(); } /* * Creates a new subpath. */ -NAN_METHOD(Context2d::BeginPath) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_new_path(context->context()); +void +Context2d::BeginPath(const Napi::CallbackInfo& info) { + cairo_new_path(context()); } /* * Marks the subpath as closed. */ -NAN_METHOD(Context2d::ClosePath) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_close_path(context->context()); +void +Context2d::ClosePath(const Napi::CallbackInfo& info) { + cairo_close_path(context()); } /* * Rotate transformation. */ -NAN_METHOD(Context2d::Rotate) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_rotate(context->context() - , info[0]->IsNumber() ? info[0]->NumberValue() : 0); +void +Context2d::Rotate(const Napi::CallbackInfo& info) { + double args[1]; + if(!checkArgs(info, args, 1)) + return; + + cairo_rotate(context(), args[0]); } /* * Modify the CTM. */ -NAN_METHOD(Context2d::Transform) { +void +Context2d::Transform(const Napi::CallbackInfo& info) { + double args[6]; + if(!checkArgs(info, args, 6)) + return; + cairo_matrix_t matrix; cairo_matrix_init(&matrix - , info[0]->IsNumber() ? info[0]->NumberValue() : 0 - , info[1]->IsNumber() ? info[1]->NumberValue() : 0 - , info[2]->IsNumber() ? info[2]->NumberValue() : 0 - , info[3]->IsNumber() ? info[3]->NumberValue() : 0 - , info[4]->IsNumber() ? info[4]->NumberValue() : 0 - , info[5]->IsNumber() ? info[5]->NumberValue() : 0); + , args[0] + , args[1] + , args[2] + , args[3] + , args[4] + , args[5]); + + cairo_transform(context(), &matrix); +} + +/* + * Get the CTM + */ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_transform(context->context(), &matrix); +Napi::Value +Context2d::GetTransform(const Napi::CallbackInfo& info) { + return get_current_transform(); } /* * Reset the CTM, used internally by setTransform(). */ -NAN_METHOD(Context2d::ResetTransform) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_identity_matrix(context->context()); +void +Context2d::ResetTransform(const Napi::CallbackInfo& info) { + cairo_identity_matrix(context()); +} + +/* + * Reset transform matrix to identity, then apply the given args. + */ + +void +Context2d::SetTransform(const Napi::CallbackInfo& info) { + Napi::Object mat; + + if (info.Length() == 1 && info[0].ToObject().UnwrapTo(&mat)) { + if (!mat.InstanceOf(env.GetInstanceData()->DOMMatrixCtor.Value()).UnwrapOr(false)) { + if (!env.IsExceptionPending()) { + Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); + } + return; + } + + cairo_matrix_t matrix; + parse_matrix_from_object(matrix, mat); + + cairo_set_matrix(context(), &matrix); + } else { + cairo_identity_matrix(context()); + Context2d::Transform(info); + } } /* * Translate transformation. */ -NAN_METHOD(Context2d::Translate) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_translate(context->context() - , info[0]->IsNumber() ? info[0]->NumberValue() : 0 - , info[1]->IsNumber() ? info[1]->NumberValue() : 0); +void +Context2d::Translate(const Napi::CallbackInfo& info) { + double args[2]; + if(!checkArgs(info, args, 2)) + return; + + cairo_translate(context(), args[0], args[1]); } /* * Scale transformation. */ -NAN_METHOD(Context2d::Scale) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_scale(context->context() - , info[0]->IsNumber() ? info[0]->NumberValue() : 0 - , info[1]->IsNumber() ? info[1]->NumberValue() : 0); +void +Context2d::Scale(const Napi::CallbackInfo& info) { + double args[2]; + if(!checkArgs(info, args, 2)) + return; + + cairo_scale(context(), args[0], args[1]); } /* * Use path as clipping region. */ -NAN_METHOD(Context2d::Clip) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->setFillRule(info[0]); - cairo_t *ctx = context->context(); +void +Context2d::Clip(const Napi::CallbackInfo& info) { + setFillRule(info[0]); + cairo_t *ctx = context(); cairo_clip_preserve(ctx); } @@ -1644,112 +2438,188 @@ NAN_METHOD(Context2d::Clip) { * Fill the path. */ -NAN_METHOD(Context2d::Fill) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->setFillRule(info[0]); - context->fill(true); +void +Context2d::Fill(const Napi::CallbackInfo& info) { + setFillRule(info[0]); + fill(true); } /* * Stroke the path. */ -NAN_METHOD(Context2d::Stroke) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->stroke(true); +void +Context2d::Stroke(const Napi::CallbackInfo& info) { + stroke(true); } /* - * Fill text at (x, y). + * Helper for fillText/strokeText + */ + +double +get_text_scale(PangoLayout *layout, double maxWidth) { + + PangoRectangle logical_rect; + pango_layout_get_pixel_extents(layout, NULL, &logical_rect); + + if (logical_rect.width > maxWidth) { + return maxWidth / logical_rect.width; + } else { + return 1.0; + } +} + +/* + * Make sure the layout's font list is up-to-date */ +void +Context2d::checkFonts() { + // If fonts have been registered, the PangoContext is using an outdated FontMap + if (canvas()->fontSerial != fontSerial) { + pango_context_set_font_map( + pango_layout_get_context(_layout), + pango_cairo_font_map_get_default() + ); + + fontSerial = canvas()->fontSerial; + } +} + +void +Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { + int argsNum = info.Length() >= 4 ? 3 : 2; + + if (argsNum == 3 && info[3].IsUndefined()) + argsNum = 2; + + double args[3]; + if(!checkArgs(info, args, argsNum, 1)) + return; -NAN_METHOD(Context2d::FillText) { - if (!info[1]->IsNumber() - || !info[2]->IsNumber()) return; + Napi::String strValue; - String::Utf8Value str(info[0]->ToString()); - double x = info[1]->NumberValue(); - double y = info[2]->NumberValue(); + if (!info[0].ToString().UnwrapTo(&strValue)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + std::string str = strValue.Utf8Value(); + double x = args[0]; + double y = args[1]; + double scaled_by = 1; - context->savePath(); - if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { - context->fill(); - context->setTextPath(*str, x, y); - } else if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { - context->setTextPath(*str, x, y); - context->fill(); + PangoLayout *layout = this->layout(); + + checkFonts(); + pango_layout_set_text(layout, str.c_str(), -1); + if (state->lang != "") { + pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str())); + } + pango_cairo_update_layout(context(), layout); + + PangoDirection pango_dir = state->direction == "ltr" ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL; + pango_context_set_base_dir(pango_layout_get_context(_layout), pango_dir); + + if (argsNum == 3) { + if (args[2] <= 0) return; + scaled_by = get_text_scale(layout, args[2]); + cairo_save(context()); + cairo_scale(context(), scaled_by, 1); + } + + savePath(); + if (state->textDrawingMode == TEXT_DRAW_GLYPHS) { + if (stroke == true) { this->stroke(); } else { this->fill(); } + setTextPath(x / scaled_by, y); + } else if (state->textDrawingMode == TEXT_DRAW_PATHS) { + setTextPath(x / scaled_by, y); + if (stroke == true) { this->stroke(); } else { this->fill(); } + } + restorePath(); + if (argsNum == 3) { + cairo_restore(context()); } - context->restorePath(); +} + +/* + * Fill text at (x, y). + */ + +void +Context2d::FillText(const Napi::CallbackInfo& info) { + paintText(info, false); } /* * Stroke text at (x ,y). */ -NAN_METHOD(Context2d::StrokeText) { - if (!info[1]->IsNumber() - || !info[2]->IsNumber()) return; +void +Context2d::StrokeText(const Napi::CallbackInfo& info) { + paintText(info, true); +} + +/* + * Gets the baseline adjustment in device pixels + */ +inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { + PangoRectangle logical_rect; + pango_layout_line_get_extents(pango_layout_get_line(layout, 0), NULL, &logical_rect); + + double scale = 1.0 / PANGO_SCALE; + double ascent = scale * pango_layout_get_baseline(layout); + double descent = scale * logical_rect.height - ascent; - String::Utf8Value str(info[0]->ToString()); - double x = info[1]->NumberValue(); - double y = info[2]->NumberValue(); + switch (baseline) { + case TEXT_BASELINE_ALPHABETIC: + return ascent; + case TEXT_BASELINE_MIDDLE: + return (ascent + descent) / 2.0; + case TEXT_BASELINE_BOTTOM: + return ascent + descent; + default: + return 0; + } +} - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +text_align_t +Context2d::resolveTextAlignment() { + text_align_t alignment = state->textAlignment; - context->savePath(); - if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { - context->stroke(); - context->setTextPath(*str, x, y); - } else if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { - context->setTextPath(*str, x, y); - context->stroke(); + // Convert start/end to left/right based on direction + if (alignment == TEXT_ALIGNMENT_START) { + return (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT; + } else if (alignment == TEXT_ALIGNMENT_END) { + return (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT; } - context->restorePath(); + + return alignment; } /* - * Set text path for the given string at (x, y). + * Set text path for the string in the layout at (x, y). + * This function is called by paintText and won't behave correctly + * if is not called from there. + * it needs pango_layout_set_text and pango_cairo_update_layout to be called before */ void -Context2d::setTextPath(const char *str, double x, double y) { - PangoRectangle ink_rect, logical_rect; - PangoFontMetrics *metrics = NULL; - - pango_layout_set_text(_layout, str, -1); - pango_cairo_update_layout(_context, _layout); +Context2d::setTextPath(double x, double y) { + PangoRectangle logical_rect; + text_align_t alignment = resolveTextAlignment(); - switch (state->textAlignment) { - // center - case 0: - pango_layout_get_pixel_extents(_layout, &ink_rect, &logical_rect); + switch (alignment) { + case TEXT_ALIGNMENT_CENTER: + pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width / 2; break; - // right - case 1: - pango_layout_get_pixel_extents(_layout, &ink_rect, &logical_rect); + case TEXT_ALIGNMENT_RIGHT: + pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; - } - - switch (state->textBaseline) { - case TEXT_BASELINE_ALPHABETIC: - metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= pango_font_metrics_get_ascent(metrics) / PANGO_SCALE; - break; - case TEXT_BASELINE_MIDDLE: - metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= (pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics))/(2.0 * PANGO_SCALE); - break; - case TEXT_BASELINE_BOTTOM: - metrics = PANGO_LAYOUT_GET_METRICS(_layout); - y -= (pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE; + default: // TEXT_ALIGNMENT_LEFT break; } - if (metrics) pango_font_metrics_unref(metrics); + y -= getBaselineAdjustment(_layout, state->textBaseline); cairo_move_to(_context, x, y); if (state->textDrawingMode == TEXT_DRAW_PATHS) { @@ -1763,32 +2633,35 @@ Context2d::setTextPath(const char *str, double x, double y) { * Adds a point to the current subpath. */ -NAN_METHOD(Context2d::LineTo) { - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("lineTo() x must be a number"); - if (!info[1]->IsNumber()) - return Nan::ThrowTypeError("lineTo() y must be a number"); +void +Context2d::LineTo(const Napi::CallbackInfo& info) { + double args[2]; + if(!checkArgs(info, args, 2)) + return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_line_to(context->context() - , info[0]->NumberValue() - , info[1]->NumberValue()); + cairo_line_to(context(), args[0], args[1]); } /* * Creates a new subpath at the given point. */ -NAN_METHOD(Context2d::MoveTo) { - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("moveTo() x must be a number"); - if (!info[1]->IsNumber()) - return Nan::ThrowTypeError("moveTo() y must be a number"); +void +Context2d::MoveTo(const Napi::CallbackInfo& info) { + double args[2]; + if(!checkArgs(info, args, 2)) + return; + + cairo_move_to(context(), args[0], args[1]); +} + +/* + * Get font. + */ - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_move_to(context->context() - , info[0]->NumberValue() - , info[1]->NumberValue()); +Napi::Value +Context2d::GetFont(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->font); } /* @@ -1800,38 +2673,133 @@ NAN_METHOD(Context2d::MoveTo) { * - family */ -NAN_METHOD(Context2d::SetFont) { - // Ignore invalid args - if (!info[0]->IsString() - || !info[1]->IsString() - || !info[2]->IsNumber() - || !info[3]->IsString() - || !info[4]->IsString()) return; +void +Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string str = value.As().Utf8Value(); + if (!str.length()) return; - String::Utf8Value weight(info[0]); - String::Utf8Value style(info[1]); - double size = info[2]->NumberValue(); - String::Utf8Value unit(info[3]); - String::Utf8Value family(info[4]); + bool success; + auto props = FontParser::parse(str, &success); + if (!success) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + PangoFontDescription *desc = pango_font_description_copy(state->fontDescription); + pango_font_description_free(state->fontDescription); - PangoFontDescription *desc = pango_font_description_copy(context->state->fontDescription); - pango_font_description_free(context->state->fontDescription); + PangoStyle style = props.fontStyle == FontStyle::Italic ? PANGO_STYLE_ITALIC + : props.fontStyle == FontStyle::Oblique ? PANGO_STYLE_OBLIQUE + : PANGO_STYLE_NORMAL; + pango_font_description_set_style(desc, style); - pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(*style)); - pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(*weight)); + pango_font_description_set_weight(desc, static_cast(props.fontWeight)); - if (strlen(*family) > 0) pango_font_description_set_family(desc, *family); + std::string family = props.fontFamily.empty() ? "" : props.fontFamily[0]; + for (size_t i = 1; i < props.fontFamily.size(); i++) { + family += "," + props.fontFamily[i]; + } + if (family.length() > 0) { + // See #1643 - Pango understands "sans" whereas CSS uses "sans-serif" + std::string s1(family); + std::string s2("sans-serif"); + if (streq_casein(s1, s2)) { + pango_font_description_set_family(desc, "sans"); + } else { + pango_font_description_set_family(desc, family.c_str()); + } + } PangoFontDescription *sys_desc = Canvas::ResolveFontDescription(desc); pango_font_description_free(desc); - if (size > 0) pango_font_description_set_absolute_size(sys_desc, size * PANGO_SCALE); + if (props.fontSize > 0) pango_font_description_set_absolute_size(sys_desc, props.fontSize * PANGO_SCALE); - context->state->fontDescription = sys_desc; + state->fontDescription = sys_desc; + pango_layout_set_font_description(_layout, sys_desc); - pango_layout_set_font_description(context->_layout, sys_desc); + state->font = str; +} + +/* + * Get text baseline. + */ + +Napi::Value +Context2d::GetTextBaseline(const Napi::CallbackInfo& info) { + const char* baseline; + switch (state->textBaseline) { + default: + case TEXT_BASELINE_ALPHABETIC: baseline = "alphabetic"; break; + case TEXT_BASELINE_TOP: baseline = "top"; break; + case TEXT_BASELINE_BOTTOM: baseline = "bottom"; break; + case TEXT_BASELINE_MIDDLE: baseline = "middle"; break; + case TEXT_BASELINE_IDEOGRAPHIC: baseline = "ideographic"; break; + case TEXT_BASELINE_HANGING: baseline = "hanging"; break; + } + return Napi::String::New(env, baseline); +} + +/* + * Set text baseline. + */ + +void +Context2d::SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string opStr = value.As(); + const std::map modes = { + {"alphabetic", TEXT_BASELINE_ALPHABETIC}, + {"top", TEXT_BASELINE_TOP}, + {"bottom", TEXT_BASELINE_BOTTOM}, + {"middle", TEXT_BASELINE_MIDDLE}, + {"ideographic", TEXT_BASELINE_IDEOGRAPHIC}, + {"hanging", TEXT_BASELINE_HANGING} + }; + auto op = modes.find(opStr); + if (op == modes.end()) return; + + state->textBaseline = op->second; +} + +/* + * Get text align. + */ + +Napi::Value +Context2d::GetTextAlign(const Napi::CallbackInfo& info) { + const char* align; + switch (state->textAlignment) { + case TEXT_ALIGNMENT_LEFT: align = "left"; break; + case TEXT_ALIGNMENT_START: align = "start"; break; + case TEXT_ALIGNMENT_CENTER: align = "center"; break; + case TEXT_ALIGNMENT_RIGHT: align = "right"; break; + case TEXT_ALIGNMENT_END: align = "end"; break; + default: align = "start"; + } + return Napi::String::New(env, align); +} + +/* + * Set text align. + */ + +void +Context2d::SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; + + std::string opStr = value.As(); + const std::map modes = { + {"center", TEXT_ALIGNMENT_CENTER}, + {"left", TEXT_ALIGNMENT_LEFT}, + {"start", TEXT_ALIGNMENT_START}, + {"right", TEXT_ALIGNMENT_RIGHT}, + {"end", TEXT_ALIGNMENT_END} + }; + auto op = modes.find(opStr); + if (op == modes.end()) return; + + state->textAlignment = op->second; } /* @@ -1841,145 +2809,139 @@ NAN_METHOD(Context2d::SetFont) { * fontBoundingBoxAscent, fontBoundingBoxDescent */ -NAN_METHOD(Context2d::MeasureText) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); +Napi::Value +Context2d::MeasureText(const Napi::CallbackInfo& info) { + cairo_t *ctx = this->context(); + + Napi::String str; + if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined(); - String::Utf8Value str(info[0]->ToString()); - Local obj = Nan::New(); + Napi::Object obj = Napi::Object::New(env); - PangoRectangle ink_rect, logical_rect; + PangoRectangle _ink_rect, _logical_rect; + float_rectangle ink_rect, logical_rect; PangoFontMetrics *metrics; - PangoLayout *layout = context->layout(); + PangoLayout *layout = this->layout(); - pango_layout_set_text(layout, *str, -1); + checkFonts(); + pango_layout_set_text(layout, str.Utf8Value().c_str(), -1); + if (state->lang != "") { + pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str())); + } pango_cairo_update_layout(ctx, layout); - pango_layout_get_pixel_extents(layout, &ink_rect, &logical_rect); + // Normally you could use pango_layout_get_pixel_extents and be done, or use + // pango_extents_to_pixels, but both of those round the pixels, so we have to + // divide by PANGO_SCALE manually + pango_layout_get_extents(layout, &_ink_rect, &_logical_rect); + + float inverse_pango_scale = 1. / PANGO_SCALE; + + logical_rect.x = _logical_rect.x * inverse_pango_scale; + logical_rect.y = _logical_rect.y * inverse_pango_scale; + logical_rect.width = _logical_rect.width * inverse_pango_scale; + logical_rect.height = _logical_rect.height * inverse_pango_scale; + + ink_rect.x = _ink_rect.x * inverse_pango_scale; + ink_rect.y = _ink_rect.y * inverse_pango_scale; + ink_rect.width = _ink_rect.width * inverse_pango_scale; + ink_rect.height = _ink_rect.height * inverse_pango_scale; + metrics = PANGO_LAYOUT_GET_METRICS(layout); + text_align_t alignment = resolveTextAlignment(); + double x_offset; - switch (context->state->textAlignment) { - case 0: // center - x_offset = logical_rect.width / 2; + switch (alignment) { + case TEXT_ALIGNMENT_CENTER: + x_offset = logical_rect.width / 2.; break; - case 1: // right + case TEXT_ALIGNMENT_RIGHT: x_offset = logical_rect.width; break; - default: // left + case TEXT_ALIGNMENT_LEFT: + default: x_offset = 0.0; } - double y_offset; - switch (context->state->textBaseline) { - case TEXT_BASELINE_ALPHABETIC: - y_offset = -pango_font_metrics_get_ascent(metrics) / PANGO_SCALE; - break; - case TEXT_BASELINE_MIDDLE: - y_offset = -(pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics))/(2.0 * PANGO_SCALE); - break; - case TEXT_BASELINE_BOTTOM: - y_offset = -(pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE; - break; - default: - y_offset = 0.0; - } - - obj->Set(Nan::New("width").ToLocalChecked(), - Nan::New(logical_rect.width)); - obj->Set(Nan::New("actualBoundingBoxLeft").ToLocalChecked(), - Nan::New(x_offset - PANGO_LBEARING(logical_rect))); - obj->Set(Nan::New("actualBoundingBoxRight").ToLocalChecked(), - Nan::New(x_offset + PANGO_RBEARING(logical_rect))); - obj->Set(Nan::New("actualBoundingBoxAscent").ToLocalChecked(), - Nan::New(-(y_offset+ink_rect.y))); - obj->Set(Nan::New("actualBoundingBoxDescent").ToLocalChecked(), - Nan::New((PANGO_DESCENT(ink_rect) + y_offset))); - obj->Set(Nan::New("emHeightAscent").ToLocalChecked(), - Nan::New(PANGO_ASCENT(logical_rect) - y_offset)); - obj->Set(Nan::New("emHeightDescent").ToLocalChecked(), - Nan::New(PANGO_DESCENT(logical_rect) + y_offset)); - obj->Set(Nan::New("alphabeticBaseline").ToLocalChecked(), - Nan::New((pango_font_metrics_get_ascent(metrics) / PANGO_SCALE) - + y_offset)); - - pango_font_metrics_unref(metrics); + double y_offset = getBaselineAdjustment(layout, state->textBaseline); - info.GetReturnValue().Set(obj); -} - -/* - * Set text baseline. - */ + obj.Set("width", Napi::Number::New(env, logical_rect.width)); + obj.Set("actualBoundingBoxLeft", Napi::Number::New(env, PANGO_LBEARING(ink_rect) + x_offset)); + obj.Set("actualBoundingBoxRight", Napi::Number::New(env, PANGO_RBEARING(ink_rect) - x_offset)); + obj.Set("actualBoundingBoxAscent", Napi::Number::New(env, y_offset + PANGO_ASCENT(ink_rect))); + obj.Set("actualBoundingBoxDescent", Napi::Number::New(env, PANGO_DESCENT(ink_rect) - y_offset)); + obj.Set("emHeightAscent", Napi::Number::New(env, -(PANGO_ASCENT(logical_rect) - y_offset))); + obj.Set("emHeightDescent", Napi::Number::New(env, PANGO_DESCENT(logical_rect) - y_offset)); + obj.Set("alphabeticBaseline", Napi::Number::New(env, -(pango_font_metrics_get_ascent(metrics) * inverse_pango_scale - y_offset))); -NAN_METHOD(Context2d::SetTextBaseline) { - if (!info[0]->IsInt32()) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textBaseline = info[0]->Int32Value(); -} - -/* - * Set text alignment. -1 0 1 - */ + pango_font_metrics_unref(metrics); -NAN_METHOD(Context2d::SetTextAlignment) { - if (!info[0]->IsInt32()) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textAlignment = info[0]->Int32Value(); + return obj; } /* * Set line dash * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ -NAN_METHOD(Context2d::SetLineDash) { - if (!info[0]->IsArray()) return; - Local dash = Local::Cast(info[0]); - uint32_t dashes = dash->Length() & 1 ? dash->Length() * 2 : dash->Length(); +void +Context2d::SetLineDash(const Napi::CallbackInfo& info) { + if (!info[0].IsArray()) return; + Napi::Array dash = info[0].As(); + uint32_t dashes = dash.Length() & 1 ? dash.Length() * 2 : dash.Length(); + uint32_t zero_dashes = 0; std::vector a(dashes); for (uint32_t i=0; i d = dash->Get(i % dash->Length()); - if (!d->IsNumber()) return; - a[i] = d->NumberValue(); - if (a[i] < 0 || isnan(a[i]) || isinf(a[i])) return; + Napi::Number d; + if (!dash.Get(i % dash.Length()).UnwrapTo(&d) || !d.IsNumber()) return; + a[i] = d.As().DoubleValue(); + if (a[i] == 0) zero_dashes++; + if (a[i] < 0 || !std::isfinite(a[i])) return; } - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = this->context(); double offset; cairo_get_dash(ctx, NULL, &offset); - cairo_set_dash(ctx, a.data(), dashes, offset); + if (zero_dashes == dashes) { + std::vector b(0); + cairo_set_dash(ctx, b.data(), 0, offset); + } else { + cairo_set_dash(ctx, a.data(), dashes, offset); + } } /* * Get line dash * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ -NAN_METHOD(Context2d::GetLineDash) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); +Napi::Value +Context2d::GetLineDash(const Napi::CallbackInfo& info) { + cairo_t *ctx = this->context(); int dashes = cairo_get_dash_count(ctx); std::vector a(dashes); cairo_get_dash(ctx, a.data(), NULL); - Local dash = Nan::New(dashes); - for (int i=0; iSet(Nan::New(i), Nan::New(a[i])); + Napi::Array dash = Napi::Array::New(env, dashes); + for (int i=0; iNumberValue(); - if (isnan(offset) || isinf(offset)) return; +void +Context2d::SetLineDashOffset(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Number numberValue; + if (!value.ToNumber().UnwrapTo(&numberValue)) return; + double offset = numberValue.DoubleValue(); + if (!std::isfinite(offset)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = this->context(); int dashes = cairo_get_dash_count(ctx); std::vector a(dashes); @@ -1991,60 +2953,60 @@ NAN_SETTER(Context2d::SetLineDashOffset) { * Get line dash offset * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ -NAN_GETTER(Context2d::GetLineDashOffset) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); +Napi::Value +Context2d::GetLineDashOffset(const Napi::CallbackInfo& info) { + cairo_t *ctx = this->context(); double offset; cairo_get_dash(ctx, NULL, &offset); - info.GetReturnValue().Set(Nan::New(offset)); + return Napi::Number::New(env, offset); } /* * Fill the rectangle defined by x, y, width and height. */ -NAN_METHOD(Context2d::FillRect) { +void +Context2d::FillRect(const Napi::CallbackInfo& info) { RECT_ARGS; if (0 == width || 0 == height) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - context->savePath(); + cairo_t *ctx = context(); + savePath(); cairo_rectangle(ctx, x, y, width, height); - context->fill(); - context->restorePath(); + fill(); + restorePath(); } /* * Stroke the rectangle defined by x, y, width and height. */ -NAN_METHOD(Context2d::StrokeRect) { +void +Context2d::StrokeRect(const Napi::CallbackInfo& info) { RECT_ARGS; if (0 == width && 0 == height) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); - context->savePath(); + cairo_t *ctx = context(); + savePath(); cairo_rectangle(ctx, x, y, width, height); - context->stroke(); - context->restorePath(); + stroke(); + restorePath(); } /* * Clears all pixels defined by x, y, width and height. */ -NAN_METHOD(Context2d::ClearRect) { +void +Context2d::ClearRect(const Napi::CallbackInfo& info) { RECT_ARGS; if (0 == width || 0 == height) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); cairo_save(ctx); - context->savePath(); + savePath(); cairo_rectangle(ctx, x, y, width, height); cairo_set_operator(ctx, CAIRO_OPERATOR_CLEAR); cairo_fill(ctx); - context->restorePath(); + restorePath(); cairo_restore(ctx); } @@ -2052,10 +3014,10 @@ NAN_METHOD(Context2d::ClearRect) { * Adds a rectangle subpath. */ -NAN_METHOD(Context2d::Rect) { +void +Context2d::Rect(const Napi::CallbackInfo& info) { RECT_ARGS; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); if (width == 0) { cairo_move_to(ctx, x, y); cairo_line_to(ctx, x, y + height); @@ -2067,36 +3029,264 @@ NAN_METHOD(Context2d::Rect) { } } -/* - * Adds an arc at x, y with the given radis and start/end angles. +// Draws an arc with two potentially different radii. +inline static +void elli_arc(cairo_t* ctx, double xc, double yc, double rx, double ry, double a1, double a2, bool clockwise=true) { + if (rx == 0. || ry == 0.) { + cairo_line_to(ctx, xc + rx, yc + ry); + } else { + cairo_save(ctx); + cairo_translate(ctx, xc, yc); + cairo_scale(ctx, rx, ry); + if (clockwise) + cairo_arc(ctx, 0., 0., 1., a1, a2); + else + cairo_arc_negative(ctx, 0., 0., 1., a2, a1); + cairo_restore(ctx); + } +} + +inline static +bool getRadius(Point& p, const Napi::Value& v) { + Napi::Env env = v.Env(); + if (v.IsObject()) { // 5.1 DOMPointInit + Napi::Value rx; + Napi::Value ry; + auto rxMaybe = v.As().Get("x"); + auto ryMaybe = v.As().Get("y"); + if (rxMaybe.UnwrapTo(&rx) && rx.IsNumber() && ryMaybe.UnwrapTo(&ry) && ry.IsNumber()) { + auto rxv = rx.As().DoubleValue(); + auto ryv = ry.As().DoubleValue(); + if (!std::isfinite(rxv) || !std::isfinite(ryv)) + return true; + if (rxv < 0 || ryv < 0) { + Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException(); + + return true; + } + p.x = rxv; + p.y = ryv; + return false; + } + } else if (v.IsNumber()) { // 5.2 unrestricted double + auto rv = v.As().DoubleValue(); + if (!std::isfinite(rv)) + return true; + if (rv < 0) { + Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException(); + + return true; + } + p.x = p.y = rv; + return false; + } + return true; +} + +/** + * https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect + * x, y, w, h, [radius|[radii]] */ +void +Context2d::RoundRect(const Napi::CallbackInfo& info) { + RECT_ARGS; + cairo_t *ctx = this->context(); + + // 4. Let normalizedRadii be an empty list + Point normalizedRadii[4]; + size_t nRadii = 4; + + if (info[4].IsUndefined()) { + for (size_t i = 0; i < 4; i++) + normalizedRadii[i].x = normalizedRadii[i].y = 0.; + + } else if (info[4].IsArray()) { + auto radiiList = info[4].As(); + nRadii = radiiList.Length(); + if (!(nRadii >= 1 && nRadii <= 4)) { + Napi::RangeError::New(env, "radii must be a list of one, two, three or four radii.").ThrowAsJavaScriptException(); + return; + } + // 5. For each radius of radii + for (size_t i = 0; i < nRadii; i++) { + Napi::Value r; + if (!radiiList.Get(i).UnwrapTo(&r) || getRadius(normalizedRadii[i], r)) + return; + } -NAN_METHOD(Context2d::Arc) { - if (!info[0]->IsNumber() - || !info[1]->IsNumber() - || !info[2]->IsNumber() - || !info[3]->IsNumber() - || !info[4]->IsNumber()) return; + } else { + // 2. If radii is a double, then set radii to <> + if (getRadius(normalizedRadii[0], info[4])) + return; + for (size_t i = 1; i < 4; i++) { + normalizedRadii[i].x = normalizedRadii[0].x; + normalizedRadii[i].y = normalizedRadii[0].y; + } + } - bool anticlockwise = info[5]->BooleanValue(); + Point upperLeft, upperRight, lowerRight, lowerLeft; + if (nRadii == 4) { + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[1]; + lowerRight = normalizedRadii[2]; + lowerLeft = normalizedRadii[3]; + } else if (nRadii == 3) { + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[1]; + lowerLeft = normalizedRadii[1]; + lowerRight = normalizedRadii[2]; + } else if (nRadii == 2) { + upperLeft = normalizedRadii[0]; + lowerRight = normalizedRadii[0]; + upperRight = normalizedRadii[1]; + lowerLeft = normalizedRadii[1]; + } else { + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[0]; + lowerRight = normalizedRadii[0]; + lowerLeft = normalizedRadii[0]; + } - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + bool clockwise = true; + if (width < 0) { + clockwise = false; + x += width; + width = -width; + std::swap(upperLeft, upperRight); + std::swap(lowerLeft, lowerRight); + } - if (anticlockwise && M_PI * 2 != info[4]->NumberValue()) { - cairo_arc_negative(ctx - , info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue() - , info[4]->NumberValue()); + if (height < 0) { + clockwise = !clockwise; + y += height; + height = -height; + std::swap(upperLeft, lowerLeft); + std::swap(upperRight, lowerRight); + } + + // 11. Corner curves must not overlap. Scale radii to prevent this. + { + auto top = upperLeft.x + upperRight.x; + auto right = upperRight.y + lowerRight.y; + auto bottom = lowerRight.x + lowerLeft.x; + auto left = upperLeft.y + lowerLeft.y; + auto scale = std::min({ width / top, height / right, width / bottom, height / left }); + if (scale < 1.) { + upperLeft.x *= scale; + upperLeft.y *= scale; + upperRight.x *= scale; + upperRight.y *= scale; + lowerLeft.x *= scale; + lowerLeft.y *= scale; + lowerRight.x *= scale; + lowerRight.y *= scale; + } + } + + // 12. Draw + cairo_move_to(ctx, x + upperLeft.x, y); + if (clockwise) { + cairo_line_to(ctx, x + width - upperRight.x, y); + elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0.); + cairo_line_to(ctx, x + width, y + height - lowerRight.y); + elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2.); + cairo_line_to(ctx, x + lowerLeft.x, y + height); + elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI); + cairo_line_to(ctx, x, y + upperLeft.y); + elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2.); } else { - cairo_arc(ctx - , info[0]->NumberValue() - , info[1]->NumberValue() - , info[2]->NumberValue() - , info[3]->NumberValue() - , info[4]->NumberValue()); + elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2., false); + cairo_line_to(ctx, x, y + upperLeft.y); + elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI, false); + cairo_line_to(ctx, x + lowerLeft.x, y + height); + elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2., false); + cairo_line_to(ctx, x + width, y + height - lowerRight.y); + elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0., false); + cairo_line_to(ctx, x + width - upperRight.x, y); + } + cairo_close_path(ctx); +} + +// Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp +static void canonicalizeAngle(double& startAngle, double& endAngle) { + // Make 0 <= startAngle < 2*PI + double newStartAngle = std::fmod(startAngle, twoPi); + if (newStartAngle < 0) { + newStartAngle += twoPi; + // Check for possible catastrophic cancellation in cases where + // newStartAngle was a tiny negative number (c.f. crbug.com/503422) + if (newStartAngle >= twoPi) + newStartAngle -= twoPi; + } + double delta = newStartAngle - startAngle; + startAngle = newStartAngle; + endAngle = endAngle + delta; +} + +// Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp +static double adjustEndAngle(double startAngle, double endAngle, bool counterclockwise) { + double newEndAngle = endAngle; + /* http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-arc + * If the counterclockwise argument is false and endAngle-startAngle is equal to or greater than 2pi, or, + * if the counterclockwise argument is true and startAngle-endAngle is equal to or greater than 2pi, + * then the arc is the whole circumference of this ellipse, and the point at startAngle along this circle's circumference, + * measured in radians clockwise from the ellipse's semi-major axis, acts as both the start point and the end point. + */ + if (!counterclockwise && endAngle - startAngle >= twoPi) + newEndAngle = startAngle + twoPi; + else if (counterclockwise && startAngle - endAngle >= twoPi) + newEndAngle = startAngle - twoPi; + /* + * Otherwise, the arc is the path along the circumference of this ellipse from the start point to the end point, + * going anti-clockwise if the counterclockwise argument is true, and clockwise otherwise. + * Since the points are on the ellipse, as opposed to being simply angles from zero, + * the arc can never cover an angle greater than 2pi radians. + */ + /* NOTE: When startAngle = 0, endAngle = 2Pi and counterclockwise = true, the spec does not indicate clearly. + * We draw the entire circle, because some web sites use arc(x, y, r, 0, 2*Math.PI, true) to draw circle. + * We preserve backward-compatibility. + */ + else if (!counterclockwise && startAngle > endAngle) + newEndAngle = startAngle + (twoPi - std::fmod(startAngle - endAngle, twoPi)); + else if (counterclockwise && startAngle < endAngle) + newEndAngle = startAngle - (twoPi - std::fmod(endAngle - startAngle, twoPi)); + return newEndAngle; +} + +/* + * Adds an arc at x, y with the given radii and start/end angles. + */ + +void +Context2d::Arc(const Napi::CallbackInfo& info) { + double args[5]; + if(!checkArgs(info, args, 5)) + return; + + auto x = args[0]; + auto y = args[1]; + auto radius = args[2]; + auto startAngle = args[3]; + auto endAngle = args[4]; + + if (radius < 0) { + Napi::RangeError::New(env, "The radius provided is negative.").ThrowAsJavaScriptException(); + return; + } + + Napi::Boolean counterclockwiseValue; + if (!info[5].ToBoolean().UnwrapTo(&counterclockwiseValue)) return; + bool counterclockwise = counterclockwiseValue.Value(); + + cairo_t *ctx = context(); + + canonicalizeAngle(startAngle, endAngle); + endAngle = adjustEndAngle(startAngle, endAngle, counterclockwise); + + if (counterclockwise) { + cairo_arc_negative(ctx, x, y, radius, startAngle, endAngle); + } else { + cairo_arc(ctx, x, y, radius, startAngle, endAngle); } } @@ -2106,15 +3296,13 @@ NAN_METHOD(Context2d::Arc) { * Implementation influenced by WebKit. */ -NAN_METHOD(Context2d::ArcTo) { - if (!info[0]->IsNumber() - || !info[1]->IsNumber() - || !info[2]->IsNumber() - || !info[3]->IsNumber() - || !info[4]->IsNumber()) return; +void +Context2d::ArcTo(const Napi::CallbackInfo& info) { + double args[5]; + if(!checkArgs(info, args, 5)) + return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); // Current path point double x, y; @@ -2122,12 +3310,12 @@ NAN_METHOD(Context2d::ArcTo) { Point p0(x, y); // Point (x0,y0) - Point p1(info[0]->NumberValue(), info[1]->NumberValue()); + Point p1(args[0], args[1]); // Point (x1,y1) - Point p2(info[2]->NumberValue(), info[3]->NumberValue()); + Point p2(args[2], args[3]); - float radius = info[4]->NumberValue(); + float radius = args[4]; if ((p1.x == p0.x && p1.y == p0.y) || (p1.x == p2.x && p1.y == p2.y) @@ -2206,3 +3394,109 @@ NAN_METHOD(Context2d::ArcTo) { , ea); } } + +/* + * Adds an ellipse to the path which is centered at (x, y) position with the + * radii radiusX and radiusY starting at startAngle and ending at endAngle + * going in the given direction by anticlockwise (defaulting to clockwise). + */ + +void +Context2d::Ellipse(const Napi::CallbackInfo& info) { + double args[7]; + if(!checkArgs(info, args, 7)) + return; + + double radiusX = args[2]; + double radiusY = args[3]; + + if (radiusX == 0 || radiusY == 0) return; + + double x = args[0]; + double y = args[1]; + double rotation = args[4]; + double startAngle = args[5]; + double endAngle = args[6]; + Napi::Boolean anticlockwiseValue; + + if (!info[7].ToBoolean().UnwrapTo(&anticlockwiseValue)) return; + bool anticlockwise = anticlockwiseValue.Value(); + + cairo_t *ctx = context(); + + // See https://www.cairographics.org/cookbook/ellipses/ + double xRatio = radiusX / radiusY; + + cairo_matrix_t save_matrix; + cairo_get_matrix(ctx, &save_matrix); + cairo_translate(ctx, x, y); + cairo_rotate(ctx, rotation); + cairo_scale(ctx, xRatio, 1.0); + cairo_translate(ctx, -x, -y); + if (anticlockwise && M_PI * 2 != args[4]) { + cairo_arc_negative(ctx, + x, + y, + radiusY, + startAngle, + endAngle); + } else { + cairo_arc(ctx, + x, + y, + radiusY, + startAngle, + endAngle); + } + cairo_set_matrix(ctx, &save_matrix); +} + +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + +void +Context2d::BeginTag(const Napi::CallbackInfo& info) { + std::string tagName = ""; + std::string attributes = ""; + + if (info.Length() == 0) { + Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException(); + return; + } else { + if (!info[0].IsString()) { + Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException(); + return; + } else { + tagName = info[0].As().Utf8Value(); + } + + if (info.Length() > 1) { + if (!info[1].IsString()) { + Napi::TypeError::New(env, "Attributes must be a string matching Cairo's attribute format").ThrowAsJavaScriptException(); + return; + } else { + attributes = info[1].As().Utf8Value(); + } + } + } + + cairo_tag_begin(_context, tagName.c_str(), attributes.c_str()); +} + +void +Context2d::EndTag(const Napi::CallbackInfo& info) { + if (info.Length() == 0) { + Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException(); + return; + } + + if (!info[0].IsString()) { + Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException(); + return; + } + + std::string tagName = info[0].As().Utf8Value(); + + cairo_tag_end(_context, tagName.c_str()); +} + +#endif diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index fccb5d184..1d9548895 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -1,26 +1,13 @@ - -// -// CanvasRenderingContext2d.h -// // Copyright (c) 2010 LearnBoost -// - -#ifndef __NODE_CONTEXT2D_H__ -#define __NODE_CONTEXT2D_H__ -#include -#include +#pragma once -#include "color.h" +#include "cairo.h" #include "Canvas.h" -#include "CanvasGradient.h" - -using namespace std; - -typedef enum { - TEXT_DRAW_PATHS, - TEXT_DRAW_GLYPHS -} canvas_draw_mode_t; +#include "color.h" +#include "napi.h" +#include +#include /* * State struct. @@ -29,114 +16,186 @@ typedef enum { * cairo's gstate maintains only a single source pattern at a time. */ -typedef struct { - rgba_t fill; - rgba_t stroke; - cairo_filter_t patternQuality; - cairo_pattern_t *fillPattern; - cairo_pattern_t *strokePattern; - cairo_pattern_t *fillGradient; - cairo_pattern_t *strokeGradient; - float globalAlpha; - short textAlignment; - short textBaseline; - rgba_t shadow; - int shadowBlur; - double shadowOffsetX; - double shadowOffsetY; - canvas_draw_mode_t textDrawingMode; - PangoFontDescription *fontDescription; -} canvas_state_t; +struct canvas_state_t { + rgba_t fill = { 0, 0, 0, 1 }; + rgba_t stroke = { 0, 0, 0, 1 }; + rgba_t shadow = { 0, 0, 0, 0 }; + double shadowOffsetX = 0.; + double shadowOffsetY = 0.; + cairo_pattern_t* fillPattern = nullptr; + cairo_pattern_t* strokePattern = nullptr; + cairo_pattern_t* fillGradient = nullptr; + cairo_pattern_t* strokeGradient = nullptr; + PangoFontDescription* fontDescription = nullptr; + std::string font = "10px sans-serif"; + cairo_filter_t patternQuality = CAIRO_FILTER_GOOD; + float globalAlpha = 1.f; + int shadowBlur = 0; + text_align_t textAlignment = TEXT_ALIGNMENT_START; + text_baseline_t textBaseline = TEXT_BASELINE_ALPHABETIC; + canvas_draw_mode_t textDrawingMode = TEXT_DRAW_PATHS; + bool imageSmoothingEnabled = true; + std::string direction = "ltr"; + std::string lang = ""; -void state_assign_fontFamily(canvas_state_t *state, const char *str); + canvas_state_t() { + fontDescription = pango_font_description_from_string("sans"); + pango_font_description_set_absolute_size(fontDescription, 10 * PANGO_SCALE); + } -class Context2d: public Nan::ObjectWrap { + canvas_state_t(const canvas_state_t& other) { + fill = other.fill; + stroke = other.stroke; + patternQuality = other.patternQuality; + fillPattern = other.fillPattern; + strokePattern = other.strokePattern; + fillGradient = other.fillGradient; + strokeGradient = other.strokeGradient; + globalAlpha = other.globalAlpha; + textAlignment = other.textAlignment; + textBaseline = other.textBaseline; + shadow = other.shadow; + shadowBlur = other.shadowBlur; + shadowOffsetX = other.shadowOffsetX; + shadowOffsetY = other.shadowOffsetY; + textDrawingMode = other.textDrawingMode; + fontDescription = pango_font_description_copy(other.fontDescription); + font = other.font; + imageSmoothingEnabled = other.imageSmoothingEnabled; + lang = other.lang; + } + + ~canvas_state_t() { + pango_font_description_free(fontDescription); + } +}; + +/* + * Equivalent to a PangoRectangle but holds floats instead of ints + * (software pixels are stored here instead of pango units) + * + * Should be compatible with PANGO_ASCENT, PANGO_LBEARING, etc. + */ + +typedef struct { + float x; + float y; + float width; + float height; +} float_rectangle; + +class Context2d : public Napi::ObjectWrap { public: - short stateno; - canvas_state_t *states[CANVAS_MAX_STATES]; + std::stack states; canvas_state_t *state; - Context2d(Canvas *canvas); - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_METHOD(DrawImage); - static NAN_METHOD(PutImageData); - static NAN_METHOD(Save); - static NAN_METHOD(Restore); - static NAN_METHOD(Rotate); - static NAN_METHOD(Translate); - static NAN_METHOD(Scale); - static NAN_METHOD(Transform); - static NAN_METHOD(ResetTransform); - static NAN_METHOD(IsPointInPath); - static NAN_METHOD(BeginPath); - static NAN_METHOD(ClosePath); - static NAN_METHOD(AddPage); - static NAN_METHOD(Clip); - static NAN_METHOD(Fill); - static NAN_METHOD(Stroke); - static NAN_METHOD(FillText); - static NAN_METHOD(StrokeText); - static NAN_METHOD(SetFont); - static NAN_METHOD(SetFillColor); - static NAN_METHOD(SetStrokeColor); - static NAN_METHOD(SetFillPattern); - static NAN_METHOD(SetStrokePattern); - static NAN_METHOD(SetTextBaseline); - static NAN_METHOD(SetTextAlignment); - static NAN_METHOD(SetLineDash); - static NAN_METHOD(GetLineDash); - static NAN_METHOD(MeasureText); - static NAN_METHOD(BezierCurveTo); - static NAN_METHOD(QuadraticCurveTo); - static NAN_METHOD(LineTo); - static NAN_METHOD(MoveTo); - static NAN_METHOD(FillRect); - static NAN_METHOD(StrokeRect); - static NAN_METHOD(ClearRect); - static NAN_METHOD(Rect); - static NAN_METHOD(Arc); - static NAN_METHOD(ArcTo); - static NAN_METHOD(GetImageData); - static NAN_GETTER(GetPatternQuality); - static NAN_GETTER(GetGlobalCompositeOperation); - static NAN_GETTER(GetGlobalAlpha); - static NAN_GETTER(GetShadowColor); - static NAN_GETTER(GetFillColor); - static NAN_GETTER(GetStrokeColor); - static NAN_GETTER(GetMiterLimit); - static NAN_GETTER(GetLineCap); - static NAN_GETTER(GetLineJoin); - static NAN_GETTER(GetLineWidth); - static NAN_GETTER(GetLineDashOffset); - static NAN_GETTER(GetShadowOffsetX); - static NAN_GETTER(GetShadowOffsetY); - static NAN_GETTER(GetShadowBlur); - static NAN_GETTER(GetAntiAlias); - static NAN_GETTER(GetTextDrawingMode); - static NAN_GETTER(GetFilter); - static NAN_SETTER(SetPatternQuality); - static NAN_SETTER(SetGlobalCompositeOperation); - static NAN_SETTER(SetGlobalAlpha); - static NAN_SETTER(SetShadowColor); - static NAN_SETTER(SetMiterLimit); - static NAN_SETTER(SetLineCap); - static NAN_SETTER(SetLineJoin); - static NAN_SETTER(SetLineWidth); - static NAN_SETTER(SetLineDashOffset); - static NAN_SETTER(SetShadowOffsetX); - static NAN_SETTER(SetShadowOffsetY); - static NAN_SETTER(SetShadowBlur); - static NAN_SETTER(SetAntiAlias); - static NAN_SETTER(SetTextDrawingMode); - static NAN_SETTER(SetFilter); + Context2d(const Napi::CallbackInfo& info); + static void Initialize(Napi::Env& env, Napi::Object& target); + void DrawImage(const Napi::CallbackInfo& info); + void PutImageData(const Napi::CallbackInfo& info); + void Save(const Napi::CallbackInfo& info); + void Restore(const Napi::CallbackInfo& info); + void Rotate(const Napi::CallbackInfo& info); + void Translate(const Napi::CallbackInfo& info); + void Scale(const Napi::CallbackInfo& info); + void Transform(const Napi::CallbackInfo& info); + Napi::Value GetTransform(const Napi::CallbackInfo& info); + void ResetTransform(const Napi::CallbackInfo& info); + void SetTransform(const Napi::CallbackInfo& info); + Napi::Value IsPointInPath(const Napi::CallbackInfo& info); + void BeginPath(const Napi::CallbackInfo& info); + void ClosePath(const Napi::CallbackInfo& info); + void AddPage(const Napi::CallbackInfo& info); + void Clip(const Napi::CallbackInfo& info); + void Fill(const Napi::CallbackInfo& info); + void Stroke(const Napi::CallbackInfo& info); + void FillText(const Napi::CallbackInfo& info); + void StrokeText(const Napi::CallbackInfo& info); + static Napi::Value SetFont(const Napi::CallbackInfo& info); + static Napi::Value SetFillColor(const Napi::CallbackInfo& info); + static Napi::Value SetStrokeColor(const Napi::CallbackInfo& info); + static Napi::Value SetStrokePattern(const Napi::CallbackInfo& info); + static Napi::Value SetTextAlignment(const Napi::CallbackInfo& info); + void SetLineDash(const Napi::CallbackInfo& info); + Napi::Value GetLineDash(const Napi::CallbackInfo& info); + Napi::Value MeasureText(const Napi::CallbackInfo& info); + void BezierCurveTo(const Napi::CallbackInfo& info); + void QuadraticCurveTo(const Napi::CallbackInfo& info); + void LineTo(const Napi::CallbackInfo& info); + void MoveTo(const Napi::CallbackInfo& info); + void FillRect(const Napi::CallbackInfo& info); + void StrokeRect(const Napi::CallbackInfo& info); + void ClearRect(const Napi::CallbackInfo& info); + void Rect(const Napi::CallbackInfo& info); + void RoundRect(const Napi::CallbackInfo& info); + void Arc(const Napi::CallbackInfo& info); + void ArcTo(const Napi::CallbackInfo& info); + void Ellipse(const Napi::CallbackInfo& info); + Napi::Value GetImageData(const Napi::CallbackInfo& info); + Napi::Value CreateImageData(const Napi::CallbackInfo& info); + static Napi::Value GetStrokeColor(const Napi::CallbackInfo& info); + Napi::Value CreatePattern(const Napi::CallbackInfo& info); + Napi::Value CreateLinearGradient(const Napi::CallbackInfo& info); + Napi::Value CreateRadialGradient(const Napi::CallbackInfo& info); + Napi::Value GetFormat(const Napi::CallbackInfo& info); + Napi::Value GetPatternQuality(const Napi::CallbackInfo& info); + Napi::Value GetImageSmoothingEnabled(const Napi::CallbackInfo& info); + Napi::Value GetGlobalCompositeOperation(const Napi::CallbackInfo& info); + Napi::Value GetGlobalAlpha(const Napi::CallbackInfo& info); + Napi::Value GetShadowColor(const Napi::CallbackInfo& info); + Napi::Value GetMiterLimit(const Napi::CallbackInfo& info); + Napi::Value GetLineCap(const Napi::CallbackInfo& info); + Napi::Value GetLineJoin(const Napi::CallbackInfo& info); + Napi::Value GetLineWidth(const Napi::CallbackInfo& info); + Napi::Value GetLineDashOffset(const Napi::CallbackInfo& info); + Napi::Value GetShadowOffsetX(const Napi::CallbackInfo& info); + Napi::Value GetShadowOffsetY(const Napi::CallbackInfo& info); + Napi::Value GetShadowBlur(const Napi::CallbackInfo& info); + Napi::Value GetAntiAlias(const Napi::CallbackInfo& info); + Napi::Value GetTextDrawingMode(const Napi::CallbackInfo& info); + Napi::Value GetQuality(const Napi::CallbackInfo& info); + Napi::Value GetCurrentTransform(const Napi::CallbackInfo& info); + Napi::Value GetFillStyle(const Napi::CallbackInfo& info); + Napi::Value GetStrokeStyle(const Napi::CallbackInfo& info); + Napi::Value GetFont(const Napi::CallbackInfo& info); + Napi::Value GetTextBaseline(const Napi::CallbackInfo& info); + Napi::Value GetTextAlign(const Napi::CallbackInfo& info); + Napi::Value GetLanguage(const Napi::CallbackInfo& info); + void SetPatternQuality(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetImageSmoothingEnabled(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetGlobalCompositeOperation(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetGlobalAlpha(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetShadowColor(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetMiterLimit(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLineCap(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLineJoin(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLineWidth(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLineDashOffset(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetShadowOffsetX(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetShadowOffsetY(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetShadowBlur(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetAntiAlias(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetTextDrawingMode(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetQuality(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetFont(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetLanguage(const Napi::CallbackInfo& info, const Napi::Value& value); + #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + void BeginTag(const Napi::CallbackInfo& info); + void EndTag(const Napi::CallbackInfo& info); + #endif + Napi::Value GetDirection(const Napi::CallbackInfo& info); + void SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value); inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } inline bool hasShadow(); void inline setSourceRGBA(rgba_t color); void inline setSourceRGBA(cairo_t *ctx, rgba_t color); - void setTextPath(const char *str, double x, double y); + void setTextPath(double x, double y); void blur(cairo_surface_t *surface, int radius); void shadow(void (fn)(cairo_t *cr)); void shadowStart(); @@ -145,20 +204,34 @@ class Context2d: public Nan::ObjectWrap { void restorePath(); void saveState(); void restoreState(); - void inline setFillRule(v8::Local value); + void inline setFillRule(Napi::Value value); void fill(bool preserve = false); void stroke(bool preserve = false); void save(); void restore(); void setFontFromState(); + void resetState(); inline PangoLayout *layout(){ return _layout; } + ~Context2d(); + Napi::Env env; private: - ~Context2d(); + void _resetPersistentHandles(); + Napi::Value _getFillColor(); + Napi::Value _getStrokeColor(); + Napi::Value get_current_transform(); + void _setFillColor(Napi::Value arg); + void _setFillPattern(Napi::Value arg); + void _setStrokeColor(Napi::Value arg); + void _setStrokePattern(Napi::Value arg); + void checkFonts(); + void paintText(const Napi::CallbackInfo&, bool); + text_align_t resolveTextAlignment(); + Napi::Reference _fillStyle; + Napi::Reference _strokeStyle; Canvas *_canvas; - cairo_t *_context; + cairo_t *_context = nullptr; cairo_path_t *_path; - PangoLayout *_layout; + PangoLayout *_layout = nullptr; + int fontSerial = 1; }; - -#endif diff --git a/src/CharData.h b/src/CharData.h new file mode 100644 index 000000000..ebc2dd5e1 --- /dev/null +++ b/src/CharData.h @@ -0,0 +1,231 @@ +// This is used for classifying characters according to the definition of tokens +// in the CSS standards, but could be extended for any other future uses + +#pragma once + +namespace CharData { + static constexpr uint8_t Whitespace = 0x1; + static constexpr uint8_t Newline = 0x2; + static constexpr uint8_t Hex = 0x4; + static constexpr uint8_t Nmstart = 0x8; + static constexpr uint8_t Nmchar = 0x10; + static constexpr uint8_t Sign = 0x20; + static constexpr uint8_t Digit = 0x40; + static constexpr uint8_t NumStart = 0x80; +}; + +using namespace CharData; + +constexpr const uint8_t charData[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-8 + Whitespace, // 9 (HT) + Whitespace | Newline, // 10 (LF) + 0, // 11 (VT) + Whitespace | Newline, // 12 (FF) + Whitespace | Newline, // 13 (CR) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14-31 + Whitespace, // 32 (Space) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 33-42 + Sign | NumStart, // 43 (+) + 0, // 44 + Nmchar | Sign | NumStart, // 45 (-) + 0, 0, // 46-47 + Nmchar | Digit | NumStart | Hex, // 48 (0) + Nmchar | Digit | NumStart | Hex, // 49 (1) + Nmchar | Digit | NumStart | Hex, // 50 (2) + Nmchar | Digit | NumStart | Hex, // 51 (3) + Nmchar | Digit | NumStart | Hex, // 52 (4) + Nmchar | Digit | NumStart | Hex, // 53 (5) + Nmchar | Digit | NumStart | Hex, // 54 (6) + Nmchar | Digit | NumStart | Hex, // 55 (7) + Nmchar | Digit | NumStart | Hex, // 56 (8) + Nmchar | Digit | NumStart | Hex, // 57 (9) + 0, 0, 0, 0, 0, 0, 0, // 58-64 + Nmstart | Nmchar | Hex, // 65 (A) + Nmstart | Nmchar | Hex, // 66 (B) + Nmstart | Nmchar | Hex, // 67 (C) + Nmstart | Nmchar | Hex, // 68 (D) + Nmstart | Nmchar | Hex, // 69 (E) + Nmstart | Nmchar | Hex, // 70 (F) + Nmstart | Nmchar, // 71 (G) + Nmstart | Nmchar, // 72 (H) + Nmstart | Nmchar, // 73 (I) + Nmstart | Nmchar, // 74 (J) + Nmstart | Nmchar, // 75 (K) + Nmstart | Nmchar, // 76 (L) + Nmstart | Nmchar, // 77 (M) + Nmstart | Nmchar, // 78 (N) + Nmstart | Nmchar, // 79 (O) + Nmstart | Nmchar, // 80 (P) + Nmstart | Nmchar, // 81 (Q) + Nmstart | Nmchar, // 82 (R) + Nmstart | Nmchar, // 83 (S) + Nmstart | Nmchar, // 84 (T) + Nmstart | Nmchar, // 85 (U) + Nmstart | Nmchar, // 86 (V) + Nmstart | Nmchar, // 87 (W) + Nmstart | Nmchar, // 88 (X) + Nmstart | Nmchar, // 89 (Y) + Nmstart | Nmchar, // 90 (Z) + 0, // 91 + Nmstart, // 92 (\) + 0, 0, // 93-94 + Nmstart | Nmchar, // 95 (_) + 0, // 96 + Nmstart | Nmchar | Hex, // 97 (a) + Nmstart | Nmchar | Hex, // 98 (b) + Nmstart | Nmchar | Hex, // 99 (c) + Nmstart | Nmchar | Hex, // 100 (d) + Nmstart | Nmchar | Hex, // 101 (e) + Nmstart | Nmchar | Hex, // 102 (f) + Nmstart | Nmchar, // 103 (g) + Nmstart | Nmchar, // 104 (h) + Nmstart | Nmchar, // 105 (i) + Nmstart | Nmchar, // 106 (j) + Nmstart | Nmchar, // 107 (k) + Nmstart | Nmchar, // 108 (l) + Nmstart | Nmchar, // 109 (m) + Nmstart | Nmchar, // 110 (n) + Nmstart | Nmchar, // 111 (o) + Nmstart | Nmchar, // 112 (p) + Nmstart | Nmchar, // 113 (q) + Nmstart | Nmchar, // 114 (r) + Nmstart | Nmchar, // 115 (s) + Nmstart | Nmchar, // 116 (t) + Nmstart | Nmchar, // 117 (u) + Nmstart | Nmchar, // 118 (v) + Nmstart | Nmchar, // 119 (w) + Nmstart | Nmchar, // 120 (x) + Nmstart | Nmchar, // 121 (y) + Nmstart | Nmchar, // 122 (z) + 0, 0, 0, 0, 0, // 123-127 + // Non-ASCII + Nmstart | Nmchar, // 128 + Nmstart | Nmchar, // 129 + Nmstart | Nmchar, // 130 + Nmstart | Nmchar, // 131 + Nmstart | Nmchar, // 132 + Nmstart | Nmchar, // 133 + Nmstart | Nmchar, // 134 + Nmstart | Nmchar, // 135 + Nmstart | Nmchar, // 136 + Nmstart | Nmchar, // 137 + Nmstart | Nmchar, // 138 + Nmstart | Nmchar, // 139 + Nmstart | Nmchar, // 140 + Nmstart | Nmchar, // 141 + Nmstart | Nmchar, // 142 + Nmstart | Nmchar, // 143 + Nmstart | Nmchar, // 144 + Nmstart | Nmchar, // 145 + Nmstart | Nmchar, // 146 + Nmstart | Nmchar, // 147 + Nmstart | Nmchar, // 148 + Nmstart | Nmchar, // 149 + Nmstart | Nmchar, // 150 + Nmstart | Nmchar, // 151 + Nmstart | Nmchar, // 152 + Nmstart | Nmchar, // 153 + Nmstart | Nmchar, // 154 + Nmstart | Nmchar, // 155 + Nmstart | Nmchar, // 156 + Nmstart | Nmchar, // 157 + Nmstart | Nmchar, // 158 + Nmstart | Nmchar, // 159 + Nmstart | Nmchar, // 160 + Nmstart | Nmchar, // 161 + Nmstart | Nmchar, // 162 + Nmstart | Nmchar, // 163 + Nmstart | Nmchar, // 164 + Nmstart | Nmchar, // 165 + Nmstart | Nmchar, // 166 + Nmstart | Nmchar, // 167 + Nmstart | Nmchar, // 168 + Nmstart | Nmchar, // 169 + Nmstart | Nmchar, // 170 + Nmstart | Nmchar, // 171 + Nmstart | Nmchar, // 172 + Nmstart | Nmchar, // 173 + Nmstart | Nmchar, // 174 + Nmstart | Nmchar, // 175 + Nmstart | Nmchar, // 176 + Nmstart | Nmchar, // 177 + Nmstart | Nmchar, // 178 + Nmstart | Nmchar, // 179 + Nmstart | Nmchar, // 180 + Nmstart | Nmchar, // 181 + Nmstart | Nmchar, // 182 + Nmstart | Nmchar, // 183 + Nmstart | Nmchar, // 184 + Nmstart | Nmchar, // 185 + Nmstart | Nmchar, // 186 + Nmstart | Nmchar, // 187 + Nmstart | Nmchar, // 188 + Nmstart | Nmchar, // 189 + Nmstart | Nmchar, // 190 + Nmstart | Nmchar, // 191 + Nmstart | Nmchar, // 192 + Nmstart | Nmchar, // 193 + Nmstart | Nmchar, // 194 + Nmstart | Nmchar, // 195 + Nmstart | Nmchar, // 196 + Nmstart | Nmchar, // 197 + Nmstart | Nmchar, // 198 + Nmstart | Nmchar, // 199 + Nmstart | Nmchar, // 200 + Nmstart | Nmchar, // 201 + Nmstart | Nmchar, // 202 + Nmstart | Nmchar, // 203 + Nmstart | Nmchar, // 204 + Nmstart | Nmchar, // 205 + Nmstart | Nmchar, // 206 + Nmstart | Nmchar, // 207 + Nmstart | Nmchar, // 208 + Nmstart | Nmchar, // 209 + Nmstart | Nmchar, // 210 + Nmstart | Nmchar, // 211 + Nmstart | Nmchar, // 212 + Nmstart | Nmchar, // 213 + Nmstart | Nmchar, // 214 + Nmstart | Nmchar, // 215 + Nmstart | Nmchar, // 216 + Nmstart | Nmchar, // 217 + Nmstart | Nmchar, // 218 + Nmstart | Nmchar, // 219 + Nmstart | Nmchar, // 220 + Nmstart | Nmchar, // 221 + Nmstart | Nmchar, // 222 + Nmstart | Nmchar, // 223 + Nmstart | Nmchar, // 224 + Nmstart | Nmchar, // 225 + Nmstart | Nmchar, // 226 + Nmstart | Nmchar, // 227 + Nmstart | Nmchar, // 228 + Nmstart | Nmchar, // 229 + Nmstart | Nmchar, // 230 + Nmstart | Nmchar, // 231 + Nmstart | Nmchar, // 232 + Nmstart | Nmchar, // 233 + Nmstart | Nmchar, // 234 + Nmstart | Nmchar, // 235 + Nmstart | Nmchar, // 236 + Nmstart | Nmchar, // 237 + Nmstart | Nmchar, // 238 + Nmstart | Nmchar, // 239 + Nmstart | Nmchar, // 240 + Nmstart | Nmchar, // 241 + Nmstart | Nmchar, // 242 + Nmstart | Nmchar, // 243 + Nmstart | Nmchar, // 244 + Nmstart | Nmchar, // 245 + Nmstart | Nmchar, // 246 + Nmstart | Nmchar, // 247 + Nmstart | Nmchar, // 248 + Nmstart | Nmchar, // 249 + Nmstart | Nmchar, // 250 + Nmstart | Nmchar, // 251 + Nmstart | Nmchar, // 252 + Nmstart | Nmchar, // 253 + Nmstart | Nmchar, // 254 + Nmstart | Nmchar // 255 +}; diff --git a/src/FontParser.cc b/src/FontParser.cc new file mode 100644 index 000000000..773502cb3 --- /dev/null +++ b/src/FontParser.cc @@ -0,0 +1,605 @@ +// This is written to exactly parse the `font` shorthand in CSS2: +// https://www.w3.org/TR/CSS22/fonts.html#font-shorthand +// https://www.w3.org/TR/CSS22/syndata.html#tokenization +// +// We may want to update it for CSS 3 (e.g. font-stretch, or updated +// tokenization) but I've only ever seen one or two issues filed in node-canvas +// due to parsing in my 8 years on the project + +#include "FontParser.h" +#include "CharData.h" +#include +#include + +Token::Token(Type type, std::string value) : type_(type), value_(std::move(value)) {} + +Token::Token(Type type, double value) : type_(type), value_(value) {} + +Token::Token(Type type) : type_(type), value_(std::string{}) {} + +const std::string& +Token::getString() const { + static const std::string empty; + auto* str = std::get_if(&value_); + return str ? *str : empty; +} + +double +Token::getNumber() const { + auto* num = std::get_if(&value_); + return num ? *num : 0.0f; +} + +Tokenizer::Tokenizer(std::string_view input) : input_(input) {} + +std::string +Tokenizer::utf8Encode(uint32_t codepoint) { + std::string result; + + if (codepoint < 0x80) { + result += static_cast(codepoint); + } else if (codepoint < 0x800) { + result += static_cast((codepoint >> 6) | 0xc0); + result += static_cast((codepoint & 0x3f) | 0x80); + } else if (codepoint < 0x10000) { + result += static_cast((codepoint >> 12) | 0xe0); + result += static_cast(((codepoint >> 6) & 0x3f) | 0x80); + result += static_cast((codepoint & 0x3f) | 0x80); + } else { + result += static_cast((codepoint >> 18) | 0xf0); + result += static_cast(((codepoint >> 12) & 0x3f) | 0x80); + result += static_cast(((codepoint >> 6) & 0x3f) | 0x80); + result += static_cast((codepoint & 0x3f) | 0x80); + } + + return result; +} + +char +Tokenizer::peek() const { + return position_ < input_.length() ? input_[position_] : '\0'; +} + +char +Tokenizer::advance() { + return position_ < input_.length() ? input_[position_++] : '\0'; +} + +Token +Tokenizer::parseNumber() { + enum class State { + Start, + AfterSign, + Digits, + AfterDecimal, + AfterE, + AfterESign, + ExponentDigits + }; + + size_t start = position_; + size_t ePosition = 0; + State state = State::Start; + bool valid = false; + + while (position_ < input_.length()) { + char c = peek(); + uint8_t flags = charData[static_cast(c)]; + + switch (state) { + case State::Start: + if (flags & CharData::Sign) { + position_++; + state = State::AfterSign; + } else if (flags & CharData::Digit) { + position_++; + state = State::Digits; + valid = true; + } else if (c == '.') { + position_++; + state = State::AfterDecimal; + } else { + goto done; + } + break; + + case State::AfterSign: + if (flags & CharData::Digit) { + position_++; + state = State::Digits; + valid = true; + } else if (c == '.') { + position_++; + state = State::AfterDecimal; + } else { + goto done; + } + break; + + case State::Digits: + if (flags & CharData::Digit) { + position_++; + } else if (c == '.') { + position_++; + state = State::AfterDecimal; + } else if (c == 'e' || c == 'E') { + ePosition = position_; + position_++; + state = State::AfterE; + valid = false; + } else { + goto done; + } + break; + + case State::AfterDecimal: + if (flags & CharData::Digit) { + position_++; + valid = true; + state = State::Digits; + } else { + goto done; + } + break; + + case State::AfterE: + if (flags & CharData::Sign) { + position_++; + state = State::AfterESign; + } else if (flags & CharData::Digit) { + position_++; + valid = true; + state = State::ExponentDigits; + } else { + position_ = ePosition; + valid = true; + goto done; + } + break; + + case State::AfterESign: + if (flags & CharData::Digit) { + position_++; + valid = true; + state = State::ExponentDigits; + } else { + position_ = ePosition; + valid = true; + goto done; + } + break; + + case State::ExponentDigits: + if (flags & CharData::Digit) { + position_++; + } else { + goto done; + } + break; + } + } + +done: + if (!valid) { + position_ = start; + return Token(Token::Type::Invalid); + } + + std::string number_str(input_.substr(start, position_ - start)); + double value = std::stod(number_str); + return Token(Token::Type::Number, value); +} + +// Note that identifiers are always lower-case. This helps us make easier/more +// efficient comparisons, but means that font-families specified as identifiers +// will be lower-cased. Since font selection isn't case sensitive, this +// shouldn't ever be a problem. +Token +Tokenizer::parseIdentifier() { + std::string identifier; + auto flags = CharData::Nmstart; + auto start = position_; + + while (position_ < input_.length()) { + char c = peek(); + + if (c == '\\') { + advance(); + if (!parseEscape(identifier)) { + position_ = start; + return Token(Token::Type::Invalid); + } + flags = CharData::Nmchar; + } else if (charData[static_cast(c)] & flags) { + identifier += advance() + (c >= 'A' && c <= 'Z' ? 32 : 0); + flags = CharData::Nmchar; + } else { + break; + } + } + + return Token(Token::Type::Identifier, identifier); +} + +uint32_t +Tokenizer::parseUnicode() { + uint32_t value = 0; + size_t count = 0; + + while (position_ < input_.length() && count < 6) { + char c = peek(); + uint32_t digit; + + if (c >= '0' && c <= '9') { + digit = c - '0'; + } else if (c >= 'a' && c <= 'f') { + digit = c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + digit = c - 'A' + 10; + } else { + break; + } + + value = value * 16 + digit; + advance(); + count++; + } + + // Optional whitespace after hex escape + char c = peek(); + if (c == '\r') { + advance(); + if (peek() == '\n') advance(); + } else if (isWhitespace(c)) { + advance(); + } + + return value; +} + +bool +Tokenizer::parseEscape(std::string& str) { + char c = peek(); + auto flags = charData[static_cast(c)]; + + if (flags & CharData::Hex) { + str += utf8Encode(parseUnicode()); + return true; + } else if (!(flags & CharData::Newline) && !(flags & CharData::Hex)) { + str += advance(); + return true; + } + + return false; +} + +Token +Tokenizer::parseString(char quote) { + advance(); + std::string value; + auto start = position_; + + while (position_ < input_.length()) { + char c = peek(); + + if (c == quote) { + advance(); + return Token(Token::Type::QuotedString, value); + } else if (c == '\\') { + advance(); + c = peek(); + if (c == '\r') { + advance(); + if (peek() == '\n') advance(); + } else if (isNewline(c)) { + advance(); + } else { + if (!parseEscape(value)) { + position_ = start; + return Token(Token::Type::Invalid); + } + } + } else { + value += advance(); + } + } + + position_ = start; + return Token(Token::Type::Invalid); +} + +Token +Tokenizer::nextToken() { + if (position_ >= input_.length()) { + return Token(Token::Type::EndOfInput); + } + + char c = peek(); + auto flags = charData[static_cast(c)]; + + if (isWhitespace(c)) { + std::string whitespace; + while (position_ < input_.length() && isWhitespace(peek())) { + whitespace += advance(); + } + return Token(Token::Type::Whitespace, whitespace); + } + + if (flags & CharData::NumStart) { + Token token = parseNumber(); + if (token.type() != Token::Type::Invalid) return token; + } + + if (flags & CharData::Nmstart) { + Token token = parseIdentifier(); + if (token.type() != Token::Type::Invalid) return token; + } + + if (c == '"') { + Token token = parseString('"'); + if (token.type() != Token::Type::Invalid) return token; + } + + if (c == '\'') { + Token token = parseString('\''); + if (token.type() != Token::Type::Invalid) return token; + } + + switch (advance()) { + case '/': return Token(Token::Type::Slash); + case ',': return Token(Token::Type::Comma); + case '%': return Token(Token::Type::Percent); + default: return Token(Token::Type::Invalid); + } +} + +FontParser::FontParser(std::string_view input) + : tokenizer_(input) + , currentToken_(tokenizer_.nextToken()) + , nextToken_(tokenizer_.nextToken()) {} + +const std::unordered_map FontParser::weightMap = { + {"normal", 400}, + {"bold", 700}, + {"lighter", 100}, + {"bolder", 700} +}; + +const std::unordered_map FontParser::unitMap = { + {"cm", 37.8f}, + {"mm", 3.78f}, + {"in", 96.0f}, + {"pt", 96.0f / 72.0f}, + {"pc", 96.0f / 6.0f}, + {"em", 16.0f}, + {"px", 1.0f} +}; + +void +FontParser::advance() { + currentToken_ = nextToken_; + nextToken_ = tokenizer_.nextToken(); +} + +void +FontParser::skipWs() { + while (currentToken_.type() == Token::Type::Whitespace) advance(); +} + +bool +FontParser::check(Token::Type type) const { + return currentToken_.type() == type; +} + +bool +FontParser::checkWs() const { + return nextToken_.type() == Token::Type::Whitespace + || nextToken_.type() == Token::Type::EndOfInput; +} + +bool +FontParser::parseFontStyle(FontProperties& props) { + if (check(Token::Type::Identifier)) { + const auto& value = currentToken_.getString(); + if (value == "italic") { + props.fontStyle = FontStyle::Italic; + advance(); + return true; + } else if (value == "oblique") { + props.fontStyle = FontStyle::Oblique; + advance(); + return true; + } else if (value == "normal") { + props.fontStyle = FontStyle::Normal; + advance(); + return true; + } + } + + return false; +} + +bool +FontParser::parseFontVariant(FontProperties& props) { + if (check(Token::Type::Identifier)) { + const auto& value = currentToken_.getString(); + if (value == "small-caps") { + props.fontVariant = FontVariant::SmallCaps; + advance(); + return true; + } else if (value == "normal") { + props.fontVariant = FontVariant::Normal; + advance(); + return true; + } + } + + return false; +} + +bool +FontParser::parseFontWeight(FontProperties& props) { + if (check(Token::Type::Number)) { + double weightFloat = currentToken_.getNumber(); + int weight = static_cast(weightFloat); + if (weight < 1 || weight > 1000) return false; + props.fontWeight = static_cast(weight); + advance(); + return true; + } else if (check(Token::Type::Identifier)) { + const auto& value = currentToken_.getString(); + + if (auto it = weightMap.find(value); it != weightMap.end()) { + props.fontWeight = it->second; + advance(); + return true; + } + } + + return false; +} + +bool +FontParser::parseFontSize(FontProperties& props) { + if (!check(Token::Type::Number)) return false; + + props.fontSize = currentToken_.getNumber(); + advance(); + + double multiplier = 1.0f; + if (check(Token::Type::Identifier)) { + const auto& unit = currentToken_.getString(); + + if (auto it = unitMap.find(unit); it != unitMap.end()) { + multiplier = it->second; + advance(); + } else { + return false; + } + } else if (check(Token::Type::Percent)) { + multiplier = 16.0f / 100.0f; + advance(); + } else { + return false; + } + + // Technically if we consumed some tokens but couldn't parse the font-size, + // we should rewind the tokenizer, but I don't think the grammar allows for + // any valid alternates in this specific case + + props.fontSize *= multiplier; + return true; +} + +// line-height is not used by canvas ever, but should still parse +bool +FontParser::parseLineHeight(FontProperties& props) { + if (check(Token::Type::Slash)) { + advance(); + skipWs(); + if (check(Token::Type::Number)) { + advance(); + if (check(Token::Type::Percent)) { + advance(); + } else if (check(Token::Type::Identifier)) { + auto identifier = currentToken_.getString(); + if (auto it = unitMap.find(identifier); it != unitMap.end()) { + advance(); + } else { + return false; + } + } else { + return false; + } + } else if (check(Token::Type::Identifier) && currentToken_.getString() == "normal") { + advance(); + } else { + return false; + } + } + + return true; +} + +bool +FontParser::parseFontFamily(FontProperties& props) { + while (!check(Token::Type::EndOfInput)) { + std::string family = ""; + std::string trailingWs = ""; + bool found = false; + + while ( + check(Token::Type::QuotedString) || + check(Token::Type::Identifier) || + check(Token::Type::Whitespace) + ) { + if (check(Token::Type::Whitespace)) { + if (found) trailingWs += currentToken_.getString(); + } else { // Identifier, QuotedString + if (found) { + family += trailingWs; + trailingWs.clear(); + } + + family += currentToken_.getString(); + found = true; + } + + advance(); + } + + if (!found) return false; // only whitespace or non-id/string found + + props.fontFamily.push_back(family); + + if (check(Token::Type::Comma)) advance(); + } + + return true; +} + +FontProperties +FontParser::parse(const std::string& fontString, bool* success) { + FontParser parser(fontString); + auto result = parser.parseFont(); + if (success) *success = !parser.hasError_; + return result; +} + +FontProperties +FontParser::parseFont() { + FontProperties props; + uint8_t state = 0b111; + + skipWs(); + + for (size_t i = 0; i < 3 && checkWs(); i++) { + if ((state & 0b001) && parseFontStyle(props)) { + state &= 0b110; + goto match; + } + + if ((state & 0b010) && parseFontVariant(props)) { + state &= 0b101; + goto match; + } + + if ((state & 0b100) && parseFontWeight(props)) { + state &= 0b011; + goto match; + } + + break; // all attempts exhausted + match: skipWs(); // success: move to the next non-ws token + } + + if (parseFontSize(props)) { + skipWs(); + if (parseLineHeight(props) && parseFontFamily(props)) { + return props; + } + } + + hasError_ = true; + return props; +} diff --git a/src/FontParser.h b/src/FontParser.h new file mode 100644 index 000000000..c88802109 --- /dev/null +++ b/src/FontParser.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "CharData.h" + +enum class FontStyle { + Normal, + Italic, + Oblique +}; + +enum class FontVariant { + Normal, + SmallCaps +}; + +struct FontProperties { + double fontSize{16.0f}; + std::vector fontFamily; + uint16_t fontWeight{400}; + FontVariant fontVariant{FontVariant::Normal}; + FontStyle fontStyle{FontStyle::Normal}; +}; + +class Token { + public: + enum class Type { + Invalid, + Number, + Percent, + Identifier, + Slash, + Comma, + QuotedString, + Whitespace, + EndOfInput + }; + + Token(Type type, std::string value); + Token(Type type, double value); + Token(Type type); + + Type type() const { return type_; } + + const std::string& getString() const; + double getNumber() const; + + private: + Type type_; + std::variant value_; +}; + +class Tokenizer { + public: + Tokenizer(std::string_view input); + Token nextToken(); + + private: + std::string_view input_; + size_t position_{0}; + + // Util + std::string utf8Encode(uint32_t codepoint); + inline bool isWhitespace(char c) const { + return charData[static_cast(c)] & CharData::Whitespace; + } + inline bool isNewline(char c) const { + return charData[static_cast(c)] & CharData::Newline; + } + + // Moving through the string + char peek() const; + char advance(); + + // Tokenize + Token parseNumber(); + Token parseIdentifier(); + uint32_t parseUnicode(); + bool parseEscape(std::string& str); + Token parseString(char quote); +}; + +class FontParser { + public: + static FontProperties parse(const std::string& fontString, bool* success = nullptr); + + private: + static const std::unordered_map weightMap; + static const std::unordered_map unitMap; + + FontParser(std::string_view input); + + void advance(); + void skipWs(); + bool check(Token::Type type) const; + bool checkWs() const; + + bool parseFontStyle(FontProperties& props); + bool parseFontVariant(FontProperties& props); + bool parseFontWeight(FontProperties& props); + bool parseFontSize(FontProperties& props); + bool parseLineHeight(FontProperties& props); + bool parseFontFamily(FontProperties& props); + FontProperties parseFont(); + + Tokenizer tokenizer_; + Token currentToken_; + Token nextToken_; + bool hasError_{false}; +}; diff --git a/src/Image.cc b/src/Image.cc index 0147f878f..973736505 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1,15 +1,20 @@ -// -// Image.cc -// // Copyright (c) 2010 LearnBoost -// -#include "Canvas.h" #include "Image.h" -#include -#include -#include +#include "InstanceData.h" + +#include "bmp/BMPParser.h" +#include "Canvas.h" +#include +#include +#include #include +#include + +/* Cairo limit: + * https://lists.cairographics.org/archives/cairo/2010-December/021422.html + */ +static constexpr int canvas_max_side = (1 << 15) - 1; #ifdef HAVE_GIF typedef struct { @@ -19,119 +24,171 @@ typedef struct { } gif_data_t; #endif +#ifdef HAVE_JPEG +#include + +struct canvas_jpeg_error_mgr: jpeg_error_mgr { + Image* image; + jmp_buf setjmp_buffer; +}; +#endif + /* * Read closure used by loadFromBuffer. */ typedef struct { + Napi::Env* env; unsigned len; uint8_t *buf; } read_closure_t; -Nan::Persistent Image::constructor; - /* * Initialize Image. */ void -Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; - - Local ctor = Nan::New(Image::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("Image").ToLocalChecked()); - - // Prototype - Local proto = ctor->PrototypeTemplate(); - Nan::SetAccessor(proto, Nan::New("source").ToLocalChecked(), GetSource, SetSource); - Nan::SetAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight); - Nan::SetAccessor(proto, Nan::New("onload").ToLocalChecked(), GetOnload, SetOnload); - Nan::SetAccessor(proto, Nan::New("onerror").ToLocalChecked(), GetOnerror, SetOnerror); -#if CAIRO_VERSION_MINOR >= 10 - Nan::SetAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode); - ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); - ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); -#endif - Nan::Set(target, Nan::New("Image").ToLocalChecked(), ctor->GetFunction()); +Image::Initialize(Napi::Env& env, Napi::Object& exports) { + InstanceData *data = env.GetInstanceData(); + Napi::HandleScope scope(env); + + Napi::Function ctor = DefineClass(env, "Image", { + InstanceAccessor<&Image::GetComplete>("complete", napi_default_jsproperty), + InstanceAccessor<&Image::GetWidth, &Image::SetWidth>("width", napi_default_jsproperty), + InstanceAccessor<&Image::GetHeight, &Image::SetHeight>("height", napi_default_jsproperty), + InstanceAccessor<&Image::GetNaturalWidth>("naturalWidth", napi_default_jsproperty), + InstanceAccessor<&Image::GetNaturalHeight>("naturalHeight", napi_default_jsproperty), + InstanceAccessor<&Image::GetDataMode, &Image::SetDataMode>("dataMode", napi_default_jsproperty), + StaticValue("MODE_IMAGE", Napi::Number::New(env, DATA_IMAGE), napi_default_jsproperty), + StaticValue("MODE_MIME", Napi::Number::New(env, DATA_MIME), napi_default_jsproperty) + }); + + // Used internally in lib/image.js + exports.Set("GetSource", Napi::Function::New(env, &GetSource)); + exports.Set("SetSource", Napi::Function::New(env, &SetSource)); + + data->ImageCtor = Napi::Persistent(ctor); + exports.Set("Image", ctor); } /* * Initialize a new Image. */ -NAN_METHOD(Image::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - - Image *img = new Image; - img->data_mode = DATA_IMAGE; - img->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); +Image::Image(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info.Env()) { + data_mode = DATA_IMAGE; + info.This().ToObject().Unwrap().Set("onload", env.Null()); + info.This().ToObject().Unwrap().Set("onerror", env.Null()); + filename = NULL; + _data = nullptr; + _data_len = 0; + _surface = NULL; + width = height = 0; + naturalWidth = naturalHeight = 0; + state = DEFAULT; +#ifdef HAVE_RSVG + _rsvg = NULL; + _is_svg = false; + _svg_last_width = _svg_last_height = 0; +#endif } /* * Get complete boolean. */ -NAN_GETTER(Image::GetComplete) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(Image::COMPLETE == img->state)); +Napi::Value +Image::GetComplete(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(env, true); } -#if CAIRO_VERSION_MINOR >= 10 - /* * Get dataMode. */ -NAN_GETTER(Image::GetDataMode) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->data_mode)); +Napi::Value +Image::GetDataMode(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, data_mode); } /* * Set dataMode. */ -NAN_SETTER(Image::SetDataMode) { - if (value->IsNumber()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - int mode = value->Uint32Value(); - img->data_mode = (data_mode_t) mode; +void +Image::SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + int mode = value.As().Uint32Value(); + data_mode = (data_mode_t) mode; } } -#endif +/* + * Get natural width + */ + +Napi::Value +Image::GetNaturalWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, naturalWidth); +} /* * Get width. */ -NAN_GETTER(Image::GetWidth) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->width)); +Napi::Value +Image::GetWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, width); } + +/* + * Set width. + */ + +void +Image::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + width = value.As().Uint32Value(); + } +} + +/* + * Get natural height + */ + +Napi::Value +Image::GetNaturalHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, naturalHeight); +} + /* * Get height. */ -NAN_GETTER(Image::GetHeight) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->height)); +Napi::Value +Image::GetHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, height); +} +/* + * Set height. + */ + +void +Image::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + height = value.As().Uint32Value(); + } } /* * Get src path. */ -NAN_GETTER(Image::GetSource) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->filename ? img->filename : "").ToLocalChecked()); +Napi::Value +Image::GetSource(const Napi::CallbackInfo& info){ + Napi::Env env = info.Env(); + Image *img = Image::Unwrap(info.This().As()); + return Napi::String::New(env, img->filename ? img->filename : ""); } /* @@ -142,18 +199,26 @@ void Image::clearData() { if (_surface) { cairo_surface_destroy(_surface); - Nan::AdjustExternalMemory(-_data_len); + Napi::MemoryManagement::AdjustExternalMemory(env, -_data_len); _data_len = 0; _surface = NULL; } - free(_data); - _data = NULL; + delete[] _data; + _data = nullptr; free(filename); filename = NULL; +#ifdef HAVE_RSVG + if (_rsvg != NULL) { + g_object_unref(_rsvg); + _rsvg = NULL; + } +#endif + width = height = 0; + naturalWidth = naturalHeight = 0; state = DEFAULT; } @@ -161,30 +226,50 @@ Image::clearData() { * Set src path. */ -NAN_SETTER(Image::SetSource) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); +void +Image::SetSource(const Napi::CallbackInfo& info){ + Napi::Env env = info.Env(); + Napi::Object This = info.This().As(); + Image *img = Image::Unwrap(This); + cairo_status_t status = CAIRO_STATUS_READ_ERROR; + Napi::Value value = info[0]; + img->clearData(); + // Clear errno in case some unrelated previous syscall failed + errno = 0; // url string - if (value->IsString()) { - String::Utf8Value src(value); + if (value.IsString()) { + std::string src = value.As().Utf8Value(); if (img->filename) free(img->filename); - img->filename = strdup(*src); + img->filename = strdup(src.c_str()); status = img->load(); // Buffer - } else if (Buffer::HasInstance(value)) { - uint8_t *buf = (uint8_t *) Buffer::Data(value->ToObject()); - unsigned len = Buffer::Length(value->ToObject()); + } else if (value.IsBuffer()) { + uint8_t *buf = value.As>().Data(); + unsigned len = value.As>().Length(); status = img->loadFromBuffer(buf, len); } - // check status if (status) { - img->error(Canvas::Error(status)); + Napi::Value onerrorFn; + if (This.Get("onerror").UnwrapTo(&onerrorFn) && onerrorFn.IsFunction()) { + Napi::Error arg; + if (img->errorInfo.empty()) { + arg = Napi::Error::New(env, Napi::String::New(env, cairo_status_to_string(status))); + } else { + arg = img->errorInfo.toError(env); + } + onerrorFn.As().Call({ arg.Value() }); + } } else { img->loaded(); + Napi::Value onloadFn; + if (This.Get("onload").UnwrapTo(&onloadFn) && onloadFn.IsFunction()) { + onloadFn.As().Call({}); + } } } @@ -199,14 +284,18 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { memcpy(data, buf, (len < 4 ? len : 4) * sizeof(uint8_t)); if (isPNG(data)) return loadPNGFromBuffer(buf); + + if (isGIF(data)) { #ifdef HAVE_GIF - if (isGIF(data)) return loadGIFFromBuffer(buf, len); -#endif -#ifdef HAVE_JPEG -#if CAIRO_VERSION_MINOR < 10 - if (isJPEG(data)) return loadJPEGFromBuffer(buf, len); + return loadGIFFromBuffer(buf, len); #else + this->errorInfo.set("node-canvas was built without GIF support"); + return CAIRO_STATUS_READ_ERROR; +#endif + } + if (isJPEG(data)) { +#ifdef HAVE_JPEG if (DATA_IMAGE == data_mode) return loadJPEGFromBuffer(buf, len); if (DATA_MIME == data_mode) return decodeJPEGBufferIntoMimeSurface(buf, len); if ((DATA_IMAGE | DATA_MIME) == data_mode) { @@ -215,9 +304,28 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { if (status) return status; return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); } - } +#else // HAVE_JPEG + this->errorInfo.set("node-canvas was built without JPEG support"); + return CAIRO_STATUS_READ_ERROR; #endif + } + + // confirm svg using first 1000 chars + // if a very long comment precedes the root tag, isSVG returns false + unsigned head_len = (len < 1000 ? len : 1000); + if (isSVG(buf, head_len)) { +#ifdef HAVE_RSVG + return loadSVGFromBuffer(buf, len); +#else + this->errorInfo.set("node-canvas was built without SVG support"); + return CAIRO_STATUS_READ_ERROR; #endif + } + + if (isBMP(buf, len)) + return loadBMPFromBuffer(buf, len); + + this->errorInfo.set("Unsupported image type"); return CAIRO_STATUS_READ_ERROR; } @@ -230,6 +338,7 @@ Image::loadPNGFromBuffer(uint8_t *buf) { read_closure_t closure; closure.len = 0; closure.buf = buf; + closure.env = &env; _surface = cairo_image_surface_create_from_png_stream(readPNG, &closure); cairo_status_t status = cairo_surface_status(_surface); if (status) return status; @@ -248,97 +357,12 @@ Image::readPNG(void *c, uint8_t *data, unsigned int len) { return CAIRO_STATUS_SUCCESS; } -/* - * Get onload callback. - */ - -NAN_GETTER(Image::GetOnload) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - if (img->onload) { - info.GetReturnValue().Set(img->onload->GetFunction()); - } else { - info.GetReturnValue().SetNull(); - } -} - -/* - * Set onload callback. - */ - -NAN_SETTER(Image::SetOnload) { - if (value->IsFunction()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->onload = new Nan::Callback(value.As()); - } else if (value->IsNull()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - if (img->onload) { - delete img->onload; - } - img->onload = NULL; - } -} - -/* - * Get onerror callback. - */ - -NAN_GETTER(Image::GetOnerror) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - if (img->onerror) { - info.GetReturnValue().Set(img->onerror->GetFunction()); - } else { - info.GetReturnValue().SetNull(); - } -} - -/* - * Set onerror callback. - */ - -NAN_SETTER(Image::SetOnerror) { - if (value->IsFunction()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->onerror = new Nan::Callback(value.As()); - } else if (value->IsNull()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - if (img->onerror) { - delete img->onerror; - } - img->onerror = NULL; - } -} - -/* - * Initialize a new Image. - */ - -Image::Image() { - filename = NULL; - _data = NULL; - _data_len = 0; - _surface = NULL; - width = height = 0; - state = DEFAULT; - onload = NULL; - onerror = NULL; -} - /* * Destroy image and associated surface. */ Image::~Image() { clearData(); - - if (onerror) { - delete onerror; - onerror = NULL; - } - - if (onload) { - delete onload; - onload = NULL; - } } /* @@ -355,35 +379,41 @@ Image::load() { } /* - * Invoke onload (when assigned) and assign dimensions. + * Set state, assign dimensions. */ void Image::loaded() { - Nan::HandleScope scope; + Napi::HandleScope scope(env); state = COMPLETE; - width = cairo_image_surface_get_width(_surface); - height = cairo_image_surface_get_height(_surface); - _data_len = height * cairo_image_surface_get_stride(_surface); - Nan::AdjustExternalMemory(_data_len); - - if (onload != NULL) { - onload->Call(0, NULL); - } + width = naturalWidth = cairo_image_surface_get_width(_surface); + height = naturalHeight = cairo_image_surface_get_height(_surface); + _data_len = naturalHeight * cairo_image_surface_get_stride(_surface); + Napi::MemoryManagement::AdjustExternalMemory(env, _data_len); } /* - * Invoke onerror (when assigned) with the given err. + * Returns this image's surface. */ +cairo_surface_t *Image::surface() { +#ifdef HAVE_RSVG + if (_is_svg && (_svg_last_width != width || _svg_last_height != height)) { + if (_surface != NULL) { + cairo_surface_destroy(_surface); + _surface = NULL; + } -void -Image::error(Local err) { - Nan::HandleScope scope; - if (onerror != NULL) { - Local argv[1] = { err }; - onerror->Call(1, argv); + cairo_status_t status = renderSVGToSurface(); + if (status != CAIRO_STATUS_SUCCESS) { + g_object_unref(_rsvg); + Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException(); + + return NULL; + } } +#endif + return _surface; } /* @@ -396,13 +426,16 @@ Image::error(Local err) { cairo_status_t Image::loadSurface() { FILE *stream = fopen(filename, "rb"); - if (!stream) return CAIRO_STATUS_READ_ERROR; + if (!stream) { + this->errorInfo.set(NULL, "fopen", errno, filename); + return CAIRO_STATUS_READ_ERROR; + } uint8_t buf[5]; if (1 != fread(&buf, 5, 1, stream)) { fclose(stream); return CAIRO_STATUS_READ_ERROR; } - fseek(stream, 0, SEEK_SET); + rewind(stream); // png if (isPNG(buf)) { @@ -410,17 +443,53 @@ Image::loadSurface() { return loadPNG(); } - // gif + + if (isGIF(buf)) { #ifdef HAVE_GIF - if (isGIF(buf)) return loadGIF(stream); + return loadGIF(stream); +#else + this->errorInfo.set("node-canvas was built without GIF support"); + return CAIRO_STATUS_READ_ERROR; #endif + } - // jpeg + if (isJPEG(buf)) { #ifdef HAVE_JPEG - if (isJPEG(buf)) return loadJPEG(stream); + return loadJPEG(stream); +#else + this->errorInfo.set("node-canvas was built without JPEG support"); + return CAIRO_STATUS_READ_ERROR; #endif + } + + // confirm svg using first 1000 chars + // if a very long comment precedes the root tag, isSVG returns false + uint8_t head[1000] = {0}; + fseek(stream, 0 , SEEK_END); + long len = ftell(stream); + unsigned head_len = (len < 1000 ? len : 1000); + unsigned head_size = head_len * sizeof(uint8_t); + rewind(stream); + if (head_size != fread(&head, 1, head_size, stream)) { + fclose(stream); + return CAIRO_STATUS_READ_ERROR; + } + rewind(stream); + if (isSVG(head, head_len)) { +#ifdef HAVE_RSVG + return loadSVG(stream); +#else + this->errorInfo.set("node-canvas was built without SVG support"); + return CAIRO_STATUS_READ_ERROR; +#endif + } + + if (isBMP(buf, 2)) + return loadBMP(stream); fclose(stream); + + this->errorInfo.set("Unsupported image type"); return CAIRO_STATUS_READ_ERROR; } @@ -486,6 +555,7 @@ Image::loadGIF(FILE *stream) { if (!buf) { fclose(stream); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } @@ -524,12 +594,18 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { return CAIRO_STATUS_READ_ERROR; } - width = gif->SWidth; - height = gif->SHeight; + if (gif->SWidth > canvas_max_side || gif->SHeight > canvas_max_side) { + GIF_CLOSE_FILE(gif); + return CAIRO_STATUS_INVALID_SIZE; + } + + width = naturalWidth = gif->SWidth; + height = naturalHeight = gif->SHeight; - uint8_t *data = (uint8_t *) malloc(width * height * 4); + uint8_t *data = new uint8_t[naturalWidth * naturalHeight * 4]; if (!data) { GIF_CLOSE_FILE(gif); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } @@ -540,6 +616,11 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { ? img->ColorMap : gif->SColorMap; + if (colormap == nullptr) { + GIF_CLOSE_FILE(gif); + return CAIRO_STATUS_READ_ERROR; + } + int bgColor = 0; int alphaColor = get_gif_transparent_color(gif, i); if (gif->SColorMap) bgColor = (uint8_t) gif->SBackGroundColor; @@ -549,9 +630,9 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { uint32_t *dst_data = (uint32_t*) data; if (!gif->Image.Interlace) { - if (width == img->Width && height == img->Height) { - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { + if (naturalWidth == img->Width && naturalHeight == img->Height) { + for (int y = 0; y < naturalHeight; ++y) { + for (int x = 0; x < naturalWidth; ++x) { *dst_data = ((*src_data == alphaColor) ? 0 : 255) << 24 | colormap->Colors[*src_data].Red << 16 | colormap->Colors[*src_data].Green << 8 @@ -566,22 +647,25 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { int bottom = img->Top + img->Height; int right = img->Left + img->Width; - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { + uint32_t bgPixel = + ((bgColor == alphaColor) ? 0 : 255) << 24 + | colormap->Colors[bgColor].Red << 16 + | colormap->Colors[bgColor].Green << 8 + | colormap->Colors[bgColor].Blue; + + for (int y = 0; y < naturalHeight; ++y) { + for (int x = 0; x < naturalWidth; ++x) { if (y < img->Top || y >= bottom || x < img->Left || x >= right) { - *dst_data = ((bgColor == alphaColor) ? 0 : 255) << 24 - | colormap->Colors[bgColor].Red << 16 - | colormap->Colors[bgColor].Green << 8 - | colormap->Colors[bgColor].Blue; + *dst_data = bgPixel; + dst_data++; } else { *dst_data = ((*src_data == alphaColor) ? 0 : 255) << 24 | colormap->Colors[*src_data].Red << 16 | colormap->Colors[*src_data].Green << 8 | colormap->Colors[*src_data].Blue; + dst_data++; + src_data++; } - - dst_data++; - src_data++; } } } @@ -596,9 +680,9 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { uint32_t *dst_ptr; for(int z = 0; z < 4; z++) { - for(int y = ioffs[z]; y < height; y += ijumps[z]) { - dst_ptr = dst_data + width * y; - for(int x = 0; x < width; ++x) { + for(int y = ioffs[z]; y < naturalHeight; y += ijumps[z]) { + dst_ptr = dst_data + naturalWidth * y; + for(int x = 0; x < naturalWidth; ++x) { *dst_ptr = ((*src_ptr == alphaColor) ? 0 : 255) << 24 | (colormap->Colors[*src_ptr].Red) << 16 | (colormap->Colors[*src_ptr].Green) << 8 @@ -617,14 +701,14 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { _surface = cairo_image_surface_create_for_data( data , CAIRO_FORMAT_ARGB32 - , width - , height - , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width)); + , naturalWidth + , naturalHeight + , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth)); cairo_status_t status = cairo_surface_status(_surface); if (status) { - free(data); + delete[] data; return status; } @@ -640,7 +724,7 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { // libjpeg 6.2 does not have jpeg_mem_src; define it ourselves here unless // libjpeg 8 is installed. -#if JPEG_LIB_VERSION < 80 +#if JPEG_LIB_VERSION < 80 && !defined(MEM_SRCDST_SUPPORTED) /* Read JPEG image from a memory segment */ static void @@ -682,77 +766,167 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { #endif +class BufferReader : public Image::Reader { +public: + BufferReader(uint8_t* buf, unsigned len) : _buf(buf), _len(len), _idx(0) {} + + bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } + + uint8_t getNext() override { + return _buf[_idx++]; + } + + void skipBytes(unsigned n) override { _idx += n; } + +private: + uint8_t* _buf; // we do not own this + unsigned _len; + unsigned _idx; +}; + +class StreamReader : public Image::Reader { +public: + StreamReader(FILE *stream) : _stream(stream), _len(0), _idx(0) { + fseek(_stream, 0, SEEK_END); + _len = ftell(_stream); + fseek(_stream, 0, SEEK_SET); + } + + bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } + + uint8_t getNext() override { + ++_idx; + return getc(_stream); + } + + void skipBytes(unsigned n) override { + _idx += n; + fseek(_stream, _idx, SEEK_SET); + } + +private: + FILE* _stream; + unsigned _len; + unsigned _idx; +}; + +void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) { + int stride = naturalWidth * 4; + for (int y = 0; y < naturalHeight; ++y) { + jpeg_read_scanlines(args, &src, 1); + uint32_t *row = (uint32_t*)(data + stride * y); + for (int x = 0; x < naturalWidth; ++x) { + int bx = args->output_components * x; + row[x] = decode(src + bx); + } + } +} + /* * Takes an initialised jpeg_decompress_struct and decodes the * data into _surface. */ cairo_status_t -Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { - int stride = width * 4; - cairo_status_t status; +Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args, Orientation orientation) { + const int channels = 4; + cairo_status_t status = CAIRO_STATUS_SUCCESS; - uint8_t *data = (uint8_t *) malloc(width * height * 4); + uint8_t *data = new uint8_t[naturalWidth * naturalHeight * channels]; if (!data) { jpeg_abort_decompress(args); jpeg_destroy_decompress(args); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } - uint8_t *src = (uint8_t *) malloc(width * args->output_components); + uint8_t *src = new uint8_t[naturalWidth * args->output_components]; if (!src) { free(data); jpeg_abort_decompress(args); jpeg_destroy_decompress(args); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } - for (int y = 0; y < height; ++y) { - jpeg_read_scanlines(args, &src, 1); - uint32_t *row = (uint32_t *)(data + stride * y); - for (int x = 0; x < width; ++x) { - if (args->jpeg_color_space == 1) { - uint32_t *pixel = row + x; - *pixel = 255 << 24 - | src[x] << 16 - | src[x] << 8 - | src[x]; - } else { - int bx = 3 * x; - uint32_t *pixel = row + x; - *pixel = 255 << 24 - | src[bx + 0] << 16 - | src[bx + 1] << 8 - | src[bx + 2]; - } - } + // These are the three main cases to handle. libjpeg converts YCCK to CMYK + // and YCbCr to RGB by default. + switch (args->out_color_space) { + case JCS_CMYK: + jpegToARGB(args, data, src, [](uint8_t const* src) { + uint16_t k = static_cast(src[3]); + uint8_t r = k * src[0] / 255; + uint8_t g = k * src[1] / 255; + uint8_t b = k * src[2] / 255; + return 255 << 24 | r << 16 | g << 8 | b; + }); + break; + case JCS_RGB: + jpegToARGB(args, data, src, [](uint8_t const* src) { + uint8_t r = src[0], g = src[1], b = src[2]; + return 255 << 24 | r << 16 | g << 8 | b; + }); + break; + case JCS_GRAYSCALE: + jpegToARGB(args, data, src, [](uint8_t const* src) { + uint8_t v = src[0]; + return 255 << 24 | v << 16 | v << 8 | v; + }); + break; + default: + this->errorInfo.set("Unsupported JPEG encoding"); + status = CAIRO_STATUS_READ_ERROR; + break; } - _surface = cairo_image_surface_create_for_data( - data - , CAIRO_FORMAT_ARGB32 - , width - , height - , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width)); + updateDimensionsForOrientation(orientation); + + if (!status) { + _surface = cairo_image_surface_create_for_data( + data + , CAIRO_FORMAT_ARGB32 + , naturalWidth + , naturalHeight + , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth)); + } jpeg_finish_decompress(args); jpeg_destroy_decompress(args); status = cairo_surface_status(_surface); + rotatePixels(data, naturalWidth, naturalHeight, channels, orientation); + + delete[] src; + if (status) { - free(data); - free(src); + delete[] data; return status; } - free(src); - _data = data; return CAIRO_STATUS_SUCCESS; } -#if CAIRO_VERSION_MINOR >= 10 +/* + * Callback to recover from jpeg errors + */ + +static void canvas_jpeg_error_exit(j_common_ptr cinfo) { + canvas_jpeg_error_mgr *cjerr = static_cast(cinfo->err); + cjerr->output_message(cinfo); + // Return control to the setjmp point + longjmp(cjerr->setjmp_buffer, 1); +} + +// Capture libjpeg errors instead of writing stdout +static void canvas_jpeg_output_message(j_common_ptr cinfo) { + canvas_jpeg_error_mgr *cjerr = static_cast(cinfo->err); + char buff[JMSG_LENGTH_MAX]; + cjerr->format_message(cinfo, buff); + // (Only the last message will be returned to JS land.) + cjerr->image->errorInfo.set(buff); +} /* * Takes a jpeg data buffer and assigns it as mime data to a @@ -764,30 +938,50 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct args; - struct jpeg_error_mgr err; + struct canvas_jpeg_error_mgr err; + + err.image = this; args.err = jpeg_std_error(&err); + args.err->error_exit = canvas_jpeg_error_exit; + args.err->output_message = canvas_jpeg_output_message; + + // Establish the setjmp return context for canvas_jpeg_error_exit to use + if (setjmp(err.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_READ_ERROR; + } + jpeg_create_decompress(&args); jpeg_mem_src(&args, buf, len); jpeg_read_header(&args, 1); jpeg_start_decompress(&args); - width = args.output_width; - height = args.output_height; + width = naturalWidth = args.output_width; + height = naturalHeight = args.output_height; // Data alloc // 8 pixels per byte using Alpha Channel format to reduce memory requirement. - int buf_size = height * cairo_format_stride_for_width(CAIRO_FORMAT_A1, width); - uint8_t *data = (uint8_t *) malloc(buf_size); - if (!data) return CAIRO_STATUS_NO_MEMORY; + int buf_size = naturalHeight * cairo_format_stride_for_width(CAIRO_FORMAT_A1, naturalWidth); + uint8_t *data = new uint8_t[buf_size]; + if (!data) { + this->errorInfo.set(NULL, "malloc", errno); + return CAIRO_STATUS_NO_MEMORY; + } + + BufferReader reader(buf, len); + Orientation orientation = getExifOrientation(reader); + updateDimensionsForOrientation(orientation); // New image surface _surface = cairo_image_surface_create_for_data( data , CAIRO_FORMAT_A1 - , width - , height - , cairo_format_stride_for_width(CAIRO_FORMAT_A1, width)); + , naturalWidth + , naturalHeight + , cairo_format_stride_for_width(CAIRO_FORMAT_A1, naturalWidth)); // Cleanup jpeg_abort_decompress(&args); @@ -795,10 +989,12 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { cairo_status_t status = cairo_surface_status(_surface); if (status) { - free(data); + delete[] data; return status; } + rotatePixels(data, naturalWidth, naturalHeight, 1, orientation); + _data = data; return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); @@ -810,8 +1006,10 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { void clearMimeData(void *closure) { - Nan::AdjustExternalMemory(-((read_closure_t *)closure)->len); - free(((read_closure_t *) closure)->buf); + Napi::MemoryManagement::AdjustExternalMemory( + *static_cast(closure)->env, + -static_cast((static_cast(closure)->len))); + free(static_cast(closure)->buf); free(closure); } @@ -824,20 +1022,25 @@ clearMimeData(void *closure) { cairo_status_t Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { uint8_t *mime_data = (uint8_t *) malloc(len); - if (!mime_data) return CAIRO_STATUS_NO_MEMORY; + if (!mime_data) { + this->errorInfo.set(NULL, "malloc", errno); + return CAIRO_STATUS_NO_MEMORY; + } read_closure_t *mime_closure = (read_closure_t *) malloc(sizeof(read_closure_t)); if (!mime_closure) { free(mime_data); + this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; } memcpy(mime_data, data, len); + mime_closure->env = &env; mime_closure->buf = mime_data; mime_closure->len = len; - Nan::AdjustExternalMemory(len); + Napi::MemoryManagement::AdjustExternalMemory(env, len); return cairo_surface_set_mime_data(_surface , mime_type @@ -847,29 +1050,43 @@ Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { , mime_closure); } -#endif - /* * Load jpeg from buffer. */ cairo_status_t Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { + BufferReader reader(buf, len); + Orientation orientation = getExifOrientation(reader); + // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct args; - struct jpeg_error_mgr err; + struct canvas_jpeg_error_mgr err; + + err.image = this; args.err = jpeg_std_error(&err); + args.err->error_exit = canvas_jpeg_error_exit; + args.err->output_message = canvas_jpeg_output_message; + + // Establish the setjmp return context for canvas_jpeg_error_exit to use + if (setjmp(err.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_READ_ERROR; + } + jpeg_create_decompress(&args); jpeg_mem_src(&args, buf, len); jpeg_read_header(&args, 1); jpeg_start_decompress(&args); - width = args.output_width; - height = args.output_height; + width = naturalWidth = args.output_width; + height = naturalHeight = args.output_height; - return decodeJPEGIntoSurface(&args); + return decodeJPEGIntoSurface(&args, orientation); } /* @@ -880,24 +1097,53 @@ cairo_status_t Image::loadJPEG(FILE *stream) { cairo_status_t status; +#if defined(_MSC_VER) + if (false) { // Force using loadJPEGFromBuffer +#else if (data_mode == DATA_IMAGE) { // Can lazily read in the JPEG. +#endif + Orientation orientation = NORMAL; + { + StreamReader reader(stream); + orientation = getExifOrientation(reader); + rewind(stream); + } + // JPEG setup struct jpeg_decompress_struct args; - struct jpeg_error_mgr err; + struct canvas_jpeg_error_mgr err; + + err.image = this; args.err = jpeg_std_error(&err); + args.err->error_exit = canvas_jpeg_error_exit; + args.err->output_message = canvas_jpeg_output_message; + + // Establish the setjmp return context for canvas_jpeg_error_exit to use + if (setjmp(err.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_READ_ERROR; + } + jpeg_create_decompress(&args); jpeg_stdio_src(&args, stream); jpeg_read_header(&args, 1); jpeg_start_decompress(&args); - width = args.output_width; - height = args.output_height; - status = decodeJPEGIntoSurface(&args); + if (args.output_width > canvas_max_side || args.output_height > canvas_max_side) { + jpeg_destroy_decompress(&args); + return CAIRO_STATUS_INVALID_SIZE; + } + + width = naturalWidth = args.output_width; + height = naturalHeight = args.output_height; + + status = decodeJPEGIntoSurface(&args, orientation); fclose(stream); } else { // We'll need the actual source jpeg data, so read fully. -#if CAIRO_VERSION_MINOR >= 10 uint8_t *buf; unsigned len; @@ -906,7 +1152,10 @@ Image::loadJPEG(FILE *stream) { fseek(stream, 0, SEEK_SET); buf = (uint8_t *) malloc(len); - if (!buf) return CAIRO_STATUS_NO_MEMORY; + if (!buf) { + this->errorInfo.set(NULL, "malloc", errno); + return CAIRO_STATUS_NO_MEMORY; + } if (fread(buf, len, 1, stream) != 1) { status = CAIRO_STATUS_READ_ERROR; @@ -915,24 +1164,484 @@ Image::loadJPEG(FILE *stream) { if (!status) status = assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); } else if (DATA_MIME == data_mode) { status = decodeJPEGBufferIntoMimeSurface(buf, len); - } else { + } +#if defined(_MSC_VER) + else if (DATA_IMAGE == data_mode) { + status = loadJPEGFromBuffer(buf, len); + } +#endif + else { status = CAIRO_STATUS_READ_ERROR; } fclose(stream); free(buf); -#else - status = CAIRO_STATUS_READ_ERROR; -#endif } return status; } +/* + * Returns the Exif orientation if one exists, otherwise returns NORMAL + */ + +Image::Orientation +Image::getExifOrientation(Reader& jpeg) { + static const char kJpegStartOfImage = (char)0xd8; + static const char kJpegStartOfFrameBaseline = (char)0xc0; + static const char kJpegStartOfFrameProgressive = (char)0xc2; + static const char kJpegHuffmanTable = (char)0xc4; + static const char kJpegQuantizationTable = (char)0xdb; + static const char kJpegRestartInterval = (char)0xdd; + static const char kJpegComment = (char)0xfe; + static const char kJpegStartOfScan = (char)0xda; + static const char kJpegApp0 = (char)0xe0; + static const char kJpegApp1 = (char)0xe1; + + // Find the Exif tag (if it exists) + int exif_len = 0; + bool done = false; + while (!done && jpeg.hasBytes(1)) { + while (jpeg.hasBytes(1) && jpeg.getNext() != 0xff) { + // noop + } + if (jpeg.hasBytes(1)) { + char tag = jpeg.getNext(); + switch (tag) { + case kJpegStartOfImage: + break; // beginning of file, no extra bytes + case kJpegRestartInterval: + jpeg.skipBytes(4); + break; + case kJpegStartOfFrameBaseline: + case kJpegStartOfFrameProgressive: + case kJpegHuffmanTable: + case kJpegQuantizationTable: + case kJpegComment: + case kJpegApp0: + case kJpegApp1: { + if (jpeg.hasBytes(2)) { + uint16_t tag_len = 0; + tag_len |= jpeg.getNext() << 8; + tag_len |= jpeg.getNext(); + // The tag length includes the two bytes for the length + uint16_t tag_content_len = std::max(0, tag_len - 2); + if (tag != kJpegApp1 || !jpeg.hasBytes(tag_content_len)) { + jpeg.skipBytes(tag_content_len); // skip JPEG tags we ignore. + } else if (!jpeg.hasBytes(6)) { + jpeg.skipBytes(tag_content_len); // too short to have "Exif\0\0" + } else { + if (jpeg.getNext() == 'E' && jpeg.getNext() == 'x' && + jpeg.getNext() == 'i' && jpeg.getNext() == 'f' && + jpeg.getNext() == '\0' && jpeg.getNext() == '\0') { + exif_len = tag_content_len - 6; + done = true; + } else { + jpeg.skipBytes(tag_content_len); // too short to have "Exif\0\0" + } + } + } else { + done = true; // shouldn't happen: corrupt file or we have a bug + } + break; + } + case kJpegStartOfScan: + default: + done = true; // got to the image, apparently no exif tags here + break; + } + } + } + + // Parse exif if it exists. If it does, we have already checked that jpeglen + // is longer than exifStart + exifLen, so we can safely index the data + if (exif_len > 0) { + // The first two bytes of TIFF header are "II" if little-endian ("Intel") + // and "MM" if big-endian ("Motorola") + const bool isLE = (jpeg.getNext() == 'I'); + jpeg.skipBytes(3); // +1 for the other I/M, +2 for 0x002a + + auto readUint16Little = [](Reader &jpeg) -> uint32_t { + uint16_t val = uint16_t(jpeg.getNext()); + val |= uint16_t(jpeg.getNext()) << 8; + return val; + }; + auto readUint32Little = [](Reader &jpeg) -> uint32_t { + uint32_t val = uint32_t(jpeg.getNext()); + val |= uint32_t(jpeg.getNext()) << 8; + val |= uint32_t(jpeg.getNext()) << 16; + val |= uint32_t(jpeg.getNext()) << 24; + return val; + }; + auto readUint16Big = [](Reader &jpeg) -> uint32_t { + uint16_t val = uint16_t(jpeg.getNext()) << 8; + val |= uint16_t(jpeg.getNext()); + return val; + }; + auto readUint32Big = [](Reader &jpeg) -> uint32_t { + uint32_t val = uint32_t(jpeg.getNext()) << 24; + val |= uint32_t(jpeg.getNext()) << 16; + val |= uint32_t(jpeg.getNext()) << 8; + val |= uint32_t(jpeg.getNext()); + return val; + }; + // The first two bytes of TIFF header are "II" if little-endian ("Intel") + // and "MM" if big-endian ("Motorola") + auto readUint32 = [readUint32Little, readUint32Big, isLE](Reader &jpeg) -> uint32_t { + return isLE ? readUint32Little(jpeg) : readUint32Big(jpeg); + }; + auto readUint16 = [readUint16Little, readUint16Big, isLE](Reader &jpeg) -> uint32_t { + return isLE ? readUint16Little(jpeg) : readUint16Big(jpeg); + }; + // offset to the IFD0 (offset from beginning of TIFF header, II/MM, + // which is 8 bytes before where we are after reading the uint32) + jpeg.skipBytes(readUint32(jpeg) - 8); + + // Read the IFD0 ("Image File Directory 0") + // | NN | n entries in directory (2 bytes) + // | TT | tt | nnnn | vvvv | entry: tag (2b), data type (2b), + // n components (4b), value/offset (4b) + if (jpeg.hasBytes(2)) { + uint16_t nEntries = readUint16(jpeg); + for (uint16_t i = 0; i < nEntries && jpeg.hasBytes(2); ++i) { + uint16_t tag = readUint16(jpeg); + // The entry is 12 bytes. We already read the 2 bytes for the tag. + jpeg.skipBytes(6); // skip 2 for the data type, skip 4 n components. + if (tag == 0x112) { + switch (readUint16(jpeg)) { // orientation tag is always one uint16 + case 1: return NORMAL; + case 2: return MIRROR_HORIZ; + case 3: return ROTATE_180; + case 4: return MIRROR_VERT; + case 5: return MIRROR_HORIZ_AND_ROTATE_270_CW; + case 6: return ROTATE_90_CW; + case 7: return MIRROR_HORIZ_AND_ROTATE_90_CW; + case 8: return ROTATE_270_CW; + default: return NORMAL; + } + } else { + jpeg.skipBytes(4); // skip the four bytes for the value + } + } + } + } + + return NORMAL; +} + +/* + * Updates the dimensions of the bitmap according to the orientation + */ + +void Image::updateDimensionsForOrientation(Orientation orientation) { + switch (orientation) { + case ROTATE_90_CW: + case ROTATE_270_CW: + case MIRROR_HORIZ_AND_ROTATE_90_CW: + case MIRROR_HORIZ_AND_ROTATE_270_CW: { + int tmp = naturalWidth; + naturalWidth = naturalHeight; + naturalHeight = tmp; + tmp = width; + width = height; + height = tmp; + break; + } + case NORMAL: + case MIRROR_HORIZ: + case MIRROR_VERT: + case ROTATE_180: + default: { + break; + } + } +} + +/* + * Rotates the pixels to the correct orientation. + */ + +void +Image::rotatePixels(uint8_t* pixels, int width, int height, int channels, + Orientation orientation) { + auto swapPixel = [channels](uint8_t* pixels, int src_idx, int dst_idx) { + uint8_t tmp; + for (int i = 0; i < channels; ++i) { + tmp = pixels[src_idx + i]; + pixels[src_idx + i] = pixels[dst_idx + i]; + pixels[dst_idx + i] = tmp; + } + }; + + auto mirrorHoriz = [swapPixel](uint8_t* pixels, int width, int height, int channels) { + int midX = width / 2; // ok to truncate if odd, since we don't swap a center pixel + for (int y = 0; y < height; ++y) { + for (int x = 0; x < midX; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = (y * width + width - 1 - x) * channels; + swapPixel(pixels, orig_idx, new_idx); + } + } + }; + + auto mirrorVert = [swapPixel](uint8_t* pixels, int width, int height, int channels) { + int midY = height / 2; // ok to truncate if odd, since we don't swap a center pixel + for (int y = 0; y < midY; ++y) { + for (int x = 0; x < width; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = ((height - y - 1) * width + x) * channels; + swapPixel(pixels, orig_idx, new_idx); + } + } + }; + + auto rotate90 = [](uint8_t* pixels, int width, int height, int channels) { + const int n_bytes = width * height * channels; + uint8_t *unrotated = new uint8_t[n_bytes]; + if (!unrotated) { + return; + } + std::memcpy(unrotated, pixels, n_bytes); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width ; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = (x * height + height - 1 - y) * channels; + std::memcpy(pixels + new_idx, unrotated + orig_idx, channels); + } + } + }; + + auto rotate270 = [](uint8_t* pixels, int width, int height, int channels) { + const int n_bytes = width * height * channels; + uint8_t *unrotated = new uint8_t[n_bytes]; + if (!unrotated) { + return; + } + std::memcpy(unrotated, pixels, n_bytes); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width ; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = ((width - 1 - x) * height + y) * channels; + std::memcpy(pixels + new_idx, unrotated + orig_idx, channels); + } + } + }; + + switch (orientation) { + case MIRROR_HORIZ: + mirrorHoriz(pixels, width, height, channels); + break; + case MIRROR_VERT: + mirrorVert(pixels, width, height, channels); + break; + case ROTATE_180: + mirrorHoriz(pixels, width, height, channels); + mirrorVert(pixels, width, height, channels); + break; + case ROTATE_90_CW: + rotate90(pixels, height, width, channels); // swap w/h because we need orig w/h + break; + case ROTATE_270_CW: + rotate270(pixels, height, width, channels); // swap w/h because we need orig w/h + break; + case MIRROR_HORIZ_AND_ROTATE_90_CW: + mirrorHoriz(pixels, height, width, channels); // swap w/h because we need orig w/h + rotate90(pixels, height, width, channels); + break; + case MIRROR_HORIZ_AND_ROTATE_270_CW: + mirrorHoriz(pixels, height, width, channels); // swap w/h because we need orig w/h + rotate270(pixels, height, width, channels); + break; + case NORMAL: + default: + break; + } +} + #endif /* HAVE_JPEG */ +#ifdef HAVE_RSVG + +/* + * Load SVG from buffer + */ + +cairo_status_t +Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { + _is_svg = true; + + if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, nullptr))) { + return CAIRO_STATUS_READ_ERROR; + } + + double d_width; + double d_height; + + rsvg_handle_get_intrinsic_size_in_pixels(_rsvg, &d_width, &d_height); + + width = naturalWidth = d_width; + height = naturalHeight = d_height; + + if (width <= 0 || height <= 0) { + this->errorInfo.set("Width and height must be set on the svg element"); + return CAIRO_STATUS_READ_ERROR; + } + + return renderSVGToSurface(); +} + +/* + * Renders the Rsvg handle to this image's surface + */ +cairo_status_t +Image::renderSVGToSurface() { + cairo_status_t status; + + _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + + status = cairo_surface_status(_surface); + if (status != CAIRO_STATUS_SUCCESS) { + g_object_unref(_rsvg); + return status; + } + + cairo_t *cr = cairo_create(_surface); + status = cairo_status(cr); + if (status != CAIRO_STATUS_SUCCESS) { + g_object_unref(_rsvg); + return status; + } + + RsvgRectangle viewport = { + 0, // x + 0, // y + static_cast(width), + static_cast(height) + }; + gboolean render_ok = rsvg_handle_render_document(_rsvg, cr, &viewport, nullptr); + if (!render_ok) { + g_object_unref(_rsvg); + cairo_destroy(cr); + return CAIRO_STATUS_READ_ERROR; // or WRITE? + } + + cairo_destroy(cr); + + _svg_last_width = width; + _svg_last_height = height; + + return status; +} + +/* + * Load SVG + */ + +cairo_status_t +Image::loadSVG(FILE *stream) { + _is_svg = true; + + struct stat s; + int fd = fileno(stream); + + // stat + if (fstat(fd, &s) < 0) { + fclose(stream); + return CAIRO_STATUS_READ_ERROR; + } + + uint8_t *buf = (uint8_t *) malloc(s.st_size); + + if (!buf) { + fclose(stream); + return CAIRO_STATUS_NO_MEMORY; + } + + size_t read = fread(buf, s.st_size, 1, stream); + fclose(stream); + + cairo_status_t result = CAIRO_STATUS_READ_ERROR; + if (1 == read) result = loadSVGFromBuffer(buf, s.st_size); + free(buf); + + return result; +} + +#endif /* HAVE_RSVG */ + +/* + * Load BMP from buffer. + */ + +cairo_status_t Image::loadBMPFromBuffer(uint8_t *buf, unsigned len){ + BMPParser::Parser parser; + + // Reversed ARGB32 with pre-multiplied alpha + uint8_t pixFmt[5] = {2, 1, 0, 3, 1}; + parser.parse(buf, len, pixFmt); + + if (parser.getStatus() != BMPParser::Status::OK) { + errorInfo.reset(); + errorInfo.message = parser.getErrMsg(); + return CAIRO_STATUS_READ_ERROR; + } + + width = naturalWidth = parser.getWidth(); + height = naturalHeight = parser.getHeight(); + uint8_t *data = parser.getImgd(); + + _surface = cairo_image_surface_create_for_data( + data, + CAIRO_FORMAT_ARGB32, + width, + height, + cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width) + ); + + // No need to delete the data + cairo_status_t status = cairo_surface_status(_surface); + if (status) return status; + + _data = data; + parser.clearImgd(); + + return CAIRO_STATUS_SUCCESS; +} + +/* + * Load BMP. + */ + +cairo_status_t Image::loadBMP(FILE *stream){ + struct stat s; + int fd = fileno(stream); + + // Stat + if (fstat(fd, &s) < 0) { + fclose(stream); + return CAIRO_STATUS_READ_ERROR; + } + + uint8_t *buf = new uint8_t[s.st_size]; + + if (!buf) { + fclose(stream); + errorInfo.set(NULL, "malloc", errno); + return CAIRO_STATUS_NO_MEMORY; + } + + size_t read = fread(buf, s.st_size, 1, stream); + fclose(stream); + + cairo_status_t result = CAIRO_STATUS_READ_ERROR; + if (read == 1) result = loadBMPFromBuffer(buf, s.st_size); + delete[] buf; + + return result; +} + /* - * Return UNKNOWN, JPEG, or PNG based on the filename. + * Return UNKNOWN, SVG, GIF, JPEG, or PNG based on the filename. */ Image::type @@ -943,6 +1652,7 @@ Image::extension(const char *filename) { if (len >= 4 && 0 == strcmp(".gif", filename - 4)) return Image::GIF; if (len >= 4 && 0 == strcmp(".jpg", filename - 4)) return Image::JPEG; if (len >= 4 && 0 == strcmp(".png", filename - 4)) return Image::PNG; + if (len >= 4 && 0 == strcmp(".svg", filename - 4)) return Image::SVG; return Image::UNKNOWN; } @@ -972,3 +1682,39 @@ int Image::isPNG(uint8_t *data) { return 'P' == data[1] && 'N' == data[2] && 'G' == data[3]; } + +/* + * Skip " -// -#ifndef __NODE_IMAGE_H__ -#define __NODE_IMAGE_H__ +#pragma once -#include "Canvas.h" +#include +#include "CanvasError.h" +#include +#include +#include // node < 7 uses libstdc++ on macOS which lacks complete c++11 #ifdef HAVE_JPEG #include @@ -25,58 +23,94 @@ #endif #endif +#ifdef HAVE_RSVG +#include + // librsvg <= 2.36.1, identified by undefined macro, needs an extra include + #ifndef LIBRSVG_CHECK_VERSION + #include + #endif +#endif +using JPEGDecodeL = std::function; -class Image: public Nan::ObjectWrap { +class Image : public Napi::ObjectWrap { public: char *filename; int width, height; - Nan::Callback *onload; - Nan::Callback *onerror; - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_GETTER(GetSource); - static NAN_GETTER(GetOnload); - static NAN_GETTER(GetOnerror); - static NAN_GETTER(GetComplete); - static NAN_GETTER(GetWidth); - static NAN_GETTER(GetHeight); - static NAN_GETTER(GetDataMode); - static NAN_SETTER(SetSource); - static NAN_SETTER(SetOnload); - static NAN_SETTER(SetOnerror); - static NAN_SETTER(SetDataMode); - inline cairo_surface_t *surface(){ return _surface; } + int naturalWidth, naturalHeight; + Napi::Env env; + static Napi::FunctionReference constructor; + static void Initialize(Napi::Env& env, Napi::Object& target); + Image(const Napi::CallbackInfo& info); + Napi::Value GetComplete(const Napi::CallbackInfo& info); + Napi::Value GetWidth(const Napi::CallbackInfo& info); + Napi::Value GetHeight(const Napi::CallbackInfo& info); + Napi::Value GetNaturalWidth(const Napi::CallbackInfo& info); + Napi::Value GetNaturalHeight(const Napi::CallbackInfo& info); + Napi::Value GetDataMode(const Napi::CallbackInfo& info); + void SetDataMode(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value); + void SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value); + static Napi::Value GetSource(const Napi::CallbackInfo& info); + static void SetSource(const Napi::CallbackInfo& info); inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } inline int stride(){ return cairo_image_surface_get_stride(_surface); } static int isPNG(uint8_t *data); static int isJPEG(uint8_t *data); static int isGIF(uint8_t *data); + static int isSVG(uint8_t *data, unsigned len); + static int isBMP(uint8_t *data, unsigned len); static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len); inline int isComplete(){ return COMPLETE == state; } + cairo_surface_t *surface(); cairo_status_t loadSurface(); cairo_status_t loadFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadPNGFromBuffer(uint8_t *buf); cairo_status_t loadPNG(); void clearData(); +#ifdef HAVE_RSVG + cairo_status_t loadSVGFromBuffer(uint8_t *buf, unsigned len); + cairo_status_t loadSVG(FILE *stream); + cairo_status_t renderSVGToSurface(); +#endif #ifdef HAVE_GIF cairo_status_t loadGIFFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadGIF(FILE *stream); #endif #ifdef HAVE_JPEG + enum Orientation { + NORMAL, + MIRROR_HORIZ, + MIRROR_VERT, + ROTATE_180, + ROTATE_90_CW, + ROTATE_270_CW, + MIRROR_HORIZ_AND_ROTATE_90_CW, + MIRROR_HORIZ_AND_ROTATE_270_CW + }; cairo_status_t loadJPEGFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadJPEG(FILE *stream); - cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info); -#if CAIRO_VERSION_MINOR >= 10 + void jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode); + cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info, Orientation orientation); cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len); cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type); + + class Reader { + public: + virtual bool hasBytes(unsigned n) const = 0; + virtual uint8_t getNext() = 0; + virtual void skipBytes(unsigned n) = 0; + }; + Orientation getExifOrientation(Reader& jpeg); + void updateDimensionsForOrientation(Orientation orientation); + void rotatePixels(uint8_t* pixels, int width, int height, int channels, Orientation orientation); #endif -#endif - void error(Local error); + cairo_status_t loadBMPFromBuffer(uint8_t *buf, unsigned len); + cairo_status_t loadBMP(FILE *stream); + CanvasError errorInfo; void loaded(); cairo_status_t load(); - Image(); + ~Image(); enum { DEFAULT @@ -94,15 +128,19 @@ class Image: public Nan::ObjectWrap { , GIF , JPEG , PNG + , SVG } type; static type extension(const char *filename); private: cairo_surface_t *_surface; - uint8_t *_data; + uint8_t *_data = nullptr; int _data_len; - ~Image(); -}; - +#ifdef HAVE_RSVG + RsvgHandle *_rsvg; + bool _is_svg; + int _svg_last_width; + int _svg_last_height; #endif +}; diff --git a/src/ImageData.cc b/src/ImageData.cc index 73730da3c..d334ca894 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -1,135 +1,132 @@ - -// -// ImageData.cc -// // Copyright (c) 2010 LearnBoost -// #include "ImageData.h" - -Nan::Persistent ImageData::constructor; +#include "InstanceData.h" /* * Initialize ImageData. */ void -ImageData::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { - Nan::HandleScope scope; - - // Constructor - Local ctor = Nan::New(ImageData::New); - constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("ImageData").ToLocalChecked()); - - // Prototype - Local proto = ctor->PrototypeTemplate(); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight); - Nan::Set(target, Nan::New("ImageData").ToLocalChecked(), ctor->GetFunction()); +ImageData::Initialize(Napi::Env& env, Napi::Object& exports) { + Napi::HandleScope scope(env); + + InstanceData *data = env.GetInstanceData(); + + Napi::Function ctor = DefineClass(env, "ImageData", { + InstanceAccessor<&ImageData::GetWidth>("width", napi_default_jsproperty), + InstanceAccessor<&ImageData::GetHeight>("height", napi_default_jsproperty) + }); + + exports.Set("ImageData", ctor); + data->ImageDataCtor = Napi::Persistent(ctor); } /* * Initialize a new ImageData object. */ -NAN_METHOD(ImageData::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - -#if NODE_MAJOR_VERSION == 0 && NODE_MINOR_VERSION <= 10 - Local clampedArray; - Local global = Context::GetCurrent()->Global(); -#else - Local clampedArray; -#endif - +ImageData::ImageData(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + Napi::TypedArray dataArray; uint32_t width; uint32_t height; int length; - if (info[0]->IsUint32() && info[1]->IsUint32()) { - width = info[0]->Uint32Value(); + if (info[0].IsNumber() && info[1].IsNumber()) { + width = info[0].As().Uint32Value(); if (width == 0) { - Nan::ThrowRangeError("The source width is zero."); + Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException(); return; } - height = info[1]->Uint32Value(); + height = info[1].As().Uint32Value(); if (height == 0) { - Nan::ThrowRangeError("The source height is zero."); + Napi::RangeError::New(env, "The source height is zero.").ThrowAsJavaScriptException(); return; } - length = width * height * 4; - -#if NODE_MAJOR_VERSION == 0 && NODE_MINOR_VERSION <= 10 - Local sizeHandle = Nan::New(length); - Local caargv[] = { sizeHandle }; - clampedArray = global->Get(Nan::New("Uint8ClampedArray").ToLocalChecked()).As()->NewInstance(1, caargv); -#else - clampedArray = Uint8ClampedArray::New(ArrayBuffer::New(Isolate::GetCurrent(), length), 0, length); -#endif - -#if NODE_MAJOR_VERSION == 0 && NODE_MINOR_VERSION <= 10 - } else if (info[0]->ToObject()->GetIndexedPropertiesExternalArrayDataType() == kExternalPixelArray && info[1]->IsUint32()) { - clampedArray = info[0]->ToObject(); - length = clampedArray->GetIndexedPropertiesExternalArrayDataLength(); -#else - } else if (info[0]->IsUint8ClampedArray() && info[1]->IsUint32()) { - clampedArray = info[0].As(); - length = clampedArray->Length(); -#endif + length = width * height * 4; // ImageData(w, h) constructor assumes 4 BPP; documented. + + dataArray = Napi::Uint8Array::New(env, length, napi_uint8_clamped_array); + } else if ( + info[0].IsTypedArray() && + info[0].As().TypedArrayType() == napi_uint8_clamped_array && + info[1].IsNumber() + ) { + dataArray = info[0].As(); + + length = dataArray.ElementLength(); if (length == 0) { - Nan::ThrowRangeError("The input data has a zero byte length."); - return; - } - if (length % 4 != 0) { - Nan::ThrowRangeError("The input data byte length is not a multiple of 4."); + Napi::RangeError::New(env, "The input data has a zero byte length.").ThrowAsJavaScriptException(); return; } - width = info[1]->Uint32Value(); - int size = length / 4; + + // Don't assert that the ImageData length is a multiple of four because some + // data formats are not 4 BPP. + + width = info[1].As().Uint32Value(); if (width == 0) { - Nan::ThrowRangeError("The source width is zero."); + Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException(); return; } - if (size % width != 0) { - Nan::ThrowRangeError("The input data byte length is not a multiple of (4 * width)."); + + // Don't assert that the byte length is a multiple of 4 * width, ditto. + + if (info[2].IsNumber()) { // Explicit height given + height = info[2].As().Uint32Value(); + } else { // Calculate height assuming 4 BPP + int size = length / 4; + height = size / width; + } + } else if ( + info[0].IsTypedArray() && + info[0].As().TypedArrayType() == napi_uint16_array && + info[1].IsNumber() + ) { // Intended for RGB16_565 format + dataArray = info[0].As(); + + length = dataArray.ElementLength(); + if (length == 0) { + Napi::RangeError::New(env, "The input data has a zero byte length.").ThrowAsJavaScriptException(); return; } - height = size / width; - if (info[2]->IsUint32() && info[2]->Uint32Value() != height) { - Nan::ThrowRangeError("The input data byte length is not equal to (4 * width * height)."); + + width = info[1].As().Uint32Value(); + if (width == 0) { + Napi::RangeError::New(env, "The source width is zero.").ThrowAsJavaScriptException(); return; } + + if (info[2].IsNumber()) { // Explicit height given + height = info[2].As().Uint32Value(); + } else { // Calculate height assuming 2 BPP + int size = length / 2; + height = size / width; + } } else { - Nan::ThrowTypeError("Expected (Uint8ClampedArray, width[, height]) or (width, height)"); + Napi::TypeError::New(env, "Expected (Uint8ClampedArray, width[, height]), (Uint16Array, width[, height]) or (width, height)").ThrowAsJavaScriptException(); return; } - Nan::TypedArrayContents dataPtr(clampedArray); + _width = width; + _height = height; + _data = dataArray.As().Data(); - ImageData *imageData = new ImageData(reinterpret_cast(*dataPtr), width, height); - imageData->Wrap(info.This()); - info.This()->Set(Nan::New("data").ToLocalChecked(), clampedArray); - info.GetReturnValue().Set(info.This()); + info.This().As().Set("data", dataArray); } /* * Get width. */ -NAN_GETTER(ImageData::GetWidth) { - ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(imageData->width())); +Napi::Value +ImageData::GetWidth(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, width()); } /* * Get height. */ -NAN_GETTER(ImageData::GetHeight) { - ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(imageData->height())); +Napi::Value +ImageData::GetHeight(const Napi::CallbackInfo& info) { + return Napi::Number::New(env, height()); } diff --git a/src/ImageData.h b/src/ImageData.h index 074d1ff8a..32d6037d1 100644 --- a/src/ImageData.h +++ b/src/ImageData.h @@ -1,30 +1,22 @@ - -// -// ImageData.h -// // Copyright (c) 2010 LearnBoost -// -#ifndef __NODE_IMAGE_DATA_H__ -#define __NODE_IMAGE_DATA_H__ +#pragma once -#include "Canvas.h" -#include -#include +#include +#include // node < 7 uses libstdc++ on macOS which lacks complete c++11 -class ImageData: public Nan::ObjectWrap { +class ImageData : public Napi::ObjectWrap { public: - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_GETTER(GetWidth); - static NAN_GETTER(GetHeight); + static void Initialize(Napi::Env& env, Napi::Object& exports); + ImageData(const Napi::CallbackInfo& info); + Napi::Value GetWidth(const Napi::CallbackInfo& info); + Napi::Value GetHeight(const Napi::CallbackInfo& info); inline int width() { return _width; } inline int height() { return _height; } inline uint8_t *data() { return _data; } - inline int stride() { return _width * 4; } - ImageData(uint8_t *data, int width, int height) : _width(width), _height(height), _data(data) {} + + Napi::Env env; private: int _width; @@ -32,5 +24,3 @@ class ImageData: public Nan::ObjectWrap { uint8_t *_data; }; - -#endif diff --git a/src/InstanceData.h b/src/InstanceData.h new file mode 100644 index 000000000..939f2a488 --- /dev/null +++ b/src/InstanceData.h @@ -0,0 +1,15 @@ +#include + +struct InstanceData { + Napi::FunctionReference ImageBackendCtor; + Napi::FunctionReference PdfBackendCtor; + Napi::FunctionReference SvgBackendCtor; + Napi::FunctionReference CanvasCtor; + Napi::FunctionReference CanvasGradientCtor; + Napi::FunctionReference DOMMatrixCtor; + Napi::FunctionReference ImageCtor; + Napi::FunctionReference parseFont; + Napi::FunctionReference Context2dCtor; + Napi::FunctionReference ImageDataCtor; + Napi::FunctionReference CanvasPatternCtor; +}; diff --git a/src/JPEGStream.h b/src/JPEGStream.h index edc887e78..43c74f139 100644 --- a/src/JPEGStream.h +++ b/src/JPEGStream.h @@ -1,12 +1,6 @@ +#pragma once -// -// JPEGStream.h -// - -#ifndef __NODE_JPEG_STREAM_H__ -#define __NODE_JPEG_STREAM_H__ - -#include "Canvas.h" +#include "closure.h" #include #include @@ -15,12 +9,12 @@ * inspired by IJG's jdatadst.c */ -typedef struct { - struct jpeg_destination_mgr pub; - closure_t *closure; +struct closure_destination_mgr { + jpeg_destination_mgr pub; + JpegClosure* closure; JOCTET *buffer; int bufsize; -} closure_destination_mgr; +}; void init_closure_destination(j_compress_ptr cinfo){ @@ -29,17 +23,15 @@ init_closure_destination(j_compress_ptr cinfo){ boolean empty_closure_output_buffer(j_compress_ptr cinfo){ - Nan::HandleScope scope; closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest; + Napi::Env env = dest->closure->canvas->Env(); + Napi::HandleScope scope(env); + Napi::AsyncContext async(env, "canvas:empty_closure_output_buffer"); - Local buf = Nan::NewBuffer((char *)dest->buffer, dest->bufsize).ToLocalChecked(); + Napi::Object buf = Napi::Buffer::New(env, (char *)dest->buffer, dest->bufsize); // emit "data" - Local argv[2] = { - Nan::Null() - , buf - }; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)dest->closure->fn, 2, argv); + dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async); dest->buffer = (JOCTET *)malloc(dest->bufsize); cinfo->dest->next_output_byte = dest->buffer; @@ -49,30 +41,22 @@ empty_closure_output_buffer(j_compress_ptr cinfo){ void term_closure_destination(j_compress_ptr cinfo){ - Nan::HandleScope scope; closure_destination_mgr *dest = (closure_destination_mgr *) cinfo->dest; + Napi::Env env = dest->closure->canvas->Env(); + Napi::HandleScope scope(env); + Napi::AsyncContext async(env, "canvas:term_closure_destination"); /* emit remaining data */ - Local buf = Nan::NewBuffer((char *)dest->buffer, dest->bufsize - dest->pub.free_in_buffer).ToLocalChecked(); - - Local data_argv[2] = { - Nan::Null() - , buf - }; + Napi::Object buf = Napi::Buffer::New(env, (char *)dest->buffer, dest->bufsize - dest->pub.free_in_buffer); - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)dest->closure->fn, 2, data_argv); + dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async); // emit "end" - Local end_argv[2] = { - Nan::Null() - , Nan::Null() - }; - - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (v8::Local)dest->closure->fn, 2, end_argv); + dest->closure->cb.MakeCallback(env.Global(), {env.Null(), env.Null()}, async); } void -jpeg_closure_dest(j_compress_ptr cinfo, closure_t * closure, int bufsize){ +jpeg_closure_dest(j_compress_ptr cinfo, JpegClosure* closure, int bufsize){ closure_destination_mgr * dest; /* The destination object is made permanent so that multiple JPEG images @@ -98,31 +82,27 @@ jpeg_closure_dest(j_compress_ptr cinfo, closure_t * closure, int bufsize){ cinfo->dest->free_in_buffer = dest->bufsize; } -void -write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool progressive, closure_t *closure){ +void encode_jpeg(jpeg_compress_struct cinfo, cairo_surface_t *surface, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor) { int w = cairo_image_surface_get_width(surface); int h = cairo_image_surface_get_height(surface); - struct jpeg_compress_struct cinfo; - struct jpeg_error_mgr jerr; - JSAMPROW slr; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); cinfo.in_color_space = JCS_RGB; cinfo.input_components = 3; cinfo.image_width = w; cinfo.image_height = h; jpeg_set_defaults(&cinfo); if (progressive) - jpeg_simple_progression(&cinfo); - jpeg_set_quality(&cinfo, quality, (quality<25)?0:1); - jpeg_closure_dest(&cinfo, closure, bufsize); + jpeg_simple_progression(&cinfo); + jpeg_set_quality(&cinfo, quality, (quality < 25) ? 0 : 1); + cinfo.comp_info[0].h_samp_factor = chromaHSampFactor; + cinfo.comp_info[0].v_samp_factor = chromaVSampFactor; + JSAMPROW slr; jpeg_start_compress(&cinfo, TRUE); unsigned char *dst; - unsigned int *src = (unsigned int *) cairo_image_surface_get_data(surface); + unsigned int *src = (unsigned int *)cairo_image_surface_get_data(surface); int sl = 0; - dst = (unsigned char *) malloc(w * 3); + dst = (unsigned char *)malloc(w * 3); while (sl < h) { unsigned char *dp = dst; int x = 0; @@ -143,4 +123,35 @@ write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool pr jpeg_destroy_compress(&cinfo); } -#endif +void +write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, JpegClosure* closure) { + jpeg_compress_struct cinfo; + jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + jpeg_closure_dest(&cinfo, closure, bufsize); + encode_jpeg( + cinfo, + surface, + closure->quality, + closure->progressive, + closure->chromaSubsampling, + closure->chromaSubsampling); +} + +void +write_to_jpeg_buffer(cairo_surface_t* surface, JpegClosure* closure) { + jpeg_compress_struct cinfo; + jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + cinfo.client_data = closure; + cinfo.dest = closure->jpeg_dest_mgr; + encode_jpeg( + cinfo, + surface, + closure->quality, + closure->progressive, + closure->chromaSubsampling, + closure->chromaSubsampling); +} diff --git a/src/PNG.h b/src/PNG.h index 4cdb78919..30b88f85f 100644 --- a/src/PNG.h +++ b/src/PNG.h @@ -1,11 +1,12 @@ -#ifndef _CANVAS_PNG_H -#define _CANVAS_PNG_H -#include -#include +#pragma once + #include -#include -#include #include "closure.h" +#include // round +#include +#include +#include +#include #if defined(__GNUC__) && (__GNUC__ > 2) && defined(__OPTIMIZE__) #define likely(expr) (__builtin_expect (!!(expr), 1)) @@ -15,10 +16,6 @@ #define unlikely(expr) (expr) #endif -#ifndef CAIRO_FORMAT_INVALID -#define CAIRO_FORMAT_INVALID -1 -#endif - static void canvas_png_flush(png_structp png_ptr) { /* Do nothing; fflush() is said to be just a waste of energy. */ (void) png_ptr; /* Stifle compiler warning */ @@ -63,12 +60,40 @@ static void canvas_unpremultiply_data(png_structp png, png_row_infop row_info, p } } +/* Converts RGB16_565 format data to RGBA32 */ +static void canvas_convert_565_to_888(png_structp png, png_row_infop row_info, png_bytep data) { + // Loop in reverse to unpack in-place. + for (ptrdiff_t col = row_info->width - 1; col >= 0; col--) { + uint8_t* src = &data[col * sizeof(uint16_t)]; + uint8_t* dst = &data[col * 3]; + uint16_t pixel; + + memcpy(&pixel, src, sizeof(uint16_t)); + + // Convert and rescale to the full 0-255 range + // See http://stackoverflow.com/a/29326693 + const uint8_t red5 = (pixel & 0xF800) >> 11; + const uint8_t green6 = (pixel & 0x7E0) >> 5; + const uint8_t blue5 = (pixel & 0x001F); + + dst[0] = ((red5 * 255 + 15) / 31); + dst[1] = ((green6 * 255 + 31) / 63); + dst[2] = ((blue5 * 255 + 15) / 31); + } +} + struct canvas_png_write_closure_t { cairo_write_func_t write_func; - void *closure; + PngClosure* closure; }; -static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr write_func, void *closure) { +#ifdef PNG_SETJMP_SUPPORTED +bool setjmp_wrapper(png_structp png) { + return setjmp(png_jmpbuf(png)); +} +#endif + +static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr write_func, canvas_png_write_closure_t *closure) { unsigned int i; cairo_status_t status = CAIRO_STATUS_SUCCESS; uint8_t *data; @@ -99,8 +124,9 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ return status; } + int stride = cairo_image_surface_get_stride(surface); for (i = 0; i < height; i++) { - rows[i] = (png_byte *) data + i * cairo_image_surface_get_stride(surface); + rows[i] = (png_byte *) data + i * stride; } #ifdef PNG_USER_MEM_SUPPORTED @@ -125,7 +151,7 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ } #ifdef PNG_SETJMP_SUPPORTED - if (setjmp (png_jmpbuf (png))) { + if (setjmp_wrapper(png)) { png_destroy_write_struct(&png, &info); free(rows); return status; @@ -133,10 +159,16 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ #endif png_set_write_fn(png, closure, write_func, canvas_png_flush); - png_set_compression_level(png, ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->compression_level); - png_set_filter(png, 0, ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->filter); + png_set_compression_level(png, closure->closure->compressionLevel); + png_set_filter(png, 0, closure->closure->filters); + if (closure->closure->resolution != 0) { + uint32_t res = static_cast(round(static_cast(closure->closure->resolution) * 39.3701)); + png_set_pHYs(png, info, res, res, PNG_RESOLUTION_METER); + } + + cairo_format_t format = cairo_image_surface_get_format(surface); - switch (cairo_image_surface_get_format(surface)) { + switch (format) { case CAIRO_FORMAT_ARGB32: bpc = 8; png_color_type = PNG_COLOR_TYPE_RGB_ALPHA; @@ -162,8 +194,11 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ png_set_packswap(png); #endif break; - case CAIRO_FORMAT_INVALID: case CAIRO_FORMAT_RGB16_565: + bpc = 8; // 565 gets upconverted to 888 + png_color_type = PNG_COLOR_TYPE_RGB; + break; + case CAIRO_FORMAT_INVALID: default: status = CAIRO_STATUS_INVALID_FORMAT; png_destroy_write_struct(&png, &info); @@ -171,11 +206,40 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ return status; } + if ((format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) && + closure->closure->palette != NULL) { + png_color_type = PNG_COLOR_TYPE_PALETTE; + } + png_set_IHDR(png, info, width, height, bpc, png_color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); - white.gray = (1 << bpc) - 1; - white.red = white.blue = white.green = white.gray; - png_set_bKGD(png, info, &white); + if (png_color_type == PNG_COLOR_TYPE_PALETTE) { + size_t nColors = closure->closure->nPaletteColors; + uint8_t* colors = closure->closure->palette; + uint8_t backgroundIndex = closure->closure->backgroundIndex; + png_colorp pngPalette = (png_colorp)png_malloc(png, nColors * sizeof(png_colorp)); + png_bytep transparency = (png_bytep)png_malloc(png, nColors * sizeof(png_bytep)); + for (i = 0; i < nColors; i++) { + pngPalette[i].red = colors[4 * i]; + pngPalette[i].green = colors[4 * i + 1]; + pngPalette[i].blue = colors[4 * i + 2]; + transparency[i] = colors[4 * i + 3]; + } + png_set_PLTE(png, info, pngPalette, nColors); + png_set_tRNS(png, info, transparency, nColors, NULL); + png_set_packing(png); // pack pixels + // have libpng free palette and trans: + png_data_freer(png, info, PNG_DESTROY_WILL_FREE_DATA, PNG_FREE_PLTE | PNG_FREE_TRNS); + png_color_16 bkg; + bkg.index = backgroundIndex; + png_set_bKGD(png, info, &bkg); + } + + if (png_color_type != PNG_COLOR_TYPE_PALETTE) { + white.gray = (1 << bpc) - 1; + white.red = white.blue = white.green = white.gray; + png_set_bKGD(png, info, &white); + } /* We have to call png_write_info() before setting up the write * transformation, since it stores data internally in 'png' @@ -184,6 +248,8 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ png_write_info(png, info); if (png_color_type == PNG_COLOR_TYPE_RGB_ALPHA) { png_set_write_user_transform_fn(png, canvas_unpremultiply_data); + } else if (format == CAIRO_FORMAT_RGB16_565) { + png_set_write_user_transform_fn(png, canvas_convert_565_to_888); } else if (png_color_type == PNG_COLOR_TYPE_RGB) { png_set_write_user_transform_fn(png, canvas_convert_data_to_bytes); png_set_filler(png, 0, PNG_FILLER_AFTER); @@ -212,7 +278,7 @@ static void canvas_stream_write_func(png_structp png, png_bytep data, png_size_t } } -static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, void *closure) { +static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo_write_func_t write_func, PngClosure* closure) { struct canvas_png_write_closure_t png_closure; if (cairo_surface_status(surface)) { @@ -224,4 +290,3 @@ static cairo_status_t canvas_write_to_png_stream(cairo_surface_t *surface, cairo return canvas_write_png(surface, canvas_stream_write_func, &png_closure); } -#endif diff --git a/src/Point.h b/src/Point.h index 5baef1049..a61f8b1ba 100644 --- a/src/Point.h +++ b/src/Point.h @@ -1,19 +1,11 @@ - - -// -// Point.h -// // Copyright (c) 2010 LearnBoost -// +#pragma once -#ifndef __NODE_POINT_H__ -#define __NODE_POINT_H__ - -template +template class Point { public: T x, y; - Point(T x, T y): x(x), y(y) {} + Point(T x=0, T y=0): x(x), y(y) {} + Point(const Point&) = default; + Point& operator=(const Point&) = default; }; - -#endif /* __NODE_POINT_H__ */ diff --git a/src/Util.h b/src/Util.h new file mode 100644 index 000000000..0e6d1d89c --- /dev/null +++ b/src/Util.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +inline bool streq_casein(std::string& str1, std::string& str2) { + return str1.size() == str2.size() && std::equal(str1.begin(), str1.end(), str2.begin(), [](char& c1, char& c2) { + return c1 == c2 || std::toupper(c1) == std::toupper(c2); + }); +} diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc new file mode 100644 index 000000000..4607fb646 --- /dev/null +++ b/src/backend/Backend.cc @@ -0,0 +1,73 @@ +#include "Backend.h" +#include +#include + +Backend::Backend(std::string name, Napi::CallbackInfo& info) : name(name), env(info.Env()) { + int width = 0; + int height = 0; + if (info[0].IsNumber()) width = info[0].As().Int32Value(); + if (info[1].IsNumber()) height = info[1].As().Int32Value(); + this->width = width; + this->height = height; +} + +void Backend::setCanvas(Canvas* _canvas) +{ + this->canvas = _canvas; +} + + + +std::string Backend::getName() +{ + return name; +} + +int Backend::getWidth() +{ + return this->width; +} +void Backend::setWidth(int width_) +{ + this->destroySurface(); + this->width = width_; +} + +int Backend::getHeight() +{ + return this->height; +} +void Backend::setHeight(int height_) +{ + this->destroySurface(); + this->height = height_; +} + +bool Backend::isSurfaceValid() { + bool isValid = true; + + cairo_status_t status = cairo_surface_status(ensureSurface()); + + if (status != CAIRO_STATUS_SUCCESS) { + error = cairo_status_to_string(status); + isValid = false; + } + + return isValid; +} + + +BackendOperationNotAvailable::BackendOperationNotAvailable(Backend* backend, + std::string operation_name) + : operation_name(operation_name) +{ + msg = "operation " + operation_name + + " not supported by backend " + backend->getName(); +}; + +BackendOperationNotAvailable::~BackendOperationNotAvailable() throw() {}; + +const char* BackendOperationNotAvailable::what() const throw() +{ + return msg.c_str(); +}; diff --git a/src/backend/Backend.h b/src/backend/Backend.h new file mode 100644 index 000000000..d51eb7601 --- /dev/null +++ b/src/backend/Backend.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include "../dll_visibility.h" +#include +#include +#include + +class Canvas; + +class Backend +{ + private: + const std::string name; + const char* error = NULL; + + protected: + int width; + int height; + Canvas* canvas = nullptr; + + Backend(std::string name, Napi::CallbackInfo& info); + + public: + Napi::Env env; + + void setCanvas(Canvas* canvas); + + virtual cairo_surface_t* ensureSurface() = 0; + virtual void destroySurface() = 0; + + DLL_PUBLIC std::string getName(); + + DLL_PUBLIC int getWidth(); + virtual void setWidth(int width); + + DLL_PUBLIC int getHeight(); + virtual void setHeight(int height); + + // Overridden by ImageBackend. SVG and PDF thus always return INVALID. + virtual cairo_format_t getFormat() { + return CAIRO_FORMAT_INVALID; + } + + bool isSurfaceValid(); + inline const char* getError(){ return error; } +}; + + +class BackendOperationNotAvailable: public std::exception +{ + private: + std::string operation_name; + std::string msg; + + public: + BackendOperationNotAvailable(Backend* backend, std::string operation_name); + ~BackendOperationNotAvailable() throw(); + + const char* what() const throw(); +}; diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc new file mode 100644 index 000000000..1fede0736 --- /dev/null +++ b/src/backend/ImageBackend.cc @@ -0,0 +1,67 @@ +#include "ImageBackend.h" +#include "../InstanceData.h" +#include +#include + +ImageBackend::ImageBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("image", info) {} + +ImageBackend::~ImageBackend() { + destroySurface(); +} + +// This returns an approximate value only, suitable for +// Napi::MemoryManagement:: AdjustExternalMemory. +// The formats that don't map to intrinsic types (RGB30, A1) round up. +int32_t ImageBackend::approxBytesPerPixel() { + switch (format) { + case CAIRO_FORMAT_ARGB32: + case CAIRO_FORMAT_RGB24: + return 4; +#ifdef CAIRO_FORMAT_RGB30 + case CAIRO_FORMAT_RGB30: + return 3; +#endif + case CAIRO_FORMAT_RGB16_565: + return 2; + case CAIRO_FORMAT_A8: + case CAIRO_FORMAT_A1: + return 1; + default: + return 0; + } +} + +cairo_surface_t* ImageBackend::ensureSurface() { + if (!surface) { + surface = cairo_image_surface_create(format, width, height); + assert(surface); + Napi::MemoryManagement::AdjustExternalMemory(env, approxBytesPerPixel() * width * height); + } + return surface; +} + +void ImageBackend::destroySurface() { + if (surface) { + cairo_surface_destroy(surface); + surface = nullptr; + Napi::MemoryManagement::AdjustExternalMemory(env, -approxBytesPerPixel() * width * height); + } +} + +cairo_format_t ImageBackend::getFormat() { + return format; +} + +void ImageBackend::setFormat(cairo_format_t _format) { + this->destroySurface(); + this->format = _format; +} + +Napi::FunctionReference ImageBackend::constructor; + +void ImageBackend::Initialize(Napi::Object target) { + Napi::Env env = target.Env(); + Napi::Function ctor = DefineClass(env, "ImageBackend", {}); + InstanceData* data = env.GetInstanceData(); + data->ImageBackendCtor = Napi::Persistent(ctor); +} diff --git a/src/backend/ImageBackend.h b/src/backend/ImageBackend.h new file mode 100644 index 000000000..14946c7b9 --- /dev/null +++ b/src/backend/ImageBackend.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Backend.h" +#include + +class ImageBackend : public Napi::ObjectWrap, public Backend +{ + private: + cairo_surface_t* ensureSurface(); + void destroySurface(); + cairo_format_t format = DEFAULT_FORMAT; + cairo_surface_t* surface = nullptr; + + public: + ImageBackend(Napi::CallbackInfo& info); + ~ImageBackend(); + + cairo_format_t getFormat(); + void setFormat(cairo_format_t format); + + int32_t approxBytesPerPixel(); + + static Napi::FunctionReference constructor; + static void Initialize(Napi::Object target); + const static cairo_format_t DEFAULT_FORMAT = CAIRO_FORMAT_ARGB32; +}; diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc new file mode 100644 index 000000000..4eb46168c --- /dev/null +++ b/src/backend/PdfBackend.cc @@ -0,0 +1,40 @@ +#include "PdfBackend.h" + +#include +#include +#include "../InstanceData.h" +#include "../Canvas.h" +#include "../closure.h" + +PdfBackend::PdfBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("pdf", info) {} + +PdfBackend::~PdfBackend() { + destroySurface(); +} + +cairo_surface_t* PdfBackend::ensureSurface() { + if (!surface) { + _closure = new PdfSvgClosure(canvas); + surface = cairo_pdf_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); + } + return surface; +} + +void PdfBackend::destroySurface() { + if (surface) { + cairo_surface_finish(surface); + cairo_surface_destroy(surface); + surface = nullptr; + assert(_closure); + delete _closure; + _closure = nullptr; + } +} + +void +PdfBackend::Initialize(Napi::Object target) { + Napi::Env env = target.Env(); + InstanceData* data = env.GetInstanceData(); + Napi::Function ctor = DefineClass(env, "PdfBackend", {}); + data->PdfBackendCtor = Napi::Persistent(ctor); +} diff --git a/src/backend/PdfBackend.h b/src/backend/PdfBackend.h new file mode 100644 index 000000000..6ae8415c8 --- /dev/null +++ b/src/backend/PdfBackend.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Backend.h" +#include "../closure.h" +#include + +class PdfBackend : public Napi::ObjectWrap, public Backend +{ + private: + cairo_surface_t* ensureSurface(); + void destroySurface(); + cairo_surface_t* surface = nullptr; + + public: + PdfSvgClosure* _closure = NULL; + inline PdfSvgClosure* closure() { return _closure; } + + PdfBackend(Napi::CallbackInfo& info); + ~PdfBackend(); + + static Napi::FunctionReference constructor; + static void Initialize(Napi::Object target); + static Napi::Value New(const Napi::CallbackInfo& info); +}; diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc new file mode 100644 index 000000000..475c07dea --- /dev/null +++ b/src/backend/SvgBackend.cc @@ -0,0 +1,44 @@ +#include "SvgBackend.h" + +#include +#include +#include "../Canvas.h" +#include "../closure.h" +#include "../InstanceData.h" +#include + +using namespace Napi; + +SvgBackend::SvgBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("svg", info) {} + +SvgBackend::~SvgBackend() { + destroySurface(); +} + +cairo_surface_t* SvgBackend::ensureSurface() { + if (!surface) { + assert(!_closure); + _closure = new PdfSvgClosure(canvas); + surface = cairo_svg_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); + } + return surface; +} + +void SvgBackend::destroySurface() { + if (surface) { + cairo_surface_finish(surface); + cairo_surface_destroy(surface); + surface = nullptr; + assert(_closure); + delete _closure; + _closure = nullptr; + } + } + +void +SvgBackend::Initialize(Napi::Object target) { + Napi::Env env = target.Env(); + Napi::Function ctor = DefineClass(env, "SvgBackend", {}); + InstanceData* data = env.GetInstanceData(); + data->SvgBackendCtor = Napi::Persistent(ctor); +} diff --git a/src/backend/SvgBackend.h b/src/backend/SvgBackend.h new file mode 100644 index 000000000..f44842690 --- /dev/null +++ b/src/backend/SvgBackend.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Backend.h" +#include "../closure.h" +#include + +class SvgBackend : public Napi::ObjectWrap, public Backend +{ + private: + cairo_surface_t* ensureSurface(); + void destroySurface(); + cairo_surface_t* surface = nullptr; + + public: + PdfSvgClosure* _closure = NULL; + inline PdfSvgClosure* closure() { return _closure; } + + SvgBackend(Napi::CallbackInfo& info); + ~SvgBackend(); + + static void Initialize(Napi::Object target); +}; diff --git a/src/bmp/BMPParser.cc b/src/bmp/BMPParser.cc new file mode 100644 index 000000000..9058be8af --- /dev/null +++ b/src/bmp/BMPParser.cc @@ -0,0 +1,459 @@ +#include "BMPParser.h" + +#include +#include + +using namespace std; +using namespace BMPParser; + +#define MAX_IMG_SIZE 10000 + +#define E(cond, msg) if(cond) return setErr(msg) +#define EU(cond, msg) if(cond) return setErrUnsupported(msg) +#define EX(cond, msg) if(cond) return setErrUnknown(msg) + +#define I1() get() +#define U1() get() +#define I2() get() +#define U2() get() +#define I4() get() +#define U4() get() + +#define I1UC() get() +#define U1UC() get() +#define I2UC() get() +#define U2UC() get() +#define I4UC() get() +#define U4UC() get() + +#define CHECK_OVERRUN(ptr, size, type) \ + if((ptr) + (size) - data > len){ \ + setErr("unexpected end of file"); \ + return type(); \ + } + +Parser::~Parser(){ + data = nullptr; + ptr = nullptr; + + if(imgd){ + delete[] imgd; + imgd = nullptr; + } +} + +void Parser::parse(uint8_t *buf, int bufSize, uint8_t *format){ + assert(status == Status::EMPTY); + + data = ptr = buf; + len = bufSize; + + // Start parsing file header + setOp("file header"); + + // File header signature + string fhSig = getStr(2); + string temp = "file header signature"; + EU(fhSig == "BA", temp + " \"BA\""); + EU(fhSig == "CI", temp + " \"CI\""); + EU(fhSig == "CP", temp + " \"CP\""); + EU(fhSig == "IC", temp + " \"IC\""); + EU(fhSig == "PT", temp + " \"PT\""); + EX(fhSig != "BM", temp); // BM + + // Length of the file should not be larger than `len` + E(U4() > static_cast(len), "inconsistent file size"); + + // Skip unused values + skip(4); + + // Offset where the pixel array (bitmap data) can be found + auto imgdOffset = U4(); + + // Start parsing DIB header + setOp("DIB header"); + + // Prepare some variables in case they are needed + uint32_t compr = 0; + uint32_t redShift = 0, greenShift = 0, blueShift = 0, alphaShift = 0; + uint32_t redMask = 0, greenMask = 0, blueMask = 0, alphaMask = 0; + double redMultp = 0, greenMultp = 0, blueMultp = 0, alphaMultp = 0; + + /** + * Type of the DIB (device-independent bitmap) header + * is determined by its size. Most BMP files use BITMAPINFOHEADER. + */ + auto dibSize = U4(); + temp = "DIB header"; + EU(dibSize == 64, temp + " \"OS22XBITMAPHEADER\""); + EU(dibSize == 16, temp + " \"OS22XBITMAPHEADER\""); + + uint32_t infoHeader = dibSize == 40 ? 1 : + dibSize == 52 ? 2 : + dibSize == 56 ? 3 : + dibSize == 108 ? 4 : + dibSize == 124 ? 5 : 0; + + // BITMAPCOREHEADER, BITMAP*INFOHEADER, BITMAP*HEADER + auto isDibValid = dibSize == 12 || infoHeader; + EX(!isDibValid, temp); + + // Image width + w = dibSize == 12 ? U2() : I4(); + E(!w, "image width is 0"); + E(w < 0, "negative image width"); + E(w > MAX_IMG_SIZE, "too large image width"); + + // Image height (specification allows negative values) + h = dibSize == 12 ? U2() : I4(); + E(!h, "image height is 0"); + E(h > MAX_IMG_SIZE, "too large image height"); + + bool isHeightNegative = h < 0; + if(isHeightNegative) h = -h; + + // Number of color planes (must be 1) + E(U2() != 1, "number of color planes must be 1"); + + // Bits per pixel (color depth) + auto bpp = U2(); + auto isBppValid = bpp == 1 || bpp == 4 || bpp == 8 || bpp == 16 || bpp == 24 || bpp == 32; + EU(!isBppValid, "color depth"); + + // Calculate image data size and padding + uint32_t expectedImgdSize = (((w * bpp + 31) >> 5) << 2) * h; + uint32_t rowPadding = (-w * bpp & 31) >> 3; + uint32_t imgdSize = 0; + + // Color palette data + uint8_t* paletteStart = nullptr; + uint32_t palColNum = 0; + + if(infoHeader){ + // Compression type + compr = U4(); + temp = "compression type"; + EU(compr == 1, temp + " \"BI_RLE8\""); + EU(compr == 2, temp + " \"BI_RLE4\""); + EU(compr == 4, temp + " \"BI_JPEG\""); + EU(compr == 5, temp + " \"BI_PNG\""); + EU(compr == 6, temp + " \"BI_ALPHABITFIELDS\""); + EU(compr == 11, temp + " \"BI_CMYK\""); + EU(compr == 12, temp + " \"BI_CMYKRLE8\""); + EU(compr == 13, temp + " \"BI_CMYKRLE4\""); + + // BI_RGB and BI_BITFIELDS + auto isComprValid = compr == 0 || compr == 3; + EX(!isComprValid, temp); + + // Ensure that BI_BITFIELDS appears only with 16-bit or 32-bit color + E(compr == 3 && !(bpp == 16 || bpp == 32), "compression BI_BITFIELDS can be used only with 16-bit and 32-bit color depth"); + + // Size of the image data + imgdSize = U4(); + + // Horizontal and vertical resolution (ignored) + skip(8); + + // Number of colors in the palette or 0 if no palette is present + palColNum = U4(); + EU(palColNum && bpp > 8, "color palette and bit depth combination"); + if(palColNum) paletteStart = data + dibSize + 14; + + // Number of important colors used or 0 if all colors are important (generally ignored) + skip(4); + + if(infoHeader >= 2){ + // If BI_BITFIELDS are used, calculate masks, otherwise ignore them + if(compr == 3){ + calcMaskShift(redShift, redMask, redMultp); + calcMaskShift(greenShift, greenMask, greenMultp); + calcMaskShift(blueShift, blueMask, blueMultp); + if(infoHeader >= 3) calcMaskShift(alphaShift, alphaMask, alphaMultp); + if(status == Status::ERROR) return; + }else{ + skip(16); + } + + // Ensure that the color space is LCS_WINDOWS_COLOR_SPACE or sRGB + if(infoHeader >= 4 && !palColNum){ + string colSpace = getStr(4, 1); + EU(colSpace != "Win " && colSpace != "sRGB", "color space \"" + colSpace + "\""); + } + } + } + + // Skip to the image data (there may be other chunks between, but they are optional) + E(ptr - data > imgdOffset, "image data overlaps with another structure"); + ptr = data + imgdOffset; + + // Start parsing image data + setOp("image data"); + + if(!imgdSize){ + // Value 0 is allowed only for BI_RGB compression type + E(compr != 0, "missing image data size"); + imgdSize = expectedImgdSize; + }else{ + E(imgdSize < expectedImgdSize, "invalid image data size"); + } + + // Ensure that all image data is present + E(ptr - data + imgdSize > len, "not enough image data"); + + // Direction of reading rows + int yStart = h - 1; + int yEnd = -1; + int dy = isHeightNegative ? 1 : -1; + + // In case of negative height, read rows backward + if(isHeightNegative){ + yStart = 0; + yEnd = h; + } + + // Allocate output image data array + int buffLen = w * h << 2; + imgd = new (nothrow) uint8_t[buffLen]; + E(!imgd, "unable to allocate memory"); + + // Prepare color values + uint8_t color[4] = {0}; + uint8_t &red = color[0]; + uint8_t &green = color[1]; + uint8_t &blue = color[2]; + uint8_t &alpha = color[3]; + + // Check if pre-multiplied alpha is used + bool premul = format ? format[4] : 0; + + // Main loop + for(int y = yStart; y != yEnd; y += dy){ + // Use in-byte offset for bpp < 8 + uint8_t colOffset = 0; + uint8_t cval = 0; + uint32_t val = 0; + + for(int x = 0; x != w; x++){ + // Index in the output image data + int i = (x + y * w) << 2; + + switch(compr){ + case 0: // BI_RGB + switch(bpp){ + case 1: + if(colOffset) ptr--; + cval = (U1UC() >> (7 - colOffset)) & 1; + + if(palColNum){ + uint8_t* entry = paletteStart + (cval << 2); + blue = get(entry); + green = get(entry + 1); + red = get(entry + 2); + if(status == Status::ERROR) return; + }else{ + red = green = blue = cval ? 255 : 0; + } + + alpha = 255; + colOffset = (colOffset + 1) & 7; + break; + + case 4: + if(colOffset) ptr--; + cval = (U1UC() >> (4 - colOffset)) & 15; + + if(palColNum){ + uint8_t* entry = paletteStart + (cval << 2); + blue = get(entry); + green = get(entry + 1); + red = get(entry + 2); + if(status == Status::ERROR) return; + }else{ + red = green = blue = cval << 4; + } + + alpha = 255; + colOffset = (colOffset + 4) & 7; + break; + + case 8: + cval = U1UC(); + + if(palColNum){ + uint8_t* entry = paletteStart + (cval << 2); + blue = get(entry); + green = get(entry + 1); + red = get(entry + 2); + if(status == Status::ERROR) return; + }else{ + red = green = blue = cval; + } + + alpha = 255; + break; + + case 16: + // RGB555 + val = U1UC(); + val |= U1UC() << 8; + red = (val >> 10) << 3; + green = (val >> 5) << 3; + blue = val << 3; + alpha = 255; + break; + + case 24: + blue = U1UC(); + green = U1UC(); + red = U1UC(); + alpha = 255; + break; + + case 32: + blue = U1UC(); + green = U1UC(); + red = U1UC(); + + if(infoHeader >= 3){ + alpha = U1UC(); + }else{ + alpha = 255; + skip(1); + } + break; + } + break; + + case 3: // BI_BITFIELDS + uint32_t col = bpp == 16 ? U2UC() : U4UC(); + red = ((col >> redShift) & redMask) * redMultp + .5; + green = ((col >> greenShift) & greenMask) * greenMultp + .5; + blue = ((col >> blueShift) & blueMask) * blueMultp + .5; + alpha = alphaMask ? ((col >> alphaShift) & alphaMask) * alphaMultp + .5 : 255; + break; + } + + /** + * Pixel format: + * red, + * green, + * blue, + * alpha, + * is alpha pre-multiplied + * Default is [0, 1, 2, 3, 0] + */ + + if(premul && alpha != 255){ + double a = alpha / 255.; + red = static_cast(red * a + .5); + green = static_cast(green * a + .5); + blue = static_cast(blue * a + .5); + } + + if(format){ + imgd[i] = color[format[0]]; + imgd[i + 1] = color[format[1]]; + imgd[i + 2] = color[format[2]]; + imgd[i + 3] = color[format[3]]; + }else{ + imgd[i] = red; + imgd[i + 1] = green; + imgd[i + 2] = blue; + imgd[i + 3] = alpha; + } + } + + // Skip unused bytes in the current row + skip(rowPadding); + } + + if(status == Status::ERROR) return; + status = Status::OK; +}; + +void Parser::clearImgd(){ imgd = nullptr; } +int32_t Parser::getWidth() const{ return w; } +int32_t Parser::getHeight() const{ return h; } +uint8_t *Parser::getImgd() const{ return imgd; } +Status Parser::getStatus() const{ return status; } + +string Parser::getErrMsg() const{ + return "Error while processing " + getOp() + " - " + err; +} + +template inline T Parser::get(){ + if(check) + CHECK_OVERRUN(ptr, sizeof(T), T); + T val; + std::memcpy(&val, ptr, sizeof(T)); + ptr += sizeof(T); + return val; +} + +template inline T Parser::get(uint8_t* pointer){ + if(check) + CHECK_OVERRUN(pointer, sizeof(T), T); + T val = *(T*)pointer; + return val; +} + +string Parser::getStr(int size, bool reverse){ + CHECK_OVERRUN(ptr, size, string); + string val = ""; + + while(size--){ + if(reverse) val = string(1, static_cast(*ptr++)) + val; + else val += static_cast(*ptr++); + } + + return val; +} + +inline void Parser::skip(int size){ + CHECK_OVERRUN(ptr, size, void); + ptr += size; +} + +void Parser::calcMaskShift(uint32_t& shift, uint32_t& mask, double& multp){ + mask = U4(); + shift = 0; + + if(mask == 0) return; + + while(~mask & 1){ + mask >>= 1; + shift++; + } + + E(mask & (mask + 1), "invalid color mask"); + + multp = 255. / mask; +} + +void Parser::setOp(string val){ + if(status != Status::EMPTY) return; + op = val; +} + +string Parser::getOp() const{ + return op; +} + +void Parser::setErrUnsupported(string msg){ + setErr("unsupported " + msg); +} + +void Parser::setErrUnknown(string msg){ + setErr("unknown " + msg); +} + +void Parser::setErr(string msg){ + if(status != Status::EMPTY) return; + err = msg; + status = Status::ERROR; +} + +string Parser::getErr() const{ + return err; +} diff --git a/src/bmp/BMPParser.h b/src/bmp/BMPParser.h new file mode 100644 index 000000000..d3a42542e --- /dev/null +++ b/src/bmp/BMPParser.h @@ -0,0 +1,60 @@ +#pragma once + +#ifdef ERROR +#define ERROR_ ERROR +#undef ERROR +#endif + +#include // node < 7 uses libstdc++ on macOS which lacks complete c++11 +#include + +namespace BMPParser{ + enum Status{ + EMPTY, + OK, + ERROR, + }; + + class Parser{ + public: + Parser()=default; + ~Parser(); + void parse(uint8_t *buf, int bufSize, uint8_t *format=nullptr); + void clearImgd(); + int32_t getWidth() const; + int32_t getHeight() const; + uint8_t *getImgd() const; + Status getStatus() const; + std::string getErrMsg() const; + + private: + Status status = Status::EMPTY; + uint8_t *data = nullptr; + uint8_t *ptr = nullptr; + int len = 0; + int32_t w = 0; + int32_t h = 0; + uint8_t *imgd = nullptr; + std::string err = ""; + std::string op = ""; + + template inline T get(); + template inline T get(uint8_t* pointer); + std::string getStr(int len, bool reverse=false); + inline void skip(int len); + void calcMaskShift(uint32_t& shift, uint32_t& mask, double& multp); + + void setOp(std::string val); + std::string getOp() const; + + void setErrUnsupported(std::string msg); + void setErrUnknown(std::string msg); + void setErr(std::string msg); + std::string getErr() const; + }; +} + +#ifdef ERROR_ +#define ERROR ERROR_ +#undef ERROR_ +#endif diff --git a/src/bmp/LICENSE.md b/src/bmp/LICENSE.md new file mode 100644 index 000000000..ea89a5dfe --- /dev/null +++ b/src/bmp/LICENSE.md @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/src/closure.cc b/src/closure.cc new file mode 100644 index 000000000..3290db2e5 --- /dev/null +++ b/src/closure.cc @@ -0,0 +1,52 @@ +#include "closure.h" +#include "Canvas.h" + +#ifdef HAVE_JPEG +void JpegClosure::init_destination(j_compress_ptr cinfo) { + JpegClosure* closure = (JpegClosure*)cinfo->client_data; + closure->vec.resize(PAGE_SIZE); + closure->jpeg_dest_mgr->next_output_byte = &closure->vec[0]; + closure->jpeg_dest_mgr->free_in_buffer = closure->vec.size(); +} + +boolean JpegClosure::empty_output_buffer(j_compress_ptr cinfo) { + JpegClosure* closure = (JpegClosure*)cinfo->client_data; + size_t currentSize = closure->vec.size(); + closure->vec.resize(currentSize * 1.5); + closure->jpeg_dest_mgr->next_output_byte = &closure->vec[currentSize]; + closure->jpeg_dest_mgr->free_in_buffer = closure->vec.size() - currentSize; + return true; +} + +void JpegClosure::term_destination(j_compress_ptr cinfo) { + JpegClosure* closure = (JpegClosure*)cinfo->client_data; + size_t finalSize = closure->vec.size() - closure->jpeg_dest_mgr->free_in_buffer; + closure->vec.resize(finalSize); +} +#endif + +void +EncodingWorker::Init(void (*work_fn)(Closure*), Closure* closure) { + this->work_fn = work_fn; + this->closure = closure; +} + +void +EncodingWorker::Execute() { + this->work_fn(this->closure); +} + +void +EncodingWorker::OnWorkComplete(Napi::Env env, napi_status status) { + Napi::HandleScope scope(env); + + if (closure->status) { + closure->cb.Call({ closure->canvas->CairoError(closure->status).Value() }); + } else { + Napi::Object buf = Napi::Buffer::Copy(env, &closure->vec[0], closure->vec.size()); + closure->cb.Call({ env.Null(), buf }); + } + + closure->canvas->Unref(); + delete closure; +} diff --git a/src/closure.h b/src/closure.h index 1fb6bced2..ce5ec489c 100644 --- a/src/closure.h +++ b/src/closure.h @@ -1,65 +1,93 @@ - -// -// closure.h -// // Copyright (c) 2010 LearnBoost -// -#ifndef __NODE_CLOSURE_H__ -#define __NODE_CLOSURE_H__ +#pragma once + +#include "Canvas.h" -#ifdef __unix__ - #include +#ifdef HAVE_JPEG +#include #endif +#include +#include +#include // node < 7 uses libstdc++ on macOS which lacks complete c++11 +#include + #ifndef PAGE_SIZE #define PAGE_SIZE 4096 #endif -#include - /* - * PNG stream closure. + * Image encoding closures. */ -typedef struct { - Nan::Callback *pfn; - Local fn; - unsigned len; - unsigned max_len; - uint8_t *data; - Canvas *canvas; - cairo_status_t status; - uint32_t compression_level; - uint32_t filter; -} closure_t; +struct Closure { + std::vector vec; + Napi::FunctionReference cb; + Canvas* canvas = nullptr; + cairo_status_t status = CAIRO_STATUS_SUCCESS; -/* - * Initialize the given closure. - */ + static cairo_status_t writeVec(void *c, const uint8_t *odata, unsigned len) { + Closure* closure = static_cast(c); + try { + closure->vec.insert(closure->vec.end(), odata, odata + len); + } catch (const std::bad_alloc &) { + return CAIRO_STATUS_NO_MEMORY; + } + return CAIRO_STATUS_SUCCESS; + } -cairo_status_t -closure_init(closure_t *closure, Canvas *canvas, unsigned int compression_level, unsigned int filter) { - closure->len = 0; - closure->canvas = canvas; - closure->data = (uint8_t *) malloc(closure->max_len = PAGE_SIZE); - if (!closure->data) return CAIRO_STATUS_NO_MEMORY; - closure->compression_level = compression_level; - closure->filter = filter; - return CAIRO_STATUS_SUCCESS; -} + Closure(Canvas* canvas) : canvas(canvas) {}; +}; -/* - * Free the given closure's data, - * and hint V8 at the memory dealloc. - */ +struct PdfSvgClosure : Closure { + PdfSvgClosure(Canvas* canvas) : Closure(canvas) {}; +}; + +struct PngClosure : Closure { + uint32_t compressionLevel = 6; + uint32_t filters = PNG_ALL_FILTERS; + uint32_t resolution = 0; // 0 = unspecified + // Indexed PNGs: + uint32_t nPaletteColors = 0; + uint8_t* palette = nullptr; + uint8_t backgroundIndex = 0; + + PngClosure(Canvas* canvas) : Closure(canvas) {}; +}; + +#ifdef HAVE_JPEG +struct JpegClosure : Closure { + uint32_t quality = 75; + uint32_t chromaSubsampling = 2; + bool progressive = false; + jpeg_destination_mgr* jpeg_dest_mgr = nullptr; -void -closure_destroy(closure_t *closure) { - if (closure->len) { - free(closure->data); - Nan::AdjustExternalMemory(-((intptr_t) closure->max_len)); + static void init_destination(j_compress_ptr cinfo); + static boolean empty_output_buffer(j_compress_ptr cinfo); + static void term_destination(j_compress_ptr cinfo); + + JpegClosure(Canvas* canvas) : Closure(canvas) { + jpeg_dest_mgr = new jpeg_destination_mgr; + jpeg_dest_mgr->init_destination = init_destination; + jpeg_dest_mgr->empty_output_buffer = empty_output_buffer; + jpeg_dest_mgr->term_destination = term_destination; + }; + + ~JpegClosure() { + delete jpeg_dest_mgr; } -} +}; +#endif + +class EncodingWorker : public Napi::AsyncWorker { + public: + EncodingWorker(Napi::Env env): Napi::AsyncWorker(env) {}; + void Init(void (*work_fn)(Closure*), Closure* closure); + void Execute() override; + void OnWorkComplete(Napi::Env env, napi_status status) override; -#endif /* __NODE_CLOSURE_H__ */ + private: + void (*work_fn)(Closure*) = nullptr; + Closure* closure = nullptr; +}; diff --git a/src/color.cc b/src/color.cc index bede494e9..f82629460 100644 --- a/src/color.cc +++ b/src/color.cc @@ -1,14 +1,14 @@ - -// -// color.cc -// // Copyright (c) 2010 LearnBoost -// #include "color.h" -#include + +#include #include +#include +#include #include +#include +#include // Compatibility with Visual Studio versions prior to VS2015 #if defined(_MSC_VER) && _MSC_VER < 1900 @@ -60,7 +60,7 @@ parse_css_number(const char** pStr, parsed_t *pParsed) { const char*& str = *pStr; const char* startStr = str; if (!str || !*str) - return false; + return false; parsed_t integerPart = 0; parsed_t fractionPart = 0; int divisorForFraction = 1; @@ -68,7 +68,7 @@ parse_css_number(const char** pStr, parsed_t *pParsed) { int exponent = 0; int digits = 0; bool inFraction = false; - + if (*str == '-') { ++str; sign = -1; @@ -83,7 +83,7 @@ parse_css_number(const char** pStr, parsed_t *pParsed) { } else { ++digits; - + if (inFraction) { fractionPart = fractionPart*10 + (*str - '0'); divisorForFraction *= 10; @@ -137,7 +137,7 @@ clip(T value, T minValue, T maxValue) { /* * Wrap value to the range [0, limit] */ - + template static T wrap_float(T value, T limit) { @@ -159,8 +159,9 @@ wrap_float(T value, T limit) { static bool parse_rgb_channel(const char** pStr, uint8_t *pChannel) { - int channel; - if (parse_integer(pStr, &channel)) { + float f_channel; + if (parse_css_number(pStr, &f_channel)) { + int channel = (int) ceil(f_channel); *pChannel = clip(channel, 0, 255); return true; } @@ -186,7 +187,7 @@ parse_degrees(const char** pStr, float *pDegrees) { */ static bool -parse_clipped_percentage(const char** pStr, float *pFraction) { +parse_clipped_percentage(const char** pStr, float *pFraction) { float percentage; bool result = parse_css_number(pStr,&percentage); const char*& str = *pStr; @@ -210,6 +211,9 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { #define WHITESPACE_OR_COMMA \ while (' ' == *str || ',' == *str) ++str; +#define WHITESPACE_OR_COMMA_OR_SLASH \ + while (' ' == *str || ',' == *str || '/' == *str) ++str; + #define CHANNEL(NAME) \ if (!parse_rgb_channel(&str, &NAME)) \ return 0; \ @@ -225,12 +229,25 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { #define LIGHTNESS(NAME) SATURATION(NAME) #define ALPHA(NAME) \ - if (*str >= '1' && *str <= '9') { \ - NAME = 1; \ + if (*str >= '1' && *str <= '9') { \ + NAME = 0; \ + float n = .1f; \ + while(*str >='0' && *str <= '9') { \ + NAME += (*str - '0') * n; \ + str++; \ + } \ + while(*str == ' ')str++; \ + if(*str != '%') { \ + NAME = 1; \ + } \ } else { \ - if ('0' == *str) ++str; \ + if ('0' == *str) { \ + NAME = 0; \ + ++str; \ + } \ if ('.' == *str) { \ ++str; \ + NAME = 0; \ float n = .1f; \ while (*str >= '0' && *str <= '9') { \ NAME += (*str++ - '0') * n; \ @@ -243,11 +260,7 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { /* * Named colors. */ - -static struct named_color { - const char *name; - uint32_t val; -} named_colors[] = { +static const std::map named_colors = { { "transparent", 0xFFFFFF00} , { "aliceblue", 0xF0F8FFFF } , { "antiquewhite", 0xFAEBD7FF } @@ -397,7 +410,6 @@ static struct named_color { , { "whitesmoke", 0xF5F5F5FF } , { "yellow", 0xFFFF00FF } , { "yellowgreen", 0x9ACD32FF } - , { NULL, 0 } }; /* @@ -457,16 +469,16 @@ rgba_create(uint32_t rgba) { void rgba_to_string(rgba_t rgba, char *buf, size_t len) { if (1 == rgba.a) { - snprintf(buf, len, "#%.2x%.2x%.2x" - , (int) (rgba.r * 255) - , (int) (rgba.g * 255) - , (int) (rgba.b * 255)); + snprintf(buf, len, "#%.2x%.2x%.2x", + static_cast(round(rgba.r * 255)), + static_cast(round(rgba.g * 255)), + static_cast(round(rgba.b * 255))); } else { - snprintf(buf, len, "rgba(%d, %d, %d, %.2f)" - , (int) (rgba.r * 255) - , (int) (rgba.g * 255) - , (int) (rgba.b * 255) - , rgba.a); + snprintf(buf, len, "rgba(%d, %d, %d, %.2f)", + static_cast(round(rgba.r * 255)), + static_cast(round(rgba.g * 255)), + static_cast(round(rgba.b * 255)), + rgba.a); } } @@ -550,6 +562,20 @@ rgba_from_rgb(uint8_t r, uint8_t g, uint8_t b) { return rgba_from_rgba(r, g, b, 255); } +/* + * Return rgba from #RRGGBBAA + */ + +static int32_t +rgba_from_hex8_string(const char *str) { + return rgba_from_rgba( + (h(str[0]) << 4) + h(str[1]), + (h(str[2]) << 4) + h(str[3]), + (h(str[4]) << 4) + h(str[5]), + (h(str[6]) << 4) + h(str[7]) + ); +} + /* * Return rgb from "#RRGGBB". */ @@ -563,6 +589,20 @@ rgba_from_hex6_string(const char *str) { ); } +/* +* Return rgba from #RGBA +*/ + +static int32_t +rgba_from_hex4_string(const char *str) { + return rgba_from_rgba( + (h(str[0]) << 4) + h(str[0]), + (h(str[1]) << 4) + h(str[1]), + (h(str[2]) << 4) + h(str[2]), + (h(str[3]) << 4) + h(str[3]) + ); +} + /* * Return rgb from "#RGB" */ @@ -586,13 +626,15 @@ rgba_from_rgb_string(const char *str, short *ok) { str += 4; WHITESPACE; uint8_t r = 0, g = 0, b = 0; + float a=1.f; CHANNEL(r); WHITESPACE_OR_COMMA; CHANNEL(g); WHITESPACE_OR_COMMA; CHANNEL(b); - WHITESPACE; - return *ok = 1, rgba_from_rgb(r, g, b); + WHITESPACE_OR_COMMA_OR_SLASH; + ALPHA(a); + return *ok = 1, rgba_from_rgba(r, g, b, (int) (255 * a)); } return *ok = 0; } @@ -607,13 +649,13 @@ rgba_from_rgba_string(const char *str, short *ok) { str += 5; WHITESPACE; uint8_t r = 0, g = 0, b = 0; - float a = 0; + float a = 1.f; CHANNEL(r); WHITESPACE_OR_COMMA; CHANNEL(g); WHITESPACE_OR_COMMA; CHANNEL(b); - WHITESPACE_OR_COMMA; + WHITESPACE_OR_COMMA_OR_SLASH; ALPHA(a); WHITESPACE; return *ok = 1, rgba_from_rgba(r, g, b, (int) (a * 255)); @@ -671,9 +713,11 @@ rgba_from_hsl_string(const char *str, short *ok) { /* * Return rgb from: - * + * * - "#RGB" + * - "#RGBA" * - "#RRGGBB" + * - "#RRGGBBAA" * */ @@ -681,8 +725,12 @@ static int32_t rgba_from_hex_string(const char *str, short *ok) { size_t len = strlen(str); *ok = 1; - if (6 == len) return rgba_from_hex6_string(str); - if (3 == len) return rgba_from_hex3_string(str); + switch (len) { + case 8: return rgba_from_hex8_string(str); + case 6: return rgba_from_hex6_string(str); + case 4: return rgba_from_hex4_string(str); + case 3: return rgba_from_hex3_string(str); + } return *ok = 0; } @@ -692,20 +740,23 @@ rgba_from_hex_string(const char *str, short *ok) { static int32_t rgba_from_name_string(const char *str, short *ok) { - int i = 0; - struct named_color color; - while ((color = named_colors[i++]).name) { - if (*str == *color.name && 0 == strcmp(str, color.name)) - return *ok = 1, color.val; + WHITESPACE; + std::string lowered(str); + std::transform(lowered.begin(), lowered.end(), lowered.begin(), tolower); + auto color = named_colors.find(lowered); + if (color != named_colors.end()) { + return *ok = 1, color->second; } return *ok = 0; } /* * Return rgb from: - * + * * - #RGB + * - #RGBA * - #RRGGBB + * - #RRGGBBAA * - rgb(r,g,b) * - rgba(r,g,b,a) * - hsl(h,s,l) @@ -716,7 +767,8 @@ rgba_from_name_string(const char *str, short *ok) { int32_t rgba_from_string(const char *str, short *ok) { - if ('#' == str[0]) + WHITESPACE; + if ('#' == str[0]) return rgba_from_hex_string(++str, ok); if (str == strstr(str, "rgba")) return rgba_from_rgba_string(str, ok); diff --git a/src/color.h b/src/color.h index c570c4a45..137c1d6b3 100644 --- a/src/color.h +++ b/src/color.h @@ -1,17 +1,9 @@ - -// -// color.h -// // Copyright (c) 2010 LearnBoost -// -#ifndef __COLOR_PARSER_H__ -#define __COLOR_PARSER_H__ +#pragma once -#include -#include -#include -#include +#include // node < 7 uses libstdc++ on macOS which lacks complete c++11 +#include /* * RGBA struct. @@ -36,5 +28,3 @@ rgba_to_string(rgba_t rgba, char *buf, size_t len); void rgba_inspect(int32_t rgba); - -#endif /* __COLOR_PARSER_H__ */ diff --git a/src/dll_visibility.h b/src/dll_visibility.h new file mode 100644 index 000000000..7a1f98450 --- /dev/null +++ b/src/dll_visibility.h @@ -0,0 +1,20 @@ +#ifndef DLL_PUBLIC + +#if defined _WIN32 + #ifdef __GNUC__ + #define DLL_PUBLIC __attribute__ ((dllexport)) + #else + #define DLL_PUBLIC __declspec(dllexport) + #endif + #define DLL_LOCAL +#else + #if __GNUC__ >= 4 + #define DLL_PUBLIC __attribute__ ((visibility ("default"))) + #define DLL_LOCAL __attribute__ ((visibility ("hidden"))) + #else + #define DLL_PUBLIC + #define DLL_LOCAL + #endif +#endif + +#endif diff --git a/src/init.cc b/src/init.cc old mode 100755 new mode 100644 index dc2fa5963..ad9207846 --- a/src/init.cc +++ b/src/init.cc @@ -1,36 +1,67 @@ - -// -// init.cc -// // Copyright (c) 2010 LearnBoost -// -#include +#include #include -#include + +#include +#if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1, 10, 0) +// CAIRO_FORMAT_RGB16_565: undeprecated in v1.10.0 +// CAIRO_STATUS_INVALID_SIZE: v1.10.0 +// CAIRO_FORMAT_INVALID: v1.10.0 +// Lots of the compositing operators: v1.10.0 +// JPEG MIME tracking: v1.10.0 +// Note: CAIRO_FORMAT_RGB30 is v1.12.0 and still optional +#error("cairo v1.10.0 or later is required") +#endif + +#include "Backends.h" #include "Canvas.h" -#include "Image.h" -#include "ImageData.h" #include "CanvasGradient.h" #include "CanvasPattern.h" #include "CanvasRenderingContext2d.h" +#include "Image.h" +#include "ImageData.h" +#include "InstanceData.h" + #include #include FT_FREETYPE_H +/* + * Save some external modules as private references. + */ + +static void +setDOMMatrix(const Napi::CallbackInfo& info) { + InstanceData* data = info.Env().GetInstanceData(); + data->DOMMatrixCtor = Napi::Persistent(info[0].As()); +} + +static void +setParseFont(const Napi::CallbackInfo& info) { + InstanceData* data = info.Env().GetInstanceData(); + data->parseFont = Napi::Persistent(info[0].As()); +} + // Compatibility with Visual Studio versions prior to VS2015 #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf #endif -NAN_MODULE_INIT(init) { - Canvas::Initialize(target); - Image::Initialize(target); - ImageData::Initialize(target); - Context2d::Initialize(target); - Gradient::Initialize(target); - Pattern::Initialize(target); +Napi::Object init(Napi::Env env, Napi::Object exports) { + env.SetInstanceData(new InstanceData()); + + Backends::Initialize(env, exports); + Canvas::Initialize(env, exports); + Image::Initialize(env, exports); + ImageData::Initialize(env, exports); + Context2d::Initialize(env, exports); + Gradient::Initialize(env, exports); + Pattern::Initialize(env, exports); - target->Set(Nan::New("cairoVersion").ToLocalChecked(), Nan::New(cairo_version_string()).ToLocalChecked()); + exports.Set("setDOMMatrix", Napi::Function::New(env, &setDOMMatrix)); + exports.Set("setParseFont", Napi::Function::New(env, &setParseFont)); + + exports.Set("cairoVersion", Napi::String::New(env, cairo_version_string())); #ifdef HAVE_JPEG #ifndef JPEG_LIB_VERSION_MAJOR @@ -50,27 +81,36 @@ NAN_MODULE_INIT(init) { #endif char jpeg_version[10]; - if (JPEG_LIB_VERSION_MINOR > 0) { + static bool minor_gt_0 = JPEG_LIB_VERSION_MINOR > 0; + if (minor_gt_0) { snprintf(jpeg_version, 10, "%d%c", JPEG_LIB_VERSION_MAJOR, JPEG_LIB_VERSION_MINOR + 'a' - 1); } else { snprintf(jpeg_version, 10, "%d", JPEG_LIB_VERSION_MAJOR); } - target->Set(Nan::New("jpegVersion").ToLocalChecked(), Nan::New(jpeg_version).ToLocalChecked()); + exports.Set("jpegVersion", Napi::String::New(env, jpeg_version)); #endif #ifdef HAVE_GIF #ifndef GIF_LIB_VERSION char gif_version[10]; snprintf(gif_version, 10, "%d.%d.%d", GIFLIB_MAJOR, GIFLIB_MINOR, GIFLIB_RELEASE); - target->Set(Nan::New("gifVersion").ToLocalChecked(), Nan::New(gif_version).ToLocalChecked()); + exports.Set("gifVersion", Napi::String::New(env, gif_version)); #else - target->Set(Nan::New("gifVersion").ToLocalChecked(), Nan::New(GIF_LIB_VERSION).ToLocalChecked()); + exports.Set("gifVersion", Napi::String::New(env, GIF_LIB_VERSION)); +#endif #endif + +#ifdef HAVE_RSVG + exports.Set("rsvgVersion", Napi::String::New(env, LIBRSVG_VERSION)); #endif + exports.Set("pangoVersion", Napi::String::New(env, PANGO_VERSION_STRING)); + char freetype_version[10]; snprintf(freetype_version, 10, "%d.%d.%d", FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); - target->Set(Nan::New("freetypeVersion").ToLocalChecked(), Nan::New(freetype_version).ToLocalChecked()); + exports.Set("freetypeVersion", Napi::String::New(env, freetype_version)); + + return exports; } -NODE_MODULE(canvas,init); +NODE_API_MODULE(canvas, init); diff --git a/src/register_font.cc b/src/register_font.cc index 75fc87cc8..ae2ece584 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -1,3 +1,5 @@ +#include "register_font.h" + #include #include #include @@ -6,6 +8,7 @@ #include #elif defined(_WIN32) #include +#include #else #include #endif @@ -92,35 +95,12 @@ to_utf8(FT_Byte* buf, FT_UInt len, FT_UShort pid, FT_UShort eid) { * system, fall back to the other */ -typedef struct _NameDef { - const char *buf; - int rank; // the higher the more desirable -} NameDef; - -gint -_name_def_compare(gconstpointer a, gconstpointer b) { - return ((NameDef*)a)->rank > ((NameDef*)b)->rank ? -1 : 1; -} - -// Some versions of GTK+ do not have this, particualrly the one we -// currently link to in node-canvas's wiki -void -_free_g_list_item(gpointer data, gpointer user_data) { - NameDef *d = (NameDef *)data; - free((void *)(d->buf)); -} - -void -_g_list_free_full(GList *list) { - g_list_foreach(list, _free_g_list_item, NULL); - g_list_free(list); -} - char * get_family_name(FT_Face face) { FT_SfntName name; - GList *list = NULL; - char *utf8name = NULL; + + int best_rank = -1; + char* best_buf = NULL; for (unsigned i = 0; i < FT_Get_Sfnt_Name_Count(face); ++i) { FT_Get_Sfnt_Name(face, i, &name); @@ -129,20 +109,19 @@ get_family_name(FT_Face face) { char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); if (buf) { - NameDef *d = (NameDef*)malloc(sizeof(NameDef)); - d->buf = (const char*)buf; - d->rank = GET_NAME_RANK(name); - - list = g_list_insert_sorted(list, (gpointer)d, _name_def_compare); + int rank = GET_NAME_RANK(name); + if (rank > best_rank) { + best_rank = rank; + if (best_buf) free(best_buf); + best_buf = buf; + } else { + free(buf); + } } } } - GList *best_def = g_list_first(list); - if (best_def) utf8name = (char*) strdup(((NameDef*)best_def->data)->buf); - if (list) _g_list_free_full(list); - - return utf8name; + return best_buf; } PangoWeight @@ -191,6 +170,41 @@ get_pango_style(FT_Long flags) { } } +#ifdef _WIN32 +std::unique_ptr +u8ToWide(const char* str) { + int iBufferSize = MultiByteToWideChar(CP_UTF8, 0, str, -1, (wchar_t*)NULL, 0); + if(!iBufferSize){ + return nullptr; + } + std::unique_ptr wpBufWString = std::unique_ptr{ new wchar_t[static_cast(iBufferSize)] }; + if(!MultiByteToWideChar(CP_UTF8, 0, str, -1, wpBufWString.get(), iBufferSize)){ + return nullptr; + } + return wpBufWString; +} + +static unsigned long +stream_read_func(FT_Stream stream, unsigned long offset, unsigned char* buffer, unsigned long count){ + HANDLE hFile = reinterpret_cast(stream->descriptor.pointer); + DWORD numberOfBytesRead; + OVERLAPPED overlapped; + overlapped.Offset = offset; + overlapped.OffsetHigh = 0; + overlapped.hEvent = NULL; + if(!ReadFile(hFile, buffer, count, &numberOfBytesRead, &overlapped)){ + return 0; + } + return numberOfBytesRead; +}; + +static void +stream_close_func(FT_Stream stream){ + HANDLE hFile = reinterpret_cast(stream->descriptor.pointer); + CloseHandle(hFile); +} +#endif + /* * Return a PangoFontDescription that will resolve to the font file */ @@ -200,23 +214,71 @@ get_pango_font_description(unsigned char* filepath) { FT_Library library; FT_Face face; PangoFontDescription *desc = pango_font_description_new(); - +#ifdef _WIN32 + // FT_New_Face use fopen. + // Unable to find the file when supplied the multibyte string path on the Windows platform and throw error "Could not parse font file". + // This workaround fixes this by reading the font file uses win32 wide character API. + std::unique_ptr wFilepath = u8ToWide((char*)filepath); + if(!wFilepath){ + return NULL; + } + HANDLE hFile = CreateFileW( + wFilepath.get(), + GENERIC_READ, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + if(!hFile){ + return NULL; + } + LARGE_INTEGER liSize; + if(!GetFileSizeEx(hFile, &liSize)) { + CloseHandle(hFile); + return NULL; + } + FT_Open_Args args; + args.flags = FT_OPEN_STREAM; + FT_StreamRec stream; + stream.base = NULL; + stream.size = liSize.QuadPart; + stream.pos = 0; + stream.descriptor.pointer = hFile; + stream.read = stream_read_func; + stream.close = stream_close_func; + args.stream = &stream; + if ( + !FT_Init_FreeType(&library) && + !FT_Open_Face(library, &args, 0, &face)) { +#else if (!FT_Init_FreeType(&library) && !FT_New_Face(library, (const char*)filepath, 0, &face)) { +#endif TT_OS2 *table = (TT_OS2*)FT_Get_Sfnt_Table(face, FT_SFNT_OS2); if (table) { char *family = get_family_name(face); - if (family) pango_font_description_set_family_static(desc, family); + if (!family) { + pango_font_description_free(desc); + FT_Done_Face(face); + FT_Done_FreeType(library); + + return NULL; + } + + pango_font_description_set_family(desc, family); + free(family); pango_font_description_set_weight(desc, get_pango_weight(table->usWeightClass)); pango_font_description_set_stretch(desc, get_pango_stretch(table->usWidthClass)); pango_font_description_set_style(desc, get_pango_style(face->style_flags)); FT_Done_Face(face); + FT_Done_FreeType(library); return desc; } } - pango_font_description_free(desc); return NULL; @@ -234,7 +296,13 @@ register_font(unsigned char *filepath) { CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); success = CTFontManagerRegisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); #elif defined(_WIN32) - success = AddFontResourceEx((LPCSTR)filepath, FR_PRIVATE, 0) != 0; + std::unique_ptr wFilepath = u8ToWide((char*)filepath); + if(wFilepath){ + success = AddFontResourceExW(wFilepath.get(), FR_PRIVATE, 0) != 0; + }else{ + success = false; + } + #else success = FcConfigAppFontAddFile(FcConfigGetCurrent(), (FcChar8 *)(filepath)); #endif @@ -249,3 +317,36 @@ register_font(unsigned char *filepath) { return true; } +/* + * Deregister font from the OS + * Note that Linux (FontConfig) can only dereregister ALL fonts at once. + */ + +bool +deregister_font(unsigned char *filepath) { + bool success; + + #ifdef __APPLE__ + CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); + success = CTFontManagerUnregisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); + #elif defined(_WIN32) + std::unique_ptr wFilepath = u8ToWide((char*)filepath); + if(wFilepath){ + success = RemoveFontResourceExW(wFilepath.get(), FR_PRIVATE, 0) != 0; + }else{ + success = false; + } + #else + FcConfigAppFontClear(FcConfigGetCurrent()); + success = true; + #endif + + if (!success) return false; + + // Tell Pango to throw away the current FontMap and create a new one. This + // has the effect of deregistering the font in Pango by re-looking up all + // font families. + pango_cairo_font_map_set_default(NULL); + + return true; +} diff --git a/src/register_font.h b/src/register_font.h index 33d006bab..a4fcd598e 100644 --- a/src/register_font.h +++ b/src/register_font.h @@ -1,5 +1,7 @@ +#pragma once + #include PangoFontDescription *get_pango_font_description(unsigned char *filepath); bool register_font(unsigned char *filepath); - +bool deregister_font(unsigned char *filepath); diff --git a/test/canvas.test.js b/test/canvas.test.js index 72bf6be90..4655c31c7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1,1117 +1,2566 @@ +/* eslint-env mocha */ + +'use strict' + /** * Module dependencies. */ - -var Canvas = require('../') - , assert = require('assert') - , parseFont = Canvas.Context2d.parseFont - , fs = require('fs') - , os = require('os'); - -console.log(); -console.log(' canvas: %s', Canvas.version); -console.log(' cairo: %s', Canvas.cairoVersion); +const assert = require('assert') +const os = require('os') +const path = require('path') +const { Readable } = require('stream') + +const { + createCanvas, + createImageData, + loadImage, + registerFont, + Canvas, + deregisterAllFonts +} = require('../') + +function assertApprox(actual, expected, tol) { + assert(Math.abs(expected - actual) <= tol, + "Expected " + actual + " to be " + expected + " +/- " + tol); +} describe('Canvas', function () { - it('should require new', function () { - assert.throws(function () { Canvas(); }, TypeError); + // Run with --expose-gc and uncomment this line to help find memory problems: + // afterEach(gc); + + it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { + const c = new Canvas(10, 10) + assert.throws(function () { Canvas.prototype.width }, /invalid argument/i) + assert(!c.hasOwnProperty('width')) + assert('width' in c) + assert('width' in Canvas.prototype) + }) + + it('registerFont', function () { + // Minimal test to make sure nothing is thrown + registerFont('./examples/pfennigFont/Pfennig.ttf', { family: 'Pfennig' }) + registerFont('./examples/pfennigFont/PfennigBold.ttf', { family: 'Pfennig', weight: 'bold' }) + + // Test to multi byte file path support + registerFont('./examples/pfennigFont/pfennigMultiByte🚀.ttf', { family: 'Pfennig' }) + + deregisterAllFonts() }); - it('.version', function () { - assert.ok(/^\d+\.\d+\.\d+$/.test(Canvas.version)); - }); + it('color serialization', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d'); - it('.cairoVersion', function () { - assert.ok(/^\d+\.\d+\.\d+$/.test(Canvas.cairoVersion)); - }); + ['fillStyle', 'strokeStyle', 'shadowColor'].forEach(function (prop) { + ctx[prop] = '#FFFFFF' + assert.equal('#ffffff', ctx[prop], prop + ' #FFFFFF -> #ffffff, got ' + ctx[prop]) - it('.parseFont()', function () { - var tests = [ - '20px Arial' - , { size: 20, unit: 'px', family: 'Arial' } - , '20pt Arial' - , { size: 26.666666666666668, unit: 'pt', family: 'Arial' } - , '20.5pt Arial' - , { size: 27.333333333333332, unit: 'pt', family: 'Arial' } - , '20% Arial' - , { size: 20, unit: '%', family: 'Arial' } - , '20mm Arial' - , { size: 75.59055118110237, unit: 'mm', family: 'Arial' } - , '20px serif' - , { size: 20, unit: 'px', family: 'serif' } - , '20px sans-serif' - , { size: 20, unit: 'px', family: 'sans-serif' } - , '20px monospace' - , { size: 20, unit: 'px', family: 'monospace' } - , '50px Arial, sans-serif' - , { size: 50, unit: 'px', family: 'Arial,sans-serif' } - , 'bold italic 50px Arial, sans-serif' - , { style: 'italic', weight: 'bold', size: 50, unit: 'px', family: 'Arial,sans-serif' } - , '50px Helvetica , Arial, sans-serif' - , { size: 50, unit: 'px', family: 'Helvetica,Arial,sans-serif' } - , '50px "Helvetica Neue", sans-serif' - , { size: 50, unit: 'px', family: 'Helvetica Neue,sans-serif' } - , '50px "Helvetica Neue", "foo bar baz" , sans-serif' - , { size: 50, unit: 'px', family: 'Helvetica Neue,foo bar baz,sans-serif' } - , "50px 'Helvetica Neue'" - , { size: 50, unit: 'px', family: 'Helvetica Neue' } - , 'italic 20px Arial' - , { size: 20, unit: 'px', style: 'italic', family: 'Arial' } - , 'oblique 20px Arial' - , { size: 20, unit: 'px', style: 'oblique', family: 'Arial' } - , 'normal 20px Arial' - , { size: 20, unit: 'px', style: 'normal', family: 'Arial' } - , '300 20px Arial' - , { size: 20, unit: 'px', weight: '300', family: 'Arial' } - , '800 20px Arial' - , { size: 20, unit: 'px', weight: '800', family: 'Arial' } - , 'bolder 20px Arial' - , { size: 20, unit: 'px', weight: 'bolder', family: 'Arial' } - , 'lighter 20px Arial' - , { size: 20, unit: 'px', weight: 'lighter', family: 'Arial' } - ]; - - for (var i = 0, len = tests.length; i < len; ++i) { - var str = tests[i++] - , obj = tests[i] - , actual = parseFont(str); - - if (!obj.style) obj.style = 'normal'; - if (!obj.weight) obj.weight = 'normal'; - - assert.deepEqual(obj, actual); - } - }); + ctx[prop] = '#FFF' + assert.equal('#ffffff', ctx[prop], prop + ' #FFF -> #ffffff, got ' + ctx[prop]) - it('color serialization', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); + ctx[prop] = 'rgba(128, 200, 128, 1)' + assert.equal('#80c880', ctx[prop], prop + ' rgba(128, 200, 128, 1) -> #80c880, got ' + ctx[prop]) - ['fillStyle', 'strokeStyle', 'shadowColor'].forEach(function(prop){ - ctx[prop] = '#FFFFFF'; - assert.equal('#ffffff', ctx[prop], prop + ' #FFFFFF -> #ffffff, got ' + ctx[prop]); + ctx[prop] = 'rgba(128,80,0,0.5)' + assert.equal('rgba(128, 80, 0, 0.50)', ctx[prop], prop + ' rgba(128,80,0,0.5) -> rgba(128, 80, 0, 0.5), got ' + ctx[prop]) - ctx[prop] = '#FFF'; - assert.equal('#ffffff', ctx[prop], prop + ' #FFF -> #ffffff, got ' + ctx[prop]); + ctx[prop] = 'rgba(128,80,0,0.75)' + assert.equal('rgba(128, 80, 0, 0.75)', ctx[prop], prop + ' rgba(128,80,0,0.75) -> rgba(128, 80, 0, 0.75), got ' + ctx[prop]) - ctx[prop] = 'rgba(128, 200, 128, 1)'; - assert.equal('#80c880', ctx[prop], prop + ' rgba(128, 200, 128, 1) -> #80c880, got ' + ctx[prop]); + if (prop === 'shadowColor') return - ctx[prop] = 'rgba(128,80,0,0.5)'; - assert.equal('rgba(128, 80, 0, 0.50)', ctx[prop], prop + ' rgba(128,80,0,0.5) -> rgba(128, 80, 0, 0.5), got ' + ctx[prop]); + const grad = ctx.createLinearGradient(0, 0, 0, 150) + ctx[prop] = grad + assert.strictEqual(grad, ctx[prop], prop + ' pattern getter failed') + }) + }) - ctx[prop] = 'rgba(128,80,0,0.75)'; - assert.equal('rgba(128, 80, 0, 0.75)', ctx[prop], prop + ' rgba(128,80,0,0.75) -> rgba(128, 80, 0, 0.75), got ' + ctx[prop]); + it('color parser', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') - if ('shadowColor' == prop) return; + ctx.fillStyle = '#ffccaa' + assert.equal('#ffccaa', ctx.fillStyle) - var grad = ctx.createLinearGradient(0,0,0,150); - ctx[prop] = grad; - assert.strictEqual(grad, ctx[prop], prop + ' pattern getter failed'); - }); - }); + ctx.fillStyle = '#FFCCAA' + assert.equal('#ffccaa', ctx.fillStyle) - it('color parser', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); + ctx.fillStyle = '#FCA' + assert.equal('#ffccaa', ctx.fillStyle) - ctx.fillStyle = '#ffccaa'; - assert.equal('#ffccaa', ctx.fillStyle); + ctx.fillStyle = '#fff' + ctx.fillStyle = '#FGG' + assert.equal('#ff0000', ctx.fillStyle) - ctx.fillStyle = '#FFCCAA'; - assert.equal('#ffccaa', ctx.fillStyle); + ctx.fillStyle = ' #FCA' + assert.equal('#ffccaa', ctx.fillStyle) - ctx.fillStyle = '#FCA'; - assert.equal('#ffccaa', ctx.fillStyle); + ctx.fillStyle = ' #ffccaa' + assert.equal('#ffccaa', ctx.fillStyle) - ctx.fillStyle = '#fff'; - ctx.fillStyle = '#FGG'; - assert.equal('#ff0000', ctx.fillStyle); - ctx.fillStyle = '#fff'; - ctx.fillStyle = 'afasdfasdf'; - assert.equal('#ffffff', ctx.fillStyle); + ctx.fillStyle = '#fff' + ctx.fillStyle = 'afasdfasdf' + assert.equal('#ffffff', ctx.fillStyle) - ctx.fillStyle = 'rgb(255,255,255)'; - assert.equal('#ffffff', ctx.fillStyle); + // #rgba and #rrggbbaa + ctx.fillStyle = '#ffccaa80' + assert.equal('rgba(255, 204, 170, 0.50)', ctx.fillStyle) - ctx.fillStyle = 'rgb(0,0,0)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = '#acf8' + assert.equal('rgba(170, 204, 255, 0.53)', ctx.fillStyle) - ctx.fillStyle = 'rgb( 0 , 0 , 0)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = '#BEAD' + assert.equal('rgba(187, 238, 170, 0.87)', ctx.fillStyle) - ctx.fillStyle = 'rgba( 0 , 0 , 0, 1)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'rgb(255,255,255)' + assert.equal('#ffffff', ctx.fillStyle) - ctx.fillStyle = 'rgba( 255, 200, 90, 0.5)'; - assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle); + ctx.fillStyle = 'rgb(0,0,0)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'rgba( 255, 200, 90, 0.75)'; - assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle); + ctx.fillStyle = 'rgb( 0 , 0 , 0)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'rgba( 255, 200, 90, 0.7555)'; - assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle); + ctx.fillStyle = 'rgba( 0 , 0 , 0, 1)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'rgba( 255, 200, 90, .7555)'; - assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 0.5)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) - ctx.fillStyle = 'rgb(0, 0, 9000)'; - assert.equal('#0000ff', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 0.75)' + assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle) - ctx.fillStyle = 'rgba(0, 0, 0, 42.42)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 0.7555)' + assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle) - // hsl / hsla tests + ctx.fillStyle = 'rgba( 255, 200, 90, .7555)' + assert.equal('rgba(255, 200, 90, 0.75)', ctx.fillStyle) - ctx.fillStyle = 'hsl(0, 0%, 0%)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'rgb(0, 0, 9000)' + assert.equal('#0000ff', ctx.fillStyle) - ctx.fillStyle = 'hsl(3600, -10%, -10%)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'rgba(0, 0, 0, 42.42)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'hsl(10, 100%, 42%)'; - assert.equal('#d62400', ctx.fillStyle); + ctx.fillStyle = 'rgba(255, 250, 255)'; + assert.equal('#fffaff', ctx.fillStyle); - ctx.fillStyle = 'hsl(370, 120%, 42%)'; - assert.equal('#d62400', ctx.fillStyle); + ctx.fillStyle = 'rgba(124, 58, 26, 0)'; + assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); - ctx.fillStyle = 'hsl(0, 100%, 100%)'; - assert.equal('#ffffff', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) - ctx.fillStyle = 'hsl(0, 150%, 150%)'; - assert.equal('#ffffff', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 50 %)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) - ctx.fillStyle = 'hsl(237, 76%, 25%)'; - assert.equal('#0f1470', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - ctx.fillStyle = 'hsl(240, 73%, 25%)'; - assert.equal('#11116e', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90, 10 %)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - ctx.fillStyle = 'hsl(262, 32%, 42%)'; - assert.equal('#62498d', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90 / 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) - ctx.fillStyle = 'hsla(0, 0%, 0%, 1)'; - assert.equal('#000000', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90 / 0.5)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) - ctx.fillStyle = 'hsla(0, 100%, 100%, 1)'; - assert.equal('#ffffff', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - ctx.fillStyle = 'hsla(120, 25%, 75%, 0.5)'; - assert.equal('rgba(175, 207, 175, 0.50)', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255, 200, 90 / 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - ctx.fillStyle = 'hsla(240, 75%, 25%, 0.75)'; - assert.equal('rgba(16, 16, 112, 0.75)', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255 200 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - ctx.fillStyle = 'hsla(172.0, 33.00000e0%, 42%, 1)'; - assert.equal('#488e85', ctx.fillStyle); + ctx.fillStyle = 'rgba( 255 200 90 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - ctx.fillStyle = 'hsl(124.5, 76.1%, 47.6%)'; - assert.equal('#1dd62b', ctx.fillStyle); + ctx.fillStyle = 'rgb(0, 0, 0, 42.42)' + assert.equal('#000000', ctx.fillStyle) - ctx.fillStyle = 'hsl(1.24e2, 760e-1%, 4.7e1%)'; - assert.equal('#1dd329', ctx.fillStyle); - }); + ctx.fillStyle = 'rgb(255, 250, 255)'; + assert.equal('#fffaff', ctx.fillStyle); - it('Canvas#type', function () { - var canvas = new Canvas(10, 10); - assert('image' == canvas.type); - var canvas = new Canvas(10, 10, 'pdf'); - assert('pdf' == canvas.type); - var canvas = new Canvas(10, 10, 'svg'); - assert('svg' == canvas.type); - var canvas = new Canvas(10, 10, 'hey'); - assert('image' == canvas.type); - }); + ctx.fillStyle = 'rgb(124, 58, 26, 0)'; + assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); - it('Canvas#getContext("2d")', function () { - var canvas = new Canvas(200, 300) - , ctx = canvas.getContext('2d'); - assert.ok('object' == typeof ctx); - assert.equal(canvas, ctx.canvas, 'context.canvas is not canvas'); - assert.equal(ctx, canvas.context, 'canvas.context is not context'); - }); + ctx.fillStyle = 'rgb( 255, 200, 90, 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) - it('Canvas#{width,height}=', function () { - var canvas = new Canvas(100, 200); - assert.equal(100, canvas.width); - assert.equal(200, canvas.height); - - canvas = new Canvas; - assert.equal(0, canvas.width); - assert.equal(0, canvas.height); - - canvas.width = 50; - canvas.height = 50; - assert.equal(50, canvas.width); - assert.equal(50, canvas.height); - }); + ctx.fillStyle = 'rgb( 255, 200, 90, 50 %)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) - it('Canvas#stride', function() { - var canvas = new Canvas(24, 10); - assert.ok(canvas.stride >= 24, 'canvas.stride is too short'); - assert.ok(canvas.stride < 1024, 'canvas.stride seems too long'); - }); + ctx.fillStyle = 'rgb( 255, 200, 90, 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - it('Canvas#getContext("invalid")', function () { - assert.equal(null, new Canvas(200, 300).getContext('invalid')); - }); + ctx.fillStyle = 'rgb( 255, 200, 90, 10 %)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - it('Context2d#patternQuality', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); - - assert.equal('good', ctx.patternQuality); - ctx.patternQuality = 'best'; - assert.equal('best', ctx.patternQuality); - ctx.patternQuality = 'invalid'; - assert.equal('best', ctx.patternQuality); - }); + ctx.fillStyle = 'rgb( 255, 200, 90 / 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) - it('Context2d#font=', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); + ctx.fillStyle = 'rgb( 255, 200, 90 / 0.5)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) - assert.equal('10px sans-serif', ctx.font); - ctx.font = '15px Arial, sans-serif'; - assert.equal('15px Arial, sans-serif', ctx.font); - }); + ctx.fillStyle = 'rgb( 255, 200, 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - it('Context2d#lineWidth=', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); - - ctx.lineWidth = 10.0; - assert.equal(10, ctx.lineWidth); - ctx.lineWidth = Infinity; - assert.equal(10, ctx.lineWidth); - ctx.lineWidth = -Infinity; - assert.equal(10, ctx.lineWidth); - ctx.lineWidth = -5; - assert.equal(10, ctx.lineWidth); - ctx.lineWidth = 0; - assert.equal(10, ctx.lineWidth); - }); + ctx.fillStyle = 'rgb( 255, 200, 90 / 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - it('Context2d#antiAlias=', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); - - assert.equal('default', ctx.antialias); - ctx.antialias = 'none'; - assert.equal('none', ctx.antialias); - ctx.antialias = 'gray'; - assert.equal('gray', ctx.antialias); - ctx.antialias = 'subpixel'; - assert.equal('subpixel', ctx.antialias); - ctx.antialias = 'invalid'; - assert.equal('subpixel', ctx.antialias); - ctx.antialias = 1; - assert.equal('subpixel', ctx.antialias); - }); + ctx.fillStyle = 'rgb( 255 200 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - it('Context2d#lineCap=', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); + ctx.fillStyle = 'rgb( 255 200 90 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) - assert.equal('butt', ctx.lineCap); - ctx.lineCap = 'round'; - assert.equal('round', ctx.lineCap); - }); + ctx.fillStyle = ' rgb( 255 100 90 0.1)' + assert.equal('rgba(255, 100, 90, 0.10)', ctx.fillStyle) - it('Context2d#lineJoin=', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); + ctx.fillStyle = 'rgb(124.00, 58, 26, 0)'; + assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); - assert.equal('miter', ctx.lineJoin); - ctx.lineJoin = 'round'; - assert.equal('round', ctx.lineJoin); - }); + ctx.fillStyle = 'rgb( 255, 200.09, 90, 40%)' + assert.equal('rgba(255, 201, 90, 0.40)', ctx.fillStyle) - it('Context2d#globalAlpha=', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); + ctx.fillStyle = 'rgb( 255.00, 199.03, 90, 50 %)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) - assert.equal(1, ctx.globalAlpha); - ctx.globalAlpha = 0.5 - assert.equal(0.5, ctx.globalAlpha); - }); + ctx.fillStyle = 'rgb( 255, 300.09, 90, 40%)' + assert.equal('rgba(255, 255, 90, 0.40)', ctx.fillStyle) + // hsl / hsla tests - it('Context2d#isPointInPath()', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); - - ctx.rect(5,5,100,100); - ctx.rect(50,100,10,10); - assert.ok(ctx.isPointInPath(10,10)); - assert.ok(ctx.isPointInPath(10,50)); - assert.ok(ctx.isPointInPath(100,100)); - assert.ok(ctx.isPointInPath(105,105)); - assert.ok(!ctx.isPointInPath(106,105)); - assert.ok(!ctx.isPointInPath(150,150)); - - assert.ok(ctx.isPointInPath(50,110)); - assert.ok(ctx.isPointInPath(60,110)); - assert.ok(!ctx.isPointInPath(70,110)); - assert.ok(!ctx.isPointInPath(50,120)); - }); + ctx.fillStyle = 'hsl(0, 0%, 0%)' + assert.equal('#000000', ctx.fillStyle) - it('Context2d#textAlign', function () { - var canvas = new Canvas(200,200) - , ctx = canvas.getContext('2d'); - - assert.equal('start', ctx.textAlign); - ctx.textAlign = 'center'; - assert.equal('center', ctx.textAlign); - ctx.textAlign = 'right'; - assert.equal('right', ctx.textAlign); - ctx.textAlign = 'end'; - assert.equal('end', ctx.textAlign); - ctx.textAlign = 'fail'; - assert.equal('end', ctx.textAlign); - }); + ctx.fillStyle = 'hsl(3600, -10%, -10%)' + assert.equal('#000000', ctx.fillStyle) - it('Canvas#toBuffer()', function () { - var buf = new Canvas(200,200).toBuffer(); - assert.equal('PNG', buf.slice(1,4).toString()); - }); + ctx.fillStyle = 'hsl(10, 100%, 42%)' + assert.equal('#d62400', ctx.fillStyle) - it('Canvas#toBuffer() async', function (done) { - new Canvas(200, 200).toBuffer(function(err, buf){ - assert.ok(!err); - assert.equal('PNG', buf.slice(1,4).toString()); - done(); - }); - }); + ctx.fillStyle = 'hsl(370, 120%, 42%)' + assert.equal('#d62400', ctx.fillStyle) - describe('#toBuffer("raw")', function() { - var canvas = new Canvas(10, 10) - , ctx = canvas.getContext('2d'); + ctx.fillStyle = 'hsl(0, 100%, 100%)' + assert.equal('#ffffff', ctx.fillStyle) - ctx.clearRect(0, 0, 10, 10); + ctx.fillStyle = 'hsl(0, 150%, 150%)' + assert.equal('#ffffff', ctx.fillStyle) - ctx.fillStyle = 'rgba(200, 200, 200, 0.505)'; - ctx.fillRect(0, 0, 5, 5); + ctx.fillStyle = 'hsl(237, 76%, 25%)' + assert.equal('#0f1470', ctx.fillStyle) - ctx.fillStyle = 'red'; - ctx.fillRect(5, 0, 5, 5); + ctx.fillStyle = ' hsl(0, 150%, 150%)' + assert.equal('#ffffff', ctx.fillStyle) - ctx.fillStyle = '#00ff00'; - ctx.fillRect(0, 5, 5, 5); + ctx.fillStyle = 'hsl(240, 73%, 25%)' + assert.equal('#11116e', ctx.fillStyle) - ctx.fillStyle = 'black'; - ctx.fillRect(5, 5, 4, 5); + ctx.fillStyle = 'hsl(262, 32%, 42%)' + assert.equal('#62498d', ctx.fillStyle) - /** Output: - * *****RRRRR - * *****RRRRR - * *****RRRRR - * *****RRRRR - * *****RRRRR - * GGGGGBBBB- - * GGGGGBBBB- - * GGGGGBBBB- - * GGGGGBBBB- - * GGGGGBBBB- - */ + ctx.fillStyle = 'hsla(0, 0%, 0%, 1)' + assert.equal('#000000', ctx.fillStyle) - var buf = canvas.toBuffer('raw'); - var stride = canvas.stride; + ctx.fillStyle = 'hsla(0, 100%, 100%, 1)' + assert.equal('#ffffff', ctx.fillStyle) - var endianness = os.endianness(); + ctx.fillStyle = 'hsla(120, 25%, 75%, 0.5)' + assert.equal('rgba(175, 207, 175, 0.50)', ctx.fillStyle) - function assertPixel(u32, x, y, message) { - var expected = '0x' + u32.toString(16); + ctx.fillStyle = 'hsla(240, 75%, 25%, 0.75)' + assert.equal('rgba(16, 16, 112, 0.75)', ctx.fillStyle) - // Buffer doesn't have readUInt32(): it only has readUInt32LE() and - // readUInt32BE(). - var px = buf['readUInt32' + endianness](y * stride + x * 4); - var actual = '0x' + px.toString(16); + ctx.fillStyle = 'hsla(172.0, 33.00000e0%, 42%, 1)' + assert.equal('#488e85', ctx.fillStyle) - assert.equal(actual, expected, message); - } + ctx.fillStyle = 'hsl(124.5, 76.1%, 47.6%)' + assert.equal('#1dd62b', ctx.fillStyle) - it('should have the correct size', function() { - assert.equal(buf.length, stride * 10); - }); + ctx.fillStyle = 'hsl(1.24e2, 760e-1%, 4.7e1%)' + assert.equal('#1dd329', ctx.fillStyle) - it('does not premultiply alpha', function() { - assertPixel(0x80646464, 0, 0, 'first semitransparent pixel'); - assertPixel(0x80646464, 4, 4, 'last semitransparent pixel'); - }); + // case-insensitive (#235) + ctx.fillStyle = 'sILveR' + assert.equal(ctx.fillStyle, '#c0c0c0') + }) - it('draws red', function() { - assertPixel(0xffff0000, 5, 0, 'first red pixel'); - assertPixel(0xffff0000, 9, 4, 'last red pixel'); - }); + it('Canvas#type', function () { + let canvas = createCanvas(10, 10) + assert.equal(canvas.type, 'image') + canvas = createCanvas(10, 10, 'pdf') + assert.equal(canvas.type, 'pdf') + canvas = createCanvas(10, 10, 'svg') + assert.equal(canvas.type, 'svg') + canvas = createCanvas(10, 10, 'hey') + assert.equal(canvas.type, 'image') + }) - it('draws green', function() { - assertPixel(0xff00ff00, 0, 5, 'first green pixel'); - assertPixel(0xff00ff00, 4, 9, 'last green pixel'); - }); + it('Canvas#getContext("2d")', function () { + const canvas = createCanvas(200, 300) + const ctx = canvas.getContext('2d') + assert.ok(typeof ctx === 'object') + assert.equal(canvas, ctx.canvas, 'context.canvas is not canvas') + assert.equal(ctx, canvas.context, 'canvas.context is not context') + + const MAX_IMAGE_SIZE = 32767; + + [ + [0, 0, 1], + [1, 0, 1], + [MAX_IMAGE_SIZE, 0, 1], + [MAX_IMAGE_SIZE + 1, 0, 3], + [MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, null], + [MAX_IMAGE_SIZE + 1, MAX_IMAGE_SIZE, 3], + [MAX_IMAGE_SIZE + 1, MAX_IMAGE_SIZE + 1, 3], + [Math.pow(2, 30), 0, 3], + [Math.pow(2, 30), 1, 3], + [Math.pow(2, 32), 0, 1], + [Math.pow(2, 32), 1, 1] + ].forEach(params => { + const width = params[0] + const height = params[1] + const errorLevel = params[2] + + let level = 3 + + try { + const canvas = createCanvas(width, height) + level-- + + const ctx = canvas.getContext('2d') + level-- + + ctx.getImageData(0, 0, 1, 1) + level-- + } catch (err) {} + + if (errorLevel !== null) { assert.strictEqual(level, errorLevel) } + }) + }) + + it('Canvas#getContext("2d", {pixelFormat: string})', function () { + let canvas, context + + // default: + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGBA32' }) + assert.equal(context.pixelFormat, 'RGBA32') + + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGBA32' }) + assert.equal(context.pixelFormat, 'RGBA32') + + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGB24' }) + assert.equal(context.pixelFormat, 'RGB24') + + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'A8' }) + assert.equal(context.pixelFormat, 'A8') + + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'A1' }) + assert.equal(context.pixelFormat, 'A1') + + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGB16_565' }) + assert.equal(context.pixelFormat, 'RGB16_565') + + // Not tested: RGB30 + }) + + it('Canvas#getContext("2d", {alpha: boolean})', function () { + let canvas, context + + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { alpha: true }) + assert.equal(context.pixelFormat, 'RGBA32') + + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { alpha: false }) + assert.equal(context.pixelFormat, 'RGB24') + + // alpha takes priority: + canvas = createCanvas(10, 10) + context = canvas.getContext('2d', { pixelFormat: 'RGBA32', alpha: false }) + assert.equal(context.pixelFormat, 'RGB24') + }) - it('draws black', function() { - assertPixel(0xff000000, 5, 5, 'first black pixel'); - assertPixel(0xff000000, 8, 9, 'last black pixel'); - }); + it('Canvas#{width,height}=', function () { + const canvas = createCanvas(100, 200) + const context = canvas.getContext('2d') + + assert.equal(canvas.width, 100) + assert.equal(canvas.height, 200) + + context.globalAlpha = 0.5 + context.fillStyle = '#0f0' + context.strokeStyle = '#0f0' + context.font = '20px arial' + context.fillRect(0, 0, 1, 1) + + canvas.width = 50 + canvas.height = 70 + assert.equal(canvas.width, 50) + assert.equal(canvas.height, 70) + + context.font = '20px arial' + assert.equal(context.font, '20px arial') + canvas.width |= 0 + + assert.equal(context.lineWidth, 1) // #1095 + assert.equal(context.globalAlpha, 1) // #1292 + assert.equal(context.fillStyle, '#000000') + assert.equal(context.strokeStyle, '#000000') + assert.equal(context.font, '10px sans-serif') + assert.strictEqual(context.getImageData(0, 0, 1, 1).data.join(','), '0,0,0,0') + }) + + it('Canvas#width= (resurfacing) doesn\'t crash when fillStyle is a pattern (#1357)', function (done) { + const canvas = createCanvas(100, 200) + const ctx = canvas.getContext('2d') + + loadImage(path.join(__dirname, '/fixtures/checkers.png')).then(img => { + const pattern = ctx.createPattern(img, 'repeat') + ctx.fillStyle = pattern + ctx.fillRect(0, 0, 300, 300) + canvas.width = 200 // cause canvas to resurface + done() + }) + }) + + it('SVG Canvas#width changes don\'t crash (#1380)', function () { + const myCanvas = createCanvas(100, 100, 'svg') + myCanvas.width = 120 + }) + + it('Canvas#stride', function () { + const canvas = createCanvas(24, 10) + assert.ok(canvas.stride >= 24, 'canvas.stride is too short') + assert.ok(canvas.stride < 1024, 'canvas.stride seems too long') + + // TODO test stride on other formats + }) - it('leaves undrawn pixels black, transparent', function() { - assertPixel(0x0, 9, 5, 'first undrawn pixel'); - assertPixel(0x0, 9, 9, 'last undrawn pixel'); - }); + it('Canvas#getContext("invalid")', function () { + assert.equal(null, createCanvas(200, 300).getContext('invalid')) + }) - it('is immutable', function() { - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, 10, 10); - canvas.toBuffer('raw'); // (side-effect: flushes canvas) - assertPixel(0xffff0000, 5, 0, 'first red pixel'); - }); - }); + it('Context2d#patternQuality', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal('good', ctx.patternQuality) + ctx.patternQuality = 'best' + assert.equal('best', ctx.patternQuality) + ctx.patternQuality = 'invalid' + assert.equal('best', ctx.patternQuality) + }) + + it('Context2d#imageSmoothingEnabled', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal(true, ctx.imageSmoothingEnabled) + ctx.imageSmoothingEnabled = false + assert.equal('good', ctx.patternQuality) + assert.equal(false, ctx.imageSmoothingEnabled) + assert.equal('good', ctx.patternQuality) + }) + + it('Context2d#font=', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal(ctx.font, '10px sans-serif') + ctx.font = '15px Arial, sans-serif' + assert.equal(ctx.font, '15px Arial, sans-serif') + + ctx.font = 'Helvetica, sans' // invalid + assert.equal(ctx.font, '15px Arial, sans-serif') + }) + + it('Context2d#lineWidth=', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + ctx.lineWidth = 10.0 + assert.equal(10, ctx.lineWidth) + ctx.lineWidth = Infinity + assert.equal(10, ctx.lineWidth) + ctx.lineWidth = -Infinity + assert.equal(10, ctx.lineWidth) + ctx.lineWidth = -5 + assert.equal(10, ctx.lineWidth) + ctx.lineWidth = 0 + assert.equal(10, ctx.lineWidth) + }) + + it('Context2d#antiAlias=', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal('default', ctx.antialias) + ctx.antialias = 'none' + assert.equal('none', ctx.antialias) + ctx.antialias = 'gray' + assert.equal('gray', ctx.antialias) + ctx.antialias = 'subpixel' + assert.equal('subpixel', ctx.antialias) + ctx.antialias = 'invalid' + assert.equal('subpixel', ctx.antialias) + ctx.antialias = 1 + assert.equal('subpixel', ctx.antialias) + }) + + it('Context2d#lineCap=', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal('butt', ctx.lineCap) + ctx.lineCap = 'round' + assert.equal('round', ctx.lineCap) + }) + + it('Context2d#lineJoin=', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal('miter', ctx.lineJoin) + ctx.lineJoin = 'round' + assert.equal('round', ctx.lineJoin) + }) + + it('Context2d#globalAlpha=', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal(1, ctx.globalAlpha) + ctx.globalAlpha = 0.5 + assert.equal(0.5, ctx.globalAlpha) + }) + + it('Context2d#isPointInPath()', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + ctx.rect(5, 5, 100, 100) + ctx.rect(50, 100, 10, 10) + assert.ok(ctx.isPointInPath(10, 10)) + assert.ok(ctx.isPointInPath(10, 50)) + assert.ok(ctx.isPointInPath(100, 100)) + assert.ok(ctx.isPointInPath(105, 105)) + assert.ok(!ctx.isPointInPath(106, 105)) + assert.ok(!ctx.isPointInPath(150, 150)) + + assert.ok(ctx.isPointInPath(50, 110)) + assert.ok(ctx.isPointInPath(60, 110)) + assert.ok(!ctx.isPointInPath(70, 110)) + assert.ok(!ctx.isPointInPath(50, 120)) + }) + + it('Context2d#textAlign', function () { + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') + + assert.equal('start', ctx.textAlign) + ctx.textAlign = 'center' + assert.equal('center', ctx.textAlign) + ctx.textAlign = 'right' + assert.equal('right', ctx.textAlign) + ctx.textAlign = 'end' + assert.equal('end', ctx.textAlign) + ctx.textAlign = 'fail' + assert.equal('end', ctx.textAlign) + }) + + describe('#toBuffer', function () { + it('Canvas#toBuffer()', function () { + const buf = createCanvas(200, 200).toBuffer() + assert.equal('PNG', buf.slice(1, 4).toString()) + }) + + it('Canvas#toBuffer("image/png")', function () { + const buf = createCanvas(200, 200).toBuffer('image/png') + assert.equal('PNG', buf.slice(1, 4).toString()) + }) + + it('Canvas#toBuffer("image/png", {resolution: 96})', function () { + const buf = createCanvas(200, 200).toBuffer('image/png', { resolution: 96 }) + // 3780 ppm ~= 96 ppi + let foundpHYs = false + for (let i = 0; i < buf.length - 12; i++) { + if (buf[i] === 0x70 && + buf[i + 1] === 0x48 && + buf[i + 2] === 0x59 && + buf[i + 3] === 0x73) { // pHYs + foundpHYs = true + assert.equal(buf[i + 4], 0) + assert.equal(buf[i + 5], 0) + assert.equal(buf[i + 6], 0x0e) + assert.equal(buf[i + 7], 0xc4) // x + assert.equal(buf[i + 8], 0) + assert.equal(buf[i + 9], 0) + assert.equal(buf[i + 10], 0x0e) + assert.equal(buf[i + 11], 0xc4) // y + } + } + assert.ok(foundpHYs, 'missing pHYs header') + }) + + it('Canvas#toBuffer("image/png", {compressionLevel: 5})', function () { + const buf = createCanvas(200, 200).toBuffer('image/png', { compressionLevel: 5 }) + assert.equal('PNG', buf.slice(1, 4).toString()) + }) + + it('Canvas#toBuffer("image/png", {filters: PNG_ALL_FILTERS})', function () { + const buf = createCanvas(200, 200).toBuffer('image/png', { filters: Canvas.PNG_ALL_FILTERS }) + assert.equal('PNG', buf.slice(1, 4).toString()) + }) + + it('Canvas#toBuffer("image/jpeg")', function () { + const buf = createCanvas(200, 200).toBuffer('image/jpeg') + assert.equal(buf[0], 0xff) + assert.equal(buf[1], 0xd8) + assert.equal(buf[buf.byteLength - 2], 0xff) + assert.equal(buf[buf.byteLength - 1], 0xd9) + }) + + it('Canvas#toBuffer("image/jpeg", {quality: 0.95})', function () { + const buf = createCanvas(200, 200).toBuffer('image/jpeg', { quality: 0.95 }) + assert.equal(buf[0], 0xff) + assert.equal(buf[1], 0xd8) + assert.equal(buf[buf.byteLength - 2], 0xff) + assert.equal(buf[buf.byteLength - 1], 0xd9) + }) + + it('Canvas#toBuffer(callback)', function (done) { + createCanvas(200, 200).toBuffer(function (err, buf) { + assert.ok(!err) + assert.equal('PNG', buf.slice(1, 4).toString()) + done() + }) + }) + + it('Canvas#toBuffer(callback, "image/jpeg")', function (done) { + createCanvas(200, 200).toBuffer(function (err, buf) { + assert.ok(!err) + assert.equal(buf[0], 0xff) + assert.equal(buf[1], 0xd8) + assert.equal(buf[buf.byteLength - 2], 0xff) + assert.equal(buf[buf.byteLength - 1], 0xd9) + done() + }, 'image/jpeg') + }) + + it('Canvas#toBuffer(callback, "image/jpeg", {quality: 0.95})', function (done) { + createCanvas(200, 200).toBuffer(function (err, buf) { + assert.ok(!err) + assert.equal(buf[0], 0xff) + assert.equal(buf[1], 0xd8) + assert.equal(buf[buf.byteLength - 2], 0xff) + assert.equal(buf[buf.byteLength - 1], 0xd9) + done() + }, 'image/jpeg', { quality: 0.95 }) + }) + + describe('#toBuffer("raw")', function () { + const canvas = createCanvas(11, 10) + const ctx = canvas.getContext('2d') + + ctx.clearRect(0, 0, 11, 10) + + ctx.fillStyle = 'rgba(200, 200, 200, 0.505)' + ctx.fillRect(0, 0, 5, 5) + + ctx.fillStyle = 'red' + ctx.fillRect(5, 0, 5, 5) + + ctx.fillStyle = '#00ff00' + ctx.fillRect(0, 5, 5, 5) + + ctx.fillStyle = 'black' + ctx.fillRect(5, 5, 4, 5) + + /** Output: + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * *****RRRRR- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + * GGGGGBBBB-- + */ + + const buf = canvas.toBuffer('raw') + const stride = canvas.stride + + const endianness = os.endianness() + + function assertPixel (u32, x, y, message) { + const expected = '0x' + u32.toString(16) + + // Buffer doesn't have readUInt32(): it only has readUInt32LE() and + // readUInt32BE(). + const px = buf['readUInt32' + endianness](y * stride + x * 4) + const actual = '0x' + px.toString(16) + + assert.equal(actual, expected, message) + } + + it('should have the correct size', function () { + assert.equal(buf.length, stride * 10) + }) + + it('does not premultiply alpha', function () { + assertPixel(0x80646464, 0, 0, 'first semitransparent pixel') + assertPixel(0x80646464, 4, 4, 'last semitransparent pixel') + }) + + it('draws red', function () { + assertPixel(0xffff0000, 5, 0, 'first red pixel') + assertPixel(0xffff0000, 9, 4, 'last red pixel') + }) + + it('draws green', function () { + assertPixel(0xff00ff00, 0, 5, 'first green pixel') + assertPixel(0xff00ff00, 4, 9, 'last green pixel') + }) + + it('draws black', function () { + assertPixel(0xff000000, 5, 5, 'first black pixel') + assertPixel(0xff000000, 8, 9, 'last black pixel') + }) + + it('leaves undrawn pixels black, transparent', function () { + assertPixel(0x0, 9, 5, 'first undrawn pixel') + assertPixel(0x0, 9, 9, 'last undrawn pixel') + }) + + it('is immutable', function () { + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 10, 10) + canvas.toBuffer('raw') // (side-effect: flushes canvas) + assertPixel(0xffff0000, 5, 0, 'first red pixel') + }) + }) + + it('Canvas#toBuffer("application/pdf")', function () { + const buf = createCanvas(200, 200, 'pdf').toBuffer('application/pdf') + assert.equal('PDF', buf.slice(1, 4).toString()) + }) + }) describe('#toDataURL()', function () { - var canvas = new Canvas(200, 200) - , ctx = canvas.getContext('2d'); + const canvas = createCanvas(200, 200) + const ctx = canvas.getContext('2d') - ctx.fillRect(0,0,100,100); - ctx.fillStyle = 'red'; - ctx.fillRect(100,0,100,100); + ctx.fillRect(0, 0, 100, 100) + ctx.fillStyle = 'red' + ctx.fillRect(100, 0, 100, 100) it('toDataURL() works and defaults to PNG', function () { - assert.ok(0 == canvas.toDataURL().indexOf('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL().startsWith('data:image/png;base64,')) + }) it('toDataURL(0.5) works and defaults to PNG', function () { - assert.ok(0 == canvas.toDataURL(0.5).indexOf('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL(0.5).startsWith('data:image/png;base64,')) + }) it('toDataURL(undefined) works and defaults to PNG', function () { - assert.ok(0 == canvas.toDataURL(undefined).indexOf('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL(undefined).startsWith('data:image/png;base64,')) + }) it('toDataURL("image/png") works', function () { - assert.ok(0 == canvas.toDataURL('image/png').indexOf('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL('image/png').startsWith('data:image/png;base64,')) + }) it('toDataURL("image/png", 0.5) works', function () { - assert.ok(0 == canvas.toDataURL('image/png').indexOf('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL('image/png').startsWith('data:image/png;base64,')) + }) it('toDataURL("iMaGe/PNg") works', function () { - assert.ok(0 == canvas.toDataURL('iMaGe/PNg').indexOf('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL('iMaGe/PNg').startsWith('data:image/png;base64,')) + }) - it('toDataURL("image/jpeg") throws', function () { - assert.throws( - function () { - canvas.toDataURL('image/jpeg'); - }, - function (err) { - return err.message === 'Missing required callback function for format "image/jpeg"'; - } - ); - }); + it('toDataURL("image/jpeg") works', function () { + assert.ok(canvas.toDataURL('image/jpeg').startsWith('data:image/jpeg;base64,')) + }) it('toDataURL(function (err, str) {...}) works and defaults to PNG', function (done) { - new Canvas(200,200).toDataURL(function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL(function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) + + it('toDataURL(function (err, str) {...}) is async even with no canvas data', function (done) { + createCanvas().toDataURL(function (err, str) { + assert.ifError(err) + assert.ok(str === 'data:,') + done() + }) + }) it('toDataURL(0.5, function (err, str) {...}) works and defaults to PNG', function (done) { - new Canvas(200,200).toDataURL(0.5, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL(0.5, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) it('toDataURL(undefined, function (err, str) {...}) works and defaults to PNG', function (done) { - new Canvas(200,200).toDataURL(undefined, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL(undefined, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) it('toDataURL("image/png", function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/png', function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/png', function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) it('toDataURL("image/png", 0.5, function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/png', 0.5, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/png;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/png', 0.5, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/png;base64,') === 0) + done() + }) + }) it('toDataURL("image/png", {}) works', function () { - assert.ok(0 == canvas.toDataURL('image/png', {}).indexOf('data:image/png;base64,')); - }); + assert.ok(canvas.toDataURL('image/png', {}).startsWith('data:image/png;base64,')) + }) - it('toDataURL("image/jpeg", {}) throws', function () { - assert.throws( - function () { - canvas.toDataURL('image/jpeg', {}); - }, - function (err) { - return err.message === 'Missing required callback function for format "image/jpeg"'; - } - ); - }); + it('toDataURL("image/jpeg", {}) works', function () { + assert.ok(canvas.toDataURL('image/jpeg', {}).startsWith('data:image/jpeg;base64,')) + }) it('toDataURL("image/jpeg", function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/jpeg', function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/jpeg', function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) it('toDataURL("iMAge/JPEG", function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('iMAge/JPEG', function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('iMAge/JPEG', function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) it('toDataURL("image/jpeg", undefined, function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/jpeg', undefined, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/jpeg', undefined, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) it('toDataURL("image/jpeg", 0.5, function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/jpeg', 0.5, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); + createCanvas(200, 200).toDataURL('image/jpeg', 0.5, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) it('toDataURL("image/jpeg", opts, function (err, str) {...}) works', function (done) { - new Canvas(200,200).toDataURL('image/jpeg', {quality: 100}, function(err, str){ - assert.ifError(err); - assert.ok(0 === str.indexOf('data:image/jpeg;base64,')); - done(); - }); - }); - }); + createCanvas(200, 200).toDataURL('image/jpeg', { quality: 100 }, function (err, str) { + assert.ifError(err) + assert.ok(str.indexOf('data:image/jpeg;base64,') === 0) + done() + }) + }) + }) + + describe('Context2d#createImageData(width, height)', function () { + it('works', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + + const imageData = ctx.createImageData(2, 6) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6 * 4, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('works, A8 format', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) + + const imageData = ctx.createImageData(2, 6) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6 * 1, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('works, A1 format', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d', { pixelFormat: 'A1' }) + + const imageData = ctx.createImageData(2, 6) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(Math.ceil(2 * 6 / 8), imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + + it('works, RGB24 format', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d', { pixelFormat: 'RGB24' }) + + const imageData = ctx.createImageData(2, 6) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6 * 4, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('works, RGB16_565 format', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d', { pixelFormat: 'RGB16_565' }) + + const imageData = ctx.createImageData(2, 6) + assert(imageData.data instanceof Uint16Array) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + }) + + describe('Context2d#measureText()', function () { + it('Context2d#measureText().width', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + + assert.ok(ctx.measureText('foo').width) + assert.ok(ctx.measureText('foo').width !== ctx.measureText('foobar').width) + assert.ok(ctx.measureText('foo').width !== ctx.measureText(' foo').width) + }) + + it('works', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + ctx.font = '20px Arial' + + ctx.textBaseline = 'alphabetic' + let metrics = ctx.measureText('Alphabet') + // Actual value depends on font library version. Have observed values + // between 0 and 0.769. + assertApprox(metrics.alphabeticBaseline, 0.5, 0.5) + // Positive = going up from the baseline + assert.ok(metrics.actualBoundingBoxAscent > 0) + // Positive = going down from the baseline + assertApprox(metrics.actualBoundingBoxDescent, 5, 2) + + ctx.textBaseline = 'bottom' + metrics = ctx.measureText('Alphabet') + assert.strictEqual(ctx.textBaseline, 'bottom') + assertApprox(metrics.alphabeticBaseline, 5, 2) + assert.ok(metrics.actualBoundingBoxAscent > 0) + // On the baseline or slightly above + assert.ok(metrics.actualBoundingBoxDescent <= 0) + }) + + it('actualBoundingBox is correct for left, center and right alignment (#1909)', function () { + const canvas = createCanvas(0, 0) + const ctx = canvas.getContext('2d') + + // positive actualBoundingBoxLeft indicates a distance going left from the + // given alignment point. + + // positive actualBoundingBoxRight indicates a distance going right from + // the given alignment point. + + ctx.textAlign = 'left' + const lm = ctx.measureText('aaaa') + assertApprox(lm.actualBoundingBoxLeft, -1, 6) + assertApprox(lm.actualBoundingBoxRight, 21, 6) + + ctx.textAlign = 'center' + const cm = ctx.measureText('aaaa') + assertApprox(cm.actualBoundingBoxLeft, 9, 6) + assertApprox(cm.actualBoundingBoxRight, 11, 6) + + ctx.textAlign = 'right' + const rm = ctx.measureText('aaaa') + assertApprox(rm.actualBoundingBoxLeft, 19, 6) + assertApprox(rm.actualBoundingBoxRight, 1, 6) + }) + + it('resolves text alignment wrt Context2d#direction #2508', function () { + const canvas = createCanvas(0, 0) + const ctx = canvas.getContext('2d') + + ctx.textAlign = "left"; + const leftMetrics = ctx.measureText('hello'); + assert(leftMetrics.actualBoundingBoxLeft < leftMetrics.actualBoundingBoxRight, "leftMetrics.actualBoundingBoxLeft < leftMetrics.actualBoundingBoxRight"); + + ctx.textAlign = "right"; + const rightMetrics = ctx.measureText('hello'); + assert(rightMetrics.actualBoundingBoxLeft > rightMetrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight"); + + ctx.textAlign = "start"; + + ctx.direction = "ltr"; + const ltrStartMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(ltrStartMetrics, leftMetrics, "ltr start metrics should equal left metrics"); + + ctx.direction = "rtl"; + const rtlStartMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(rtlStartMetrics, rightMetrics, "rtl start metrics should equal right metrics"); + + ctx.textAlign = "end"; + + ctx.direction = "ltr"; + const ltrEndMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(ltrEndMetrics, rightMetrics, "ltr end metrics should equal right metrics"); + + ctx.direction = "rtl"; + const rtlEndMetrics = ctx.measureText('hello'); + assert.deepStrictEqual(rtlEndMetrics, leftMetrics, "rtl end metrics should equal left metrics"); + }) + }) + + it('Context2d#fillText()', function () { + [ + [['A', 10, 10], true], + [['A', 10, 10, undefined], true], + [['A', 10, 10, NaN], false] + ].forEach(([args, shouldDraw]) => { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + ctx.fillText(...args) + + assert.strictEqual(ctx.getImageData(0, 0, 20, 20).data.some(a => a), shouldDraw) + }) + }) + + it('Context2d#currentTransform', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + + ctx.scale(0.1, 0.3) + const mat1 = ctx.currentTransform + assert.equal(mat1.a, 0.1) + assert.equal(mat1.b, 0) + assert.equal(mat1.c, 0) + assert.equal(mat1.d, 0.3) + assert.equal(mat1.e, 0) + assert.equal(mat1.f, 0) + + ctx.resetTransform() + const mat2 = ctx.currentTransform + assert.equal(mat2.a, 1) + assert.equal(mat2.d, 1) + + ctx.currentTransform = mat1 + const mat3 = ctx.currentTransform + assert.equal(mat3.a, 0.1) + assert.equal(mat3.d, 0.3) + + assert.deepEqual(ctx.currentTransform, ctx.getTransform()) + + ctx.setTransform(ctx.getTransform()) + assert.deepEqual(mat3, ctx.getTransform()) + + ctx.setTransform(mat3.a, mat3.b, mat3.c, mat3.d, mat3.e, mat3.f) + assert.deepEqual(mat3, ctx.getTransform()) + }) - it('Context2d#createImageData(width, height)', function () { - var canvas = new Canvas(20, 20) - , ctx = canvas.getContext('2d'); + it('Context2d#createImageData(ImageData)', function () { + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') - var imageData = ctx.createImageData(2,6); - assert.equal(2, imageData.width); - assert.equal(6, imageData.height); - assert.equal(2 * 6 * 4, imageData.data.length); + const imageData = ctx.createImageData(ctx.createImageData(2, 6)) + assert.equal(2, imageData.width) + assert.equal(6, imageData.height) + assert.equal(2 * 6 * 4, imageData.data.length) + }) - assert.equal(0, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(0, imageData.data[3]); - }); + describe('Context2d#getImageData()', function () { + function createTestCanvas (useAlpha, attributes) { + const canvas = createCanvas(3, 6) + const ctx = canvas.getContext('2d', attributes) - it('Context2d#measureText().width', function () { - var canvas = new Canvas(20, 20) - , ctx = canvas.getContext('2d'); + ctx.fillStyle = useAlpha ? 'rgba(255,0,0,0.25)' : '#f00' + ctx.fillRect(0, 0, 1, 6) - assert.ok(ctx.measureText('foo').width); - assert.ok(ctx.measureText('foo').width != ctx.measureText('foobar').width); - assert.ok(ctx.measureText('foo').width != ctx.measureText(' foo').width); - }); + ctx.fillStyle = useAlpha ? 'rgba(0,255,0,0.5)' : '#0f0' + ctx.fillRect(1, 0, 1, 6) - it('Context2d#createImageData(ImageData)', function () { - var canvas = new Canvas(20, 20) - , ctx = canvas.getContext('2d'); + ctx.fillStyle = useAlpha ? 'rgba(0,0,255,0.75)' : '#00f' + ctx.fillRect(2, 0, 1, 6) - var imageData = ctx.createImageData(ctx.createImageData(2, 6)); - assert.equal(2, imageData.width); - assert.equal(6, imageData.height); - assert.equal(2 * 6 * 4, imageData.data.length); - }); + return ctx + } - it('Context2d#getImageData()', function () { - var canvas = new Canvas(3, 6) - , ctx = canvas.getContext('2d'); - - ctx.fillStyle = '#f00'; - ctx.fillRect(0,0,1,6); - - ctx.fillStyle = '#0f0'; - ctx.fillRect(1,0,1,6); - - ctx.fillStyle = '#00f'; - ctx.fillRect(2,0,1,6); - - // Full width - var imageData = ctx.getImageData(0,0,3,6); - assert.equal(3, imageData.width); - assert.equal(6, imageData.height); - assert.equal(3 * 6 * 4, imageData.data.length); - - assert.equal(255, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); - - assert.equal(0, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(0, imageData.data[6]); - assert.equal(255, imageData.data[7]); - - assert.equal(0, imageData.data[8]); - assert.equal(0, imageData.data[9]); - assert.equal(255, imageData.data[10]); - assert.equal(255, imageData.data[11]); - - // Slice - var imageData = ctx.getImageData(0,0,2,1); - assert.equal(2, imageData.width); - assert.equal(1, imageData.height); - assert.equal(8, imageData.data.length); - - assert.equal(255, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); - - assert.equal(0, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(0, imageData.data[6]); - assert.equal(255, imageData.data[7]); - - // Assignment - var data = ctx.getImageData(0,0,5,5).data; - data[0] = 50; - assert.equal(50, data[0]); - data[0] = 280; - assert.equal(255, data[0]); - data[0] = -4444; - assert.equal(0, data[0]); - }); + it('works, full width, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, 0, 3, 6) + + assert.equal(3, imageData.width) + assert.equal(6, imageData.height) + assert.equal(3 * 6 * 4, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(0, imageData.data[9]) + assert.equal(255, imageData.data[10]) + assert.equal(255, imageData.data[11]) + }) + + it('works, full width, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, 0, 3, 6) + assert.equal(3, imageData.width) + assert.equal(6, imageData.height) + assert.equal(3 * 6 * 4, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(0, imageData.data[9]) + assert.equal(255, imageData.data[10]) + assert.equal(255, imageData.data[11]) + }) + + it('works, full width, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, 0, 3, 6) + assert.equal(3, imageData.width) + assert.equal(6, imageData.height) + assert.equal(3 * 6, imageData.data.length) + + assert.equal((255 & 0b11111) << 11, imageData.data[0]) + assert.equal((255 & 0b111111) << 5, imageData.data[1]) + assert.equal((255 & 0b11111), imageData.data[2]) + + assert.equal((255 & 0b11111) << 11, imageData.data[3]) + assert.equal((255 & 0b111111) << 5, imageData.data[4]) + assert.equal((255 & 0b11111), imageData.data[5]) + }) + + it('works, full width, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, 0, 3, 6) + assert.equal(3, imageData.width) + assert.equal(6, imageData.height) + assert.equal(3 * 6, imageData.data.length) + + assert.equal(63, imageData.data[0]) + assert.equal(127, imageData.data[1]) + assert.equal(191, imageData.data[2]) + + assert.equal(63, imageData.data[3]) + assert.equal(127, imageData.data[4]) + assert.equal(191, imageData.data[5]) + }) + + it('works, full width, A1') + + it('works, full width, RGB30') + + it('works, slice, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, slice, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, slice, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2 * 1, imageData.data.length) + + assert.equal((255 & 0b11111) << 11, imageData.data[0]) + assert.equal((255 & 0b111111) << 5, imageData.data[1]) + }) + + it('works, slice, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2 * 1, imageData.data.length) + + assert.equal(63, imageData.data[0]) + assert.equal(127, imageData.data[1]) + }) + + it('works, slice, A1') + + it('works, slice, RGB30') + + describe('slice partially outside the canvas', function () { + describe('left', function () { + if('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + }) + }) + + describe('right', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(255, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(255, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal((255 & 0b11111), imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(191, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + }) + + describe('left and right', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(20, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(255, imageData.data[9]) + assert.equal(0, imageData.data[10]) + assert.equal(255, imageData.data[11]) + + assert.equal(0, imageData.data[12]) + assert.equal(0, imageData.data[13]) + assert.equal(255, imageData.data[14]) + assert.equal(255, imageData.data[15]) + + assert.equal(0, imageData.data[16]) + assert.equal(0, imageData.data[17]) + assert.equal(0, imageData.data[18]) + assert.equal(0, imageData.data[19]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(20, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(255, imageData.data[9]) + assert.equal(0, imageData.data[10]) + assert.equal(255, imageData.data[11]) + + assert.equal(0, imageData.data[12]) + assert.equal(0, imageData.data[13]) + assert.equal(255, imageData.data[14]) + assert.equal(255, imageData.data[15]) + + assert.equal(0, imageData.data[16]) + assert.equal(0, imageData.data[17]) + assert.equal(0, imageData.data[18]) + assert.equal(0, imageData.data[19]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(5, imageData.data.length) + + assert.equal(0, imageData.data[0]) + + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + assert.equal((255 & 0b111111) << 5, imageData.data[2]) + assert.equal((255 & 0b11111), imageData.data[3]) + + assert.equal(0, imageData.data[4]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(5, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + assert.equal(127, imageData.data[2]) + assert.equal(191, imageData.data[3]) + assert.equal(0, imageData.data[4]) + }) + }) + + describe('top', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + }) + }) + + describe('bottom', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal((255 & 0b11111) << 11, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(63, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + }) + + describe('top to bottom', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(32, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(255, imageData.data[24]) + assert.equal(0, imageData.data[25]) + assert.equal(0, imageData.data[26]) + assert.equal(255, imageData.data[27]) + + assert.equal(0, imageData.data[28]) + assert.equal(0, imageData.data[29]) + assert.equal(0, imageData.data[30]) + assert.equal(0, imageData.data[31]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(32, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(255, imageData.data[24]) + assert.equal(0, imageData.data[25]) + assert.equal(0, imageData.data[26]) + assert.equal(255, imageData.data[27]) + + assert.equal(0, imageData.data[28]) + assert.equal(0, imageData.data[29]) + assert.equal(0, imageData.data[30]) + assert.equal(0, imageData.data[31]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + assert.equal((255 & 0b11111) << 11, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + assert.equal(63, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + }) + }) + + it('works, assignment', function () { + const ctx = createTestCanvas() + const data = ctx.getImageData(0, 0, 5, 5).data + data[0] = 50 + assert.equal(50, data[0]) + data[0] = 280 + assert.equal(255, data[0]) + data[0] = -4444 + assert.equal(0, data[0]) + }) + + it('throws if indexes are invalid', function () { + const ctx = createTestCanvas() + assert.throws(function () { ctx.getImageData(0, 0, 0, 0) }, /IndexSizeError/) + }) + + it('throws if canvas is a PDF canvas (#1853)', function () { + const canvas = createCanvas(3, 6, 'pdf') + const ctx = canvas.getContext('2d') + assert.throws(() => { + ctx.getImageData(0, 0, 3, 6) + }) + }) + + describe('does not throw if rectangle is outside the canvas (#2024)', function () { + it('on the left', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(-11, 0, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('on the right', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(98, 0, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('on the top', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(0, -12, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('on the bottom', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(0, 98, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + }) + }) it('Context2d#createPattern(Canvas)', function () { - var pattern = new Canvas(2,2) - , checkers = pattern.getContext('2d'); + let pattern = createCanvas(2, 2) + const checkers = pattern.getContext('2d') // white - checkers.fillStyle = '#fff'; - checkers.fillRect(0,0,2,2); + checkers.fillStyle = '#fff' + checkers.fillRect(0, 0, 2, 2) // black - checkers.fillStyle = '#000'; - checkers.fillRect(0,0,1,1); - checkers.fillRect(1,1,1,1); + checkers.fillStyle = '#000' + checkers.fillRect(0, 0, 1, 1) + checkers.fillRect(1, 1, 1, 1) - var imageData = checkers.getImageData(0,0,2,2); - assert.equal(2, imageData.width); - assert.equal(2, imageData.height); - assert.equal(16, imageData.data.length); + let imageData = checkers.getImageData(0, 0, 2, 2) + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(16, imageData.data.length) // (0,0) black - assert.equal(0, imageData.data[0]); - assert.equal(0, imageData.data[1]); - assert.equal(0, imageData.data[2]); - assert.equal(255, imageData.data[3]); + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) // (1,0) white - assert.equal(255, imageData.data[4]); - assert.equal(255, imageData.data[5]); - assert.equal(255, imageData.data[6]); - assert.equal(255, imageData.data[7]); + assert.equal(255, imageData.data[4]) + assert.equal(255, imageData.data[5]) + assert.equal(255, imageData.data[6]) + assert.equal(255, imageData.data[7]) // (0,1) white - assert.equal(255, imageData.data[8]); - assert.equal(255, imageData.data[9]); - assert.equal(255, imageData.data[10]); - assert.equal(255, imageData.data[11]); + assert.equal(255, imageData.data[8]) + assert.equal(255, imageData.data[9]) + assert.equal(255, imageData.data[10]) + assert.equal(255, imageData.data[11]) // (1,1) black - assert.equal(0, imageData.data[12]); - assert.equal(0, imageData.data[13]); - assert.equal(0, imageData.data[14]); - assert.equal(255, imageData.data[15]); - - var canvas = new Canvas(20, 20) - , ctx = canvas.getContext('2d') - , pattern = ctx.createPattern(pattern); - - ctx.fillStyle = pattern; - ctx.fillRect(0,0,20,20); - - var imageData = ctx.getImageData(0,0,20,20); - assert.equal(20, imageData.width); - assert.equal(20, imageData.height); - assert.equal(1600, imageData.data.length); - - var i=0, b = true; - while(i=width/2 && y>=height/2) ? 255 : 0; + func(i, clr); + } + } + } + + // create a canvas with a single repeat of the pattern within its dims + function makeCheckerboard(w, h){ + let check = createCanvas(w, h), + ctx = check.getContext('2d'), + bmp = ctx.createImageData(w, h); + eachPixel(bmp, (i, clr) => bmp.data.set([clr,clr,clr, 255], i)); + ctx.putImageData(bmp, 0, 0); + return check; + } + + // verify that the region looks like a single 4-quadrant checkerboard cell + function isCheckerboard(ctx, w, h){ + let bmp = ctx.getImageData(0, 0, w, h); + eachPixel(bmp, (i, clr) => { + let [r, g, b, a] = bmp.data.slice(i, i+4); + assert.ok(r==clr && g==clr && b==clr && a==255); + }) + } + + let w = 160, h = 160, + canvas = createCanvas(w, h), + ctx = canvas.getContext('2d'), + pat = ctx.createPattern(makeCheckerboard(w, h), 'repeat'), + mat = new DOMMatrix(); + + ctx.patternQuality='nearest'; + ctx.fillStyle = pat; + + // draw a single repeat of the pattern at each scale and then confirm that + // the transformation succeeded + [1, .5, .25, .125, 0.0625].forEach(mag => { + mat = new DOMMatrix().scale(mag); + pat.setTransform(mat); + ctx.fillRect(0,0, w*mag, h*mag); + isCheckerboard(ctx, w*mag, h*mag); + }) }); - it('Context2d#createPattern(Image)', function () { - var img = new Canvas.Image(); - img.src = __dirname + '/fixtures/checkers.png'; - - var canvas = new Canvas(20, 20) - , ctx = canvas.getContext('2d') - , pattern = ctx.createPattern(img); - - ctx.fillStyle = pattern; - ctx.fillRect(0,0,20,20); - - var imageData = ctx.getImageData(0,0,20,20); - assert.equal(20, imageData.width); - assert.equal(20, imageData.height); - assert.equal(1600, imageData.data.length); - - var i=0, b = true; - while(i { + ctx.putImageData(srcImageData, -1, -1) + }) + }) + + it('works for negative source values', function () { + const canvas = createCanvas(2, 2) + const ctx = canvas.getContext('2d') + const srcImageData = createImageData(new Uint8ClampedArray([ + 1, 2, 3, 255, 5, 6, 7, 255, + 0, 1, 2, 255, 4, 5, 6, 255 + ]), 2) + + ctx.putImageData(srcImageData, -1, -1) + + const resImageData = ctx.getImageData(0, 0, 2, 2) + assert.deepEqual(resImageData.data, new Uint8ClampedArray([ + 4, 5, 6, 255, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ])) + }) + + it('works, RGBA32', function () { + const canvas = createCanvas(2, 1) + const ctx = canvas.getContext('2d') + ctx.fillStyle = '#f00' + ctx.fillRect(0, 0, 1, 1) + + // Copy left pixel to the right pixel + ctx.putImageData(ctx.getImageData(0, 0, 1, 1), 1, 0) + + const pixel = ctx.getImageData(1, 0, 1, 1) + + assert.equal(pixel.data[0], 255) + assert.equal(pixel.data[1], 0) + assert.equal(pixel.data[2], 0) + assert.equal(pixel.data[3], 255) + }) + + it('works, RGB24/alpha:false', function () { + const canvas = createCanvas(2, 1) + const ctx = canvas.getContext('2d', { pixelFormat: 'RGB24' }) + ctx.fillStyle = '#f00' + ctx.fillRect(0, 0, 1, 1) + + // Copy left pixel to the right pixel + ctx.putImageData(ctx.getImageData(0, 0, 1, 1), 1, 0) + + const pixel = ctx.getImageData(1, 0, 1, 1) + + assert.equal(pixel.data[0], 255) + assert.equal(pixel.data[1], 0) + assert.equal(pixel.data[2], 0) + assert.equal(pixel.data[3], 255) + }) + + it('works, A8', function () { + const canvas = createCanvas(2, 1) + const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) + + const imgData = ctx.getImageData(0, 0, 2, 1) + imgData.data[0] = 4 + imgData.data[1] = 21 + ctx.putImageData(imgData, 0, 0) + + const pixel = ctx.getImageData(0, 0, 2, 1) + + assert.equal(pixel.data[0], 4) + assert.equal(pixel.data[1], 21) + }) + + it('works, RGB16_565', function () { + const canvas = createCanvas(2, 1) + const ctx = canvas.getContext('2d', { pixelFormat: 'RGB16_565' }) + + const imgData = ctx.getImageData(0, 0, 2, 1) + imgData.data[0] = 65535 // 2**16 - 1 + imgData.data[1] = 65500 + ctx.putImageData(imgData, 0, 0) + + const pixel = ctx.getImageData(0, 0, 2, 1) + + assert.equal(pixel.data[0], 65535) + assert.equal(pixel.data[1], 65500) + }) + }) + + it('Canvas#createPNGStream()', function (done) { + const canvas = createCanvas(20, 20) + const stream = canvas.createPNGStream() + assert(stream instanceof Readable) + let firstChunk = true + stream.on('data', function (chunk) { if (firstChunk) { - firstChunk = false; - assert.equal('PNG', chunk.slice(1,4).toString()); + firstChunk = false + assert.equal('PNG', chunk.slice(1, 4).toString()) } - }); - stream.on('end', function(){ - done(); - }); - stream.on('error', function(err) { - done(err); - }); - }); - - it('Canvas#createSyncPDFStream()', function (done) { - var canvas = new Canvas(20, 20, 'pdf'); - var stream = canvas.createSyncPDFStream(); - var firstChunk = true; + }) + stream.on('end', function () { + done() + }) + stream.on('error', function (err) { + done(err) + }) + }) + + it('Canvas#createPDFStream()', function (done) { + const canvas = createCanvas(20, 20, 'pdf') + const stream = canvas.createPDFStream() + assert(stream instanceof Readable) + let firstChunk = true stream.on('data', function (chunk) { if (firstChunk) { - firstChunk = false; - assert.equal('PDF', chunk.slice(1, 4).toString()); + firstChunk = false + assert.equal(chunk.slice(1, 4).toString(), 'PDF') } - }); + }) stream.on('end', function () { - done(); - }); + done() + }) stream.on('error', function (err) { - done(err); - }); - }); - - it('Canvas#jpegStream()', function (done) { - var canvas = new Canvas(640, 480); - var stream = canvas.jpegStream(); - var firstChunk = true; - var bytes = 0; - stream.on('data', function(chunk){ + done(err) + }) + }) + + it('Canvas#createJPEGStream()', function (done) { + const canvas = createCanvas(640, 480) + const stream = canvas.createJPEGStream() + assert(stream instanceof Readable) + let firstChunk = true + let bytes = 0 + stream.on('data', function (chunk) { if (firstChunk) { - firstChunk = false; - assert.equal(0xFF, chunk[0]); - assert.equal(0xD8, chunk[1]); - assert.equal(0xFF, chunk[2]); + firstChunk = false + assert.equal(0xFF, chunk[0]) + assert.equal(0xD8, chunk[1]) + assert.equal(0xFF, chunk[2]) } - bytes += chunk.length; - }); - stream.on('end', function(){ - assert.equal(bytes, 5427); - done(); - }); - stream.on('error', function(err) { - done(err); - }); - }); - - it('Canvas#jpegStream() should clamp buffer size (#674)', function (done) { - var c = new Canvas(10, 10); - var SIZE = 10 * 1024 * 1024; - var s = c.jpegStream({bufsize: SIZE}); - s.on('data', function (chunk) { - assert(chunk.length < SIZE); - }); - s.on('end', done); - }); + bytes += chunk.length + }) + stream.on('end', function () { + assert.equal(bytes, 5427) + done() + }) + stream.on('error', function (err) { + done(err) + }) + }) + + // based on https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format + // end of image marker (FF D9) must exist to maintain JPEG standards + it('EOI at end of Canvas#createJPEGStream()', function (done) { + const canvas = createCanvas(640, 480) + const stream = canvas.createJPEGStream() + const chunks = [] + stream.on('data', function (chunk) { + chunks.push(chunk) + }) + stream.on('end', function () { + const lastTwoBytes = chunks.pop().slice(-2) + assert.equal(0xFF, lastTwoBytes[0]) + assert.equal(0xD9, lastTwoBytes[1]) + done() + }) + stream.on('error', function (err) { + done(err) + }) + }) - it('Context2d#fill()', function() { - var canvas = new Canvas(2, 2); - var ctx = canvas.getContext('2d'); + it('Context2d#fill()', function () { + const canvas = createCanvas(2, 2) + const ctx = canvas.getContext('2d') // fill whole canvas with white - ctx.fillStyle = '#fff'; - ctx.fillRect(0, 0, 2, 2); + ctx.fillStyle = '#fff' + ctx.fillRect(0, 0, 2, 2) - var imageData, n; + let imageData, n // black - ctx.fillStyle = '#000'; - ctx.rect(0, 0, 2, 1); - ctx.rect(1, 0, 1, 2); + ctx.fillStyle = '#000' + ctx.rect(0, 0, 2, 1) + ctx.rect(1, 0, 1, 2) - ctx.fill('evenodd'); + ctx.fill('evenodd') // b | w // ----- // w | b - imageData = ctx.getImageData(0, 0, 2, 2); + imageData = ctx.getImageData(0, 0, 2, 2) // (0, 0) black - n = 0; - assert.equal(imageData.data[n*4+0], 0); - assert.equal(imageData.data[n*4+1], 0); - assert.equal(imageData.data[n*4+2], 0); - assert.equal(imageData.data[n*4+3], 255); + n = 0 + assert.equal(imageData.data[n * 4 + 0], 0) + assert.equal(imageData.data[n * 4 + 1], 0) + assert.equal(imageData.data[n * 4 + 2], 0) + assert.equal(imageData.data[n * 4 + 3], 255) // (0, 1) white - n = 1; - assert.equal(imageData.data[n*4+0], 255); - assert.equal(imageData.data[n*4+1], 255); - assert.equal(imageData.data[n*4+2], 255); - assert.equal(imageData.data[n*4+3], 255); + n = 1 + assert.equal(imageData.data[n * 4 + 0], 255) + assert.equal(imageData.data[n * 4 + 1], 255) + assert.equal(imageData.data[n * 4 + 2], 255) + assert.equal(imageData.data[n * 4 + 3], 255) // (1, 0) white - n = 2; - assert.equal(imageData.data[n*4+0], 255); - assert.equal(imageData.data[n*4+1], 255); - assert.equal(imageData.data[n*4+2], 255); - assert.equal(imageData.data[n*4+3], 255); + n = 2 + assert.equal(imageData.data[n * 4 + 0], 255) + assert.equal(imageData.data[n * 4 + 1], 255) + assert.equal(imageData.data[n * 4 + 2], 255) + assert.equal(imageData.data[n * 4 + 3], 255) // (1, 1) black - n = 3; - assert.equal(imageData.data[n*4+0], 0); - assert.equal(imageData.data[n*4+1], 0); - assert.equal(imageData.data[n*4+2], 0); - assert.equal(imageData.data[n*4+3], 255); + n = 3 + assert.equal(imageData.data[n * 4 + 0], 0) + assert.equal(imageData.data[n * 4 + 1], 0) + assert.equal(imageData.data[n * 4 + 2], 0) + assert.equal(imageData.data[n * 4 + 3], 255) // should not retain previous value 'evenodd' - ctx.fill(); + ctx.fill() // b | b // ----- // w | b - imageData = ctx.getImageData(0, 0, 2, 2); + imageData = ctx.getImageData(0, 0, 2, 2) // (0, 0) black - n = 0; - assert.equal(imageData.data[n*4+0], 0); - assert.equal(imageData.data[n*4+1], 0); - assert.equal(imageData.data[n*4+2], 0); - assert.equal(imageData.data[n*4+3], 255); + n = 0 + assert.equal(imageData.data[n * 4 + 0], 0) + assert.equal(imageData.data[n * 4 + 1], 0) + assert.equal(imageData.data[n * 4 + 2], 0) + assert.equal(imageData.data[n * 4 + 3], 255) // (0, 1) black - n = 1; - assert.equal(imageData.data[n*4+0], 0); - assert.equal(imageData.data[n*4+1], 0); - assert.equal(imageData.data[n*4+2], 0); - assert.equal(imageData.data[n*4+3], 255); + n = 1 + assert.equal(imageData.data[n * 4 + 0], 0) + assert.equal(imageData.data[n * 4 + 1], 0) + assert.equal(imageData.data[n * 4 + 2], 0) + assert.equal(imageData.data[n * 4 + 3], 255) // (1, 0) white - n = 2; - assert.equal(imageData.data[n*4+0], 255); - assert.equal(imageData.data[n*4+1], 255); - assert.equal(imageData.data[n*4+2], 255); - assert.equal(imageData.data[n*4+3], 255); + n = 2 + assert.equal(imageData.data[n * 4 + 0], 255) + assert.equal(imageData.data[n * 4 + 1], 255) + assert.equal(imageData.data[n * 4 + 2], 255) + assert.equal(imageData.data[n * 4 + 3], 255) // (1, 1) black - n = 3; - assert.equal(imageData.data[n*4+0], 0); - assert.equal(imageData.data[n*4+1], 0); - assert.equal(imageData.data[n*4+2], 0); - assert.equal(imageData.data[n*4+3], 255); - }); + n = 3 + assert.equal(imageData.data[n * 4 + 0], 0) + assert.equal(imageData.data[n * 4 + 1], 0) + assert.equal(imageData.data[n * 4 + 2], 0) + assert.equal(imageData.data[n * 4 + 3], 255) + }) it('Context2d#clip()', function () { - var canvas = new Canvas(2, 2); - var ctx = canvas.getContext('2d'); + const canvas = createCanvas(2, 2) + const ctx = canvas.getContext('2d') // fill whole canvas with white - ctx.fillStyle = '#fff'; - ctx.fillRect(0, 0, 2, 2); + ctx.fillStyle = '#fff' + ctx.fillRect(0, 0, 2, 2) - var imageData, n; + let imageData, n // black - ctx.fillStyle = '#000'; - ctx.rect(0, 0, 2, 1); - ctx.rect(1, 0, 1, 2); + ctx.fillStyle = '#000' + ctx.rect(0, 0, 2, 1) + ctx.rect(1, 0, 1, 2) - ctx.clip('evenodd'); - ctx.fillRect(0, 0, 2, 2); + ctx.clip('evenodd') + ctx.fillRect(0, 0, 2, 2) // b | w // ----- // w | b - imageData = ctx.getImageData(0, 0, 2, 2); + imageData = ctx.getImageData(0, 0, 2, 2) // (0, 0) black - n = 0; - assert.equal(imageData.data[n*4+0], 0); - assert.equal(imageData.data[n*4+1], 0); - assert.equal(imageData.data[n*4+2], 0); - assert.equal(imageData.data[n*4+3], 255); + n = 0 + assert.equal(imageData.data[n * 4 + 0], 0) + assert.equal(imageData.data[n * 4 + 1], 0) + assert.equal(imageData.data[n * 4 + 2], 0) + assert.equal(imageData.data[n * 4 + 3], 255) // (0, 1) white - n = 1; - assert.equal(imageData.data[n*4+0], 255); - assert.equal(imageData.data[n*4+1], 255); - assert.equal(imageData.data[n*4+2], 255); - assert.equal(imageData.data[n*4+3], 255); + n = 1 + assert.equal(imageData.data[n * 4 + 0], 255) + assert.equal(imageData.data[n * 4 + 1], 255) + assert.equal(imageData.data[n * 4 + 2], 255) + assert.equal(imageData.data[n * 4 + 3], 255) // (1, 0) white - n = 2; - assert.equal(imageData.data[n*4+0], 255); - assert.equal(imageData.data[n*4+1], 255); - assert.equal(imageData.data[n*4+2], 255); - assert.equal(imageData.data[n*4+3], 255); + n = 2 + assert.equal(imageData.data[n * 4 + 0], 255) + assert.equal(imageData.data[n * 4 + 1], 255) + assert.equal(imageData.data[n * 4 + 2], 255) + assert.equal(imageData.data[n * 4 + 3], 255) // (1, 1) black - n = 3; - assert.equal(imageData.data[n*4+0], 0); - assert.equal(imageData.data[n*4+1], 0); - assert.equal(imageData.data[n*4+2], 0); - assert.equal(imageData.data[n*4+3], 255); - - ctx.clip(); - ctx.fillRect(0, 0, 2, 2); + n = 3 + assert.equal(imageData.data[n * 4 + 0], 0) + assert.equal(imageData.data[n * 4 + 1], 0) + assert.equal(imageData.data[n * 4 + 2], 0) + assert.equal(imageData.data[n * 4 + 3], 255) + + ctx.clip() + ctx.fillRect(0, 0, 2, 2) // b | b // ----- // w | b - imageData = ctx.getImageData(0, 0, 2, 2); + imageData = ctx.getImageData(0, 0, 2, 2) // (0, 0) black - n = 0; - assert.equal(imageData.data[n*4+0], 0); - assert.equal(imageData.data[n*4+1], 0); - assert.equal(imageData.data[n*4+2], 0); - assert.equal(imageData.data[n*4+3], 255); + n = 0 + assert.equal(imageData.data[n * 4 + 0], 0) + assert.equal(imageData.data[n * 4 + 1], 0) + assert.equal(imageData.data[n * 4 + 2], 0) + assert.equal(imageData.data[n * 4 + 3], 255) // (0, 1) white - n = 1; - assert.equal(imageData.data[n*4+0], 255); - assert.equal(imageData.data[n*4+1], 255); - assert.equal(imageData.data[n*4+2], 255); - assert.equal(imageData.data[n*4+3], 255); + n = 1 + assert.equal(imageData.data[n * 4 + 0], 255) + assert.equal(imageData.data[n * 4 + 1], 255) + assert.equal(imageData.data[n * 4 + 2], 255) + assert.equal(imageData.data[n * 4 + 3], 255) // (1, 0) white - n = 2; - assert.equal(imageData.data[n*4+0], 255); - assert.equal(imageData.data[n*4+1], 255); - assert.equal(imageData.data[n*4+2], 255); - assert.equal(imageData.data[n*4+3], 255); + n = 2 + assert.equal(imageData.data[n * 4 + 0], 255) + assert.equal(imageData.data[n * 4 + 1], 255) + assert.equal(imageData.data[n * 4 + 2], 255) + assert.equal(imageData.data[n * 4 + 3], 255) // (1, 1) black - n = 3; - assert.equal(imageData.data[n*4+0], 0); - assert.equal(imageData.data[n*4+1], 0); - assert.equal(imageData.data[n*4+2], 0); - assert.equal(imageData.data[n*4+3], 255); - }); + n = 3 + assert.equal(imageData.data[n * 4 + 0], 0) + assert.equal(imageData.data[n * 4 + 1], 0) + assert.equal(imageData.data[n * 4 + 2], 0) + assert.equal(imageData.data[n * 4 + 3], 255) + }) it('Context2d#IsPointInPath()', function () { - var canvas = new Canvas(4, 4); - var ctx = canvas.getContext('2d'); - - ctx.rect(0, 0, 4, 2); - ctx.rect(2, 0, 2, 4); - ctx.stroke(); - - assert.ok(ctx.isPointInPath(1, 1, 'evenodd')); - assert.ok(!ctx.isPointInPath(3, 1, 'evenodd')); - assert.ok(ctx.isPointInPath(3, 1)); - assert.ok(!ctx.isPointInPath(1, 3, 'evenodd')); - assert.ok(ctx.isPointInPath(3, 3, 'evenodd')); - }); + const canvas = createCanvas(4, 4) + const ctx = canvas.getContext('2d') + + ctx.rect(0, 0, 4, 2) + ctx.rect(2, 0, 2, 4) + ctx.stroke() + + assert.ok(ctx.isPointInPath(1, 1, 'evenodd')) + assert.ok(!ctx.isPointInPath(3, 1, 'evenodd')) + assert.ok(ctx.isPointInPath(3, 1)) + assert.ok(!ctx.isPointInPath(1, 3, 'evenodd')) + assert.ok(ctx.isPointInPath(3, 3, 'evenodd')) + }) + + it('Context2d#rotate(angle)', function () { + const canvas = createCanvas(4, 4) + const ctx = canvas.getContext('2d') + + // Number + ctx.resetTransform() + testAngle(1.23, 1.23) + + // String + ctx.resetTransform() + testAngle('-4.56e-1', -0.456) + + // Boolean + ctx.resetTransform() + testAngle(true, 1) + + // Array + ctx.resetTransform() + testAngle([7.8], 7.8) + + // Object + const obj = Object.create(null) + if (+process.version.match(/\d+/) >= 6) { obj[Symbol.toPrimitive] = function () { return 0.89 } } else { obj.valueOf = function () { return 0.89 } } + ctx.resetTransform() + testAngle(obj, 0.89) + + // NaN + ctx.resetTransform() + ctx.rotate(0.91) + testAngle(NaN, 0.91) + + // Infinite value + ctx.resetTransform() + ctx.rotate(0.94) + testAngle(-Infinity, 0.94) + + function testAngle (angle, expected) { + ctx.rotate(angle) + + const mat = ctx.currentTransform + const sin = Math.sin(expected) + const cos = Math.cos(expected) + + assert.ok(Math.abs(mat.m11 - cos) < Number.EPSILON) + assert.ok(Math.abs(mat.m12 - sin) < Number.EPSILON) + assert.ok(Math.abs(mat.m21 + sin) < Number.EPSILON) + assert.ok(Math.abs(mat.m22 - cos) < Number.EPSILON) + } + }) + + it('Context2d#drawImage()', function () { + const canvas = createCanvas(500, 500) + const ctx = canvas.getContext('2d') + + // Drawing canvas to itself + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 500, 500) + ctx.fillStyle = 'black' + ctx.fillRect(5, 5, 10, 10) + ctx.drawImage(canvas, 20, 20) + + let imgd = ctx.getImageData(0, 0, 500, 500) + let data = imgd.data + let count = 0 + + for (let i = 0; i < 500 * 500 * 4; i += 4) { + if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) { count++ } + } -}); + assert.strictEqual(count, 10 * 10 * 2) + + // Drawing zero-width image + ctx.drawImage(canvas, 0, 0, 0, 0, 0, 0, 0, 0) + ctx.drawImage(canvas, 0, 0, 0, 0, 1, 1, 1, 1) + ctx.drawImage(canvas, 1, 1, 1, 1, 0, 0, 0, 0) + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 500, 500) + + imgd = ctx.getImageData(0, 0, 500, 500) + data = imgd.data + count = 0 + + for (let i = 0; i < 500 * 500 * 4; i += 4) { + if (data[i] === 255 && data[i + 1] === 255 && data[i + 2] === 255) { count++ } + } + + assert.strictEqual(count, 500 * 500) + }) + + it('Context2d#SetFillColor()', function () { + const canvas = createCanvas(2, 2) + const ctx = canvas.getContext('2d') + + ctx.fillStyle = '#808080' + ctx.fillRect(0, 0, 2, 2) + const data = ctx.getImageData(0, 0, 2, 2).data + + data.forEach(function (byte, index) { + if (index + 1 & 3) { assert.strictEqual(byte, 128) } else { assert.strictEqual(byte, 255) } + }) + }) + + describe('Context2d#save()/restore()', function () { + // Based on WPT meta:2d.state.saverestore + const state = [ // non-default values to test with + ['strokeStyle', '#ff0000'], + ['fillStyle', '#ff0000'], + ['globalAlpha', 0.5], + ['lineWidth', 0.5], + ['lineCap', 'round'], + ['lineJoin', 'round'], + ['miterLimit', 0.5], + ['shadowOffsetX', 5], + ['shadowOffsetY', 5], + ['shadowBlur', 5], + ['shadowColor', '#ff0000'], + ['globalCompositeOperation', 'copy'], + ['font', '25px serif'], + ['textAlign', 'center'], + ['textBaseline', 'bottom'], + // Added vs. WPT + ['imageSmoothingEnabled', false], + // ['imageSmoothingQuality', ], // not supported by node-canvas, #2114 + ['lineDashOffset', 1.0], + // Non-standard properties: + ['patternQuality', 'best'], + // ['quality', 'best'], // doesn't do anything, TODO remove + ['textDrawingMode', 'glyph'], + ['antialias', 'gray'], + ['lang', 'eu'] + ] + + for (const [k, v] of state) { + it(`2d.state.saverestore.${k}`, function () { + const canvas = createCanvas(0, 0) + const ctx = canvas.getContext('2d') + + // restore() undoes modification: + let old = ctx[k] + ctx.save() + ctx[k] = v + ctx.restore() + assert.strictEqual(ctx[k], old) + + // save() doesn't modify the value: + ctx[k] = v + old = ctx[k] + ctx.save() + assert.strictEqual(ctx[k], old) + ctx.restore() + }) + } + }) + + describe('Context2d#beingTag()/endTag()', function () { + before(function () { + const canvas = createCanvas(20, 20, 'pdf') + const ctx = canvas.getContext('2d') + if (!('beginTag' in ctx)) { + this.skip() + } + }) + + it('generates a pdf', function () { + const canvas = createCanvas(20, 20, 'pdf') + const ctx = canvas.getContext('2d') + ctx.beginTag('Link', "uri='http://example.com'") + ctx.strokeText('hello', 0, 0) + ctx.endTag('Link') + const buf = canvas.toBuffer('application/pdf') + assert.equal('PDF', buf.slice(1, 4).toString()) + }) + + it('requires tag argument', function () { + const canvas = createCanvas(20, 20, 'pdf') + const ctx = canvas.getContext('2d') + assert.throws(() => { ctx.beginTag() }) + }) + + it('requires attributes to be a string', function () { + const canvas = createCanvas(20, 20, 'pdf') + const ctx = canvas.getContext('2d') + assert.throws(() => { ctx.beginTag('Link', {}) }) + }) + }) + + describe('loadImage', function () { + it('doesn\'t crash when you don\'t specify width and height', async function () { + const err = {name: "Error"}; + + // TODO: remove this when we have a static build or something + if (os.platform() !== 'win32') { + err.message = "Width and height must be set on the svg element"; + } + + await assert.rejects(async () => { + const svg = ``; + await loadImage(Buffer.from(svg)); + }, err); + }); + }); +}) diff --git a/test/dommatrix.test.js b/test/dommatrix.test.js new file mode 100644 index 000000000..71be6d59e --- /dev/null +++ b/test/dommatrix.test.js @@ -0,0 +1,653 @@ +/* eslint-env mocha */ + +'use stricit' + +const {DOMMatrix} = require('../') + +const assert = require('assert') + +// This doesn't need to be precise; we're not testing the engine's trig +// implementations. +const TOLERANCE = 0.001 +function assertApprox (actual, expected, tolerance) { + if (typeof tolerance !== 'number') tolerance = TOLERANCE + assert.ok(expected > actual - tolerance && expected < actual + tolerance, + `Expected ${expected} to equal ${actual} +/- ${tolerance}`) +} +function assertApproxDeep (actual, expected, tolerance) { + expected.forEach(function (value, index) { + assertApprox(actual[index], value) + }) +} + +describe('DOMMatrix', function () { + const Avals = [4, 5, 1, 8, 0, 3, 6, 1, 3, 5, 0, 9, 2, 4, 6, 1] + const Bvals = [1, 5, 1, 0, 0, 3, 6, 1, 3, 5, 7, 2, 2, 0, 6, 1] + const Xvals = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0] + const AxB = new Float64Array([7, 25, 31, 22, 20, 43, 24, 58, 37, 73, 45, 94, 28, 44, 8, 71]) + const BxA = new Float64Array([23, 40, 89, 15, 20, 39, 66, 16, 21, 30, 87, 14, 22, 52, 74, 17]) + + describe('constructor, general', function () { + it('aliases a,b,c,d,e,f properly', function () { + const y = new DOMMatrix(Avals) + assert.strictEqual(y.a, y.m11) + assert.strictEqual(y.b, y.m12) + assert.strictEqual(y.c, y.m21) + assert.strictEqual(y.d, y.m22) + assert.strictEqual(y.e, y.m41) + assert.strictEqual(y.f, y.m42) + }) + + it('parses lists of transforms per spec', function () { + const y = new DOMMatrix('matrix(1, -2, 3.2, 4.5e2, 3.5E-1, +2) matrix(1, 2, 4, 1, 0, 0)') + assert.strictEqual(y.a, 7.4) + assert.strictEqual(y.b, 898) + assert.strictEqual(y.c, 7.2) + assert.strictEqual(y.d, 442) + assert.strictEqual(y.e, 0.35) + assert.strictEqual(y.f, 2) + assert.strictEqual(y.is2D, true) + }) + + it('parses matrix2d(<16 numbers>) per spec', function () { + const y = new DOMMatrix('matrix3d(1, -0, 0, 0, -2.12, 1, 0, 0, 3e2, 0, +1, 1.252, 0, 0, 0, 1)') + assert.deepEqual(y.toFloat64Array(), new Float64Array([ + 1, 0, 0, 0, + -2.12, 1, 0, 0, + 300, 0, 1, 1.252, + 0, 0, 0, 1 + ])) + assert.strictEqual(y.is2D, false) + }) + + it('sets is2D to true if matrix2d(<16 numbers>) is 2D', function () { + const y = new DOMMatrix('matrix3d(1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)') + assert.deepEqual(y.toFloat64Array(), new Float64Array([ + 1, 2, 0, 0, + 3, 4, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ])) + assert.strictEqual(y.is2D, true) + }) + }) + + describe('fromMatrix', function () {}) + describe('fromFloat32Array', function () {}) + describe('fromFloat64Array', function () {}) + + describe('multiply', function () { + it('performs self.other, returning a new DOMMatrix', function () { + const A = new DOMMatrix(Avals) + const B = new DOMMatrix(Bvals) + const C = B.multiply(A) + assert.deepEqual(C.toFloat64Array(), BxA) + assert.notStrictEqual(A, C) + assert.notStrictEqual(B, C) + }) + }) + + describe('multiplySelf', function () { + it('performs self.other, mutating self', function () { + const A = new DOMMatrix(Avals) + const B = new DOMMatrix(Bvals) + const C = B.multiplySelf(A) + assert.deepEqual(C.toFloat64Array(), BxA) + assert.strictEqual(C, B) + }) + }) + + describe('preMultiplySelf', function () { + it('performs other.self, mutating self', function () { + const A = new DOMMatrix(Avals) + const B = new DOMMatrix(Bvals) + const C = B.preMultiplySelf(A) + assert.deepEqual(C.toFloat64Array(), AxB) + assert.strictEqual(C, B) + }) + }) + + describe('translate', function () {}) + + describe('translateSelf', function () { + it('works, 1 arg', function () { + const A = new DOMMatrix() + A.translateSelf(1) + assert.deepEqual(A.toFloat64Array(), new Float64Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 0, 0, 1 + ])) + }) + + it('works, 2 args', function () { + const A = new DOMMatrix(Avals) + const C = A.translateSelf(2, 5) + assert.deepEqual(C.toFloat64Array(), new Float64Array([ + 4, 5, 1, 8, + 0, 3, 6, 1, + 3, 5, 0, 9, + 10, 29, 38, 22 + ])) + }) + + it('works, 3 args', function () { + const A = new DOMMatrix() + A.translateSelf(1, 2, 3) + assert.deepEqual(A.toFloat64Array(), new Float64Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 2, 3, 1 + ])) + }) + }) + + describe('scale', function () { + const x = new DOMMatrix() + it('works, 1 arg', function () { + assert.deepEqual(x.scale(2).toFloat64Array(), new Float64Array([ + 2, 0, 0, 0, + 0, 2, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ])) + }) + + it('works, 2 args', function () { + assert.deepEqual(x.scale(2, 3).toFloat64Array(), new Float64Array([ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ])) + }) + + it('works, 3 args', function () { + assert.deepEqual(x.scale(2, 3, 4).toFloat64Array(), new Float64Array([ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 4, 0, + 0, 0, 0, 1 + ])) + }) + + it('works, 4 args', function () { + assert.deepEqual(x.scale(2, 3, 4, 5).toFloat64Array(), new Float64Array([ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 4, 0, + -5, 0, 0, 1 + ])) + }) + + it('works, 5 args', function () { + assert.deepEqual(x.scale(2, 3, 4, 5, 6).toFloat64Array(), new Float64Array([ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 4, 0, + -5, -12, 0, 1 + ])) + }) + + it('works, 6 args', function () { + assert.deepEqual(x.scale(2, 1, 1, 0, 0, 0).toFloat64Array(), new Float64Array([ + 2, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ])) + + assert.deepEqual(x.scale(2, 3, 2, 0, 0, 0).toFloat64Array(), new Float64Array([ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 2, 0, + 0, 0, 0, 1 + ])) + + assert.deepEqual(x.scale(5, -1, 2, 1, 3, 2).toFloat64Array(), new Float64Array([ + 5, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, 2, 0, + -4, 6, -2, 1 + ])) + }) + }) + + describe('scaleSelf', function () {}) + + describe('scale3d', function () { + const x = new DOMMatrix(Avals) + + it('works, 0 args', function () { + assert.deepEqual(x.scale3d().toFloat64Array(), new Float64Array(Avals)) + }) + + it('works, 1 arg', function () { + assert.deepEqual(x.scale3d(2).toFloat64Array(), new Float64Array([ + 8, 10, 2, 16, + 0, 6, 12, 2, + 6, 10, 0, 18, + 2, 4, 6, 1 + ])) + }) + + it('works, 2 args', function () { + assert.deepEqual(x.scale3d(2, 3).toFloat64Array(), new Float64Array([ + 8, 10, 2, 16, + 0, 6, 12, 2, + 6, 10, 0, 18, + -10, -11, 3, -23 + ])) + }) + + it('works, 3 args', function () { + assert.deepEqual(x.scale3d(2, 3, 4).toFloat64Array(), new Float64Array([ + 8, 10, 2, 16, + 0, 6, 12, 2, + 6, 10, 0, 18, + -10, -23, -21, -27 + ])) + }) + + it('works, 4 args', function () { + assert.deepEqual(x.scale3d(2, 3, 4, 5).toFloat64Array(), new Float64Array([ + 8, 10, 2, 16, + 0, 6, 12, 2, + 6, 10, 0, 18, + -25, -48, -21, -72 + ])) + }) + }) + + describe('scale3dSelf', function () {}) + + describe('rotate', function () { + it('works, 1 arg', function () { + const x = new DOMMatrix() + const y = x.rotate(70) + assertApproxDeep(y.toFloat64Array(), new Float64Array([ + 0.3420201, 0.9396926, 0, 0, + -0.939692, 0.3420201, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ])) + }) + + it('works, 2 args', function () { + const x = new DOMMatrix() + const y = x.rotate(70, 30) + assertApproxDeep(y.toFloat64Array(), new Float64Array([ + 0.8660254, 0, -0.5, 0, + 0.4698463, 0.3420201, 0.8137976, 0, + 0.1710100, -0.9396926, 0.2961981, 0, + 0, 0, 0, 1 + ])) + assert.strictEqual(y.is2D, false) + }) + + it('works, 3 args', function () { + const x = new DOMMatrix() + const y = x.rotate(70, 30, 50) + assertApproxDeep(y.toFloat64Array(), new Float64Array([ + 0.5566703, 0.6634139, -0.5, 0, + 0.0400087, 0.5797694, 0.8137976, 0, + 0.8297694, -0.4730214, 0.2961981, 0, + 0, 0, 0, 1])) + }) + }) + + describe('rotateSelf', function () {}) + + describe('rotateFromVector', function () { + const x = new DOMMatrix(Avals) + it('works, no args and x/y=0', function () { + assert.deepEqual(x.rotateFromVector().toFloat64Array(), new Float64Array(Avals)) + assert.deepEqual(x.rotateFromVector(5).toFloat64Array(), new Float64Array(Avals)) + assert.deepEqual(x.rotateFromVector(0, 0).toFloat64Array(), new Float64Array(Avals)) + }) + + it('works', function () { + const y = x.rotateFromVector(4, 2).toFloat64Array() + const expect = new Float64Array([ + 3.5777087, 5.8137767, 3.5777087, 7.6026311, + -1.7888543, 0.4472135, 4.9193495, -2.6832815, + 3, 5, 0, 9, + 2, 4, 6, 1 + ]) + assertApproxDeep(expect, y) + }) + }) + + describe('rotateFromVectorSelf', function () {}) + + describe('rotateAxisAngle', function () { + it('works, 0 args', function () { + const x = new DOMMatrix(Avals) + const y = x.rotateAxisAngle().toFloat64Array() + assert.deepEqual(y, new Float64Array(Avals)) + }) + + it('works, 4 args', function () { + const x = new DOMMatrix(Avals) + const y = x.rotateAxisAngle(2, 4, 1, 35).toFloat64Array() + const expect = new Float64Array([ + 1.9640922, 2.4329989, 2.0179538, 2.6719387, + 0.6292488, 4.0133545, 5.6853755, 3.0697681, + 4.5548203, 6.0805840, -0.7774101, 11.3770500, + 2, 4, 6, 1 + ]) + assertApproxDeep(expect, y) + }) + }) + + describe('rotateFromAngleSelf', function () {}) + + describe('skewX', function () { + it('works', function () { + const x = new DOMMatrix(Avals) + const y = x.skewX(30).toFloat64Array() + const expect = new Float64Array([ + 4, 5, 1, 8, + 2.3094010, 5.8867513, 6.5773502, 5.6188021, + 3, 5, 0, 9, + 2, 4, 6, 1 + ]) + assertApproxDeep(expect, y) + }) + }) + + describe('skewXSelf', function () {}) + + describe('skewY', function () { + it('works', function () { + const x = new DOMMatrix(Avals) + const y = x.skewY(30).toFloat64Array() + const expect = new Float64Array([ + 4, 6.7320508, 4.4641016, 8.5773502, + 0, 3, 6, 1, + 3, 5, 0, 9, + 2, 4, 6, 1 + ]) + assertApproxDeep(expect, y) + }) + }) + + describe('skewYSelf', function () {}) + + describe('flipX', function () { + it('works', function () { + const x = new DOMMatrix() + x.rotateSelf(70) + const y = x.flipX() + assertApprox(y.a, -0.34202) + assertApprox(y.b, -0.93969) + assertApprox(y.c, -0.93969) + assertApprox(y.d, 0.34202) + assert.strictEqual(y.e, 0) + assert.strictEqual(y.f, 0) + }) + }) + + describe('flipY', function () { + it('works', function () { + const x = new DOMMatrix() + x.rotateSelf(70) + const y = x.flipY() + assertApprox(y.a, 0.34202) + assertApprox(y.b, 0.93969) + assertApprox(y.c, 0.93969) + assertApprox(y.d, -0.34202) + assert.strictEqual(y.e, 0) + assert.strictEqual(y.f, 0) + }) + }) + + describe('invertSelf', function () { + it('works for invertible matrices', function () { + const d = new DOMMatrix(Avals) + d.invertSelf() + assertApprox(d.m11, 0.9152542372881356) + assertApprox(d.m12, -0.01694915254237288) + assertApprox(d.m13, -0.7966101694915254) + assertApprox(d.m14, -0.13559322033898305) + assertApprox(d.m21, -1.8305084745762712) + assertApprox(d.m22, -0.9661016949152542) + assertApprox(d.m23, 1.5932203389830508) + assertApprox(d.m24, 1.271186440677966) + assertApprox(d.m31, 0.7966101694915254) + assertApprox(d.m32, 0.559322033898305) + assertApprox(d.m33, -0.711864406779661) + assertApprox(d.m34, -0.5254237288135594) + assertApprox(d.m41, 0.711864406779661) + assertApprox(d.m42, 0.5423728813559322) + assertApprox(d.m43, -0.5084745762711864) + assertApprox(d.m44, -0.6610169491525424) + }) + + it('works for non-invertible matrices', function () { + const d = new DOMMatrix(Xvals) + d.invertSelf() + assert.strictEqual(isNaN(d.m11), true) + assert.strictEqual(isNaN(d.m12), true) + assert.strictEqual(isNaN(d.m13), true) + assert.strictEqual(isNaN(d.m14), true) + assert.strictEqual(isNaN(d.m21), true) + assert.strictEqual(isNaN(d.m22), true) + assert.strictEqual(isNaN(d.m23), true) + assert.strictEqual(isNaN(d.m24), true) + assert.strictEqual(isNaN(d.m31), true) + assert.strictEqual(isNaN(d.m32), true) + assert.strictEqual(isNaN(d.m33), true) + assert.strictEqual(isNaN(d.m34), true) + assert.strictEqual(isNaN(d.m41), true) + assert.strictEqual(isNaN(d.m42), true) + assert.strictEqual(isNaN(d.m43), true) + assert.strictEqual(isNaN(d.m44), true) + assert.strictEqual(d.is2D, false) + }) + }) + + describe('inverse', function () { + it('preserves the original DOMMatrix', function () { + const d = new DOMMatrix(Avals) + const d2 = d.inverse() + assert.strictEqual(d.m11, Avals[0]) + assert.strictEqual(d.m12, Avals[1]) + assert.strictEqual(d.m13, Avals[2]) + assert.strictEqual(d.m14, Avals[3]) + assert.strictEqual(d.m21, Avals[4]) + assert.strictEqual(d.m22, Avals[5]) + assert.strictEqual(d.m23, Avals[6]) + assert.strictEqual(d.m24, Avals[7]) + assert.strictEqual(d.m31, Avals[8]) + assert.strictEqual(d.m32, Avals[9]) + assert.strictEqual(d.m33, Avals[10]) + assert.strictEqual(d.m34, Avals[11]) + assert.strictEqual(d.m41, Avals[12]) + assert.strictEqual(d.m42, Avals[13]) + assert.strictEqual(d.m43, Avals[14]) + assert.strictEqual(d.m44, Avals[15]) + assertApprox(d2.m11, 0.9152542372881356) + assertApprox(d2.m12, -0.01694915254237288) + assertApprox(d2.m13, -0.7966101694915254) + assertApprox(d2.m14, -0.13559322033898305) + assertApprox(d2.m21, -1.8305084745762712) + assertApprox(d2.m22, -0.9661016949152542) + assertApprox(d2.m23, 1.5932203389830508) + assertApprox(d2.m24, 1.271186440677966) + assertApprox(d2.m31, 0.7966101694915254) + assertApprox(d2.m32, 0.559322033898305) + assertApprox(d2.m33, -0.711864406779661) + assertApprox(d2.m34, -0.5254237288135594) + assertApprox(d2.m41, 0.711864406779661) + assertApprox(d2.m42, 0.5423728813559322) + assertApprox(d2.m43, -0.5084745762711864) + assertApprox(d2.m44, -0.6610169491525424) + }) + + it('preserves the original DOMMatrix for non-invertible matrices', function () { + const d = new DOMMatrix(Xvals) + const d2 = d.inverse() + assert.strictEqual(d.m11, Xvals[0]) + assert.strictEqual(d.m12, Xvals[1]) + assert.strictEqual(d.m13, Xvals[2]) + assert.strictEqual(d.m14, Xvals[3]) + assert.strictEqual(d.m21, Xvals[4]) + assert.strictEqual(d.m22, Xvals[5]) + assert.strictEqual(d.m23, Xvals[6]) + assert.strictEqual(d.m24, Xvals[7]) + assert.strictEqual(d.m31, Xvals[8]) + assert.strictEqual(d.m32, Xvals[9]) + assert.strictEqual(d.m33, Xvals[10]) + assert.strictEqual(d.m34, Xvals[11]) + assert.strictEqual(d.m41, Xvals[12]) + assert.strictEqual(d.m42, Xvals[13]) + assert.strictEqual(d.m43, Xvals[14]) + assert.strictEqual(d.m44, Xvals[15]) + assert.strictEqual(isNaN(d2.m11), true) + assert.strictEqual(isNaN(d2.m12), true) + assert.strictEqual(isNaN(d2.m13), true) + assert.strictEqual(isNaN(d2.m14), true) + assert.strictEqual(isNaN(d2.m21), true) + assert.strictEqual(isNaN(d2.m22), true) + assert.strictEqual(isNaN(d2.m23), true) + assert.strictEqual(isNaN(d2.m24), true) + assert.strictEqual(isNaN(d2.m31), true) + assert.strictEqual(isNaN(d2.m32), true) + assert.strictEqual(isNaN(d2.m33), true) + assert.strictEqual(isNaN(d2.m34), true) + assert.strictEqual(isNaN(d2.m41), true) + assert.strictEqual(isNaN(d2.m42), true) + assert.strictEqual(isNaN(d2.m43), true) + assert.strictEqual(isNaN(d2.m44), true) + assert.strictEqual(d2.is2D, false) + }) + }) + + describe('transformPoint', function () { + it('works', function () { + const x = new DOMMatrix() + let r = x.transformPoint({ x: 1, y: 2, z: 3 }) + assert.strictEqual(r.x, 1) + assert.strictEqual(r.y, 2) + assert.strictEqual(r.z, 3) + assert.strictEqual(r.w, 1) + + x.rotateSelf(70) + r = x.transformPoint({ x: 2, y: 3, z: 4 }) + assertApprox(r.x, -2.13503) + assertApprox(r.y, 2.905445) + assert.strictEqual(r.z, 4) + assert.strictEqual(r.w, 1) + }) + }) + + describe('toFloat32Array', function () { + it('works', function () { + const x = new DOMMatrix() + const y = x.toFloat32Array() + assert.ok(y instanceof Float32Array) + assert.deepEqual(y, new Float32Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ])) + }) + }) + + describe('toFloat64Array', function () { + it('works', function () { + const x = new DOMMatrix() + const y = x.toFloat64Array() + assert.ok(y instanceof Float64Array) + assert.deepEqual(y, new Float64Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ])) + }) + }) + + describe('toString', function () { + it('works, 2d', function () { + const x = new DOMMatrix() + assert.equal(x.toString(), 'matrix(1, 0, 0, 1, 0, 0)') + }) + + it('works, 3d', function () { + const x = new DOMMatrix() + x.m31 = 1 + assert.equal(x.is2D, false) + assert.equal(x.toString(), + 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1)') + }) + }) + + describe('toJSON', function () { + it('works, 2d', function () { + const x = new DOMMatrix() + assert.deepStrictEqual(x.toJSON(), { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0, + m11: 1, + m12: 0, + m13: 0, + m14: 0, + m21: 0, + m22: 1, + m23: 0, + m23: 0, + m31: 0, + m32: 0, + m33: 1, + m34: 0, + m41: 0, + m42: 0, + m43: 0, + m44: 1, + is2D: true, + isIdentity: true, + }) + }) + + it('works, 3d', function () { + const x = new DOMMatrix() + x.m31 = 1 + assert.equal(x.is2D, false) + assert.deepStrictEqual(x.toJSON(), { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0, + m11: 1, + m12: 0, + m13: 0, + m14: 0, + m21: 0, + m22: 1, + m23: 0, + m23: 0, + m31: 1, + m32: 0, + m33: 1, + m34: 0, + m41: 0, + m42: 0, + m43: 0, + m44: 1, + is2D: false, + isIdentity: false, + }) + }) + }) +}) diff --git a/test/fixtures/159-crash1.jpg b/test/fixtures/159-crash1.jpg new file mode 100644 index 000000000..0f3bbd0a7 Binary files /dev/null and b/test/fixtures/159-crash1.jpg differ diff --git a/test/fixtures/bmp/1-bit.bmp b/test/fixtures/bmp/1-bit.bmp new file mode 100644 index 000000000..04c5f892e Binary files /dev/null and b/test/fixtures/bmp/1-bit.bmp differ diff --git a/test/fixtures/bmp/24-bit.bmp b/test/fixtures/bmp/24-bit.bmp new file mode 100644 index 000000000..4f1f51086 Binary files /dev/null and b/test/fixtures/bmp/24-bit.bmp differ diff --git a/test/fixtures/bmp/32-bit.bmp b/test/fixtures/bmp/32-bit.bmp new file mode 100644 index 000000000..edc32b449 Binary files /dev/null and b/test/fixtures/bmp/32-bit.bmp differ diff --git a/test/fixtures/bmp/4-bit.bmp b/test/fixtures/bmp/4-bit.bmp new file mode 100644 index 000000000..53d53c457 Binary files /dev/null and b/test/fixtures/bmp/4-bit.bmp differ diff --git a/test/fixtures/bmp/bomb.bmp b/test/fixtures/bmp/bomb.bmp new file mode 100644 index 000000000..60703159a Binary files /dev/null and b/test/fixtures/bmp/bomb.bmp differ diff --git a/test/fixtures/bmp/min.bmp b/test/fixtures/bmp/min.bmp new file mode 100644 index 000000000..688af7180 Binary files /dev/null and b/test/fixtures/bmp/min.bmp differ diff --git a/test/fixtures/bmp/negative-height.bmp b/test/fixtures/bmp/negative-height.bmp new file mode 100644 index 000000000..5a0ab3ae3 Binary files /dev/null and b/test/fixtures/bmp/negative-height.bmp differ diff --git a/test/fixtures/bmp/palette.bmp b/test/fixtures/bmp/palette.bmp new file mode 100644 index 000000000..0a6207e3c Binary files /dev/null and b/test/fixtures/bmp/palette.bmp differ diff --git a/test/fixtures/bmp/v3-header.bmp b/test/fixtures/bmp/v3-header.bmp new file mode 100644 index 000000000..d6e3b9e59 Binary files /dev/null and b/test/fixtures/bmp/v3-header.bmp differ diff --git a/test/fixtures/chrome.jpg b/test/fixtures/chrome.jpg new file mode 100644 index 000000000..29fd36ae8 Binary files /dev/null and b/test/fixtures/chrome.jpg differ diff --git a/test/fixtures/exif-orientation-f1.jpg b/test/fixtures/exif-orientation-f1.jpg new file mode 100644 index 000000000..64847b1d2 Binary files /dev/null and b/test/fixtures/exif-orientation-f1.jpg differ diff --git a/test/fixtures/exif-orientation-f2.jpg b/test/fixtures/exif-orientation-f2.jpg new file mode 100644 index 000000000..75064ea1c Binary files /dev/null and b/test/fixtures/exif-orientation-f2.jpg differ diff --git a/test/fixtures/exif-orientation-f3.jpg b/test/fixtures/exif-orientation-f3.jpg new file mode 100644 index 000000000..851503f04 Binary files /dev/null and b/test/fixtures/exif-orientation-f3.jpg differ diff --git a/test/fixtures/exif-orientation-f4.jpg b/test/fixtures/exif-orientation-f4.jpg new file mode 100644 index 000000000..045b1349b Binary files /dev/null and b/test/fixtures/exif-orientation-f4.jpg differ diff --git a/test/fixtures/exif-orientation-f5.jpg b/test/fixtures/exif-orientation-f5.jpg new file mode 100644 index 000000000..ebdcf4db7 Binary files /dev/null and b/test/fixtures/exif-orientation-f5.jpg differ diff --git a/test/fixtures/exif-orientation-f6.jpg b/test/fixtures/exif-orientation-f6.jpg new file mode 100644 index 000000000..439c72ed4 Binary files /dev/null and b/test/fixtures/exif-orientation-f6.jpg differ diff --git a/test/fixtures/exif-orientation-f7.jpg b/test/fixtures/exif-orientation-f7.jpg new file mode 100644 index 000000000..2d91716b7 Binary files /dev/null and b/test/fixtures/exif-orientation-f7.jpg differ diff --git a/test/fixtures/exif-orientation-f8.jpg b/test/fixtures/exif-orientation-f8.jpg new file mode 100644 index 000000000..11d855364 Binary files /dev/null and b/test/fixtures/exif-orientation-f8.jpg differ diff --git a/test/fixtures/exif-orientation-fi.jpg b/test/fixtures/exif-orientation-fi.jpg new file mode 100644 index 000000000..21a92636e Binary files /dev/null and b/test/fixtures/exif-orientation-fi.jpg differ diff --git a/test/fixtures/exif-orientation-fm.jpg b/test/fixtures/exif-orientation-fm.jpg new file mode 100644 index 000000000..992de3ed1 Binary files /dev/null and b/test/fixtures/exif-orientation-fm.jpg differ diff --git a/test/fixtures/exif-orientation-fn.jpg b/test/fixtures/exif-orientation-fn.jpg new file mode 100644 index 000000000..43a475099 Binary files /dev/null and b/test/fixtures/exif-orientation-fn.jpg differ diff --git a/test/fixtures/existing.png b/test/fixtures/existing.png new file mode 100644 index 000000000..720a5cfec Binary files /dev/null and b/test/fixtures/existing.png differ diff --git a/test/fixtures/grayscale.jpg b/test/fixtures/grayscale.jpg new file mode 100644 index 000000000..4988e9e55 Binary files /dev/null and b/test/fixtures/grayscale.jpg differ diff --git a/test/fixtures/halved-1.jpeg b/test/fixtures/halved-1.jpeg new file mode 100644 index 000000000..d7c7e0f79 Binary files /dev/null and b/test/fixtures/halved-1.jpeg differ diff --git a/test/fixtures/halved-2.jpeg b/test/fixtures/halved-2.jpeg new file mode 100644 index 000000000..056e2eaf4 Binary files /dev/null and b/test/fixtures/halved-2.jpeg differ diff --git a/test/fixtures/newcontent.png b/test/fixtures/newcontent.png new file mode 100644 index 000000000..110288315 Binary files /dev/null and b/test/fixtures/newcontent.png differ diff --git a/test/fixtures/quadrants.png b/test/fixtures/quadrants.png new file mode 100644 index 000000000..c5460893e Binary files /dev/null and b/test/fixtures/quadrants.png differ diff --git a/test/fixtures/tree.svg b/test/fixtures/tree.svg new file mode 100644 index 000000000..b9adb0802 --- /dev/null +++ b/test/fixtures/tree.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/fixtures/ycck.jpg b/test/fixtures/ycck.jpg new file mode 100644 index 000000000..0fbcd3b0c Binary files /dev/null and b/test/fixtures/ycck.jpg differ diff --git a/test/fontParser.test.js b/test/fontParser.test.js new file mode 100644 index 000000000..0302466b9 --- /dev/null +++ b/test/fontParser.test.js @@ -0,0 +1,118 @@ +/* eslint-env mocha */ + +'use strict' + +/** + * Module dependencies. + */ +const assert = require('assert') +const {Canvas} = require('..'); + +const tests = [ + '20px Arial', + { size: 20, families: ['arial'] }, + '20pt Arial', + { size: 26.666667461395264, families: ['arial'] }, + '20.5pt Arial', + { size: 27.333334147930145, families: ['arial'] }, + '20% Arial', + { size: 3.1999999284744263, families: ['arial'] }, + '20mm Arial', + { size: 75.59999942779541, families: ['arial'] }, + '20px serif', + { size: 20, families: ['serif'] }, + '20px sans-serif', + { size: 20, families: ['sans-serif'] }, + '20px monospace', + { size: 20, families: ['monospace'] }, + '50px Arial, sans-serif', + { size: 50, families: ['arial', 'sans-serif'] }, + 'bold italic 50px Arial, sans-serif', + { style: 1, weight: 700, size: 50, families: ['arial', 'sans-serif'] }, + '50px Helvetica , Arial, sans-serif', + { size: 50, families: ['helvetica', 'arial', 'sans-serif'] }, + '50px "Helvetica Neue", sans-serif', + { size: 50, families: ['Helvetica Neue', 'sans-serif'] }, + '50px "Helvetica Neue", "foo bar baz" , sans-serif', + { size: 50, families: ['Helvetica Neue', 'foo bar baz', 'sans-serif'] }, + "50px 'Helvetica Neue'", + { size: 50, families: ['Helvetica Neue'] }, + 'italic 20px Arial', + { size: 20, style: 1, families: ['arial'] }, + 'oblique 20px Arial', + { size: 20, style: 2, families: ['arial'] }, + 'normal 20px Arial', + { size: 20, families: ['arial'] }, + '300 20px Arial', + { size: 20, weight: 300, families: ['arial'] }, + '800 20px Arial', + { size: 20, weight: 800, families: ['arial'] }, + 'bolder 20px Arial', + { size: 20, weight: 700, families: ['arial'] }, + 'lighter 20px Arial', + { size: 20, weight: 100, families: ['arial'] }, + 'normal normal normal 16px Impact', + { size: 16, families: ['impact'] }, + 'italic small-caps bolder 16px cursive', + { size: 16, style: 1, variant: 1, weight: 700, families: ['cursive'] }, + '20px "new century schoolbook", serif', + { size: 20, families: ['new century schoolbook', 'serif'] }, + '20px "Arial bold 300"', // synthetic case with weight keyword inside family + { size: 20, families: ['Arial bold 300'] }, + `50px "Helvetica 'Neue'", "foo \\"bar\\" baz" , "Someone's weird \\'edge\\' case", sans-serif`, + { size: 50, families: [`Helvetica 'Neue'`, 'foo "bar" baz', `Someone's weird 'edge' case`, 'sans-serif'] }, + 'Helvetica, sans', + undefined, + '123px thefont/123abc', + undefined, + '123px /\tnormal thefont', + {size: 123, families: ['thefont']}, + '12px/1.2whoops arial', + undefined, + 'bold bold 12px thefont', + undefined, + 'italic italic 12px Arial', + undefined, + 'small-caps bold italic small-caps 12px Arial', + undefined, + 'small-caps bold oblique 12px \'A\'ri\\61l', + {size: 12, style: 2, weight: 700, variant: 1, families: ['Arial']}, + '12px/34% "The\\\n Word"', + {size: 12, families: ['The Word']}, + '', + undefined, + 'normal normal normal 1%/normal a , \'b\'', + {size: 0.1599999964237213, families: ['a', 'b']}, + 'normalnormalnormal 1px/normal a', + undefined, + '12px _the_font', + {size: 12, families: ['_the_font']}, + '9px 7 birds', + undefined, + '2em "Courier', + undefined, + `2em \\'Courier\\"`, + {size: 32, families: ['\'courier"']}, + '1px \\10abcde', + {size: 1, families: [String.fromCodePoint(parseInt('10abcd', 16)) + 'e']}, + '3E+2 1e-1px yay', + {weight: 300, size: 0.1, families: ['yay']} +]; + +describe('Font parser', function () { + for (let i = 0; i < tests.length; i++) { + const str = tests[i++] + it(str, function () { + const expected = tests[i] + const actual = Canvas.parseFont(str) + + if (expected) { + if (expected.style == null) expected.style = 0 + if (expected.weight == null) expected.weight = 400 + if (expected.variant == null) expected.variant = 0 + } + + assert.deepEqual(actual, expected) + }) + } +}) diff --git a/test/image.test.js b/test/image.test.js index ae39d6207..edae78beb 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -1,209 +1,544 @@ +/* eslint-env mocha */ + +'use strict' /** * Module dependencies. */ +const assert = require('assert') +const assertRejects = require('assert-rejects') +const fs = require('fs') +const path = require('path') + +const { createCanvas, loadImage, rsvgVersion, Image } = require('../') +const HAVE_SVG = rsvgVersion !== undefined -var Canvas = require('../') - , Image = Canvas.Image - , assert = require('assert'); -var png_checkers = __dirname + '/fixtures/checkers.png'; -var png_clock = __dirname + '/fixtures/clock.png'; +const pngCheckers = path.join(__dirname, '/fixtures/checkers.png') +const pngClock = path.join(__dirname, '/fixtures/clock.png') +const jpgChrome = path.join(__dirname, '/fixtures/chrome.jpg') +const jpgFace = path.join(__dirname, '/fixtures/face.jpeg') +const svgTree = path.join(__dirname, '/fixtures/tree.svg') +const bmpDir = path.join(__dirname, '/fixtures/bmp') describe('Image', function () { - it('should require new', function () { - assert.throws(function () { Image(); }, TypeError); - }); - - it('Image', function () { - assert.ok(Image instanceof Function); - }); - - it('Image#onload', function () { - var img = new Image - , onloadCalled = 0; - - assert.strictEqual(null, img.onload); - assert.strictEqual(false, img.complete); - - img.onload = function () { - onloadCalled += 1; - assert.strictEqual(img.src, png_clock); - }; - - img.src = png_clock; - assert.strictEqual(1, onloadCalled); - assert.strictEqual(img.src, png_clock); - - assert.strictEqual(true, img.complete); - assert.strictEqual(320, img.width); - assert.strictEqual(320, img.height); - }); - - it('test Image#onload multiple times', function() { - var img = new Image - , onloadCalled = 0; - - img.onload = function() { - onloadCalled += 1; - }; - - img.src = png_checkers; - assert.equal(img.src, png_checkers); - assert.strictEqual(true, img.complete); - assert.strictEqual(2, img.width); - assert.strictEqual(2, img.height); - - img.src = png_clock; - assert.equal(img.src, png_clock); - assert.strictEqual(true, img.complete); - assert.strictEqual(320, img.width); - assert.strictEqual(320, img.height); - - assert.equal(onloadCalled, 2); - - onloadCalled = 0; - img.onload = function() { - onloadCalled += 1; - }; - img.src = png_clock; - assert.equal(onloadCalled, 1); - }); - - it('Image#onerror', function () { - var img = new Image - , error - , onerrorCalled = 0; - - assert.strictEqual(null, img.onerror); - assert.strictEqual(false, img.complete); - - img.onload = function () { - assert.fail('called onload'); - }; - - img.onerror = function (err) { - onerrorCalled += 1; - error = err; - }; - - try { - img.src = png_clock + 's'; - } catch (err) { - assert.fail('error did not invoke onerror(): ' + err); + it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { + const img = new Image() + assert.throws(function () { Image.prototype.width }, /invalid argument/i) + assert(!img.hasOwnProperty('width')) + assert('width' in img) + assert(Image.prototype.hasOwnProperty('width')) + }) + + it('Image has class string of `HTMLImageElement`', async function () { + const img = new Image() + assert.strictEqual(Object.prototype.toString.call(img), '[object HTMLImageElement]') + }) + + it('loads JPEG image', function () { + return loadImage(jpgFace).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + + assert.strictEqual(img.src, jpgFace) + assert.strictEqual(img.width, 485) + assert.strictEqual(img.height, 401) + assert.strictEqual(img.complete, true) + }) + }) + + it('loads JPEG data URL', function () { + const base64Encoded = fs.readFileSync(jpgFace, 'base64') + const dataURL = `data:image/png;base64,${base64Encoded}` + + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + + assert.strictEqual(img.src, dataURL) + assert.strictEqual(img.width, 485) + assert.strictEqual(img.height, 401) + assert.strictEqual(img.complete, true) + }) + }) + + it('loads PNG image', function () { + return loadImage(pngClock).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + + assert.strictEqual(img.src, pngClock) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) + assert.strictEqual(img.complete, true) + }) + }) + + it('loads PNG data URL', function () { + const base64Encoded = fs.readFileSync(pngClock, 'base64') + const dataURL = `data:image/png;base64,${base64Encoded}` + + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + + assert.strictEqual(img.src, dataURL) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) + assert.strictEqual(img.complete, true) + }) + }) + + it('detects invalid PNG', function (done) { + if (process.platform === 'win32') this.skip() // TODO + const img = new Image() + img.onerror = () => { + assert.strictEqual(img.complete, true) + done() + } + img.src = Buffer.from('89504E470D', 'hex') + }) + + it('propagates exceptions thrown by onload', function () { + class MyError extends Error {} + const img = new Image() + img.onload = () => { + throw new MyError() + } + assert.throws(() => { + img.src = jpgFace + }, MyError) + }) + + it('propagates exceptions thrown by onerror', function () { + class MyError extends Error {} + const img = new Image() + img.onerror = () => { + throw new MyError() } + assert.throws(() => { + img.src = Buffer.from('', 'hex') + }, MyError) + }) + + it('loads SVG data URL base64', function () { + if (!HAVE_SVG) this.skip() + const base64Enc = fs.readFileSync(svgTree, 'base64') + const dataURL = `data:image/svg+xml;base64,${base64Enc}` + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + assert.strictEqual(img.width, 200) + assert.strictEqual(img.height, 200) + assert.strictEqual(img.complete, true) + }) + }) + + it('loads SVG data URL utf8', function () { + if (!HAVE_SVG) this.skip() + const utf8Encoded = fs.readFileSync(svgTree, 'utf8') + const dataURL = `data:image/svg+xml;utf8,${utf8Encoded}` + return loadImage(dataURL).then((img) => { + assert.strictEqual(img.onerror, null) + assert.strictEqual(img.onload, null) + assert.strictEqual(img.width, 200) + assert.strictEqual(img.height, 200) + assert.strictEqual(img.complete, true) + }) + }) + + it('calls Image#onload multiple times', function () { + return loadImage(pngClock).then((img) => { + let onloadCalled = 0 + + img.onload = () => { onloadCalled += 1 } + + img.src = pngCheckers + assert.strictEqual(img.src, pngCheckers) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 2) + assert.strictEqual(img.height, 2) + + img.src = pngClock + assert.strictEqual(img.src, pngClock) + assert.strictEqual(true, img.complete) + assert.strictEqual(320, img.width) + assert.strictEqual(320, img.height) + + assert.strictEqual(onloadCalled, 2) + + onloadCalled = 0 + img.onload = () => { onloadCalled += 1 } + + img.src = pngClock + assert.strictEqual(onloadCalled, 1) + }) + }) + + it('handles errors', function () { + return assertRejects(loadImage(`${pngClock}fail`), Error) + }) + + it('returns a nice, coded error for fopen failures', function (done) { + const img = new Image() + img.onerror = err => { + assert.equal(err.message, 'No such file or directory') + assert.equal(err.path, 'path/to/nothing') + assert.equal(err.syscall, 'fopen') + assert.strictEqual(img.complete, true) + done() + } + img.src = 'path/to/nothing' + }) + + it('captures errors from libjpeg', function (done) { + const img = new Image() + img.onerror = err => { + assert.equal(err.message, 'JPEG datastream contains no image') + assert.strictEqual(img.complete, true) + done() + } + img.src = path.join(__dirname, '/fixtures/159-crash1.jpg') + }) - assert.strictEqual(1, onerrorCalled); - assert.strictEqual(img.src, png_clock + 's'); - assert.strictEqual(false, img.complete); + it('calls Image#onerror multiple times', function () { + return loadImage(pngClock).then((img) => { + let onloadCalled = 0 + let onerrorCalled = 0 - assert.ok(error instanceof Error, 'did not invoke onerror() with error'); - }); + img.onload = () => { onloadCalled += 1 } + img.onerror = () => { onerrorCalled += 1 } - it('test Image#onerror multiple calls', function() { - var img = new Image - , onerrorCalled = 0; + img.src = `${pngClock}s1` + assert.strictEqual(img.src, `${pngClock}s1`) + img.src = `${pngClock}s2` + assert.strictEqual(img.src, `${pngClock}s2`) - img.onload = function() { - assert.fail('called onload'); - }; + assert.strictEqual(onerrorCalled, 2) - img.onerror = function() { - onerrorCalled += 1; - }; + onerrorCalled = 0 + img.onerror = () => { onerrorCalled += 1 } - img.src = png_clock + 's1'; - assert.equal(img.src, png_clock + 's1'); + img.src = `${pngClock}s3` + assert.strictEqual(img.src, `${pngClock}s3`) - img.src = png_clock + 's2'; - assert.equal(img.src, png_clock + 's2'); + assert.strictEqual(onerrorCalled, 1) + assert.strictEqual(onloadCalled, 0) + }) + }) - assert.equal(onerrorCalled, 2); + it('Image#{width,height}', function () { + return loadImage(pngClock).then((img) => { + img.src = '' + assert.strictEqual(img.width, 0) + assert.strictEqual(img.height, 0) - onerrorCalled = 0; - img.onerror = function() { - onerrorCalled += 1; - }; - img.src = png_clock + 's3'; - assert.equal(img.src, png_clock + 's3'); - assert.equal(onerrorCalled, 1); - }); + img.src = pngClock + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) + }) + }) - it('Image#{width,height}', function () { - var img = new Image - , onloadCalled = 0; + it('Image#src set empty buffer', function () { + return loadImage(pngClock).then((img) => { + let onerrorCalled = 0 - assert.strictEqual(0, img.width); - assert.strictEqual(0, img.height); + img.onerror = () => { onerrorCalled += 1 } - img.onload = function () { - onloadCalled += 1; - assert.strictEqual(320, img.width); - assert.strictEqual(320, img.height); - }; + img.src = Buffer.alloc(0) + assert.strictEqual(img.width, 0) + assert.strictEqual(img.height, 0) + assert.strictEqual(img.complete, true) - img.src = png_clock; - assert.strictEqual(1, onloadCalled); - assert.strictEqual(320, img.width); - assert.strictEqual(320, img.height); - }); + assert.strictEqual(onerrorCalled, 1) + }) + }) - it('Image#src set empty buffer', function () { - var image = new Canvas.Image(); - image.src = new Buffer(0); - image.src = new Buffer(''); - }); - - it('should unbind Image#onload', function() { - var img = new Image - , onloadCalled = 0; - - img.onload = function() { - onloadCalled += 1; - }; - - img.src = png_checkers; - assert.equal(img.src, png_checkers); - assert.strictEqual(true, img.complete); - assert.strictEqual(2, img.width); - assert.strictEqual(2, img.height); - - assert.equal(onloadCalled, 1); - - onloadCalled = 0; - img.onload = null; - img.src = png_clock; - assert.equal(img.src, png_clock); - assert.strictEqual(true, img.complete); - assert.strictEqual(320, img.width); - assert.strictEqual(320, img.height); - - assert.equal(onloadCalled, 0); - }); - - it('should unbind Image#onerror', function() { - var img = new Image - , onerrorCalled = 0; - - - img.onload = function() { - assert.fail('called onload'); - }; - - img.onerror = function() { - onerrorCalled += 1; - }; - - img.src = png_clock + 's1'; - assert.equal(img.src, png_clock + 's1'); - - assert.equal(onerrorCalled, 1); - - onerrorCalled = 0; - img.onerror = null; - img.src = png_clock + 's3'; - assert.equal(img.src, png_clock + 's3'); - assert.equal(onerrorCalled, 0); - }); -}); + it('should unbind Image#onload', function () { + return loadImage(pngClock).then((img) => { + let onloadCalled = 0 + + img.onload = () => { onloadCalled += 1 } + + img.src = pngCheckers + assert.strictEqual(img.src, pngCheckers) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 2) + assert.strictEqual(img.height, 2) + + assert.strictEqual(onloadCalled, 1) + + onloadCalled = 0 + img.onload = null + + img.src = pngClock + assert.strictEqual(img.src, pngClock) + assert.strictEqual(img.complete, true) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) + + assert.strictEqual(onloadCalled, 0) + }) + }) + + it('should unbind Image#onerror', function () { + return loadImage(pngClock).then((img) => { + let onloadCalled = 0 + let onerrorCalled = 0 + + img.onload = () => { onloadCalled += 1 } + img.onerror = () => { onerrorCalled += 1 } + + img.src = `${pngClock}s1` + assert.strictEqual(img.src, `${pngClock}s1`) + + img.src = `${pngClock}s2` + assert.strictEqual(img.src, `${pngClock}s2`) + + assert.strictEqual(onerrorCalled, 2) + + onerrorCalled = 0 + img.onerror = null + + img.src = `${pngClock}s3` + assert.strictEqual(img.src, `${pngClock}s3`) + + assert.strictEqual(onloadCalled, 0) + assert.strictEqual(onerrorCalled, 0) + }) + }) + + it('does not crash on invalid images', function () { + function withIncreasedByte (source, index) { + const copy = source.slice(0) + + copy[index] += 1 + + return copy + } + + const source = fs.readFileSync(jpgChrome) + + const corruptSources = [ + withIncreasedByte(source, 0), + withIncreasedByte(source, 1), + withIncreasedByte(source, 1060), + withIncreasedByte(source, 1061), + withIncreasedByte(source, 1062), + withIncreasedByte(source, 1063), + withIncreasedByte(source, 1064), + withIncreasedByte(source, 1065), + withIncreasedByte(source, 1066), + withIncreasedByte(source, 1067), + withIncreasedByte(source, 1068), + withIncreasedByte(source, 1069) + ] + + return Promise.all(corruptSources.map(src => loadImage(src).catch(() => null))) + }) + + it('does not contain `source` property', function () { + const keys = Reflect.ownKeys(Image.prototype) + assert.ok(!keys.includes('source')) + assert.ok(!keys.includes('getSource')) + assert.ok(!keys.includes('setSource')) + }) + + describe('supports BMP', function () { + it('parses 1-bit image', function (done) { + const img = new Image() + + img.onload = () => { + assert.strictEqual(img.width, 111) + assert.strictEqual(img.height, 72) + done() + } + + img.onerror = err => { throw err } + img.src = path.join(bmpDir, '1-bit.bmp') + }) + + it('parses 4-bit image', function (done) { + const img = new Image() + + img.onload = () => { + assert.strictEqual(img.width, 32) + assert.strictEqual(img.height, 32) + done() + } + + img.onerror = err => { throw err } + img.src = path.join(bmpDir, '4-bit.bmp') + }) + + it('parses 8-bit image') + + it('parses 24-bit image', function (done) { + const img = new Image() + + img.onload = () => { + assert.strictEqual(img.width, 2) + assert.strictEqual(img.height, 2) + + testImgd(img, [ + 0, 0, 255, 255, + 0, 255, 0, 255, + 255, 0, 0, 255, + 255, 255, 255, 255 + ]) + + done() + } + + img.onerror = err => { throw err } + img.src = path.join(bmpDir, '24-bit.bmp') + }) + + it('parses 32-bit image', function (done) { + const img = new Image() + + img.onload = () => { + assert.strictEqual(img.width, 4) + assert.strictEqual(img.height, 2) + + testImgd(img, [ + 0, 0, 255, 255, + 0, 255, 0, 255, + 255, 0, 0, 255, + 255, 255, 255, 255, + 0, 0, 255, 127, + 0, 255, 0, 127, + 255, 0, 0, 127, + 255, 255, 255, 127 + ]) + + done() + } + + img.onerror = err => { throw err } + img.src = fs.readFileSync(path.join(bmpDir, '32-bit.bmp')) // Also tests loading from buffer + }) + + it('parses minimal BMP', function (done) { + const img = new Image() + + img.onload = () => { + assert.strictEqual(img.width, 1) + assert.strictEqual(img.height, 1) + + testImgd(img, [ + 255, 0, 0, 255 + ]) + + done() + } + + img.onerror = err => { throw err } + img.src = path.join(bmpDir, 'min.bmp') + }) + + it('properly handles negative height', function (done) { + const img = new Image() + + img.onload = () => { + assert.strictEqual(img.width, 1) + assert.strictEqual(img.height, 2) + + testImgd(img, [ + 255, 0, 0, 255, + 0, 255, 0, 255 + ]) + + done() + } + + img.onerror = err => { throw err } + img.src = path.join(bmpDir, 'negative-height.bmp') + }) + + it('color palette', function (done) { + const img = new Image() + + img.onload = () => { + assert.strictEqual(img.width, 32) + assert.strictEqual(img.height, 32) + done() + } + + img.onerror = err => { throw err } + img.src = path.join(bmpDir, 'palette.bmp') + }) + + it('V3 header', function (done) { + const img = new Image() + + img.onload = () => { + assert.strictEqual(img.width, 256) + assert.strictEqual(img.height, 192) + done() + } + + img.onerror = err => { throw err } + img.src = path.join(bmpDir, 'v3-header.bmp') + }) + + it('V5 header') + + it('catches BMP errors', function (done) { + const img = new Image() + + img.onload = () => { + throw new Error('Invalid image should not be loaded properly') + } + + img.onerror = err => { + const msg = 'Error while processing file header - unexpected end of file' + assert.strictEqual(err.message, msg) + done() + } + + img.src = Buffer.from('BM') + }) + + it('BMP bomb', function (done) { + const img = new Image() + + img.onload = () => { + throw new Error('Invalid image should not be loaded properly') + } + + img.onerror = err => { + if (!err) throw new Error('Expected a error') + done() + } + + img.src = path.join(bmpDir, 'bomb.bmp') + }) + + it('rejects when loadImage is called with null', async function () { + await assert.rejects( + loadImage(null), + ) + }) + + it('rejects when loadImage is called with undefined', async function () { + await assert.rejects( + loadImage(undefined), + ) + }) + + it('rejects when loadImage is called with empty string', async function () { + await assert.rejects( + loadImage(''), + ) + }) + + function testImgd (img, data) { + const ctx = createCanvas(img.width, img.height).getContext('2d') + ctx.drawImage(img, 0, 0) + const actualData = ctx.getImageData(0, 0, img.width, img.height).data + assert.strictEqual(String(actualData), String(data)) + } + }) +}) diff --git a/test/imageData.test.js b/test/imageData.test.js index fe2f46a44..774bcf14e 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -1,62 +1,81 @@ -'use strict'; +/* eslint-env mocha */ -var Canvas = require('../') - , ImageData = Canvas.ImageData - , assert = require('assert'); +'use strict' + +const {createImageData} = require('../') +const {ImageData} = require('../') + +const assert = require('assert') describe('ImageData', function () { - it('should require new', function () { - assert.throws(function () { ImageData(); }, TypeError); - }); + it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { + assert.throws(function () { ImageData.prototype.width }, /invalid argument/i) + }) + + it('stringifies as [object ImageData]', function () { + const imageData = createImageData(2, 3) + assert.strictEqual(imageData.toString(), '[object ImageData]') + }) + + it('gives class string as `ImageData`', function () { + const imageData = createImageData(2, 3) + assert.strictEqual(Object.prototype.toString.call(imageData), '[object ImageData]') + }) it('should throw with invalid numeric arguments', function () { - assert.throws(function () { - new ImageData(0, 0); - }, /width is zero/); - assert.throws(function () { - new ImageData(1, 0); - }, /height is zero/); - assert.throws(function () { - new ImageData(0); - }, TypeError); - }); + assert.throws(() => { createImageData(0, 0) }, /width is zero/) + assert.throws(() => { createImageData(1, 0) }, /height is zero/) + assert.throws(() => { createImageData(0) }, TypeError) + }) it('should construct with width and height', function () { - var imagedata = new ImageData(2, 3); - assert.strictEqual(imagedata.width, 2); - assert.strictEqual(imagedata.height, 3); - assert(imagedata.data instanceof Uint8ClampedArray); - assert.strictEqual(imagedata.data.length, 24); - }); + const imageData = createImageData(2, 3) + + assert.strictEqual(imageData.width, 2) + assert.strictEqual(imageData.height, 3) + + assert.ok(imageData.data instanceof Uint8ClampedArray) + assert.strictEqual(imageData.data.length, 24) + }) it('should throw with invalid typed array', function () { - assert.throws(function () { - new ImageData(new Uint8ClampedArray(0), 0); - }, /input data has a zero byte length/); - assert.throws(function () { - new ImageData(new Uint8ClampedArray(3), 0); - }, /input data byte length is not a multiple of 4/); - assert.throws(function () { - new ImageData(new Uint8ClampedArray(16), 3); - }, RangeError); - assert.throws(function () { - new ImageData(new Uint8ClampedArray(12), 3, 5); - }, RangeError); - }); - - it('should construct with typed array', function () { - var data = new Uint8ClampedArray(2 * 3 * 4); - var imagedata = new ImageData(data, 2); - assert.strictEqual(imagedata.width, 2); - assert.strictEqual(imagedata.height, 3); - assert(imagedata.data instanceof Uint8ClampedArray); - assert.strictEqual(imagedata.data.length, 24); - - data = new Uint8ClampedArray(3 * 4 * 4); - imagedata = new ImageData(data, 3, 4); - assert.strictEqual(imagedata.width, 3); - assert.strictEqual(imagedata.height, 4); - assert(imagedata.data instanceof Uint8ClampedArray); - assert.strictEqual(imagedata.data.length, 48); - }); -}); + assert.throws(() => { createImageData(new Uint8ClampedArray(0), 0) }, /input data has a zero byte length/) + assert.throws(() => { createImageData(new Uint8ClampedArray(3), 0) }, /source width is zero/) + // Note: Some errors thrown by browsers are not thrown by node-canvas + // because our ImageData can support different BPPs. + }) + + it('should construct with Uint8ClampedArray', function () { + let data, imageData + + data = new Uint8ClampedArray(2 * 3 * 4) + imageData = createImageData(data, 2) + assert.strictEqual(imageData.width, 2) + assert.strictEqual(imageData.height, 3) + assert(imageData.data instanceof Uint8ClampedArray) + assert.strictEqual(imageData.data.length, 24) + + data = new Uint8ClampedArray(3 * 4 * 4) + imageData = createImageData(data, 3, 4) + assert.strictEqual(imageData.width, 3) + assert.strictEqual(imageData.height, 4) + assert(imageData.data instanceof Uint8ClampedArray) + assert.strictEqual(imageData.data.length, 48) + }) + + it('should construct with Uint16Array', function () { + let data = new Uint16Array(2 * 3 * 2) + let imagedata = createImageData(data, 2) + assert.strictEqual(imagedata.width, 2) + assert.strictEqual(imagedata.height, 3) + assert(imagedata.data instanceof Uint16Array) + assert.strictEqual(imagedata.data.length, 12) + + data = new Uint16Array(3 * 4 * 2) + imagedata = createImageData(data, 3, 4) + assert.strictEqual(imagedata.width, 3) + assert.strictEqual(imagedata.height, 4) + assert(imagedata.data instanceof Uint16Array) + assert.strictEqual(imagedata.data.length, 24) + }) +}) diff --git a/test/public/app.html b/test/public/app.html index 8948b7884..6a9dacd45 100644 --- a/test/public/app.html +++ b/test/public/app.html @@ -12,6 +12,7 @@

node-canvas

The tests below assert visual and api integrity by running the exact same code utilizing the client canvas api, as well as node-canvas.

+ diff --git a/test/public/app.js b/test/public/app.js index ff53f4a5c..b25ea11a2 100644 --- a/test/public/app.js +++ b/test/public/app.js @@ -18,34 +18,68 @@ function pdfLink (name) { }) } -function localRendering (name) { - var canvas = create('canvas', { width: 200, height: 200, title: name }) - - window.tests[name](canvas.getContext('2d'), function () {}) - +function localRendering (name, callback) { + const canvas = create('canvas', { width: 200, height: 200, title: name }) + const tests = window.tests + const ctx = canvas.getContext('2d', { alpha: true }) + const initialFillStyle = ctx.fillStyle + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 200, 200) + ctx.fillStyle = initialFillStyle + if (tests[name].length === 2) { + tests[name](ctx, callback) + } else { + tests[name](ctx) + callback(null) + } return canvas } +function getDifference (canvas, image, outputCanvas) { + const imgCanvas = create('canvas', { width: 200, height: 200 }) + const ctx = imgCanvas.getContext('2d', { alpha: true }) + const output = outputCanvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200) + ctx.drawImage(image, 0, 0, 200, 200) + const imageDataCanvas = ctx.getImageData(0, 0, 200, 200).data + const imageDataGolden = canvas.getContext('2d', { alpha: true }).getImageData(0, 0, 200, 200).data + window.pixelmatch(imageDataCanvas, imageDataGolden, output.data, 200, 200, { + includeAA: false, + threshold: 0.15 + }) + outputCanvas.getContext('2d', { alpha: true }).putImageData(output, 0, 0) + return outputCanvas +} + function clearTests () { - var table = document.getElementById('tests') + const table = document.getElementById('tests') if (table) document.body.removeChild(table) } function runTests () { clearTests() - var testNames = Object.keys(window.tests) + const testNames = Object.keys(window.tests) - var table = create('table', { id: 'tests' }, [ + const table = create('table', { id: 'tests' }, [ create('thead', {}, [ create('th', { textContent: 'node-canvas' }), create('th', { textContent: 'browser canvas' }), + create('th', { textContent: 'visual diffs' }), create('th', { textContent: '' }) ]), create('tbody', {}, testNames.map(function (name) { + const img = create('img') + const canvasOuput = create('canvas', { width: 200, height: 200, title: name }) + const canvas = localRendering(name, function () { + img.onload = function () { + getDifference(canvas, img, canvasOuput) + } + img.src = '/render?name=' + encodeURIComponent(name) + }) return create('tr', {}, [ - create('td', {}, [create('img', { src: '/render?name=' + encodeURIComponent(name) })]), - create('td', {}, [localRendering(name)]), + create('td', {}, [img]), + create('td', {}, [canvas]), + create('td', {}, [canvasOuput]), create('td', {}, [create('h3', { textContent: name }), pdfLink(name)]) ]) })) diff --git a/test/public/style.css b/test/public/style.css index 6e4d276b4..75116758e 100644 --- a/test/public/style.css +++ b/test/public/style.css @@ -26,14 +26,22 @@ p.msg { } table tr td:nth-child(1), -table tr td:nth-child(2) { +table tr td:nth-child(2), +table tr td:nth-child(3) { width: 200px; } -table tr td:nth-child(3) { +table tr td:nth-child(4) { padding: 0 45px; } table tr td p { margin: 5px 0; } + +table th { + background: white; + position: -webkit-sticky; + position: sticky; + top: 0; +} diff --git a/test/public/tests.js b/test/public/tests.js index 4f24d7ab1..582c0ce28 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1,14 +1,19 @@ -var Image -var imageSrc -var tests = {} +let DOMMatrix +let Image +let imageSrc +const tests = {} + +/* global btoa */ if (typeof module !== 'undefined' && module.exports) { module.exports = tests Image = require('../../').Image + DOMMatrix = require('../../').DOMMatrix imageSrc = function (filename) { return require('path').join(__dirname, '..', 'fixtures', filename) } } else { window.tests = tests Image = window.Image + DOMMatrix = window.DOMMatrix imageSrc = function (filename) { return filename } } @@ -32,7 +37,7 @@ tests['fillRect()'] = function (ctx) { } function renderLevel (minimumLevel, level, y) { - var x + let x for (x = 0; x < 243 / level; ++x) { drawBlock(x, y, level) } @@ -69,17 +74,17 @@ tests['fillRect()'] = function (ctx) { function getPointColour (x, y) { x = x / 121.5 - 1 y = -y / 121.5 + 1 - var x2y2 = x * x + y * y + const x2y2 = x * x + y * y if (x2y2 > 1) { return '#000' } - var root = Math.sqrt(1 - x2y2) - var x3d = x * 0.7071067812 + root / 2 - y / 2 - var y3d = x * 0.7071067812 - root / 2 + y / 2 - var z3d = 0.7071067812 * root + 0.7071067812 * y - var brightness = -x / 2 + root * 0.7071067812 + y / 2 + const root = Math.sqrt(1 - x2y2) + const x3d = x * 0.7071067812 + root / 2 - y / 2 + const y3d = x * 0.7071067812 - root / 2 + y / 2 + const z3d = 0.7071067812 * root + 0.7071067812 * y + let brightness = -x / 2 + root * 0.7071067812 + y / 2 if (brightness < 0) brightness = 0 return ( 'rgb(' + Math.round(brightness * 127.5 * (1 - y3d)) + @@ -92,6 +97,48 @@ tests['fillRect()'] = function (ctx) { render(1) } +tests['roundRect()'] = function (ctx) { + if (!ctx.roundRect) { + ctx.textAlign = 'center' + ctx.fillText('roundRect() not supported', 100, 100, 190) + ctx.fillText('try Chrome instead', 100, 115, 190) + return + } + ctx.roundRect(5, 5, 60, 60, 20) + ctx.fillStyle = 'red' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(5, 70, 60, 60, [10, 15, 20, 25]) + ctx.fillStyle = 'blue' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(70, 5, 60, 60, [10]) + ctx.fillStyle = 'green' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(70, 70, 60, 60, [10, 15]) + ctx.fillStyle = 'orange' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(135, 5, 60, 60, [10, 15, 20]) + ctx.fillStyle = 'pink' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(135, 70, 60, 60, [{ x: 30, y: 10 }, { x: 5, y: 20 }]) + ctx.fillStyle = 'darkseagreen' + ctx.fill() + + ctx.beginPath() + ctx.roundRect(5, 135, 8, 60, 15) + ctx.fillStyle = 'purple' + ctx.fill() +} + tests['lineTo()'] = function (ctx) { // Filled triangle ctx.beginPath() @@ -113,24 +160,24 @@ tests['arc()'] = function (ctx) { ctx.beginPath() ctx.arc(75, 75, 50, 0, Math.PI * 2, true) // Outer circle ctx.moveTo(110, 75) - ctx.arc(75, 75, 35, 0, Math.PI, false) // Mouth + ctx.arc(75, 75, 35, 0, Math.PI, false) // Mouth ctx.moveTo(65, 65) - ctx.arc(60, 65, 5, 0, Math.PI * 2, true) // Left eye + ctx.arc(60, 65, 5, 0, Math.PI * 2, true) // Left eye ctx.moveTo(95, 65) - ctx.arc(90, 65, 5, 0, Math.PI * 2, true) // Right eye + ctx.arc(90, 65, 5, 0, Math.PI * 2, true) // Right eye ctx.stroke() } tests['arc() 2'] = function (ctx) { - for (var i = 0; i < 4; i++) { - for (var j = 0; j < 3; j++) { + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 3; j++) { ctx.beginPath() - var x = 25 + j * 50 // x coordinate - var y = 25 + i * 50 // y coordinate - var radius = 20 // Arc radius - var startAngle = 0 // Starting point on circle - var endAngle = Math.PI + (Math.PI * j) / 2 // End point on circle - var anticlockwise = (i % 2) === 1 // clockwise or anticlockwise + const x = 25 + j * 50 // x coordinate + const y = 25 + i * 50 // y coordinate + const radius = 20 // Arc radius + const startAngle = 0 // Starting point on circle + const endAngle = Math.PI + (Math.PI * j) / 2 // End point on circle + const anticlockwise = (i % 2) === 1 // clockwise or anticlockwise ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise) @@ -143,6 +190,35 @@ tests['arc() 2'] = function (ctx) { } } +tests['arc()() #1736'] = function (ctx) { + let centerX = 512 + let centerY = 512 + let startAngle = 6.283185307179586 // exactly 2pi + let endAngle = 7.5398223686155035 + let innerRadius = 359.67999999999995 + let outerRadius = 368.64 + + ctx.scale(0.2, 0.2) + + ctx.beginPath() + ctx.moveTo(centerX + Math.cos(startAngle) * innerRadius, centerY + Math.sin(startAngle) * innerRadius) + ctx.lineTo(centerX + Math.cos(startAngle) * outerRadius, centerY + Math.sin(startAngle) * outerRadius) + ctx.arc(centerX, centerY, outerRadius, startAngle, endAngle, false) + ctx.lineTo(centerX + Math.cos(endAngle) * innerRadius, centerY + Math.sin(endAngle) * innerRadius) + ctx.arc(centerX, centerY, innerRadius, endAngle, startAngle, true) + ctx.closePath() + ctx.stroke() +} + +tests['arc()() #1808'] = function (ctx) { + ctx.scale(0.5, 0.5) + ctx.beginPath() + ctx.arc(256, 256, 50, 0, 2 * Math.PI, true) + ctx.arc(256, 256, 25, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.fill() +} + tests['arcTo()'] = function (ctx) { ctx.fillStyle = '#08C8EE' ctx.translate(-50, -50) @@ -162,6 +238,54 @@ tests['arcTo()'] = function (ctx) { ctx.fillText('node', 120, 155) } +tests['ellipse() 1'] = function (ctx) { + const n = 8 + for (let i = 0; i < n; i++) { + ctx.beginPath() + const a = i * 2 * Math.PI / n + const x = 100 + 50 * Math.cos(a) + const y = 100 + 50 * Math.sin(a) + ctx.ellipse(x, y, 10, 15, a, 0, 2 * Math.PI) + ctx.stroke() + } +} + +tests['ellipse() 2'] = function (ctx) { + const n = 8 + for (let i = 0; i < n; i++) { + ctx.beginPath() + const a = i * 2 * Math.PI / n + const x = 100 + 50 * Math.cos(a) + const y = 100 + 50 * Math.sin(a) + ctx.ellipse(x, y, 10, 15, a, 0, a) + ctx.stroke() + } +} + +tests['ellipse() 3'] = function (ctx) { + const n = 8 + for (let i = 0; i < n; i++) { + ctx.beginPath() + const a = i * 2 * Math.PI / n + const x = 100 + 50 * Math.cos(a) + const y = 100 + 50 * Math.sin(a) + ctx.ellipse(x, y, 10, 15, a, 0, a, true) + ctx.stroke() + } +} + +tests['ellipse() 4'] = function (ctx) { + const n = 8 + for (let i = 0; i < n; i++) { + ctx.beginPath() + const a = i * 2 * Math.PI / n + const x = 100 + 50 * Math.cos(a) + const y = 100 + 50 * Math.sin(a) + ctx.ellipse(x, y, 10, 15, a, a, 0, true) + ctx.stroke() + } +} + tests['bezierCurveTo()'] = function (ctx) { ctx.beginPath() ctx.moveTo(75, 40) @@ -187,12 +311,12 @@ tests['quadraticCurveTo()'] = function (ctx) { } tests['transform()'] = function (ctx) { - var sin = Math.sin(Math.PI / 6) - var cos = Math.cos(Math.PI / 6) + const sin = Math.sin(Math.PI / 6) + const cos = Math.cos(Math.PI / 6) ctx.translate(100, 100) ctx.scale(0.5, 0.5) - var c = 0 - for (var i = 0; i <= 12; i++) { + let c = 0 + for (let i = 0; i <= 12; i++) { c = Math.floor(255 / 12 * i) ctx.fillStyle = 'rgb(' + c + ',' + c + ',' + c + ')' ctx.fillRect(0, 0, 100, 10) @@ -210,11 +334,11 @@ tests['rotate()'] = function (ctx) { tests['rotate() 2'] = function (ctx) { ctx.translate(75, 75) - for (var i = 1; i < 6; i++) { // Loop through rings (from inside to out) + for (let i = 1; i < 6; i++) { // Loop through rings (from inside to out) ctx.save() ctx.fillStyle = 'rgb(' + (51 * i) + ',' + (255 - 51 * i) + ',255)' - for (var j = 0; j < i * 6; j++) { // draw individual dots + for (let j = 0; j < i * 6; j++) { // draw individual dots ctx.rotate(Math.PI * 2 / (i * 6)) ctx.beginPath() ctx.arc(0, i * 12.5, 5, 0, Math.PI * 2, true) @@ -227,8 +351,8 @@ tests['rotate() 2'] = function (ctx) { tests['translate()'] = function (ctx) { ctx.fillRect(0, 0, 300, 300) - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { ctx.save() ctx.strokeStyle = '#9CFF00' ctx.translate(50 + j * 100, 50 + i * 100) @@ -237,15 +361,18 @@ tests['translate()'] = function (ctx) { } } function drawSpirograph (ctx, R, r, O) { - var x1 = R - O - var y1 = 0 - var i = 1 + let x1 = R - O + let y1 = 0 + let i = 1 + let x2 + let y2 + ctx.beginPath() ctx.moveTo(x1, y1) do { if (i > 20000) break - var x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72)) - var y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72)) + x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72)) + y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72)) ctx.lineTo(x2, y2) x1 = x2 y1 = y2 @@ -263,7 +390,7 @@ tests['scale()'] = function (ctx) { // Uniform scaling ctx.save() ctx.translate(50, 50) - drawSpirograph(ctx, 22, 6, 5) // no scaling + drawSpirograph(ctx, 22, 6, 5) // no scaling ctx.translate(100, 0) ctx.scale(0.75, 0.75) @@ -306,15 +433,18 @@ tests['scale()'] = function (ctx) { drawSpirograph(ctx, 22, 6, 5) ctx.restore() function drawSpirograph (ctx, R, r, O) { - var x1 = R - O - var y1 = 0 - var i = 1 + let x1 = R - O + let y1 = 0 + let i = 1 + let x2 + let y2 + ctx.beginPath() ctx.moveTo(x1, y1) do { if (i > 20000) break - var x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72)) - var y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72)) + x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72)) + y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72)) ctx.lineTo(x2, y2) x1 = x2 y1 = y2 @@ -349,7 +479,7 @@ tests['clip() 2'] = function (ctx) { ctx.clip() // draw background - var lingrad = ctx.createLinearGradient(0, -75, 0, 75) + const lingrad = ctx.createLinearGradient(0, -75, 0, 75) lingrad.addColorStop(0, '#232256') lingrad.addColorStop(1, '#143778') @@ -357,11 +487,10 @@ tests['clip() 2'] = function (ctx) { ctx.fillRect(-75, -75, 150, 150) // draw stars - for (var j = 1; j < 50; j++) { + for (let j = 1; j < 50; j++) { ctx.save() ctx.fillStyle = '#fff' - ctx.translate(75 - Math.floor(Math.random() * 150), - 75 - Math.floor(Math.random() * 150)) + ctx.translate(75 - Math.floor(Math.random() * 150), 75 - Math.floor(Math.random() * 150)) drawStar(ctx, Math.floor(Math.random() * 4) + 2) ctx.restore() } @@ -369,7 +498,7 @@ tests['clip() 2'] = function (ctx) { ctx.save() ctx.beginPath() ctx.moveTo(r, 0) - for (var i = 0; i < 9; i++) { + for (let i = 0; i < 9; i++) { ctx.rotate(Math.PI / 5) if ((i % 2) === 0) { ctx.lineTo((r / 0.525731) * 0.200811, 0) @@ -383,14 +512,148 @@ tests['clip() 2'] = function (ctx) { } } +tests['createPattern()'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + const pattern = ctx.createPattern(img, 'repeat') + ctx.scale(0.1, 0.1) + ctx.fillStyle = pattern + ctx.fillRect(100, 100, 800, 800) + ctx.strokeStyle = pattern + ctx.lineWidth = 200 + ctx.strokeRect(1100, 1100, 800, 800) + done() + } + img.src = imageSrc('face.jpeg') +} + +tests['createPattern() with globalAlpha'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + const pattern = ctx.createPattern(img, 'repeat') + ctx.scale(0.1, 0.1) + ctx.globalAlpha = 0.6 + ctx.fillStyle = pattern + ctx.fillRect(100, 100, 800, 800) + ctx.globalAlpha = 0.2 + ctx.strokeStyle = pattern + ctx.lineWidth = 200 + ctx.strokeRect(1100, 1100, 800, 800) + done() + } + img.src = imageSrc('face.jpeg') +} + +tests['createPattern() repeat-x and repeat-y'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.scale(0.1, 0.1) + ctx.lineStyle = 'black' + ctx.lineWidth = 10 + ctx.fillStyle = ctx.createPattern(img, 'repeat-x') + ctx.fillRect(0, 0, 900, 900) + ctx.strokeRect(0, 0, 900, 900) + ctx.translate(1000, 1000) + ctx.fillStyle = ctx.createPattern(img, 'repeat-y') + ctx.fillRect(0, 0, 900, 900) + ctx.strokeRect(0, 0, 900, 900) + done() + } + img.src = imageSrc('face.jpeg') +} + +tests['createPattern() no-repeat'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.scale(0.1, 0.1) + ctx.lineStyle = 'black' + ctx.lineWidth = 10 + ctx.fillStyle = ctx.createPattern(img, 'no-repeat') + ctx.fillRect(0, 0, 900, 900) + ctx.strokeRect(0, 0, 900, 900) + ctx.fillStyle = ctx.createPattern(img, 'repeat') + ctx.fillRect(1000, 1000, 900, 900) + ctx.strokeRect(1000, 1000, 900, 900) + done() + } + img.src = imageSrc('face.jpeg') +} + +tests['createPattern() then setTransform and fill'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + var pattern = ctx.createPattern(img, 'repeat') + ctx.fillStyle = pattern + ctx.scale(0.125, 0.125) + + ctx.fillRect(0, 0, 800, 800) + + pattern.setTransform(new DOMMatrix().translate(100, 100)) + ctx.fillRect(0, 800, 800, 800) + + pattern.setTransform(new DOMMatrix().rotate(45)) + ctx.fillRect(800, 0, 800, 800) + + pattern.setTransform(new DOMMatrix().rotate(45).scale(4)) + ctx.fillRect(800, 800, 800, 800) + done() + } + img.src = imageSrc('quadrants.png') +} + +tests['createPattern() then setTransform and stroke'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + var pattern = ctx.createPattern(img, 'repeat') + ctx.lineWidth = 150 + ctx.strokeStyle = pattern + ctx.scale(0.125, 0.125) + + ctx.strokeRect(100, 100, 500, 500) + + pattern.setTransform(new DOMMatrix().translate(100, 100)) + ctx.strokeRect(100, 900, 500, 500) + + pattern.setTransform(new DOMMatrix().rotate(45)) + ctx.strokeRect(900, 100, 500, 500) + + pattern.setTransform(new DOMMatrix().rotate(45).scale(4)) + ctx.strokeRect(900, 900, 500, 500) + done() + } + img.src = imageSrc('quadrants.png') +} + +tests['createPattern() then setTransform with no-repeat'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + var pattern = ctx.createPattern(img, 'no-repeat') + ctx.fillStyle = pattern + ctx.scale(0.125, 0.125) + + ctx.fillRect(0, 0, 800, 800) + + pattern.setTransform(new DOMMatrix().translate(100, 900)) + ctx.fillRect(0, 800, 800, 800) + + pattern.setTransform(new DOMMatrix().translate(800, 0).rotate(45)) + ctx.fillRect(800, 0, 800, 800) + + pattern.setTransform(new DOMMatrix().translate(800, 800).rotate(45).scale(4)) + ctx.fillRect(800, 800, 800, 800) + done() + } + img.src = imageSrc('quadrants.png') +} + tests['createLinearGradient()'] = function (ctx) { - var lingrad = ctx.createLinearGradient(0, 0, 0, 150) + const lingrad = ctx.createLinearGradient(0, 0, 0, 150) lingrad.addColorStop(0, '#00ABEB') lingrad.addColorStop(0.5, '#fff') lingrad.addColorStop(0.5, '#26C000') lingrad.addColorStop(1, '#fff') - var lingrad2 = ctx.createLinearGradient(0, 50, 0, 95) + const lingrad2 = ctx.createLinearGradient(0, 50, 0, 95) lingrad2.addColorStop(0.5, '#000') lingrad2.addColorStop(1, 'rgba(0,0,0,0)') @@ -399,26 +662,81 @@ tests['createLinearGradient()'] = function (ctx) { ctx.fillRect(10, 10, 130, 130) ctx.strokeRect(50, 50, 50, 50) + + // Specifically test that setting the fillStyle to the current fillStyle works + ctx.fillStyle = '#13b575' + ctx.fillStyle = ctx.fillStyle // eslint-disable-line no-self-assign + ctx.fillRect(65, 65, 20, 20) + + const lingrad3 = ctx.createLinearGradient(0, 0, 200, 0) + lingrad3.addColorStop(0, 'rgba(0,255,0,0.5)') + lingrad3.addColorStop(0.33, 'rgba(255,255,0,0.5)') + lingrad3.addColorStop(0.66, 'rgba(0,255,255,0.5)') + lingrad3.addColorStop(1, 'rgba(255,0,255,0.5)') + ctx.fillStyle = lingrad3 + ctx.fillRect(0, 170, 200, 30) +} + +tests['createLinearGradient() with opacity'] = function (ctx) { + const lingrad = ctx.createLinearGradient(0, 0, 0, 200) + lingrad.addColorStop(0, '#00FF00') + lingrad.addColorStop(0.33, '#FF0000') + lingrad.addColorStop(0.66, '#0000FF') + lingrad.addColorStop(1, '#00FFFF') + ctx.fillStyle = lingrad + ctx.strokeStyle = lingrad + ctx.lineWidth = 10 + ctx.globalAlpha = 0.4 + ctx.strokeRect(5, 5, 190, 190) + ctx.fillRect(0, 0, 50, 50) + ctx.globalAlpha = 0.6 + ctx.strokeRect(35, 35, 130, 130) + ctx.fillRect(50, 50, 50, 50) + ctx.globalAlpha = 0.8 + ctx.strokeRect(65, 65, 70, 70) + ctx.fillRect(100, 100, 50, 50) + ctx.globalAlpha = 0.95 + ctx.fillRect(150, 150, 50, 50) +} + +tests['createLinearGradient() and transforms'] = function (ctx) { + const lingrad = ctx.createLinearGradient(0, -100, 0, 100) + lingrad.addColorStop(0, '#00FF00') + lingrad.addColorStop(0.33, '#FF0000') + lingrad.addColorStop(0.66, '#0000FF') + lingrad.addColorStop(1, '#00FFFF') + ctx.fillStyle = lingrad + ctx.translate(100, 100) + ctx.beginPath() + ctx.moveTo(-100, -100) + ctx.lineTo(100, -100) + ctx.lineTo(100, 100) + ctx.lineTo(-100, 100) + ctx.closePath() + ctx.globalAlpha = 0.5 + ctx.rotate(1.570795) + ctx.scale(0.6, 0.6) + ctx.fill() } tests['createRadialGradient()'] = function (ctx) { // Create gradients - var radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30) + const radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30) radgrad.addColorStop(0, '#A7D30C') radgrad.addColorStop(0.9, '#019F62') radgrad.addColorStop(1, 'rgba(1,159,98,0)') - var radgrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50) + const radgrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50) radgrad2.addColorStop(0, '#FF5F98') radgrad2.addColorStop(0.75, '#FF0188') radgrad2.addColorStop(1, 'rgba(255,1,136,0)') - var radgrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40) + const radgrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40) radgrad3.addColorStop(0, '#00C9FF') radgrad3.addColorStop(0.8, '#00B5E2') radgrad3.addColorStop(1, 'rgba(0,201,255,0)') - var radgrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90) + const radgrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90) radgrad4.addColorStop(0, '#F4F201') radgrad4.addColorStop(0.8, '#E4C700') radgrad4.addColorStop(1, 'rgba(228,199,0,0)') @@ -434,7 +752,7 @@ tests['createRadialGradient()'] = function (ctx) { ctx.fillRect(0, 0, 150, 150) } -tests['globalAlpha'] = function (ctx) { +tests.globalAlpha = function (ctx) { ctx.globalAlpha = 0.5 ctx.fillStyle = 'rgba(0,0,0,0.5)' ctx.strokeRect(0, 0, 50, 50) @@ -460,25 +778,25 @@ tests['globalAlpha 2'] = function (ctx) { ctx.globalAlpha = 0.2 - for (var i = 0; i < 7; i++) { + for (let i = 0; i < 7; i++) { ctx.beginPath() ctx.arc(75, 75, 10 + 10 * i, 0, Math.PI * 2, true) ctx.fill() } } -tests['fillStyle'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { +tests.fillStyle = function (ctx) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } } -tests['strokeStyle'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { +tests.strokeStyle = function (ctx) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.strokeStyle = 'rgb(0,' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ')' ctx.beginPath() @@ -503,7 +821,7 @@ tests['fill with stroke'] = function (ctx) { tests['floating point coordinates'] = function (ctx) { ctx.lineCap = 'square' - for (var i = 0; i < 70; i += 3.05) { + for (let i = 0; i < 70; i += 3.05) { ctx.rect(i + 3, 10.5, 0, 130) ctx.moveTo(i + 77, 10.5) ctx.lineTo(i + 77, 140.5) @@ -511,8 +829,8 @@ tests['floating point coordinates'] = function (ctx) { ctx.stroke() } -tests['lineWidth'] = function (ctx) { - for (var i = 0; i < 10; i++) { +tests.lineWidth = function (ctx) { + for (let i = 0; i < 10; i++) { ctx.lineWidth = 1 + i ctx.beginPath() ctx.moveTo(5 + i * 14, 5) @@ -522,7 +840,7 @@ tests['lineWidth'] = function (ctx) { } tests['line caps'] = function (ctx) { - var lineCap = ['butt', 'round', 'square'] + const lineCap = ['butt', 'round', 'square'] ctx.strokeStyle = '#09f' ctx.beginPath() @@ -533,7 +851,7 @@ tests['line caps'] = function (ctx) { ctx.stroke() ctx.strokeStyle = 'black' - for (var i = 0; i < lineCap.length; i++) { + for (let i = 0; i < lineCap.length; i++) { ctx.lineWidth = 15 ctx.lineCap = lineCap[i] ctx.beginPath() @@ -544,9 +862,9 @@ tests['line caps'] = function (ctx) { } tests['line join'] = function (ctx) { - var lineJoin = ['round', 'bevel', 'miter'] + const lineJoin = ['round', 'bevel', 'miter'] ctx.lineWidth = 10 - for (var i = 0; i < lineJoin.length; i++) { + for (let i = 0; i < lineJoin.length; i++) { ctx.lineJoin = lineJoin[i] ctx.beginPath() ctx.moveTo(-5, 5 + i * 40) @@ -567,7 +885,7 @@ tests['lineCap default'] = function (ctx) { ctx.stroke() } -tests['lineCap'] = function (ctx) { +tests.lineCap = function (ctx) { ctx.beginPath() ctx.lineWidth = 10.0 ctx.lineCap = 'round' @@ -577,7 +895,7 @@ tests['lineCap'] = function (ctx) { ctx.stroke() } -tests['lineJoin'] = function (ctx) { +tests.lineJoin = function (ctx) { ctx.beginPath() ctx.lineWidth = 10.0 ctx.lineJoin = 'round' @@ -587,7 +905,7 @@ tests['lineJoin'] = function (ctx) { ctx.stroke() } -tests['states'] = function (ctx) { +tests.states = function (ctx) { ctx.save() ctx.rect(50, 50, 100, 100) ctx.stroke() @@ -669,7 +987,7 @@ tests['fillText()'] = function (ctx) { ctx.lineTo(10, 10) ctx.fillText('Awesome!', 50, 100) - var te = ctx.measureText('Awesome!') + const te = ctx.measureText('Awesome!') ctx.strokeStyle = 'rgba(0,0,0,0.5)' ctx.lineTo(50, 102) @@ -697,6 +1015,57 @@ tests['fillText() transformations'] = function (ctx) { ctx.fillText('bar', 50, 100) } +tests['fillText() maxWidth argument'] = function (ctx) { + ctx.font = 'Helvetica, sans' + ctx.fillText('Drawing text can be fun!', 0, 20) + + for (let i = 1; i < 6; i++) { + ctx.fillText('Drawing text can be fun!', 0, 20 * (7 - i), i * 20) + } + + ctx.fillText('Drawing text can be fun!', 0, 20 * 7) +} + +tests['maxWidth bug first usage path'] = function (ctx, done) { + ctx.textDrawingMode = 'path' + ctx.fillText('Drawing text can be fun!', 0, 20, 50) + ctx.fillText('Drawing text can be fun!', 0, 40, 50) + ctx.fillText('Drawing text can be fun changing text bug!', 0, 60, 50) + done() +} + +tests['maxWidth bug first usage glyph'] = function (ctx, done) { + ctx.textDrawingMode = 'glyph' + ctx.fillText('Drawing text can be fun!', 0, 20, 50) + ctx.fillText('Drawing text can be fun!', 0, 40, 50) + ctx.fillText('Drawing text can be fun changing text bug!', 0, 60, 50) + done() +} + +tests['fillText() maxWidth argument + textAlign center (#1253)'] = function (ctx) { + ctx.font = 'Helvetica, sans' + ctx.textAlign = 'center' + ctx.fillText('Drawing text can be fun!', 100, 20) + + for (let i = 1; i < 6; i++) { + ctx.fillText('Drawing text can be fun!', 100, 20 * (7 - i), i * 20) + } + + ctx.fillText('Drawing text can be fun!', 100, 20 * 7) +} + +tests['fillText() maxWidth argument + textAlign right'] = function (ctx) { + ctx.font = 'Helvetica, sans' + ctx.textAlign = 'right' + ctx.fillText('Drawing text can be fun!', 200, 20) + + for (let i = 1; i < 6; i++) { + ctx.fillText('Drawing text can be fun!', 200, 20 * (7 - i), i * 20) + } + + ctx.fillText('Drawing text can be fun!', 200, 20 * 7) +} + tests['strokeText()'] = function (ctx) { ctx.strokeStyle = '#666' ctx.strokeRect(0, 0, 200, 200) @@ -714,6 +1083,17 @@ tests['strokeText()'] = function (ctx) { ctx.strokeText('bar', 100, 100) } +tests['strokeText() maxWidth argument'] = function (ctx) { + ctx.font = 'Helvetica, sans' + ctx.strokeText('Drawing text can be fun!', 0, 20) + + for (let i = 1; i < 6; i++) { + ctx.strokeText('Drawing text can be fun!', 0, 20 * (7 - i), i * 20) + } + + ctx.strokeText('Drawing text can be fun!', 0, 20 * 7) +} + tests['textAlign right'] = function (ctx) { ctx.strokeStyle = '#666' ctx.strokeRect(0, 0, 200, 200) @@ -725,10 +1105,18 @@ tests['textAlign right'] = function (ctx) { ctx.lineTo(100, 0) ctx.lineTo(100, 200) ctx.stroke() - ctx.font = 'normal 20px Arial' + ctx.direction = 'ltr' ctx.textAlign = 'right' - ctx.fillText('right', 100, 100) + ctx.fillText('right ltr', 100, 70) + ctx.fillText( + 'الحق ltr', + 100, 100) + ctx.direction = 'rtl' + ctx.fillText('right rtl', 100, 130) + ctx.fillText( + 'rtl الحق', + 100, 160) } tests['textAlign left'] = function (ctx) { @@ -744,8 +1132,69 @@ tests['textAlign left'] = function (ctx) { ctx.stroke() ctx.font = 'normal 20px Arial' + ctx.direction = 'ltr' ctx.textAlign = 'left' - ctx.fillText('left', 100, 100) + ctx.fillText('left ltr', 100, 70) + ctx.fillText( + 'تركت ltr', + 100, 100) + ctx.direction = 'rtl' + ctx.fillText('left rtl', 100, 130) + ctx.fillText( + 'rtl تركت', + 100, 160) +} + +tests['textAlign start'] = function (ctx) { + ctx.strokeStyle = '#666' + ctx.strokeRect(0, 0, 200, 200) + ctx.lineTo(0, 100) + ctx.lineTo(200, 100) + ctx.stroke() + + ctx.beginPath() + ctx.lineTo(100, 0) + ctx.lineTo(100, 200) + ctx.stroke() + + ctx.font = 'normal 20px Arial' + ctx.direction = 'ltr' + ctx.textAlign = 'start' + ctx.fillText('start ltr', 100, 70) + ctx.fillText( + 'بداية ltr', + 100, 100) + ctx.direction = 'rtl' + ctx.fillText('start rtl', 100, 130) + ctx.fillText( + 'rtl بداية', + 100, 160) +} + +tests['textAlign end'] = function (ctx) { + ctx.strokeStyle = '#666' + ctx.strokeRect(0, 0, 200, 200) + ctx.lineTo(0, 100) + ctx.lineTo(200, 100) + ctx.stroke() + + ctx.beginPath() + ctx.lineTo(100, 0) + ctx.lineTo(100, 200) + ctx.stroke() + + ctx.font = 'normal 20px Arial' + ctx.direction = 'ltr' + ctx.textAlign = 'end' + ctx.fillText('end ltr', 100, 70) + ctx.fillText( + 'نهاية ltr', + 100, 100) + ctx.direction = 'rtl' + ctx.fillText('start rtl', 100, 130) + ctx.fillText( + 'rtl نهاية', + 100, 160) } tests['textAlign center'] = function (ctx) { @@ -999,236 +1448,146 @@ tests['font family invalid'] = function (ctx) { ctx.fillText('14px Invalid, Impact', 100, 100) } -tests['globalCompositeOperation source-over'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'source-over' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} +tests['font style variant weight size family'] = function (ctx) { + ctx.strokeStyle = '#666' + ctx.strokeRect(0, 0, 200, 200) + ctx.lineTo(0, 100) + ctx.lineTo(200, 100) + ctx.stroke() -tests['globalCompositeOperation source-in'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'source-in' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() + ctx.font = 'normal normal normal 16px Impact' + ctx.textAlign = 'center' + ctx.fillText('normal normal normal 16px', 100, 100) +} + +// From https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation +const gco = [ + 'source-over', 'source-in', 'source-out', 'source-atop', + 'destination-over', 'destination-in', 'destination-out', 'destination-atop', + 'lighter', 'copy', 'xor', 'multiply', 'screen', 'overlay', 'darken', + 'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', + 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity' +] + +gco.forEach(op => { + tests['globalCompositeOperator ' + op] = function (ctx, done) { + const img1 = new Image() + const img2 = new Image() + img1.onload = function () { + img2.onload = function () { + ctx.globalAlpha = 0.7 + ctx.drawImage(img1, 0, 0) + ctx.globalCompositeOperation = op + ctx.drawImage(img2, 0, 0) + done() + } + img2.src = imageSrc('newcontent.png') + } + img1.src = imageSrc('existing.png') + } +}) + +gco.forEach(op => { + tests['9 args, transform, globalCompositeOperator ' + op] = function (ctx, done) { + const img1 = new Image() + const img2 = new Image() + img1.onload = function () { + img2.onload = function () { + ctx.globalAlpha = 0.7 + ctx.drawImage(img1, 0, 0) + ctx.globalCompositeOperation = op + ctx.rotate(0.1) + ctx.scale(0.8, 1.2) + ctx.translate(5, -5) + ctx.drawImage(img2, -80, -50, 400, 400, 10, 10, 180, 180) + done() + } + img2.src = imageSrc('newcontent.png') + } + img1.src = imageSrc('existing.png') + } +}) + +tests['drawImage issue #1249'] = function (ctx, done) { + const img1 = new Image() + const img2 = new Image() + img1.onload = function () { + img2.onload = function () { + ctx.drawImage(img1, 0, 0, 200, 200) + ctx.drawImage(img2, -8, -8, 18, 18, 0, 0, 200, 200) + ctx.restore() + done() + } + img2.src = imageSrc('checkers.png') + } + img1.src = imageSrc('chrome.jpg') } -tests['globalCompositeOperation source-out'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'source-out' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() +tests['drawImage 9 arguments big numbers'] = function (ctx, done) { + const img = new Image() + ctx.imageSmoothingEnabled = false + img.onload = function () { + // we use big numbers because is over the max canvas allowed + ctx.drawImage(img, -90000, -90000, 90080, 90080, -180000, -18000, 180160, 18016) + ctx.drawImage(img, -90000, -90000, 90040, 90040, -179930, -179930, 180060, 180060) + ctx.drawImage(img, -90000, -90000, 90080, 90080, -18000, -180000, 18016, 180160) + ctx.drawImage(img, 475, 380, 90000, 90000, 20, 20, 180000, 720000) + done(null) + } + img.onerror = function () { + done(new Error('Failed to load image')) + } + img.src = imageSrc('face.jpeg') } -tests['globalCompositeOperation destination-in'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'destination-in' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() +tests['known bug #416'] = function (ctx, done) { + const img1 = new Image() + const img2 = new Image() + img1.onload = function () { + img2.onload = function () { + ctx.drawImage(img1, 0, 0) + ctx.globalCompositeOperation = 'destination-in' + ctx.save() + ctx.translate(img2.width / 2, img1.height / 2) + ctx.rotate(Math.PI / 4) + ctx.scale(0.5, 0.5) + ctx.translate(-img2.width / 2, -img1.height / 2) + ctx.drawImage(img2, 0, 0) + ctx.restore() + done() + } + img2.src = imageSrc('newcontent.png') + } + img1.src = imageSrc('existing.png') } -tests['globalCompositeOperation source-atop'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'source-atop' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} +tests.shadowBlur = function (ctx) { + ctx.fillRect(150, 10, 20, 20) -tests['globalCompositeOperation destination-out'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'destination-out' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} + ctx.lineTo(20, 5) + ctx.lineTo(100, 5) + ctx.stroke() -tests['globalCompositeOperation destination-atop'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'destination-atop' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} + ctx.shadowColor = '#000' + ctx.shadowBlur = 5 + ctx.fillRect(20, 20, 100, 100) -tests['globalCompositeOperation xor'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'xor' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} + ctx.beginPath() + ctx.lineTo(20, 150) + ctx.lineTo(100, 150) + ctx.stroke() -tests['globalCompositeOperation copy'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'copy' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() + ctx.shadowBlur = 0 + + ctx.beginPath() + ctx.lineTo(20, 180) + ctx.lineTo(100, 180) + ctx.stroke() + + ctx.fillRect(150, 150, 20, 20) } -tests['globalCompositeOperation lighter'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'lighter' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation darker'] = function (ctx) { - ctx.fillStyle = 'blue' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'darker' - ctx.fillStyle = 'red' - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation multiply'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'multiply' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation screen'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'screen' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation overlay'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'overlay' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation hard-light'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hard-light' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation hsl-hue'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hsl-hue' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation hsl-saturation'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hsl-saturation' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation hsl-color'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hsl-color' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['globalCompositeOperation hsl-luminosity'] = function (ctx) { - ctx.fillStyle = 'rgba(0,0,255,0.6)' - ctx.fillRect(0, 0, 100, 100) - ctx.globalCompositeOperation = 'hsl-luminosity' - var grad = ctx.createRadialGradient(80, 80, 5, 60, 60, 60) - grad.addColorStop(0, 'yellow') - grad.addColorStop(0.2, 'red') - grad.addColorStop(1, 'black') - ctx.fillStyle = grad - ctx.arc(80, 80, 50, 0, Math.PI * 2, false) - ctx.fill() -} - -tests['shadowBlur'] = function (ctx) { - ctx.fillRect(150, 10, 20, 20) - - ctx.lineTo(20, 5) - ctx.lineTo(100, 5) - ctx.stroke() - - ctx.shadowColor = '#000' - ctx.shadowBlur = 5 - ctx.fillRect(20, 20, 100, 100) - - ctx.beginPath() - ctx.lineTo(20, 150) - ctx.lineTo(100, 150) - ctx.stroke() - - ctx.shadowBlur = 0 - - ctx.beginPath() - ctx.lineTo(20, 180) - ctx.lineTo(100, 180) - ctx.stroke() - - ctx.fillRect(150, 150, 20, 20) -} - -tests['shadowColor'] = function (ctx) { +tests.shadowColor = function (ctx) { ctx.fillRect(150, 10, 20, 20) ctx.lineTo(20, 5) @@ -1535,7 +1894,7 @@ tests['shadow transform text'] = function (ctx) { } tests['shadow image'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.shadowColor = '#f3ac22' ctx.shadowBlur = 2 @@ -1544,14 +1903,57 @@ tests['shadow image'] = function (ctx, done) { ctx.drawImage(img, 0, 0) done(null) } + img.onerror = done + img.src = imageSrc('star.png') +} + +tests['shadow image with crop'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.shadowColor = '#000' + ctx.shadowBlur = 4 + ctx.shadowOffsetX = 4 + ctx.shadowOffsetY = 4 + + // cropped + ctx.drawImage(img, 100, 100, 150, 150, 25, 25, 150, 150) + done(null) + } img.onerror = function () { done(new Error('Failed to load image')) } - img.src = imageSrc('star.png') + img.src = imageSrc('face.jpeg') +} + +tests['shadow image with crop and zoom'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.shadowColor = '#000' + ctx.shadowBlur = 4 + ctx.shadowOffsetX = 4 + ctx.shadowOffsetY = 4 + + // cropped + ctx.drawImage(img, 100, 100, 40, 40, 25, 25, 150, 150) + done(null) + } + img.onerror = function () { + done(new Error('Failed to load image')) + } + img.src = imageSrc('face.jpeg') +} + +tests['drawImage canvas over canvas'] = function (ctx) { + // Drawing canvas to itself + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 200, 200) + ctx.fillStyle = 'black' + ctx.fillRect(5, 5, 10, 10) + ctx.drawImage(ctx.canvas, 20, 20) } tests['scaled shadow image'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.shadowColor = '#f3ac22' ctx.shadowBlur = 2 @@ -1560,10 +1962,81 @@ tests['scaled shadow image'] = function (ctx, done) { ctx.drawImage(img, 10, 10, 80, 80) done(null) } + img.onerror = done + img.src = imageSrc('star.png') +} + +tests['smoothing disabled image'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.imageSmoothingEnabled = false + ctx.patternQuality = 'good' + // cropped + ctx.drawImage(img, 0, 0, 10, 10, 0, 0, 200, 200) + done(null) + } img.onerror = function () { done(new Error('Failed to load image')) } - img.src = imageSrc('star.png') + img.src = imageSrc('face.jpeg') +} + +tests['createPattern() with globalAlpha and smoothing off scaling down'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.imageSmoothingEnabled = false + ctx.patternQuality = 'good' + const pattern = ctx.createPattern(img, 'repeat') + ctx.scale(0.1, 0.1) + ctx.globalAlpha = 0.95 + ctx.fillStyle = pattern + ctx.fillRect(100, 100, 800, 800) + ctx.globalAlpha = 1 + ctx.strokeStyle = pattern + ctx.lineWidth = 800 + ctx.strokeRect(1400, 1100, 1, 800) + done() + } + img.src = imageSrc('face.jpeg') +} + +tests['createPattern() with globalAlpha and smoothing off scaling up'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.imageSmoothingEnabled = false + ctx.patternQuality = 'good' + const pattern = ctx.createPattern(img, 'repeat') + ctx.scale(20, 20) + ctx.globalAlpha = 0.95 + ctx.fillStyle = pattern + ctx.fillRect(1, 1, 8, 3) + ctx.globalAlpha = 1 + ctx.strokeStyle = pattern + ctx.lineWidth = 2 + ctx.strokeRect(2, 6, 6, 1) + done() + } + img.src = imageSrc('face.jpeg') +} + +tests['smoothing and gradients (gradients are not influenced by patternQuality)'] = function (ctx) { + const grad1 = ctx.createLinearGradient(0, 0, 10, 10) + grad1.addColorStop(0, 'yellow') + grad1.addColorStop(0.25, 'red') + grad1.addColorStop(0.75, 'blue') + grad1.addColorStop(1, 'limegreen') + ctx.imageSmoothingEnabled = false + ctx.patternQuality = 'nearest' + ctx.globalAlpha = 0.9 + // linear grad box + ctx.fillStyle = grad1 + ctx.moveTo(0, 0) + ctx.lineTo(200, 0) + ctx.lineTo(200, 200) + ctx.lineTo(0, 200) + ctx.lineTo(0, 0) + ctx.scale(20, 20) + ctx.fill() } tests['shadow integration'] = function (ctx) { @@ -1573,13 +2046,13 @@ tests['shadow integration'] = function (ctx) { ctx.shadowColor = '#eee' ctx.lineWidth = 3 - var grad1 = ctx.createLinearGradient(105, 0, 200, 100) + const grad1 = ctx.createLinearGradient(105, 0, 200, 100) grad1.addColorStop(0, 'yellow') grad1.addColorStop(0.25, 'red') grad1.addColorStop(0.75, 'blue') grad1.addColorStop(1, 'limegreen') - var grad2 = ctx.createRadialGradient(50, 50, 10, 50, 50, 50) + const grad2 = ctx.createRadialGradient(50, 50, 10, 50, 50, 50) grad2.addColorStop(0, 'yellow') grad2.addColorStop(0.25, 'red') grad2.addColorStop(0.75, 'blue') @@ -1623,100 +2096,147 @@ tests['font state'] = function (ctx) { } tests['drawImage(img,0,0)'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } tests['drawImage(img) jpeg'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, 100, 100) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('face.jpeg') } +tests['drawImage(img) YCCK JPEG (#425)'] = function (ctx, done) { + // This also provides coverage for CMYK JPEGs + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0, 100, 100) + done(null) + } + img.onerror = done + img.src = imageSrc('ycck.jpg') +} + +tests['drawImage(img) grayscale JPEG'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0, 100, 100) + done(null) + } + img.onerror = done + img.src = imageSrc('grayscale.jpg') +} + +tests['drawImage(img) svg'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0, 100, 100) + done(null) + } + img.onerror = done + img.src = imageSrc('tree.svg') +} + +tests['drawImage(img) svg with scaling from drawImage'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, -800, -800, 1000, 1000) + done(null) + } + img.onerror = done + img.src = imageSrc('tree.svg') +} + +tests['drawImage(img) svg with scaling from ctx'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.scale(100, 100) + ctx.drawImage(img, -8, -8, 10, 10) + done(null) + } + img.onerror = done + img.src = imageSrc('tree.svg') +} + tests['drawImage(img,x,y)'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 5, 25) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } tests['drawImage(img,x,y,w,h) scale down'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 25, 25, 10, 10) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) + img.onerror = done + img.src = imageSrc('state.png') +} + +tests['drawImage(img,x,y,w,h) scale down in a scaled up context'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.scale(20, 20) + ctx.drawImage(img, 0, 0, 10, 10) + done(null) } + img.onerror = done img.src = imageSrc('state.png') } tests['drawImage(img,x,y,w,h) scale up'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, 200, 200) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } tests['drawImage(img,x,y,w,h) scale vertical'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, img.width, 200) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } tests['drawImage(img,sx,sy,sw,sh,x,y,w,h)'] = function (ctx, done) { - var img = new Image() + const img = new Image() img.onload = function () { ctx.drawImage(img, 13, 13, 45, 45, 25, 25, img.width / 2, img.height / 2) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } tests['drawImage(img,0,0) globalAlpha'] = function (ctx, done) { - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) ctx.globalAlpha = 0.5 img.onload = function () { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1724,123 +2244,132 @@ tests['drawImage(img,0,0) clip'] = function (ctx, done) { ctx.arc(50, 50, 50, 0, Math.PI * 2, false) ctx.stroke() ctx.clip() - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) ctx.globalAlpha = 0.5 img.onload = function () { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } tests['putImageData()'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 10, 10) } +tests['putImageData() 1'] = function (ctx) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { + ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' + ctx.fillRect(j * 25, i * 25, 25, 25) + } + } + const data = ctx.getImageData(0, 0, 50, 50) + ctx.putImageData(data, -10, -10) +} + tests['putImageData() 2'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } - var data = ctx.getImageData(25, 25, 50, 50) + const data = ctx.getImageData(25, 25, 50, 50) ctx.putImageData(data, 10, 10) } tests['putImageData() 3'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } - var data = ctx.getImageData(10, 25, 10, 50) + const data = ctx.getImageData(10, 25, 10, 50) ctx.putImageData(data, 50, 10) } tests['putImageData() 4'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.strokeRect(30, 30, 30, 30) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 30, 30, 10, 10, 30, 30) } tests['putImageData() 5'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.strokeRect(60, 60, 50, 30) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 60, 60, 0, 0, 50, 30) } tests['putImageData() 6'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.strokeRect(60, 60, 50, 30) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 60, 60, 10, 0, 35, 30) } tests['putImageData() 7'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.strokeRect(60, 60, 50, 30) ctx.translate(20, 20) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, 60, 60, 10, 20, 35, -10) } tests['putImageData() 8'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.translate(20, 20) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, -10, -10, 0, 20, 35, 30) } tests['putImageData() 9'] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' ctx.fillRect(j * 25, i * 25, 25, 25) } } ctx.translate(20, 20) - var data = ctx.getImageData(0, 0, 50, 50) + const data = ctx.getImageData(0, 0, 50, 50) ctx.putImageData(data, -10, -10, 0, 20, 500, 500) } @@ -1852,10 +2381,32 @@ tests['putImageData() 10'] = function (ctx) { ctx.fillStyle = 'rgba(0,0,255,1)' ctx.fillRect(100, 0, 50, 100) - var data = ctx.getImageData(0, 0, 120, 20) + const data = ctx.getImageData(0, 0, 120, 20) ctx.putImageData(data, 20, 120) } +tests['putImageData() 11'] = function (ctx) { + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' + ctx.fillRect(j * 25, i * 25, 25, 25) + } + } + const data = ctx.getImageData(-25, -25, 50, 50) + ctx.putImageData(data, 10, 10) +} + +tests['putImageData() 12'] = function (ctx) { + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' + ctx.fillRect(j * 25, i * 25, 25, 25) + } + } + const data = ctx.getImageData(175, 175, 50, 50) + ctx.putImageData(data, 10, 10) +} + tests['putImageData() alpha'] = function (ctx) { ctx.fillStyle = 'rgba(255,0,0,0.5)' ctx.fillRect(0, 0, 50, 100) @@ -1864,7 +2415,7 @@ tests['putImageData() alpha'] = function (ctx) { ctx.fillStyle = 'rgba(0,0,255,0.5)' ctx.fillRect(100, 0, 50, 100) - var data = ctx.getImageData(0, 0, 120, 20) + const data = ctx.getImageData(0, 0, 120, 20) ctx.putImageData(data, 20, 120) } @@ -1876,7 +2427,7 @@ tests['putImageData() alpha 2'] = function (ctx) { ctx.fillStyle = 'rgba(0,0,255,0.75)' ctx.fillRect(100, 0, 50, 100) - var data = ctx.getImageData(0, 0, 120, 20) + const data = ctx.getImageData(0, 0, 120, 20) ctx.putImageData(data, 20, 120) } @@ -1889,79 +2440,79 @@ tests['putImageData() globalAlpha'] = function (ctx) { ctx.fillStyle = '#00f' ctx.fillRect(100, 0, 50, 100) - var data = ctx.getImageData(0, 0, 120, 20) + const data = ctx.getImageData(0, 0, 120, 20) ctx.putImageData(data, 20, 120) } tests['putImageData() png data'] = function (ctx, done) { - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) img.onload = function () { ctx.drawImage(img, 0, 0, 200, 200) - var imageData = ctx.getImageData(0, 0, 50, 50) - var data = imageData.data - for (var i = 0, len = data.length; i < len; i += 4) { - data[i + 3] = 80 + const imageData = ctx.getImageData(0, 0, 50, 50) + const data = imageData.data + if (data instanceof Uint8ClampedArray) { + for (let i = 0, len = data.length; i < len; i += 4) { + data[i + 3] = 80 + } } ctx.putImageData(imageData, 50, 50) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } tests['putImageData() png data 2'] = function (ctx, done) { - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) img.onload = function () { ctx.drawImage(img, 0, 0, 200, 200) - var imageData = ctx.getImageData(0, 0, 50, 50) - var data = imageData.data - for (var i = 0, len = data.length; i < len; i += 4) { - data[i + 3] = 80 + const imageData = ctx.getImageData(0, 0, 50, 50) + const data = imageData.data + if (data instanceof Uint8ClampedArray) { + for (let i = 0, len = data.length; i < len; i += 4) { + data[i + 3] = 80 + } } ctx.putImageData(imageData, 50, 50, 10, 10, 20, 20) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } tests['putImageData() png data 3'] = function (ctx, done) { - var img = new Image() + const img = new Image() ctx.fillRect(50, 50, 30, 30) img.onload = function () { ctx.drawImage(img, 0, 0, 200, 200) - var imageData = ctx.getImageData(0, 0, 50, 50) - var data = imageData.data - for (var i = 0, len = data.length; i < len; i += 4) { - data[i + 0] = data[i + 0] * 0.2 - data[i + 1] = data[i + 1] * 0.2 - data[i + 2] = data[i + 2] * 0.2 + const imageData = ctx.getImageData(0, 0, 50, 50) + const data = imageData.data + if (data instanceof Uint8ClampedArray) { + for (let i = 0, len = data.length; i < len; i += 4) { + data[i + 0] = data[i + 0] * 0.2 + data[i + 1] = data[i + 1] * 0.2 + data[i + 2] = data[i + 2] * 0.2 + } } ctx.putImageData(imageData, 50, 50) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } -tests['setLineDash'] = function (ctx) { +tests.setLineDash = function (ctx) { ctx.setLineDash([10, 5, 25, 15]) - ctx.lineWidth = 17 + ctx.lineWidth = 14 - var y = 5 - var line = function (lineDash, color) { + let y = 5 + const line = function (lineDash, color) { ctx.setLineDash(lineDash) if (color) ctx.strokeStyle = color ctx.beginPath() @@ -1980,19 +2531,22 @@ tests['setLineDash'] = function (ctx) { line([10, 10, NaN]) line((function () { ctx.setLineDash([8]) - var a = ctx.getLineDash() + const a = ctx.getLineDash() a[0] -= 3 a.push(20) return a })(), 'orange') + line([0, 0], 'purple') // should be full + line([0, 0, 3, 0], 'orange') // should be full + line([0, 3, 0, 0], 'green') // should be empty } -tests['lineDashOffset'] = function (ctx) { +tests.lineDashOffset = function (ctx) { ctx.setLineDash([10, 5, 25, 15]) ctx.lineWidth = 4 - var y = 5 - var line = function (lineDashOffset, color) { + let y = 5 + const line = function (lineDashOffset, color) { ctx.lineDashOffset = lineDashOffset if (color) ctx.strokeStyle = color ctx.beginPath() @@ -2014,18 +2568,18 @@ tests['lineDashOffset'] = function (ctx) { line(60, 'orange') line(-Infinity) line(70, 'purple') - line(void 0) + line(undefined) line(80, 'black') line(ctx.lineDashOffset + 10) - for (var i = 0; i < 10; i++) { + for (let i = 0; i < 10; i++) { line(90 + i / 5, 'red') } } tests['fillStyle=\'hsl(...)\''] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'hsl(' + (360 - 60 * i) + ',' + (100 - 16.66 * j) + '%,' + (50 + (i + j) * (50 / 12)) + '%)' ctx.fillRect(j * 25, i * 25, 25, 25) } @@ -2033,10 +2587,253 @@ tests['fillStyle=\'hsl(...)\''] = function (ctx) { } tests['fillStyle=\'hsla(...)\''] = function (ctx) { - for (var i = 0; i < 6; i++) { - for (var j = 0; j < 6; j++) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 6; j++) { ctx.fillStyle = 'hsla(' + (360 - 60 * i) + ',' + (100 - 16.66 * j) + '%,50%,' + (1 - 0.16 * j) + ')' ctx.fillRect(j * 25, i * 25, 25, 25) } } } + +tests['textBaseline and scale'] = function (ctx) { + ctx.strokeStyle = '#666' + ctx.strokeRect(0, 0, 200, 200) + ctx.lineTo(0, 50) + ctx.lineTo(200, 50) + ctx.stroke() + ctx.beginPath() + ctx.lineTo(0, 150) + ctx.lineTo(200, 150) + ctx.stroke() + + ctx.font = 'normal 20px Arial' + ctx.textBaseline = 'bottom' + ctx.textAlign = 'center' + ctx.fillText('bottom', 100, 50) + + ctx.scale(0.1, 0.1) + ctx.font = 'normal 200px Arial' + ctx.textBaseline = 'bottom' + ctx.textAlign = 'center' + ctx.fillText('bottom', 1000, 1500) +} + +tests['rotated baseline'] = function (ctx) { + ctx.font = '12px Arial' + ctx.fillStyle = 'black' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.translate(100, 100) + + for (let i = 0; i < 16; i++) { + ctx.fillText('Hello world!', -50, -50) + ctx.rotate(-Math.PI / 8) + } +} + +tests['rotated and scaled baseline'] = function (ctx) { + ctx.font = '120px Arial' + ctx.fillStyle = 'black' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.translate(100, 100) + ctx.scale(0.1, 0.2) + + for (let i = 0; i < 16; i++) { + ctx.fillText('Hello world!', -50 / 0.1, -50 / 0.2) + ctx.rotate(-Math.PI / 8) + } +} + +tests['rotated and skewed baseline'] = function (ctx) { + ctx.font = '12px Arial' + ctx.fillStyle = 'black' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.translate(100, 100) + ctx.transform(1, 1, 0, 1, 1, 1) + + for (let i = 0; i < 16; i++) { + ctx.fillText('Hello world!', -50, -50) + ctx.rotate(-Math.PI / 8) + } +} + +tests['rotated, scaled and skewed baseline'] = function (ctx) { + // Known issue: we don't have a way to decompose the cairo matrix into the + // skew and rotation separately. + ctx.font = '120px Arial' + ctx.fillStyle = 'black' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.translate(100, 100) + ctx.scale(0.1, 0.2) + ctx.transform(1, 1, 0, 1, 1, 1) + + for (let i = 0; i < 16; i++) { + ctx.fillText('Hello world!', -50 / 0.1, -50 / 0.2) + ctx.rotate(-Math.PI / 8) + } +} + +tests['measureText()'] = function (ctx) { + // Note: As of Sep 2017, Chrome is the only browser with advanced TextMetrics, + // and they're behind a flag, and a few of them are missing and others are + // wrong. + function drawWithBBox (text, x, y) { + ctx.fillText(text, x, y) + ctx.strokeStyle = 'red' + ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(200, y + 0.5); ctx.stroke() + const metrics = ctx.measureText(text) + ctx.strokeStyle = 'blue' + ctx.strokeRect( + // positive numbers for actualBoundingBoxLeft indicate a distance going left + x + metrics.actualBoundingBoxLeft + 0.5, + y - metrics.actualBoundingBoxAscent + 0.5, + metrics.width, + metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent + ) + } + + ctx.font = '20px Arial' + ctx.textBaseline = 'alphabetic' + drawWithBBox('Alphabet alphabetic', 20, 50) + + drawWithBBox('weruoasnm', 50, 175) // no ascenders/descenders + + drawWithBBox(',', 100, 125) // tiny height + + ctx.textBaseline = 'bottom' + drawWithBBox('Alphabet bottom', 20, 90) + + ctx.textBaseline = 'alphabetic' + ctx.save() + ctx.rotate(Math.PI / 8) + drawWithBBox('Alphabet', 50, 100) + ctx.restore() + + ctx.textAlign = 'center' + drawWithBBox('Centered', 100, 195) + + ctx.textAlign = 'left' + drawWithBBox('Left', 10, 195) + + ctx.textAlign = 'right' + drawWithBBox('right', 195, 195) +} + +tests['glyph advances (#2184)'] = function (ctx) { + ctx.font = '8px Arial' + ctx.fillText('A float is a box that is shifted to the left or right on the current line.', 0, 8) +} + +tests['image sampling (#1084)'] = function (ctx, done) { + let loaded1, loaded2 + const img1 = new Image() + const img2 = new Image() + + img1.onload = () => { + loaded1 = true + ctx.drawImage(img1, -170 - 100, -203, 352, 352) + if (loaded2) done() + } + + img1.onerror = done + + img2.onload = () => { + loaded2 = true + ctx.drawImage(img2, 182 - 100, -203, 352, 352) + if (loaded1) done() + } + + img2.onerror = done + + img1.src = imageSrc('halved-1.jpeg') + img2.src = imageSrc('halved-2.jpeg') +} + +tests['drawImage reflection bug'] = function (ctx, done) { + const img1 = new Image() + img1.onload = function () { + ctx.drawImage(img1, 60, 30, 150, 150, 0, 0, 200, 200) + done() + } + img1.src = imageSrc('chrome.jpg') +} + +tests['drawImage reflection bug with skewing'] = function (ctx, done) { + const img1 = new Image() + img1.onload = function () { + ctx.transform(1.2, 1, 1.8, 1.3, 0, 0) + ctx.drawImage(img1, 60, 30, 150, 150, 0, 0, 200, 200) + ctx.setTransform(1.2, 1.8, 0.3, 0.8, 0, 0) + ctx.drawImage(img1, 30, 60, 150, 150, -5, -5, 200, 200) + done() + } + img1.src = imageSrc('chrome.jpg') +} + +tests['transformed drawimage'] = function (ctx) { + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 200, 200) + ctx.fillStyle = 'black' + ctx.fillRect(5, 5, 50, 50) + ctx.transform(1.2, 1, 1.8, 1.3, 0, 0) + ctx.drawImage(ctx.canvas, 0, 0) +} + +// https://github.com/noell/jpg-exif-test-images +for (let n = 1; n <= 8; n++) { + tests[`exif orientation ${n}`] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0) + done() + } + img.src = imageSrc(`exif-orientation-f${n}.jpg`) + } +} + +tests['invalid exif orientation 9'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0) + done() + } + img.src = imageSrc(`exif-orientation-fi.jpg`) +} + +tests['two exif orientations, value 1 and value 2'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0) + done() + } + img.src = imageSrc(`exif-orientation-fm.jpg`) +} + +tests['no exif orientation'] = function (ctx, done) { + const img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0) + done() + } + img.src = imageSrc(`exif-orientation-fn.jpg`) +} + +tests['scaling SVGs'] = function (ctx, done) { + const img = new Image() + + img.onload = function () { + img.width = 200 + img.height = 200 + ctx.drawImage(img, 0, 0, 200, 200) + done() + } + + img.src = 'data:image/svg+xml;base64,' + btoa(` + + + + `) +} diff --git a/test/server.js b/test/server.js index 04207becb..fe2a53218 100644 --- a/test/server.js +++ b/test/server.js @@ -1,21 +1,26 @@ -var path = require('path') -var express = require('express') +const path = require('path') +const express = require('express') -var Canvas = require('../') -var tests = require('./public/tests') +const Canvas = require('../') +const tests = require('./public/tests') -var app = express() -var port = parseInt(process.argv[2] || '4000', 10) +const app = express() +const port = parseInt(process.argv[2] || '4000', 10) function renderTest (canvas, name, cb) { if (!tests[name]) { throw new Error('Unknown test: ' + name) } + const ctx = canvas.getContext('2d', { pixelFormat: 'RGBA32' }) + const initialFillStyle = ctx.fillStyle + ctx.fillStyle = 'white' + ctx.fillRect(0, 0, 200, 200) + ctx.fillStyle = initialFillStyle if (tests[name].length === 2) { - tests[name](canvas.getContext('2d'), cb) + tests[name](ctx, cb) } else { - tests[name](canvas.getContext('2d')) + tests[name](ctx) cb(null) } } @@ -27,8 +32,12 @@ app.get('/', function (req, res) { res.sendFile(path.join(__dirname, 'public', 'app.html')) }) +app.get('/pixelmatch.js', function (req, res) { + res.sendFile(path.join(__dirname, '../node_modules/pixelmatch/', 'index.js')) +}) + app.get('/render', function (req, res, next) { - var canvas = new Canvas(200, 200) + const canvas = Canvas.createCanvas(200, 200) renderTest(canvas, req.query.name, function (err) { if (err) return next(err) @@ -39,7 +48,7 @@ app.get('/render', function (req, res, next) { }) app.get('/pdf', function (req, res, next) { - var canvas = new Canvas(200, 200, 'pdf') + const canvas = Canvas.createCanvas(200, 200, 'pdf') renderTest(canvas, req.query.name, function (err) { if (err) return next(err) diff --git a/test/wpt/drawing-text-to-the-canvas.yaml b/test/wpt/drawing-text-to-the-canvas.yaml new file mode 100644 index 000000000..e0f0d4f72 --- /dev/null +++ b/test/wpt/drawing-text-to-the-canvas.yaml @@ -0,0 +1,1061 @@ +- name: 2d.text.draw.fill.basic + desc: fillText draws filled text + manual: + testing: + - 2d.text.draw + - 2d.text.draw.fill + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('PASS', 5, 35); + expected: &passfill | + size 100 50 + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + cr.set_source_rgb(0, 1, 0) + cr.select_font_face("Arial") + cr.set_font_size(35) + cr.translate(5, 35) + cr.text_path("PASS") + cr.fill() + +- name: 2d.text.draw.fill.unaffected + desc: fillText does not start a new path or subpath + testing: + - 2d.text.draw.fill + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('FAIL', 5, 35); + + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 5,45 == 0,255,0,255; + expected: green + +- name: 2d.text.draw.fill.rtl + desc: fillText respects Right-To-Left Override characters + manual: + testing: + - 2d.text.draw + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('\u202eFAIL \xa0 \xa0 SSAP', 5, 35); + expected: *passfill +- name: 2d.text.draw.fill.maxWidth.large + desc: fillText handles maxWidth correctly + manual: + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('PASS', 5, 35, 200); + expected: *passfill +- name: 2d.text.draw.fill.maxWidth.small + desc: fillText handles maxWidth correctly + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', -100, 35, 90); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.text.draw.fill.maxWidth.zero + desc: fillText handles maxWidth correctly + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, 0); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.text.draw.fill.maxWidth.negative + desc: fillText handles maxWidth correctly + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, -1); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.text.draw.fill.maxWidth.NaN + desc: fillText handles maxWidth correctly + testing: + - 2d.text.draw.maxwidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, NaN); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.text.draw.stroke.basic + desc: strokeText draws stroked text + manual: + testing: + - 2d.text.draw + - 2d.text.draw.stroke + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.fillStyle = '#f00'; + ctx.lineWidth = 1; + ctx.font = '35px Arial, sans-serif'; + ctx.strokeText('PASS', 5, 35); + expected: | + size 100 50 + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + cr.set_source_rgb(0, 1, 0) + cr.select_font_face("Arial") + cr.set_font_size(35) + cr.set_line_width(1) + cr.translate(5, 35) + cr.text_path("PASS") + cr.stroke() + +- name: 2d.text.draw.stroke.unaffected + desc: strokeText does not start a new path or subpath + testing: + - 2d.text.draw.stroke + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + + ctx.font = '35px Arial, sans-serif'; + ctx.strokeStyle = '#f00'; + ctx.strokeText('FAIL', 5, 35); + + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 5,45 == 0,255,0,255; + expected: green + +- name: 2d.text.draw.kern.consistent + desc: Stroked and filled text should have exactly the same kerning so it overlaps + manual: + testing: + - 2d.text.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 3; + ctx.font = '20px Arial, sans-serif'; + ctx.fillText('VAVAVAVAVAVAVA', -50, 25); + ctx.fillText('ToToToToToToTo', -50, 45); + ctx.strokeText('VAVAVAVAVAVAVA', -50, 25); + ctx.strokeText('ToToToToToToTo', -50, 45); + expected: green + +# CanvasTest is: +# A = (0, 0) to (1em, 0.75em) (above baseline) +# B = (0, 0) to (1em, -0.25em) (below baseline) +# C = (0, -0.25em) to (1em, 0.75em) (the em square) plus some Xs above and below +# D = (0, -0.25em) to (1em, 0.75em) (the em square) plus some Xs left and right +# E = (0, -0.25em) to (1em, 0.75em) (the em square) +# space = empty, 1em wide +# +# At 50px, "E" will fill the canvas vertically +# At 67px, "A" will fill the canvas vertically +# +# Ideographic baseline is 0.125em above alphabetic +# Mathematical baseline is 0.375em above alphabetic +# Hanging baseline is 0.500em above alphabetic + +# WebKit doesn't block onload on font loads, so we try to make it a bit more reliable +# by waiting with step_timeout after load before drawing + +- name: 2d.text.draw.fill.maxWidth.fontface + desc: fillText works on @font-face fonts + testing: + - 2d.text.draw.maxwidth + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillText('EEEE', -50, 37.5, 40); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.fill.maxWidth.bound + desc: fillText handles maxWidth based on line size, not bounding box size + testing: + - 2d.text.draw.maxwidth + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('DD', 0, 37.5, 100); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.fontface + testing: + - 2d.text.font.fontface + fonts: + - CanvasTest + code: | + ctx.font = '67px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.fontface.repeat + desc: Draw with the font immediately, then wait a bit until and draw again. (This + crashes some version of WebKit.) + testing: + - 2d.text.font.fontface + fonts: + - CanvasTest + fonthack: 0 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.font = '67px CanvasTest'; + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillText('AA', 0, 50); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.fontface.notinpage + desc: '@font-face fonts should work even if they are not used in the page' + testing: + - 2d.text.font.fontface + fonts: + - CanvasTest + fonthack: 0 + code: | + ctx.font = '67px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + @assert pixel 5,5 ==~ 0,255,0,255; @moz-todo + @assert pixel 95,5 ==~ 0,255,0,255; @moz-todo + @assert pixel 25,25 ==~ 0,255,0,255; @moz-todo + @assert pixel 75,25 ==~ 0,255,0,255; @moz-todo + }), 500); + expected: green + +- name: 2d.text.draw.align.left + desc: textAlign left is the left of the first em square (not the bounding box) + testing: + - 2d.text.align.left + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'left'; + ctx.fillText('DD', 0, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.right + desc: textAlign right is the right of the last em square (not the bounding box) + testing: + - 2d.text.align.right + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'right'; + ctx.fillText('DD', 100, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.start.ltr + desc: textAlign start with ltr is the left edge + testing: + - 2d.text.align.left + fonts: + - CanvasTest + canvas: width="100" height="50" dir="ltr" + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'start'; + ctx.fillText('DD', 0, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.start.rtl + desc: textAlign start with rtl is the right edge + testing: + - 2d.text.align.right + - 2d.text.draw.direction + fonts: + - CanvasTest + canvas: width="100" height="50" dir="rtl" + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'start'; + ctx.fillText('DD', 100, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.end.ltr + desc: textAlign end with ltr is the right edge + testing: + - 2d.text.align.right + fonts: + - CanvasTest + canvas: width="100" height="50" dir="ltr" + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'end'; + ctx.fillText('DD', 100, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.end.rtl + desc: textAlign end with rtl is the left edge + testing: + - 2d.text.align.left + - 2d.text.draw.direction + fonts: + - CanvasTest + canvas: width="100" height="50" dir="rtl" + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'end'; + ctx.fillText('DD', 0, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.align.center + desc: textAlign center is the center of the em squares (not the bounding box) + testing: + - 2d.text.align.center + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'center'; + ctx.fillText('DD', 50, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + + +- name: 2d.text.draw.space.basic + desc: U+0020 is rendered the correct size (1em wide) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E EE', -100, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.nonspace + desc: Non-space characters are not converted to U+0020 and collapsed + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E\x0b EE', -150, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.measure.width.basic + desc: The width of character is same as font used + testing: + - 2d.text.measure + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + @assert ctx.measureText('A').width === 50; + @assert ctx.measureText('AA').width === 100; + @assert ctx.measureText('ABCD').width === 200; + + ctx.font = '100px CanvasTest'; + @assert ctx.measureText('A').width === 100; + }), 500); + }); + +- name: 2d.text.measure.width.empty + desc: The empty string has zero width + testing: + - 2d.text.measure + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + @assert ctx.measureText("").width === 0; + }), 500); + }); + +- name: 2d.text.measure.advances + desc: Testing width advances + testing: + - 2d.text.measure.advances + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + // Some platforms may return '-0'. + @assert Math.abs(ctx.measureText('Hello').advances[0]) === 0; + // Different platforms may render text slightly different. + @assert ctx.measureText('Hello').advances[1] >= 36; + @assert ctx.measureText('Hello').advances[2] >= 58; + @assert ctx.measureText('Hello').advances[3] >= 70; + @assert ctx.measureText('Hello').advances[4] >= 80; + + var tm = ctx.measureText('Hello'); + @assert ctx.measureText('Hello').advances[0] === tm.advances[0]; + @assert ctx.measureText('Hello').advances[1] === tm.advances[1]; + @assert ctx.measureText('Hello').advances[2] === tm.advances[2]; + @assert ctx.measureText('Hello').advances[3] === tm.advances[3]; + @assert ctx.measureText('Hello').advances[4] === tm.advances[4]; + }), 500); + }); + +- name: 2d.text.measure.actualBoundingBox + desc: Testing actualBoundingBox + testing: + - 2d.text.measure.actualBoundingBox + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + ctx.baseline = 'alphabetic' + // Different platforms may render text slightly different. + // Values that are nominally expected to be zero might actually vary by a pixel or so + // if the UA accounts for antialiasing at glyph edges, so we allow a slight deviation. + @assert Math.abs(ctx.measureText('A').actualBoundingBoxLeft) <= 1; + @assert ctx.measureText('A').actualBoundingBoxRight >= 50; + @assert ctx.measureText('A').actualBoundingBoxAscent >= 35; + @assert Math.abs(ctx.measureText('A').actualBoundingBoxDescent) <= 1; + + @assert ctx.measureText('D').actualBoundingBoxLeft >= 48; + @assert ctx.measureText('D').actualBoundingBoxLeft <= 52; + @assert ctx.measureText('D').actualBoundingBoxRight >= 75; + @assert ctx.measureText('D').actualBoundingBoxRight <= 80; + @assert ctx.measureText('D').actualBoundingBoxAscent >= 35; + @assert ctx.measureText('D').actualBoundingBoxAscent <= 40; + @assert ctx.measureText('D').actualBoundingBoxDescent >= 12; + @assert ctx.measureText('D').actualBoundingBoxDescent <= 15; + + @assert Math.abs(ctx.measureText('ABCD').actualBoundingBoxLeft) <= 1; + @assert ctx.measureText('ABCD').actualBoundingBoxRight >= 200; + @assert ctx.measureText('ABCD').actualBoundingBoxAscent >= 85; + @assert ctx.measureText('ABCD').actualBoundingBoxDescent >= 37; + }), 500); + }); + +- name: 2d.text.measure.fontBoundingBox + desc: Testing fontBoundingBox + testing: + - 2d.text.measure.fontBoundingBox + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + @assert ctx.measureText('A').fontBoundingBoxAscent === 85; + @assert ctx.measureText('A').fontBoundingBoxDescent === 39; + + @assert ctx.measureText('ABCD').fontBoundingBoxAscent === 85; + @assert ctx.measureText('ABCD').fontBoundingBoxDescent === 39; + }), 500); + }); + +- name: 2d.text.measure.fontBoundingBox.ahem + desc: Testing fontBoundingBox for font ahem + testing: + - 2d.text.measure.fontBoundingBox + fonts: + - Ahem + code: | + deferTest(); + var f = new FontFace("Ahem", "/fonts/Ahem.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px Ahem'; + ctx.direction = 'ltr'; + ctx.align = 'left' + @assert ctx.measureText('A').fontBoundingBoxAscent === 40; + @assert ctx.measureText('A').fontBoundingBoxDescent === 10; + + @assert ctx.measureText('ABCD').fontBoundingBoxAscent === 40; + @assert ctx.measureText('ABCD').fontBoundingBoxDescent === 10; + }), 500); + }); + +- name: 2d.text.measure.emHeights + desc: Testing emHeights + testing: + - 2d.text.measure.emHeights + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + @assert ctx.measureText('A').emHeightAscent === 37.5; + @assert ctx.measureText('A').emHeightDescent === 12.5; + @assert ctx.measureText('A').emHeightDescent + ctx.measureText('A').emHeightAscent === 50; + + @assert ctx.measureText('ABCD').emHeightAscent === 37.5; + @assert ctx.measureText('ABCD').emHeightDescent === 12.5; + @assert ctx.measureText('ABCD').emHeightDescent + ctx.measureText('ABCD').emHeightAscent === 50; + }), 500); + }); + +- name: 2d.text.measure.baselines + desc: Testing baselines + testing: + - 2d.text.measure.baselines + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + @assert Math.abs(ctx.measureText('A').getBaselines().alphabetic) === 0; + @assert ctx.measureText('A').getBaselines().ideographic === -39; + @assert ctx.measureText('A').getBaselines().hanging === 68; + + @assert Math.abs(ctx.measureText('ABCD').getBaselines().alphabetic) === 0; + @assert ctx.measureText('ABCD').getBaselines().ideographic === -39; + @assert ctx.measureText('ABCD').getBaselines().hanging === 68; + }), 500); + }); + +- name: 2d.text.drawing.style.spacing + desc: Testing letter spacing and word spacing + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + + ctx.letterSpacing = '3px'; + @assert ctx.letterSpacing === '3px'; + @assert ctx.wordSpacing === '0px'; + + ctx.wordSpacing = '5px'; + @assert ctx.letterSpacing === '3px'; + @assert ctx.wordSpacing === '5px'; + + ctx.letterSpacing = '-1px'; + ctx.wordSpacing = '-1px'; + @assert ctx.letterSpacing === '-1px'; + @assert ctx.wordSpacing === '-1px'; + + ctx.letterSpacing = '1PX'; + ctx.wordSpacing = '1EM'; + @assert ctx.letterSpacing === '1px'; + @assert ctx.wordSpacing === '1em'; + +- name: 2d.text.drawing.style.nonfinite.spacing + desc: Testing letter spacing and word spacing with nonfinite inputs + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + + function test_word_spacing(value) { + ctx.wordSpacing = value; + ctx.letterSpacing = value; + @assert ctx.wordSpacing === '0px'; + @assert ctx.letterSpacing === '0px'; + } + @nonfinite test_word_spacing(<0 NaN Infinity -Infinity>); + +- name: 2d.text.drawing.style.invalid.spacing + desc: Testing letter spacing and word spacing with invalid units + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + + function test_word_spacing(value) { + ctx.wordSpacing = value; + ctx.letterSpacing = value; + @assert ctx.wordSpacing === '0px'; + @assert ctx.letterSpacing === '0px'; + } + @nonfinite test_word_spacing(< '0s' '1min' '1deg' '1pp'>); + +- name: 2d.text.drawing.style.letterSpacing.measure + desc: Testing letter spacing and word spacing + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + var width_normal = ctx.measureText('Hello World').width; + + function test_letter_spacing(value, difference_spacing, epsilon) { + ctx.letterSpacing = value; + @assert ctx.letterSpacing === value; + @assert ctx.wordSpacing === '0px'; + width_with_letter_spacing = ctx.measureText('Hello World').width; + assert_approx_equals(width_with_letter_spacing, width_normal + difference_spacing, epsilon, "letter spacing doesn't work."); + } + + // The first value is the letter Spacing to be set, the second value the + // change in length of string 'Hello World', note that there are 11 letters + // in 'hello world', so the length difference is always letterSpacing * 11. + // and the third value is the acceptable differencee for the length change, + // note that unit such as 1cm/1mm doesn't map to an exact pixel value. + test_cases = [['3px', 33, 0], + ['5px', 55, 0], + ['-2px', -22, 0], + ['1em', 110, 0], + ['1in', 1056, 0], + ['-0.1cm', -41.65, 0.2], + ['-0.6mm', -24,95, 0.2]] + + for (const test_case of test_cases) { + test_letter_spacing(test_case[0], test_case[1], test_case[2]); + } + +- name: 2d.text.drawing.style.wordSpacing.measure + desc: Testing if word spacing is working properly + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + var width_normal = ctx.measureText('Hello World, again').width; + + function test_word_spacing(value, difference_spacing, epsilon) { + ctx.wordSpacing = value; + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === value; + width_with_word_spacing = ctx.measureText('Hello World, again').width; + assert_approx_equals(width_with_word_spacing, width_normal + difference_spacing, epsilon, "word spacing doesn't work."); + } + + // The first value is the word Spacing to be set, the second value the + // change in length of string 'Hello World', note that there are 2 words + // in 'Hello World, again', so the length difference is always wordSpacing * 2. + // and the third value is the acceptable differencee for the length change, + // note that unit such as 1cm/1mm doesn't map to an exact pixel value. + test_cases = [['3px', 6, 0], + ['5px', 10, 0], + ['-2px', -4, 0], + ['1em', 20, 0], + ['1in', 192, 0], + ['-0.1cm', -7.57, 0.2], + ['-0.6mm', -4.54, 0.2]] + + for (const test_case of test_cases) { + test_word_spacing(test_case[0], test_case[1], test_case[2]); + } + +- name: 2d.text.drawing.style.letterSpacing.change.font + desc: Set letter spacing and word spacing to font dependent value and verify it works after font change. + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + // Get the width for 'Hello World' at default size, 10px. + var width_normal = ctx.measureText('Hello World').width; + + ctx.letterSpacing = '1em'; + @assert ctx.letterSpacing === '1em'; + // 1em = 10px. Add 10px after each letter in "Hello World", + // makes it 110px longer. + var width_with_spacing = ctx.measureText('Hello World').width; + @assert width_with_spacing === width_normal + 110; + + // Changing font to 20px. Without resetting the spacing, 1em letterSpacing + // is now 20px, so it's suppose to be 220px longer without any letterSpacing set. + ctx.font = '20px serif'; + width_with_spacing = ctx.measureText('Hello World').width; + // Now calculate the reference spacing for "Hello World" with no spacing. + ctx.letterSpacing = '0em'; + width_normal = ctx.measureText('Hello World').width; + @assert width_with_spacing === width_normal + 220; + +- name: 2d.text.drawing.style.wordSpacing.change.font + desc: Set word spacing and word spacing to font dependent value and verify it works after font change. + testing: + - 2d.text.drawing.style.spacing + code: | + @assert ctx.letterSpacing === '0px'; + @assert ctx.wordSpacing === '0px'; + // Get the width for 'Hello World, again' at default size, 10px. + var width_normal = ctx.measureText('Hello World, again').width; + + ctx.wordSpacing = '1em'; + @assert ctx.wordSpacing === '1em'; + // 1em = 10px. Add 10px after each word in "Hello World, again", + // makes it 20px longer. + var width_with_spacing = ctx.measureText('Hello World, again').width; + @assert width_with_spacing === width_normal + 20; + + // Changing font to 20px. Without resetting the spacing, 1em wordSpacing + // is now 20px, so it's suppose to be 40px longer without any wordSpacing set. + ctx.font = '20px serif'; + width_with_spacing = ctx.measureText('Hello World, again').width; + // Now calculate the reference spacing for "Hello World, again" with no spacing. + ctx.wordSpacing = '0em'; + width_normal = ctx.measureText('Hello World, again').width; + @assert width_with_spacing === width_normal + 40; + +- name: 2d.text.drawing.style.fontKerning + desc: Testing basic functionalities of fontKerning for canvas + testing: + - 2d.text.drawing.style.fontKerning + code: | + @assert ctx.fontKerning === "auto"; + ctx.fontKerning = "normal"; + @assert ctx.fontKerning === "normal"; + width_normal = ctx.measureText("TAWATAVA").width; + ctx.fontKerning = "none"; + @assert ctx.fontKerning === "none"; + width_none = ctx.measureText("TAWATAVA").width; + @assert width_normal < width_none; + +- name: 2d.text.drawing.style.fontKerning.with.uppercase + desc: Testing basic functionalities of fontKerning for canvas + testing: + - 2d.text.drawing.style.fontKerning + code: | + @assert ctx.fontKerning === "auto"; + ctx.fontKerning = "Normal"; + @assert ctx.fontKerning === "normal"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "normal"; + @assert ctx.fontKerning === "normal"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "noRmal"; + @assert ctx.fontKerning === "normal"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NoRMal"; + @assert ctx.fontKerning === "normal"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NORMAL"; + @assert ctx.fontKerning === "normal"; + + ctx.fontKerning = "None"; + @assert ctx.fontKerning === "none"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "none"; + @assert ctx.fontKerning === "none"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "nOne"; + @assert ctx.fontKerning === "none"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "nonE"; + @assert ctx.fontKerning === "none"; + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NONE"; + @assert ctx.fontKerning === "none"; + +- name: 2d.text.drawing.style.fontVariant.settings + desc: Testing basic functionalities of fontKerning for canvas + testing: + - 2d.text.drawing.style.fontVariantCaps + code: | + // Setting fontVariantCaps with lower cases + @assert ctx.fontVariantCaps === "normal"; + + ctx.fontVariantCaps = "normal"; + @assert ctx.fontVariantCaps === "normal"; + + ctx.fontVariantCaps = "small-caps"; + @assert ctx.fontVariantCaps === "small-caps"; + + ctx.fontVariantCaps = "all-small-caps"; + @assert ctx.fontVariantCaps === "all-small-caps"; + + ctx.fontVariantCaps = "petite-caps"; + @assert ctx.fontVariantCaps === "petite-caps"; + + ctx.fontVariantCaps = "all-petite-caps"; + @assert ctx.fontVariantCaps === "all-petite-caps"; + + ctx.fontVariantCaps = "unicase"; + @assert ctx.fontVariantCaps === "unicase"; + + ctx.fontVariantCaps = "titling-caps"; + @assert ctx.fontVariantCaps === "titling-caps"; + + // Setting fontVariantCaps with lower cases and upper cases word. + ctx.fontVariantCaps = "nORmal"; + @assert ctx.fontVariantCaps === "normal"; + + ctx.fontVariantCaps = "smaLL-caps"; + @assert ctx.fontVariantCaps === "small-caps"; + + ctx.fontVariantCaps = "all-small-CAPS"; + @assert ctx.fontVariantCaps === "all-small-caps"; + + ctx.fontVariantCaps = "pEtitE-caps"; + @assert ctx.fontVariantCaps === "petite-caps"; + + ctx.fontVariantCaps = "All-Petite-Caps"; + @assert ctx.fontVariantCaps === "all-petite-caps"; + + ctx.fontVariantCaps = "uNIcase"; + @assert ctx.fontVariantCaps === "unicase"; + + ctx.fontVariantCaps = "titling-CAPS"; + @assert ctx.fontVariantCaps === "titling-caps"; + + // Setting fontVariantCaps with non-existing font variant. + ctx.fontVariantCaps = "abcd"; + @assert ctx.fontVariantCaps === "titling-caps"; + +- name: 2d.text.drawing.style.textRendering.settings + desc: Testing basic functionalities of textRendering in Canvas + testing: + - 2d.text.drawing.style.textRendering + code: | + // Setting textRendering with lower cases + @assert ctx.textRendering === "auto"; + + ctx.textRendering = "auto"; + @assert ctx.textRendering === "auto"; + + ctx.textRendering = "optimizespeed"; + @assert ctx.textRendering === "optimizeSpeed"; + + ctx.textRendering = "optimizelegibility"; + @assert ctx.textRendering === "optimizeLegibility"; + + ctx.textRendering = "geometricprecision"; + @assert ctx.textRendering === "geometricPrecision"; + + // Setting textRendering with lower cases and upper cases word. + ctx.textRendering = "aUto"; + @assert ctx.textRendering === "auto"; + + ctx.textRendering = "OPtimizeSpeed"; + @assert ctx.textRendering === "optimizeSpeed"; + + ctx.textRendering = "OPtimizELEgibility"; + @assert ctx.textRendering === "optimizeLegibility"; + + ctx.textRendering = "GeometricPrecision"; + @assert ctx.textRendering === "geometricPrecision"; + + // Setting textRendering with non-existing font variant. + ctx.textRendering = "abcd"; + @assert ctx.textRendering === "geometricPrecision"; + +# TODO: shadows, alpha, composite, clip \ No newline at end of file diff --git a/test/wpt/fill-and-stroke-styles.yaml b/test/wpt/fill-and-stroke-styles.yaml new file mode 100644 index 000000000..88a36119d --- /dev/null +++ b/test/wpt/fill-and-stroke-styles.yaml @@ -0,0 +1,2244 @@ +- name: 2d.fillStyle.parse.current.basic + desc: currentColor is computed from the canvas element + testing: + - 2d.colors.parse + - 2d.currentColor.onset + code: | + canvas.setAttribute('style', 'color: #0f0'); + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'currentColor'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.fillStyle.parse.current.changed + desc: currentColor is computed when the attribute is set, not when it is painted + testing: + - 2d.colors.parse + - 2d.currentColor.onset + code: | + canvas.setAttribute('style', 'color: #0f0'); + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'currentColor'; + canvas.setAttribute('style', 'color: #f00'); + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.fillStyle.parse.current.removed + desc: currentColor is solid black when the canvas element is not in a document + testing: + - 2d.colors.parse + - 2d.currentColor.outofdoc + code: | + // Try not to let it undetectably incorrectly pick up opaque-black + // from other parts of the document: + document.body.parentNode.setAttribute('style', 'color: #f00'); + document.body.setAttribute('style', 'color: #f00'); + canvas.setAttribute('style', 'color: #f00'); + + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillStyle = 'currentColor'; + ctx2.fillRect(0, 0, 100, 50); + ctx.drawImage(canvas2, 0, 0); + + document.body.parentNode.removeAttribute('style'); + document.body.removeAttribute('style'); + + @assert pixel 50,25 == 0,0,0,255; + expected: | + size 100 50 + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.fillStyle.invalidstring + testing: + - 2d.colors.invalidstring + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillStyle = 'invalid'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.fillStyle.invalidtype + testing: + - 2d.colors.invalidtype + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillStyle = null; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.fillStyle.get.solid + testing: + - 2d.colors.getcolor + - 2d.serializecolor.solid + code: | + ctx.fillStyle = '#fa0'; + @assert ctx.fillStyle === '#ffaa00'; + +- name: 2d.fillStyle.get.semitransparent + testing: + - 2d.colors.getcolor + - 2d.serializecolor.transparent + code: | + ctx.fillStyle = 'rgba(255,255,255,0.45)'; + @assert ctx.fillStyle =~ /^rgba\(255, 255, 255, 0\.4\d+\)$/; + +- name: 2d.fillStyle.get.halftransparent + testing: + - 2d.colors.getcolor + - 2d.serializecolor.transparent + code: | + ctx.fillStyle = 'rgba(255,255,255,0.5)'; + @assert ctx.fillStyle === 'rgba(255, 255, 255, 0.5)'; + +- name: 2d.fillStyle.get.transparent + testing: + - 2d.colors.getcolor + - 2d.serializecolor.transparent + code: | + ctx.fillStyle = 'rgba(0,0,0,0)'; + @assert ctx.fillStyle === 'rgba(0, 0, 0, 0)'; + +- name: 2d.fillStyle.default + testing: + - 2d.colors.default + code: | + @assert ctx.fillStyle === '#000000'; + +- name: 2d.fillStyle.toStringFunctionCallback + desc: Passing a function in to ctx.fillStyle or ctx.strokeStyle with a toString callback works as specified + testing: + 2d.colors.toStringFunctionCallback + code: | + ctx.fillStyle = { toString: function() { return "#008000"; } }; + @assert ctx.fillStyle === "#008000"; + ctx.fillStyle = {}; + @assert ctx.fillStyle === "#008000"; + ctx.fillStyle = 800000; + @assert ctx.fillStyle === "#008000"; + @assert throws TypeError ctx.fillStyle = { toString: function() { throw new TypeError; } }; + ctx.strokeStyle = { toString: function() { return "#008000"; } }; + @assert ctx.strokeStyle === "#008000"; + ctx.strokeStyle = {}; + @assert ctx.strokeStyle === "#008000"; + ctx.strokeStyle = 800000; + @assert ctx.strokeStyle === "#008000"; + @assert throws TypeError ctx.strokeStyle = { toString: function() { throw new TypeError; } }; + +- name: 2d.strokeStyle.default + testing: + - 2d.colors.default + code: | + @assert ctx.strokeStyle === '#000000'; + + +- name: 2d.gradient.object.type + desc: window.CanvasGradient exists and has the right properties + testing: + - 2d.canvasGradient.type + notes: &bindings Defined in "Web IDL" (draft) + code: | + @assert window.CanvasGradient !== undefined; + @assert window.CanvasGradient.prototype.addColorStop !== undefined; + +- name: 2d.gradient.object.return + desc: createLinearGradient() and createRadialGradient() returns objects implementing + CanvasGradient + testing: + - 2d.gradient.linear.return + - 2d.gradient.radial.return + code: | + window.CanvasGradient.prototype.thisImplementsCanvasGradient = true; + + var g1 = ctx.createLinearGradient(0, 0, 100, 0); + @assert g1.addColorStop !== undefined; + @assert g1.thisImplementsCanvasGradient === true; + + var g2 = ctx.createRadialGradient(0, 0, 10, 0, 0, 20); + @assert g2.addColorStop !== undefined; + @assert g2.thisImplementsCanvasGradient === true; + +- name: 2d.gradient.interpolate.solid + testing: + - 2d.gradient.interpolate.linear + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.color + testing: + - 2d.gradient.interpolate.linear + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, '#ff0'); + g.addColorStop(1, '#00f'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,25 ==~ 191,191,63,255 +/- 3; + @assert pixel 50,25 ==~ 127,127,127,255 +/- 3; + @assert pixel 75,25 ==~ 63,63,191,255 +/- 3; + expected: | + size 100 50 + g = cairo.LinearGradient(0, 0, 100, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.interpolate.alpha + testing: + - 2d.gradient.interpolate.linear + code: | + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, 'rgba(0,0,255, 0)'); + g.addColorStop(1, 'rgba(0,0,255, 1)'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,25 ==~ 191,191,63,255 +/- 3; + @assert pixel 50,25 ==~ 127,127,127,255 +/- 3; + @assert pixel 75,25 ==~ 63,63,191,255 +/- 3; + expected: | + size 100 50 + g = cairo.LinearGradient(0, 0, 100, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.interpolate.coloralpha + testing: + - 2d.gradient.interpolate.alpha + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, 'rgba(255,255,0, 0)'); + g.addColorStop(1, 'rgba(0,0,255, 1)'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,25 ==~ 190,190,65,65 +/- 3; + @assert pixel 50,25 ==~ 126,126,128,128 +/- 3; + @assert pixel 75,25 ==~ 62,62,192,192 +/- 3; + expected: | + size 100 50 + g = cairo.LinearGradient(0, 0, 100, 0) + g.add_color_stop_rgba(0, 1,1,0, 0) + g.add_color_stop_rgba(1, 0,0,1, 1) + cr.set_source(g) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.interpolate.outside + testing: + - 2d.gradient.outside.first + - 2d.gradient.outside.last + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(25, 0, 75, 0); + g.addColorStop(0.4, '#0f0'); + g.addColorStop(0.6, '#0f0'); + + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 20,25 ==~ 0,255,0,255; + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 80,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.zerosize.fill + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.rect(0, 0, 100, 50); + ctx.fill(); + @assert pixel 40,20 == 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.zerosize.stroke + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.strokeStyle = g; + ctx.rect(20, 20, 60, 10); + ctx.stroke(); + @assert pixel 19,19 == 0,255,0,255; + @assert pixel 20,19 == 0,255,0,255; + @assert pixel 21,19 == 0,255,0,255; + @assert pixel 19,20 == 0,255,0,255; + @assert pixel 20,20 == 0,255,0,255; + @assert pixel 21,20 == 0,255,0,255; + @assert pixel 19,21 == 0,255,0,255; + @assert pixel 20,21 == 0,255,0,255; + @assert pixel 21,21 == 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.zerosize.fillRect + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 40,20 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.interpolate.zerosize.strokeRect + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.strokeStyle = g; + ctx.strokeRect(20, 20, 60, 10); + @assert pixel 19,19 == 0,255,0,255; + @assert pixel 20,19 == 0,255,0,255; + @assert pixel 21,19 == 0,255,0,255; + @assert pixel 19,20 == 0,255,0,255; + @assert pixel 20,20 == 0,255,0,255; + @assert pixel 21,20 == 0,255,0,255; + @assert pixel 19,21 == 0,255,0,255; + @assert pixel 20,21 == 0,255,0,255; + @assert pixel 21,21 == 0,255,0,255; + expected: green + +- name: 2d.gradient.interpolate.zerosize.fillText + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.font = '100px sans-serif'; + ctx.fillText("AA", 0, 50); + _assertGreen(ctx, 100, 50); + expected: green + +- name: 2d.gradient.interpolate.zerosize.strokeText + testing: + - 2d.gradient.linear.zerosize + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(50, 25, 50, 25); // zero-length line (undefined direction) + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.strokeStyle = g; + ctx.font = '100px sans-serif'; + ctx.strokeText("AA", 0, 50); + _assertGreen(ctx, 100, 50); + expected: green + + +- name: 2d.gradient.interpolate.vertical + testing: + - 2d.gradient.interpolate.linear + code: | + var g = ctx.createLinearGradient(0, 0, 0, 50); + g.addColorStop(0, '#ff0'); + g.addColorStop(1, '#00f'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,12 ==~ 191,191,63,255 +/- 10; + @assert pixel 50,25 ==~ 127,127,127,255 +/- 5; + @assert pixel 50,37 ==~ 63,63,191,255 +/- 10; + expected: | + size 100 50 + g = cairo.LinearGradient(0, 0, 0, 50) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.interpolate.multiple + testing: + - 2d.gradient.interpolate.linear + code: | + canvas.width = 200; + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#ff0'); + g.addColorStop(0.5, '#0ff'); + g.addColorStop(1, '#f0f'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 200, 50); + @assert pixel 50,25 ==~ 127,255,127,255 +/- 3; + @assert pixel 100,25 ==~ 0,255,255,255 +/- 3; + @assert pixel 150,25 ==~ 127,127,255,255 +/- 3; + expected: | + size 200 50 + g = cairo.LinearGradient(0, 0, 200, 0) + g.add_color_stop_rgb(0.0, 1,1,0) + g.add_color_stop_rgb(0.5, 0,1,1) + g.add_color_stop_rgb(1.0, 1,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 200, 50) + cr.fill() + +- name: 2d.gradient.interpolate.overlap + testing: + - 2d.gradient.interpolate.overlap + code: | + canvas.width = 200; + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(0, '#ff0'); + g.addColorStop(0.25, '#00f'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.25, '#ff0'); + g.addColorStop(0.5, '#00f'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.75, '#00f'); + g.addColorStop(0.75, '#f00'); + g.addColorStop(0.75, '#ff0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.5, '#ff0'); + g.addColorStop(1, '#00f'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 200, 50); + @assert pixel 49,25 ==~ 0,0,255,255 +/- 16; + @assert pixel 51,25 ==~ 255,255,0,255 +/- 16; + @assert pixel 99,25 ==~ 0,0,255,255 +/- 16; + @assert pixel 101,25 ==~ 255,255,0,255 +/- 16; + @assert pixel 149,25 ==~ 0,0,255,255 +/- 16; + @assert pixel 151,25 ==~ 255,255,0,255 +/- 16; + expected: | + size 200 50 + g = cairo.LinearGradient(0, 0, 50, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(0, 0, 50, 50) + cr.fill() + + g = cairo.LinearGradient(50, 0, 100, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(50, 0, 50, 50) + cr.fill() + + g = cairo.LinearGradient(100, 0, 150, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(100, 0, 50, 50) + cr.fill() + + g = cairo.LinearGradient(150, 0, 200, 0) + g.add_color_stop_rgb(0, 1,1,0) + g.add_color_stop_rgb(1, 0,0,1) + cr.set_source(g) + cr.rectangle(150, 0, 50, 50) + cr.fill() + +- name: 2d.gradient.interpolate.overlap2 + testing: + - 2d.gradient.interpolate.overlap + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + var ps = [ 0, 1/10, 1/4, 1/3, 1/2, 3/4, 1 ]; + for (var p = 0; p < ps.length; ++p) + { + g.addColorStop(ps[p], '#0f0'); + for (var i = 0; i < 15; ++i) + g.addColorStop(ps[p], '#f00'); + g.addColorStop(ps[p], '#0f0'); + } + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 30,25 == 0,255,0,255; + @assert pixel 40,25 == 0,255,0,255; + @assert pixel 60,25 == 0,255,0,255; + @assert pixel 80,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.empty + testing: + - 2d.gradient.empty + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + var g = ctx.createLinearGradient(0, 0, 0, 50); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.gradient.object.update + testing: + - 2d.gradient.update + code: | + var g = ctx.createLinearGradient(-100, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + g.addColorStop(0.1, '#0f0'); + g.addColorStop(0.9, '#0f0'); + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.gradient.object.compare + testing: + - 2d.gradient.object + code: | + var g1 = ctx.createLinearGradient(0, 0, 100, 0); + var g2 = ctx.createLinearGradient(0, 0, 100, 0); + @assert g1 !== g2; + ctx.fillStyle = g1; + @assert ctx.fillStyle === g1; + +- name: 2d.gradient.object.crosscanvas + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + var g = document.createElement('canvas').getContext('2d').createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.gradient.object.current + testing: + - 2d.currentColor.gradient + code: | + canvas.setAttribute('style', 'color: #f00'); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createLinearGradient(0, 0, 100, 0); + g.addColorStop(0, 'currentColor'); + g.addColorStop(1, 'currentColor'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 0,0,0,255; + expected: | + size 100 50 + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.gradient.object.invalidoffset + testing: + - 2d.gradient.invalidoffset + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + @assert throws INDEX_SIZE_ERR g.addColorStop(-1, '#000'); + @assert throws INDEX_SIZE_ERR g.addColorStop(2, '#000'); + @assert throws TypeError g.addColorStop(Infinity, '#000'); + @assert throws TypeError g.addColorStop(-Infinity, '#000'); + @assert throws TypeError g.addColorStop(NaN, '#000'); + +- name: 2d.gradient.object.invalidcolor + testing: + - 2d.gradient.invalidcolor + code: | + var g = ctx.createLinearGradient(0, 0, 100, 0); + @assert throws SYNTAX_ERR g.addColorStop(0, ""); + @assert throws SYNTAX_ERR g.addColorStop(0, 'rgb(NaN%, NaN%, NaN%)'); + @assert throws SYNTAX_ERR g.addColorStop(0, 'null'); + @assert throws SYNTAX_ERR g.addColorStop(0, 'undefined'); + @assert throws SYNTAX_ERR g.addColorStop(0, null); + @assert throws SYNTAX_ERR g.addColorStop(0, undefined); + + var g = ctx.createRadialGradient(0, 0, 0, 100, 0, 0); + @assert throws SYNTAX_ERR g.addColorStop(0, ""); + @assert throws SYNTAX_ERR g.addColorStop(0, 'rgb(NaN%, NaN%, NaN%)'); + @assert throws SYNTAX_ERR g.addColorStop(0, 'null'); + @assert throws SYNTAX_ERR g.addColorStop(0, 'undefined'); + @assert throws SYNTAX_ERR g.addColorStop(0, null); + @assert throws SYNTAX_ERR g.addColorStop(0, undefined); + + +- name: 2d.gradient.linear.nonfinite + desc: createLinearGradient() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.gradient.linear.nonfinite + code: | + @nonfinite @assert throws TypeError ctx.createLinearGradient(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>); + +- name: 2d.gradient.linear.transform.1 + desc: Linear gradient coordinates are relative to the coordinate space at the time + of filling + testing: + - 2d.gradient.linear.transform + code: | + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.75, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.translate(-50, 0); + ctx.fillRect(50, 0, 100, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.linear.transform.2 + desc: Linear gradient coordinates are relative to the coordinate space at the time + of filling + testing: + - 2d.gradient.linear.transform + code: | + ctx.translate(100, 0); + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.75, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.translate(-150, 0); + ctx.fillRect(50, 0, 100, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.linear.transform.3 + desc: Linear gradient transforms do not experience broken caching effects + testing: + - 2d.gradient.linear.transform + code: | + var g = ctx.createLinearGradient(0, 0, 200, 0); + g.addColorStop(0, '#f00'); + g.addColorStop(0.25, '#0f0'); + g.addColorStop(0.75, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + ctx.translate(-50, 0); + ctx.fillRect(50, 0, 100, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.negative + desc: createRadialGradient() throws INDEX_SIZE_ERR if either radius is negative + testing: + - 2d.gradient.radial.negative + code: | + @assert throws INDEX_SIZE_ERR ctx.createRadialGradient(0, 0, -0.1, 0, 0, 1); + @assert throws INDEX_SIZE_ERR ctx.createRadialGradient(0, 0, 1, 0, 0, -0.1); + @assert throws INDEX_SIZE_ERR ctx.createRadialGradient(0, 0, -0.1, 0, 0, -0.1); + +- name: 2d.gradient.radial.nonfinite + desc: createRadialGradient() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.gradient.radial.nonfinite + code: | + @nonfinite @assert throws TypeError ctx.createRadialGradient(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>); + +- name: 2d.gradient.radial.inside1 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(50, 25, 100, 50, 25, 200); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.inside2 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(50, 25, 200, 50, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.inside3 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(50, 25, 200, 50, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(0.993, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.outside1 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(200, 25, 10, 200, 25, 20); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.outside2 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(200, 25, 20, 200, 25, 10); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.outside3 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(200, 25, 20, 200, 25, 10); + g.addColorStop(0, '#0f0'); + g.addColorStop(0.001, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.touch1 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(150, 25, 50, 200, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.touch2 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(-80, 25, 70, 0, 25, 150); + g.addColorStop(0, '#f00'); + g.addColorStop(0.01, '#0f0'); + g.addColorStop(0.99, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.touch3 + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(120, -15, 25, 140, -30, 50); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.equal + testing: + - 2d.gradient.radial.equal + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(50, 25, 20, 50, 25, 20); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.cone.behind + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(120, 25, 10, 211, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.cone.front + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(311, 25, 10, 210, 25, 100); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.bottom + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(210, 25, 100, 230, 25, 101); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.top + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(230, 25, 100, 100, 25, 101); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.beside + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(0, 100, 40, 100, 100, 50); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; @moz-todo + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; @moz-todo + @assert pixel 98,25 == 0,255,0,255; @moz-todo + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.gradient.radial.cone.cylinder + testing: + - 2d.gradient.radial.rendering + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(210, 25, 100, 230, 25, 100); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.shape1 + testing: + - 2d.gradient.radial.rendering + code: | + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(30+tol, 40); + ctx.lineTo(110, -20+tol); + ctx.lineTo(110, 100-tol); + ctx.fill(); + + var g = ctx.createRadialGradient(30+10*5/2, 40, 10*3/2, 30+10*15/4, 40, 10*9/4); + g.addColorStop(0, '#0f0'); + g.addColorStop(1, '#0f0'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.cone.shape2 + testing: + - 2d.gradient.radial.rendering + code: | + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var g = ctx.createRadialGradient(30+10*5/2, 40, 10*3/2, 30+10*15/4, 40, 10*9/4); + g.addColorStop(0, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(30-tol, 40); + ctx.lineTo(110, -20-tol); + ctx.lineTo(110, 100+tol); + ctx.fill(); + + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,1 == 0,255,0,255; @moz-todo + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.transform.1 + desc: Radial gradient coordinates are relative to the coordinate space at the time + of filling + testing: + - 2d.gradient.radial.transform + code: | + var g = ctx.createRadialGradient(0, 0, 0, 0, 0, 11.2); + g.addColorStop(0, '#0f0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.51, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.translate(50, 25); + ctx.scale(10, 10); + ctx.fillRect(-5, -2.5, 10, 5); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.transform.2 + desc: Radial gradient coordinates are relative to the coordinate space at the time + of filling + testing: + - 2d.gradient.radial.transform + code: | + ctx.translate(100, 0); + var g = ctx.createRadialGradient(0, 0, 0, 0, 0, 11.2); + g.addColorStop(0, '#0f0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.51, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.translate(-50, 25); + ctx.scale(10, 10); + ctx.fillRect(-5, -2.5, 10, 5); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.radial.transform.3 + desc: Radial gradient transforms do not experience broken caching effects + testing: + - 2d.gradient.radial.transform + code: | + var g = ctx.createRadialGradient(0, 0, 0, 0, 0, 11.2); + g.addColorStop(0, '#0f0'); + g.addColorStop(0.5, '#0f0'); + g.addColorStop(0.51, '#f00'); + g.addColorStop(1, '#f00'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + ctx.translate(50, 25); + ctx.scale(10, 10); + ctx.fillRect(-5, -2.5, 10, 5); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.gradient.conic.positive.rotation + desc: Conic gradient with positive rotation + code: | + const g = ctx.createConicGradient(3*Math.PI/2, 50, 25); + // It's red in the upper right region and green on the lower left region + g.addColorStop(0, "#f00"); + g.addColorStop(0.25, "#0f0"); + g.addColorStop(0.50, "#0f0"); + g.addColorStop(0.75, "#f00"); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,15 == 255,0,0,255; + @assert pixel 75,40 == 0,255,0,255; + expected: green + +- name: 2d.gradient.conic.negative.rotation + desc: Conic gradient with negative rotation + code: | + const g = ctx.createConicGradient(-Math.PI/2, 50, 25); + // It's red in the upper right region and green on the lower left region + g.addColorStop(0, "#f00"); + g.addColorStop(0.25, "#0f0"); + g.addColorStop(0.50, "#0f0"); + g.addColorStop(0.75, "#f00"); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 25,15 == 255,0,0,255; + @assert pixel 75,40 == 0,255,0,255; + expected: green + +- name: 2d.gradient.conic.invalid.inputs + desc: Conic gradient function with invalid inputs + code: | + @nonfinite @assert throws TypeError ctx.createConicGradient(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>); + + const g = ctx.createConicGradient(0, 0, 25); + @nonfinite @assert throws TypeError g.addColorStop(, <'#f00'>); + @nonfinite @assert throws SYNTAX_ERR g.addColorStop(<0>, ); + +- name: 2d.pattern.basic.type + testing: + - 2d.pattern.return + images: + - green.png + code: | + @assert window.CanvasPattern !== undefined; + + window.CanvasPattern.prototype.thisImplementsCanvasPattern = true; + + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + @assert pattern.thisImplementsCanvasPattern; + +- name: 2d.pattern.basic.image + testing: + - 2d.pattern.painting + images: + - green.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.basic.canvas + testing: + - 2d.pattern.painting + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50); + + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.basic.zerocanvas + testing: + - 2d.pattern.zerocanvas + code: | + canvas.width = 0; + canvas.height = 10; + @assert canvas.width === 0; + @assert canvas.height === 10; + @assert throws INVALID_STATE_ERR ctx.createPattern(canvas, 'repeat'); + + canvas.width = 10; + canvas.height = 0; + @assert canvas.width === 10; + @assert canvas.height === 0; + @assert throws INVALID_STATE_ERR ctx.createPattern(canvas, 'repeat'); + + canvas.width = 0; + canvas.height = 0; + @assert canvas.width === 0; + @assert canvas.height === 0; + @assert throws INVALID_STATE_ERR ctx.createPattern(canvas, 'repeat'); + +- name: 2d.pattern.basic.nocontext + testing: + - 2d.pattern.painting + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.transform.identity + testing: + - 2d.pattern.transform + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + pattern.setTransform(new DOMMatrix()); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.transform.infinity + testing: + - 2d.pattern.transform + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + pattern.setTransform({a: Infinity}); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.transform.invalid + testing: + - 2d.pattern.transform + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + @assert throws TypeError pattern.setTransform({a: 1, m11: 2}); + +- name: 2d.pattern.image.undefined + testing: + - 2d.pattern.IDL + notes: *bindings + code: | + @assert throws TypeError ctx.createPattern(undefined, 'repeat'); + +- name: 2d.pattern.image.null + testing: + - 2d.pattern.IDL + notes: *bindings + code: | + @assert throws TypeError ctx.createPattern(null, 'repeat'); + +- name: 2d.pattern.image.string + testing: + - 2d.pattern.IDL + notes: *bindings + code: | + @assert throws TypeError ctx.createPattern('../images/red.png', 'repeat'); + +- name: 2d.pattern.image.incomplete.nosrc + testing: + - 2d.pattern.incomplete.image + code: | + var img = new Image(); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.incomplete.immediate + testing: + - 2d.pattern.incomplete.image + images: + - red.png + code: | + var img = new Image(); + img.src = '../images/red.png'; + // This triggers the "update the image data" algorithm. + // The image will not go to the "completely available" state + // until a fetch task in the networking task source is processed, + // so the image must not be fully decodable yet: + @assert ctx.createPattern(img, 'repeat') === null; @moz-todo + +- name: 2d.pattern.image.incomplete.reload + testing: + - 2d.pattern.incomplete.image + images: + - yellow.png + - red.png + code: | + var img = document.getElementById('yellow.png'); + img.src = '../images/red.png'; + // This triggers the "update the image data" algorithm, + // and resets the image to the "unavailable" state. + // The image will not go to the "completely available" state + // until a fetch task in the networking task source is processed, + // so the image must not be fully decodable yet: + @assert ctx.createPattern(img, 'repeat') === null; @moz-todo + +- name: 2d.pattern.image.incomplete.emptysrc + testing: + - 2d.pattern.incomplete.image + images: + - red.png + mozilla: {throws: !!null ''} + code: | + var img = document.getElementById('red.png'); + img.src = ""; + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.incomplete.removedsrc + testing: + - 2d.pattern.incomplete.image + images: + - red.png + mozilla: {throws: !!null ''} + code: | + var img = document.getElementById('red.png'); + img.removeAttribute('src'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.broken + testing: + - 2d.pattern.broken.image + images: + - broken.png + code: | + var img = document.getElementById('broken.png'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.nonexistent + testing: + - 2d.pattern.nonexistent.image + images: + - no-such-image-really.png + code: | + var img = document.getElementById('no-such-image-really.png'); + @assert throws INVALID_STATE_ERR ctx.createPattern(img, 'repeat'); + +- name: 2d.pattern.svgimage.nonexistent + testing: + - 2d.pattern.nonexistent.svgimage + svgimages: + - no-such-image-really.png + code: | + var img = document.getElementById('no-such-image-really.png'); + @assert throws INVALID_STATE_ERR ctx.createPattern(img, 'repeat'); + +- name: 2d.pattern.image.nonexistent-but-loading + testing: + - 2d.pattern.nonexistent-but-loading.image + code: | + var img = document.createElement("img"); + img.src = "/images/no-such-image-really.png"; + @assert ctx.createPattern(img, 'repeat') === null; + var img = document.createElementNS("http://www.w3.org/2000/svg", "image"); + img.src = "/images/no-such-image-really.png"; + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.nosrc + testing: + - 2d.pattern.nosrc.image + code: | + var img = document.createElement("img"); + @assert ctx.createPattern(img, 'repeat') === null; + var img = document.createElementNS("http://www.w3.org/2000/svg", "image"); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.zerowidth + testing: + - 2d.pattern.zerowidth.image + images: + - red-zerowidth.svg + code: | + var img = document.getElementById('red-zerowidth.svg'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.image.zeroheight + testing: + - 2d.pattern.zeroheight.image + images: + - red-zeroheight.svg + code: | + var img = document.getElementById('red-zeroheight.svg'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.svgimage.zerowidth + testing: + - 2d.pattern.zerowidth.svgimage + svgimages: + - red-zerowidth.svg + code: | + var img = document.getElementById('red-zerowidth.svg'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.svgimage.zeroheight + testing: + - 2d.pattern.zeroheight.svgimage + svgimages: + - red-zeroheight.svg + code: | + var img = document.getElementById('red-zeroheight.svg'); + @assert ctx.createPattern(img, 'repeat') === null; + +- name: 2d.pattern.repeat.empty + testing: + - 2d.pattern.missing + images: + - green-1x1.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + var img = document.getElementById('green-1x1.png'); + var pattern = ctx.createPattern(img, ""); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 200, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.repeat.null + testing: + - 2d.pattern.unrecognised + code: | + @assert ctx.createPattern(canvas, null) != null; + +- name: 2d.pattern.repeat.undefined + testing: + - 2d.pattern.unrecognised + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, undefined); + +- name: 2d.pattern.repeat.unrecognised + testing: + - 2d.pattern.unrecognised + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, "invalid"); + +- name: 2d.pattern.repeat.unrecognisednull + testing: + - 2d.pattern.unrecognised + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, "null"); + +- name: 2d.pattern.repeat.case + testing: + - 2d.pattern.exact + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, "Repeat"); + +- name: 2d.pattern.repeat.nullsuffix + testing: + - 2d.pattern.exact + code: | + @assert throws SYNTAX_ERR ctx.createPattern(canvas, "repeat\0"); + +- name: 2d.pattern.modify.image1 + testing: + - 2d.pattern.modify + images: + - green.png + code: | + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + deferTest(); + img.onload = t.step_func_done(function () + { + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + }); + img.src = '/images/red.png'; + expected: green + +- name: 2d.pattern.modify.image2 + testing: + - 2d.pattern.modify + images: + - green.png + code: | + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 100, 50); + deferTest(); + img.onload = t.step_func_done(function () + { + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + }); + img.src = '/images/red.png'; + expected: green + +- name: 2d.pattern.modify.canvas1 + testing: + - 2d.pattern.modify + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50); + + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.modify.canvas2 + testing: + - 2d.pattern.modify + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50); + + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.crosscanvas + images: + - green.png + code: | + var img = document.getElementById('green.png'); + + var pattern = document.createElement('canvas').getContext('2d').createPattern(img, 'no-repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.basic + testing: + - 2d.pattern.painting + images: + - green.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.outside + testing: + - 2d.pattern.painting + images: + - red.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + ctx.fillRect(-100, 0, 100, 50); + ctx.fillRect(0, 50, 100, 50); + ctx.fillRect(100, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.coord1 + testing: + - 2d.pattern.painting + images: + - green.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.translate(50, 0); + ctx.fillRect(-50, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.coord2 + testing: + - 2d.pattern.painting + images: + - green.png + code: | + var img = document.getElementById('green.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 50, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + + ctx.fillStyle = pattern; + ctx.translate(50, 0); + ctx.fillRect(-50, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.norepeat.coord3 + testing: + - 2d.pattern.painting + images: + - red.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.translate(50, 25); + ctx.fillRect(-50, -25, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.basic + testing: + - 2d.pattern.painting + images: + - green-16x16.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('green-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.outside + testing: + - 2d.pattern.painting + images: + - green-16x16.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('green-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.translate(50, 25); + ctx.fillRect(-50, -25, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.coord1 + testing: + - 2d.pattern.painting + images: + - rgrg-256x256.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('rgrg-256x256.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.translate(-128, -78); + ctx.fillRect(128, 78, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.coord2 + testing: + - 2d.pattern.painting + images: + - ggrr-256x256.png + code: | + var img = document.getElementById('ggrr-256x256.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeat.coord3 + testing: + - 2d.pattern.painting + images: + - rgrg-256x256.png + code: | + var img = document.getElementById('rgrg-256x256.png'); + var pattern = ctx.createPattern(img, 'repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(-128, -78); + ctx.fillRect(128, 78, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeatx.basic + testing: + - 2d.pattern.painting + images: + - green-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 16); + + var img = document.getElementById('green-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-x'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeatx.outside + testing: + - 2d.pattern.painting + images: + - red-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-x'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 16); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeatx.coord1 + testing: + - 2d.pattern.painting + images: + - red-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-x'); + ctx.fillStyle = pattern; + ctx.translate(0, 16); + ctx.fillRect(0, -16, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 16); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeaty.basic + testing: + - 2d.pattern.painting + images: + - green-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 16, 50); + + var img = document.getElementById('green-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-y'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeaty.outside + testing: + - 2d.pattern.painting + images: + - red-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-y'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 16, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.repeaty.coord1 + testing: + - 2d.pattern.painting + images: + - red-16x16.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('red-16x16.png'); + var pattern = ctx.createPattern(img, 'repeat-y'); + ctx.fillStyle = pattern; + ctx.translate(48, 0); + ctx.fillRect(-48, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 16, 50); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.orientation.image + desc: Image patterns do not get flipped when painted + testing: + - 2d.pattern.painting + images: + - rrgg-256x256.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var img = document.getElementById('rrgg-256x256.png'); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.save(); + ctx.translate(0, -103); + ctx.fillRect(0, 103, 100, 50); + ctx.restore(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 25); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.pattern.paint.orientation.canvas + desc: Canvas patterns do not get flipped when painted + testing: + - 2d.pattern.painting + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 25); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 25, 100, 25); + + var pattern = ctx.createPattern(canvas2, 'no-repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 25); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + + +- name: 2d.pattern.animated.gif + desc: createPattern() of an animated GIF draws the first frame + testing: + - 2d.pattern.animated.image + images: + - anim-gr.gif + code: | + deferTest(); + step_timeout(function () { + var pattern = ctx.createPattern(document.getElementById('anim-gr.gif'), 'repeat'); + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, 50, 50); + step_timeout(t.step_func_done(function () { + ctx.fillRect(50, 0, 50, 50); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + }), 250); + }, 250); + expected: green + +- name: 2d.fillStyle.CSSRGB + desc: CSSRGB works as color input + testing: + - 2d.colors.CSSRGB + code: | + ctx.fillStyle = new CSSRGB(1, 0, 1); + @assert ctx.fillStyle === '#ff00ff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 255,0,255,255; + + const color = new CSSRGB(0, CSS.percent(50), 0); + ctx.fillStyle = color; + @assert ctx.fillStyle === '#008000'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,128,0,255; + color.g = 0; + ctx.fillStyle = color; + @assert ctx.fillStyle === '#000000'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,0,0,255; + + color.alpha = 0; + ctx.fillStyle = color; + @assert ctx.fillStyle === 'rgba(0, 0, 0, 0)'; + ctx.reset(); + color.alpha = 0.5; + ctx.fillStyle = color; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,0,0,128; + + ctx.fillStyle = new CSSHSL(CSS.deg(0), 1, 1).toRGB(); + @assert ctx.fillStyle === '#ffffff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 255,255,255,255; + + color.alpha = 1; + color.g = 1; + ctx.fillStyle = color; + ctx.fillRect(0, 0, 100, 50); + expected: green + +- name: 2d.fillStyle.CSSHSL + desc: CSSHSL works as color input + testing: + - 2d.colors.CSSHSL + code: | + ctx.fillStyle = new CSSHSL(CSS.deg(180), 0.5, 0.5); + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ 64,191,191,255 +/- 3; + + const color = new CSSHSL(CSS.deg(180), 1, 1); + ctx.fillStyle = color; + @assert ctx.fillStyle === '#ffffff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 255,255,255,255; + color.l = 0.5; + ctx.fillStyle = color; + @assert ctx.fillStyle === '#00ffff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,255,255; + + ctx.fillStyle = new CSSRGB(1, 0, 1).toHSL(); + @assert ctx.fillStyle === '#ff00ff'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 255,0,255,255; + + color.h = CSS.deg(120); + color.s = 1; + color.l = 0.5; + ctx.fillStyle = color; + ctx.fillRect(0, 0, 100, 50); + expected: green diff --git a/test/wpt/generate.js b/test/wpt/generate.js new file mode 100644 index 000000000..74fbcb623 --- /dev/null +++ b/test/wpt/generate.js @@ -0,0 +1,259 @@ +// This file is a port of gentestutils.py from +// https://github.com/web-platform-tests/wpt/tree/master/html/canvas/tools + +const yaml = require("js-yaml"); +const fs = require("fs"); + +const yamlFiles = fs.readdirSync(__dirname).filter(f => f.endsWith(".yaml")); +// Files that should be skipped: +const SKIP_FILES = new Set("meta.yaml"); +// Tests that should be skipped (e.g. because they cause hangs or V8 crashes): +const SKIP_TESTS = new Set([ + "2d.imageData.create2.negative", + "2d.imageData.create2.zero", + "2d.imageData.create2.nonfinite", + "2d.imageData.create1.zero", + "2d.imageData.create2.double", + "2d.imageData.get.source.outside", + "2d.imageData.get.source.negative", + "2d.imageData.get.double", + "2d.imageData.get.large.crash", // expected +]); + +function expandNonfinite(method, argstr, tail) { + // argstr is ", ..." (where usually + // 'invalid' is Infinity/-Infinity/NaN) + const args = []; + for (const arg of argstr.split(', ')) { + const [, a] = arg.match(/<(.*)>/); + args.push(a.split(' ')); + } + const calls = []; + // Start with the valid argument list + const call = []; + for (let i = 0; i < args.length; i++) { + call.push(args[i][0]); + } + // For each argument alone, try setting it to all its invalid values: + for (let i = 0; i < args.length; i++) { + for (let j = 1; j < args[i].length; j++) { + const c2 = [...call] + c2[i] = args[i][j]; + calls.push(c2); + } + } + // For all combinations of >= 2 arguments, try setting them to their first + // invalid values. (Don't do all invalid values, because the number of + // combinations explodes.) + const f = (c, start, depth) => { + for (let i = start; i < args.length; i++) { + if (args[i].length > 1) { + const a = args[i][1] + const c2 = [...c] + c2[i] = a + if (depth > 0) + calls.push(c2) + f(c2, i+1, depth+1) + } + } + }; + f(call, 0, 0); + + return calls.map(c => `${method}(${c.join(", ")})${tail}`).join("\n\t\t"); +} + +function simpleEscapeJS(str) { + return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') +} + +function escapeJS(str) { + str = simpleEscapeJS(str) + str = str.replace(/\[(\w+)\]/g, '[\\""+($1)+"\\"]') // kind of an ugly hack, for nicer failure-message output + return str +} + +/** @type {string} test */ +function convert(test) { + let code = test.code; + if (!code) return ""; + // Indent it + code = code.trim().replace(/^/gm, "\t\t"); + + code = code.replace(/@nonfinite ([^(]+)\(([^)]+)\)(.*)/g, (match, g1, g2, g3) => { + return expandNonfinite(g1, g2, g3); + }); + + code = code.replace(/@assert pixel (\d+,\d+) == (\d+,\d+,\d+,\d+);/g, + "_assertPixel(canvas, $1, $2);"); + + code = code.replace(/@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+);/g, + "_assertPixelApprox(canvas, $1, $2);"); + + code = code.replace(/@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+) \+\/- (\d+);/g, + "_assertPixelApprox(canvas, $1, $2, $3);"); + + code = code.replace(/@assert throws (\S+_ERR) (.*);/g, + 'assert.throws(function() { $2; }, /$1/);'); + + code = code.replace(/@assert throws (\S+Error) (.*);/g, + 'assert.throws(function() { $2; }, $1);'); + + code = code.replace(/@assert (.*) === (.*);/g, (match, g1, g2) => { + return `assert.strictEqual(${g1}, ${g2}, "${escapeJS(g1)}", "${escapeJS(g2)}")`; + }); + + code = code.replace(/@assert (.*) !== (.*);/g, (match, g1, g2) => { + return `assert.notStrictEqual(${g1}, ${g2}, "${escapeJS(g1)}", "${escapeJS(g2)}");`; + }); + + code = code.replace(/@assert (.*) =~ (.*);/g, (match, g1, g2) => { + return `assert.match(${g1}, ${g2});`; + }); + + code = code.replace(/@assert (.*);/g, (match, g1) => { + return `assert(${g1}, "${escapeJS(g1)}");`; + }); + + code = code.replace(/ @moz-todo/g, ""); + + code = code.replace(/@moz-UniversalBrowserRead;/g, ""); + + if (code.includes("@")) + throw new Error("@ found in code; generation failed"); + + const name = test.name.replace(/"/g, /\"/); + + const skip = SKIP_TESTS.has(name) ? ".skip" : ""; + + return ` + it${skip}("${name}", function () {${test.desc ? `\n\t\t// ${test.desc}` : ""} + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + +${code} + }); +` +} + + +for (const filename of yamlFiles) { + if (SKIP_FILES.has(filename)) + continue; + + let tests; + try { + const content = fs.readFileSync(`${__dirname}/${filename}`, "utf8"); + tests = yaml.load(content, { + filename, + // schema: yaml.DEFAULT_SCHEMA + }); + } catch (ex) { + console.error(ex.toString()); + continue; + } + + const out = fs.createWriteStream(`${__dirname}/generated/${filename.replace(".yaml", ".js")}`); + + out.write(`// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(\`createElement(\${type}) not supported\`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a \${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + \`expected \${actual} to equal \${expected} +/- \${epsilon}. \${msg}\`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: ${filename.replace(".yaml", "")}", function () { +`); + + for (const test of tests) { + out.write(convert(test)); + } + + out.write(`}); +`) + + out.end(); +} diff --git a/test/wpt/generated/drawing-text-to-the-canvas.js b/test/wpt/generated/drawing-text-to-the-canvas.js new file mode 100644 index 000000000..38cddc45b --- /dev/null +++ b/test/wpt/generated/drawing-text-to-the-canvas.js @@ -0,0 +1,1122 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: drawing-text-to-the-canvas", function () { + + it("2d.text.draw.fill.basic", function () { + // fillText draws filled text + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('PASS', 5, 35); + }); + + it("2d.text.draw.fill.unaffected", function () { + // fillText does not start a new path or subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('FAIL', 5, 35); + + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 5,45, 0,255,0,255); + }); + + it("2d.text.draw.fill.rtl", function () { + // fillText respects Right-To-Left Override characters + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('\u202eFAIL \xa0 \xa0 SSAP', 5, 35); + }); + + it("2d.text.draw.fill.maxWidth.large", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('PASS', 5, 35, 200); + }); + + it("2d.text.draw.fill.maxWidth.small", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', -100, 35, 90); + _assertGreen(ctx, 100, 50); + }); + + it("2d.text.draw.fill.maxWidth.zero", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, 0); + _assertGreen(ctx, 100, 50); + }); + + it("2d.text.draw.fill.maxWidth.negative", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, -1); + _assertGreen(ctx, 100, 50); + }); + + it("2d.text.draw.fill.maxWidth.NaN", function () { + // fillText handles maxWidth correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.font = '35px Arial, sans-serif'; + ctx.fillText('fail fail fail fail fail', 5, 35, NaN); + _assertGreen(ctx, 100, 50); + }); + + it("2d.text.draw.stroke.basic", function () { + // strokeText draws stroked text + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.fillStyle = '#f00'; + ctx.lineWidth = 1; + ctx.font = '35px Arial, sans-serif'; + ctx.strokeText('PASS', 5, 35); + }); + + it("2d.text.draw.stroke.unaffected", function () { + // strokeText does not start a new path or subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + + ctx.font = '35px Arial, sans-serif'; + ctx.strokeStyle = '#f00'; + ctx.strokeText('FAIL', 5, 35); + + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 5,45, 0,255,0,255); + }); + + it("2d.text.draw.kern.consistent", function () { + // Stroked and filled text should have exactly the same kerning so it overlaps + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 3; + ctx.font = '20px Arial, sans-serif'; + ctx.fillText('VAVAVAVAVAVAVA', -50, 25); + ctx.fillText('ToToToToToToTo', -50, 45); + ctx.strokeText('VAVAVAVAVAVAVA', -50, 25); + ctx.strokeText('ToToToToToToTo', -50, 45); + }); + + it("2d.text.draw.fill.maxWidth.fontface", function () { + // fillText works on @font-face fonts + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillText('EEEE', -50, 37.5, 40); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.fill.maxWidth.bound", function () { + // fillText handles maxWidth based on line size, not bounding box size + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('DD', 0, 37.5, 100); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.fontface", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '67px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.fontface.repeat", function () { + // Draw with the font immediately, then wait a bit until and draw again. (This crashes some version of WebKit.) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.font = '67px CanvasTest'; + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillText('AA', 0, 50); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.fontface.notinpage", function () { + // @font-face fonts should work even if they are not used in the page + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '67px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('AA', 0, 50); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.left", function () { + // textAlign left is the left of the first em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'left'; + ctx.fillText('DD', 0, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.right", function () { + // textAlign right is the right of the last em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'right'; + ctx.fillText('DD', 100, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.start.ltr", function () { + // textAlign start with ltr is the left edge + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'start'; + ctx.fillText('DD', 0, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.start.rtl", function () { + // textAlign start with rtl is the right edge + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'start'; + ctx.fillText('DD', 100, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.end.ltr", function () { + // textAlign end with ltr is the right edge + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'end'; + ctx.fillText('DD', 100, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.end.rtl", function () { + // textAlign end with rtl is the left edge + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'end'; + ctx.fillText('DD', 0, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.align.center", function () { + // textAlign center is the center of the em squares (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'center'; + ctx.fillText('DD', 50, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.basic", function () { + // U+0020 is rendered the correct size (1em wide) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E EE', -100, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.nonspace", function () { + // Non-space characters are not converted to U+0020 and collapsed + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E\x0b EE', -150, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.measure.width.basic", function () { + // The width of character is same as font used + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + assert.strictEqual(ctx.measureText('A').width, 50, "ctx.measureText('A').width", "50") + assert.strictEqual(ctx.measureText('AA').width, 100, "ctx.measureText('AA').width", "100") + assert.strictEqual(ctx.measureText('ABCD').width, 200, "ctx.measureText('ABCD').width", "200") + + ctx.font = '100px CanvasTest'; + assert.strictEqual(ctx.measureText('A').width, 100, "ctx.measureText('A').width", "100") + }), 500); + }); + }); + + it("2d.text.measure.width.empty", function () { + // The empty string has zero width + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + assert.strictEqual(ctx.measureText("").width, 0, "ctx.measureText(\"\").width", "0") + }), 500); + }); + }); + + it("2d.text.measure.advances", function () { + // Testing width advances + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + // Some platforms may return '-0'. + assert.strictEqual(Math.abs(ctx.measureText('Hello').advances[0]), 0, "Math.abs(ctx.measureText('Hello').advances[\""+(0)+"\"])", "0") + // Different platforms may render text slightly different. + assert(ctx.measureText('Hello').advances[1] >= 36, "ctx.measureText('Hello').advances[\""+(1)+"\"] >= 36"); + assert(ctx.measureText('Hello').advances[2] >= 58, "ctx.measureText('Hello').advances[\""+(2)+"\"] >= 58"); + assert(ctx.measureText('Hello').advances[3] >= 70, "ctx.measureText('Hello').advances[\""+(3)+"\"] >= 70"); + assert(ctx.measureText('Hello').advances[4] >= 80, "ctx.measureText('Hello').advances[\""+(4)+"\"] >= 80"); + + var tm = ctx.measureText('Hello'); + assert.strictEqual(ctx.measureText('Hello').advances[0], tm.advances[0], "ctx.measureText('Hello').advances[\""+(0)+"\"]", "tm.advances[\""+(0)+"\"]") + assert.strictEqual(ctx.measureText('Hello').advances[1], tm.advances[1], "ctx.measureText('Hello').advances[\""+(1)+"\"]", "tm.advances[\""+(1)+"\"]") + assert.strictEqual(ctx.measureText('Hello').advances[2], tm.advances[2], "ctx.measureText('Hello').advances[\""+(2)+"\"]", "tm.advances[\""+(2)+"\"]") + assert.strictEqual(ctx.measureText('Hello').advances[3], tm.advances[3], "ctx.measureText('Hello').advances[\""+(3)+"\"]", "tm.advances[\""+(3)+"\"]") + assert.strictEqual(ctx.measureText('Hello').advances[4], tm.advances[4], "ctx.measureText('Hello').advances[\""+(4)+"\"]", "tm.advances[\""+(4)+"\"]") + }), 500); + }); + }); + + it("2d.text.measure.actualBoundingBox", function () { + // Testing actualBoundingBox + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + ctx.baseline = 'alphabetic' + // Different platforms may render text slightly different. + // Values that are nominally expected to be zero might actually vary by a pixel or so + // if the UA accounts for antialiasing at glyph edges, so we allow a slight deviation. + assert(Math.abs(ctx.measureText('A').actualBoundingBoxLeft) <= 1, "Math.abs(ctx.measureText('A').actualBoundingBoxLeft) <= 1"); + assert(ctx.measureText('A').actualBoundingBoxRight >= 50, "ctx.measureText('A').actualBoundingBoxRight >= 50"); + assert(ctx.measureText('A').actualBoundingBoxAscent >= 35, "ctx.measureText('A').actualBoundingBoxAscent >= 35"); + assert(Math.abs(ctx.measureText('A').actualBoundingBoxDescent) <= 1, "Math.abs(ctx.measureText('A').actualBoundingBoxDescent) <= 1"); + + assert(ctx.measureText('D').actualBoundingBoxLeft >= 48, "ctx.measureText('D').actualBoundingBoxLeft >= 48"); + assert(ctx.measureText('D').actualBoundingBoxLeft <= 52, "ctx.measureText('D').actualBoundingBoxLeft <= 52"); + assert(ctx.measureText('D').actualBoundingBoxRight >= 75, "ctx.measureText('D').actualBoundingBoxRight >= 75"); + assert(ctx.measureText('D').actualBoundingBoxRight <= 80, "ctx.measureText('D').actualBoundingBoxRight <= 80"); + assert(ctx.measureText('D').actualBoundingBoxAscent >= 35, "ctx.measureText('D').actualBoundingBoxAscent >= 35"); + assert(ctx.measureText('D').actualBoundingBoxAscent <= 40, "ctx.measureText('D').actualBoundingBoxAscent <= 40"); + assert(ctx.measureText('D').actualBoundingBoxDescent >= 12, "ctx.measureText('D').actualBoundingBoxDescent >= 12"); + assert(ctx.measureText('D').actualBoundingBoxDescent <= 15, "ctx.measureText('D').actualBoundingBoxDescent <= 15"); + + assert(Math.abs(ctx.measureText('ABCD').actualBoundingBoxLeft) <= 1, "Math.abs(ctx.measureText('ABCD').actualBoundingBoxLeft) <= 1"); + assert(ctx.measureText('ABCD').actualBoundingBoxRight >= 200, "ctx.measureText('ABCD').actualBoundingBoxRight >= 200"); + assert(ctx.measureText('ABCD').actualBoundingBoxAscent >= 85, "ctx.measureText('ABCD').actualBoundingBoxAscent >= 85"); + assert(ctx.measureText('ABCD').actualBoundingBoxDescent >= 37, "ctx.measureText('ABCD').actualBoundingBoxDescent >= 37"); + }), 500); + }); + }); + + it("2d.text.measure.fontBoundingBox", function () { + // Testing fontBoundingBox + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + assert.strictEqual(ctx.measureText('A').fontBoundingBoxAscent, 85, "ctx.measureText('A').fontBoundingBoxAscent", "85") + assert.strictEqual(ctx.measureText('A').fontBoundingBoxDescent, 39, "ctx.measureText('A').fontBoundingBoxDescent", "39") + + assert.strictEqual(ctx.measureText('ABCD').fontBoundingBoxAscent, 85, "ctx.measureText('ABCD').fontBoundingBoxAscent", "85") + assert.strictEqual(ctx.measureText('ABCD').fontBoundingBoxDescent, 39, "ctx.measureText('ABCD').fontBoundingBoxDescent", "39") + }), 500); + }); + }); + + it("2d.text.measure.fontBoundingBox.ahem", function () { + // Testing fontBoundingBox for font ahem + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("Ahem", "/fonts/Ahem.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px Ahem'; + ctx.direction = 'ltr'; + ctx.align = 'left' + assert.strictEqual(ctx.measureText('A').fontBoundingBoxAscent, 40, "ctx.measureText('A').fontBoundingBoxAscent", "40") + assert.strictEqual(ctx.measureText('A').fontBoundingBoxDescent, 10, "ctx.measureText('A').fontBoundingBoxDescent", "10") + + assert.strictEqual(ctx.measureText('ABCD').fontBoundingBoxAscent, 40, "ctx.measureText('ABCD').fontBoundingBoxAscent", "40") + assert.strictEqual(ctx.measureText('ABCD').fontBoundingBoxDescent, 10, "ctx.measureText('ABCD').fontBoundingBoxDescent", "10") + }), 500); + }); + }); + + it("2d.text.measure.emHeights", function () { + // Testing emHeights + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + assert.strictEqual(ctx.measureText('A').emHeightAscent, 37.5, "ctx.measureText('A').emHeightAscent", "37.5") + assert.strictEqual(ctx.measureText('A').emHeightDescent, 12.5, "ctx.measureText('A').emHeightDescent", "12.5") + assert.strictEqual(ctx.measureText('A').emHeightDescent + ctx.measureText('A').emHeightAscent, 50, "ctx.measureText('A').emHeightDescent + ctx.measureText('A').emHeightAscent", "50") + + assert.strictEqual(ctx.measureText('ABCD').emHeightAscent, 37.5, "ctx.measureText('ABCD').emHeightAscent", "37.5") + assert.strictEqual(ctx.measureText('ABCD').emHeightDescent, 12.5, "ctx.measureText('ABCD').emHeightDescent", "12.5") + assert.strictEqual(ctx.measureText('ABCD').emHeightDescent + ctx.measureText('ABCD').emHeightAscent, 50, "ctx.measureText('ABCD').emHeightDescent + ctx.measureText('ABCD').emHeightAscent", "50") + }), 500); + }); + }); + + it("2d.text.measure.baselines", function () { + // Testing baselines + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + ctx.direction = 'ltr'; + ctx.align = 'left' + assert.strictEqual(Math.abs(ctx.measureText('A').getBaselines().alphabetic), 0, "Math.abs(ctx.measureText('A').getBaselines().alphabetic)", "0") + assert.strictEqual(ctx.measureText('A').getBaselines().ideographic, -39, "ctx.measureText('A').getBaselines().ideographic", "-39") + assert.strictEqual(ctx.measureText('A').getBaselines().hanging, 68, "ctx.measureText('A').getBaselines().hanging", "68") + + assert.strictEqual(Math.abs(ctx.measureText('ABCD').getBaselines().alphabetic), 0, "Math.abs(ctx.measureText('ABCD').getBaselines().alphabetic)", "0") + assert.strictEqual(ctx.measureText('ABCD').getBaselines().ideographic, -39, "ctx.measureText('ABCD').getBaselines().ideographic", "-39") + assert.strictEqual(ctx.measureText('ABCD').getBaselines().hanging, 68, "ctx.measureText('ABCD').getBaselines().hanging", "68") + }), 500); + }); + }); + + it("2d.text.drawing.style.spacing", function () { + // Testing letter spacing and word spacing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + + ctx.letterSpacing = '3px'; + assert.strictEqual(ctx.letterSpacing, '3px', "ctx.letterSpacing", "'3px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + + ctx.wordSpacing = '5px'; + assert.strictEqual(ctx.letterSpacing, '3px', "ctx.letterSpacing", "'3px'") + assert.strictEqual(ctx.wordSpacing, '5px', "ctx.wordSpacing", "'5px'") + + ctx.letterSpacing = '-1px'; + ctx.wordSpacing = '-1px'; + assert.strictEqual(ctx.letterSpacing, '-1px', "ctx.letterSpacing", "'-1px'") + assert.strictEqual(ctx.wordSpacing, '-1px', "ctx.wordSpacing", "'-1px'") + + ctx.letterSpacing = '1PX'; + ctx.wordSpacing = '1EM'; + assert.strictEqual(ctx.letterSpacing, '1px', "ctx.letterSpacing", "'1px'") + assert.strictEqual(ctx.wordSpacing, '1em', "ctx.wordSpacing", "'1em'") + }); + + it("2d.text.drawing.style.nonfinite.spacing", function () { + // Testing letter spacing and word spacing with nonfinite inputs + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + + function test_word_spacing(value) { + ctx.wordSpacing = value; + ctx.letterSpacing = value; + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + } + test_word_spacing(NaN); + test_word_spacing(Infinity); + test_word_spacing(-Infinity); + }); + + it("2d.text.drawing.style.invalid.spacing", function () { + // Testing letter spacing and word spacing with invalid units + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + + function test_word_spacing(value) { + ctx.wordSpacing = value; + ctx.letterSpacing = value; + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + } + test_word_spacing('0s'); + test_word_spacing('1min'); + test_word_spacing('1deg'); + test_word_spacing('1pp'); + }); + + it("2d.text.drawing.style.letterSpacing.measure", function () { + // Testing letter spacing and word spacing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + var width_normal = ctx.measureText('Hello World').width; + + function test_letter_spacing(value, difference_spacing, epsilon) { + ctx.letterSpacing = value; + assert.strictEqual(ctx.letterSpacing, value, "ctx.letterSpacing", "value") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + width_with_letter_spacing = ctx.measureText('Hello World').width; + assert_approx_equals(width_with_letter_spacing, width_normal + difference_spacing, epsilon, "letter spacing doesn't work."); + } + + // The first value is the letter Spacing to be set, the second value the + // change in length of string 'Hello World', note that there are 11 letters + // in 'hello world', so the length difference is always letterSpacing * 11. + // and the third value is the acceptable differencee for the length change, + // note that unit such as 1cm/1mm doesn't map to an exact pixel value. + test_cases = [['3px', 33, 0], + ['5px', 55, 0], + ['-2px', -22, 0], + ['1em', 110, 0], + ['1in', 1056, 0], + ['-0.1cm', -41.65, 0.2], + ['-0.6mm', -24,95, 0.2]] + + for (const test_case of test_cases) { + test_letter_spacing(test_case[0], test_case[1], test_case[2]); + } + }); + + it("2d.text.drawing.style.wordSpacing.measure", function () { + // Testing if word spacing is working properly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + var width_normal = ctx.measureText('Hello World, again').width; + + function test_word_spacing(value, difference_spacing, epsilon) { + ctx.wordSpacing = value; + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, value, "ctx.wordSpacing", "value") + width_with_word_spacing = ctx.measureText('Hello World, again').width; + assert_approx_equals(width_with_word_spacing, width_normal + difference_spacing, epsilon, "word spacing doesn't work."); + } + + // The first value is the word Spacing to be set, the second value the + // change in length of string 'Hello World', note that there are 2 words + // in 'Hello World, again', so the length difference is always wordSpacing * 2. + // and the third value is the acceptable differencee for the length change, + // note that unit such as 1cm/1mm doesn't map to an exact pixel value. + test_cases = [['3px', 6, 0], + ['5px', 10, 0], + ['-2px', -4, 0], + ['1em', 20, 0], + ['1in', 192, 0], + ['-0.1cm', -7.57, 0.2], + ['-0.6mm', -4.54, 0.2]] + + for (const test_case of test_cases) { + test_word_spacing(test_case[0], test_case[1], test_case[2]); + } + }); + + it("2d.text.drawing.style.letterSpacing.change.font", function () { + // Set letter spacing and word spacing to font dependent value and verify it works after font change. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + // Get the width for 'Hello World' at default size, 10px. + var width_normal = ctx.measureText('Hello World').width; + + ctx.letterSpacing = '1em'; + assert.strictEqual(ctx.letterSpacing, '1em', "ctx.letterSpacing", "'1em'") + // 1em = 10px. Add 10px after each letter in "Hello World", + // makes it 110px longer. + var width_with_spacing = ctx.measureText('Hello World').width; + assert.strictEqual(width_with_spacing, width_normal + 110, "width_with_spacing", "width_normal + 110") + + // Changing font to 20px. Without resetting the spacing, 1em letterSpacing + // is now 20px, so it's suppose to be 220px longer without any letterSpacing set. + ctx.font = '20px serif'; + width_with_spacing = ctx.measureText('Hello World').width; + // Now calculate the reference spacing for "Hello World" with no spacing. + ctx.letterSpacing = '0em'; + width_normal = ctx.measureText('Hello World').width; + assert.strictEqual(width_with_spacing, width_normal + 220, "width_with_spacing", "width_normal + 220") + }); + + it("2d.text.drawing.style.wordSpacing.change.font", function () { + // Set word spacing and word spacing to font dependent value and verify it works after font change. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.letterSpacing, '0px', "ctx.letterSpacing", "'0px'") + assert.strictEqual(ctx.wordSpacing, '0px', "ctx.wordSpacing", "'0px'") + // Get the width for 'Hello World, again' at default size, 10px. + var width_normal = ctx.measureText('Hello World, again').width; + + ctx.wordSpacing = '1em'; + assert.strictEqual(ctx.wordSpacing, '1em', "ctx.wordSpacing", "'1em'") + // 1em = 10px. Add 10px after each word in "Hello World, again", + // makes it 20px longer. + var width_with_spacing = ctx.measureText('Hello World, again').width; + assert.strictEqual(width_with_spacing, width_normal + 20, "width_with_spacing", "width_normal + 20") + + // Changing font to 20px. Without resetting the spacing, 1em wordSpacing + // is now 20px, so it's suppose to be 40px longer without any wordSpacing set. + ctx.font = '20px serif'; + width_with_spacing = ctx.measureText('Hello World, again').width; + // Now calculate the reference spacing for "Hello World, again" with no spacing. + ctx.wordSpacing = '0em'; + width_normal = ctx.measureText('Hello World, again').width; + assert.strictEqual(width_with_spacing, width_normal + 40, "width_with_spacing", "width_normal + 40") + }); + + it("2d.text.drawing.style.fontKerning", function () { + // Testing basic functionalities of fontKerning for canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.fontKerning, "auto", "ctx.fontKerning", "\"auto\"") + ctx.fontKerning = "normal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + width_normal = ctx.measureText("TAWATAVA").width; + ctx.fontKerning = "none"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + width_none = ctx.measureText("TAWATAVA").width; + assert(width_normal < width_none, "width_normal < width_none"); + }); + + it("2d.text.drawing.style.fontKerning.with.uppercase", function () { + // Testing basic functionalities of fontKerning for canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.fontKerning, "auto", "ctx.fontKerning", "\"auto\"") + ctx.fontKerning = "Normal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "normal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "noRmal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NoRMal"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NORMAL"; + assert.strictEqual(ctx.fontKerning, "normal", "ctx.fontKerning", "\"normal\"") + + ctx.fontKerning = "None"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "none"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "nOne"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "nonE"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + ctx.fontKerning = "Auto"; + ctx.fontKerning = "NONE"; + assert.strictEqual(ctx.fontKerning, "none", "ctx.fontKerning", "\"none\"") + }); + + it("2d.text.drawing.style.fontVariant.settings", function () { + // Testing basic functionalities of fontKerning for canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Setting fontVariantCaps with lower cases + assert.strictEqual(ctx.fontVariantCaps, "normal", "ctx.fontVariantCaps", "\"normal\"") + + ctx.fontVariantCaps = "normal"; + assert.strictEqual(ctx.fontVariantCaps, "normal", "ctx.fontVariantCaps", "\"normal\"") + + ctx.fontVariantCaps = "small-caps"; + assert.strictEqual(ctx.fontVariantCaps, "small-caps", "ctx.fontVariantCaps", "\"small-caps\"") + + ctx.fontVariantCaps = "all-small-caps"; + assert.strictEqual(ctx.fontVariantCaps, "all-small-caps", "ctx.fontVariantCaps", "\"all-small-caps\"") + + ctx.fontVariantCaps = "petite-caps"; + assert.strictEqual(ctx.fontVariantCaps, "petite-caps", "ctx.fontVariantCaps", "\"petite-caps\"") + + ctx.fontVariantCaps = "all-petite-caps"; + assert.strictEqual(ctx.fontVariantCaps, "all-petite-caps", "ctx.fontVariantCaps", "\"all-petite-caps\"") + + ctx.fontVariantCaps = "unicase"; + assert.strictEqual(ctx.fontVariantCaps, "unicase", "ctx.fontVariantCaps", "\"unicase\"") + + ctx.fontVariantCaps = "titling-caps"; + assert.strictEqual(ctx.fontVariantCaps, "titling-caps", "ctx.fontVariantCaps", "\"titling-caps\"") + + // Setting fontVariantCaps with lower cases and upper cases word. + ctx.fontVariantCaps = "nORmal"; + assert.strictEqual(ctx.fontVariantCaps, "normal", "ctx.fontVariantCaps", "\"normal\"") + + ctx.fontVariantCaps = "smaLL-caps"; + assert.strictEqual(ctx.fontVariantCaps, "small-caps", "ctx.fontVariantCaps", "\"small-caps\"") + + ctx.fontVariantCaps = "all-small-CAPS"; + assert.strictEqual(ctx.fontVariantCaps, "all-small-caps", "ctx.fontVariantCaps", "\"all-small-caps\"") + + ctx.fontVariantCaps = "pEtitE-caps"; + assert.strictEqual(ctx.fontVariantCaps, "petite-caps", "ctx.fontVariantCaps", "\"petite-caps\"") + + ctx.fontVariantCaps = "All-Petite-Caps"; + assert.strictEqual(ctx.fontVariantCaps, "all-petite-caps", "ctx.fontVariantCaps", "\"all-petite-caps\"") + + ctx.fontVariantCaps = "uNIcase"; + assert.strictEqual(ctx.fontVariantCaps, "unicase", "ctx.fontVariantCaps", "\"unicase\"") + + ctx.fontVariantCaps = "titling-CAPS"; + assert.strictEqual(ctx.fontVariantCaps, "titling-caps", "ctx.fontVariantCaps", "\"titling-caps\"") + + // Setting fontVariantCaps with non-existing font variant. + ctx.fontVariantCaps = "abcd"; + assert.strictEqual(ctx.fontVariantCaps, "titling-caps", "ctx.fontVariantCaps", "\"titling-caps\"") + }); + + it("2d.text.drawing.style.textRendering.settings", function () { + // Testing basic functionalities of textRendering in Canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Setting textRendering with lower cases + assert.strictEqual(ctx.textRendering, "auto", "ctx.textRendering", "\"auto\"") + + ctx.textRendering = "auto"; + assert.strictEqual(ctx.textRendering, "auto", "ctx.textRendering", "\"auto\"") + + ctx.textRendering = "optimizespeed"; + assert.strictEqual(ctx.textRendering, "optimizeSpeed", "ctx.textRendering", "\"optimizeSpeed\"") + + ctx.textRendering = "optimizelegibility"; + assert.strictEqual(ctx.textRendering, "optimizeLegibility", "ctx.textRendering", "\"optimizeLegibility\"") + + ctx.textRendering = "geometricprecision"; + assert.strictEqual(ctx.textRendering, "geometricPrecision", "ctx.textRendering", "\"geometricPrecision\"") + + // Setting textRendering with lower cases and upper cases word. + ctx.textRendering = "aUto"; + assert.strictEqual(ctx.textRendering, "auto", "ctx.textRendering", "\"auto\"") + + ctx.textRendering = "OPtimizeSpeed"; + assert.strictEqual(ctx.textRendering, "optimizeSpeed", "ctx.textRendering", "\"optimizeSpeed\"") + + ctx.textRendering = "OPtimizELEgibility"; + assert.strictEqual(ctx.textRendering, "optimizeLegibility", "ctx.textRendering", "\"optimizeLegibility\"") + + ctx.textRendering = "GeometricPrecision"; + assert.strictEqual(ctx.textRendering, "geometricPrecision", "ctx.textRendering", "\"geometricPrecision\"") + + // Setting textRendering with non-existing font variant. + ctx.textRendering = "abcd"; + assert.strictEqual(ctx.textRendering, "geometricPrecision", "ctx.textRendering", "\"geometricPrecision\"") + }); +}); diff --git a/test/wpt/generated/line-styles.js b/test/wpt/generated/line-styles.js new file mode 100644 index 000000000..815b3dc19 --- /dev/null +++ b/test/wpt/generated/line-styles.js @@ -0,0 +1,1136 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: line-styles", function () { + + it("2d.line.defaults", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.lineWidth, 1, "ctx.lineWidth", "1") + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + assert.strictEqual(ctx.lineJoin, 'miter', "ctx.lineJoin", "'miter'") + assert.strictEqual(ctx.miterLimit, 10, "ctx.miterLimit", "10") + }); + + it("2d.line.width.basic", function () { + // lineWidth determines the width of line strokes + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 20; + // Draw a green line over a red box, to check the line is not too small + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + // Draw a green box over a red line, to check the line is not too large + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 15, 20, 20); + + _assertPixel(canvas, 14,25, 0,255,0,255); + _assertPixel(canvas, 15,25, 0,255,0,255); + _assertPixel(canvas, 16,25, 0,255,0,255); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 34,25, 0,255,0,255); + _assertPixel(canvas, 35,25, 0,255,0,255); + _assertPixel(canvas, 36,25, 0,255,0,255); + + _assertPixel(canvas, 64,25, 0,255,0,255); + _assertPixel(canvas, 65,25, 0,255,0,255); + _assertPixel(canvas, 66,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + _assertPixel(canvas, 84,25, 0,255,0,255); + _assertPixel(canvas, 85,25, 0,255,0,255); + _assertPixel(canvas, 86,25, 0,255,0,255); + }); + + it("2d.line.width.transformed", function () { + // Line stroke widths are affected by scale transformations + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 4; + // Draw a green line over a red box, to check the line is not too small + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.save(); + ctx.scale(5, 1); + ctx.beginPath(); + ctx.moveTo(5, 15); + ctx.lineTo(5, 35); + ctx.stroke(); + ctx.restore(); + + // Draw a green box over a red line, to check the line is not too large + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.save(); + ctx.scale(-5, 1); + ctx.beginPath(); + ctx.moveTo(-15, 15); + ctx.lineTo(-15, 35); + ctx.stroke(); + ctx.restore(); + ctx.fillRect(65, 15, 20, 20); + + _assertPixel(canvas, 14,25, 0,255,0,255); + _assertPixel(canvas, 15,25, 0,255,0,255); + _assertPixel(canvas, 16,25, 0,255,0,255); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 34,25, 0,255,0,255); + _assertPixel(canvas, 35,25, 0,255,0,255); + _assertPixel(canvas, 36,25, 0,255,0,255); + + _assertPixel(canvas, 64,25, 0,255,0,255); + _assertPixel(canvas, 65,25, 0,255,0,255); + _assertPixel(canvas, 66,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + _assertPixel(canvas, 84,25, 0,255,0,255); + _assertPixel(canvas, 85,25, 0,255,0,255); + _assertPixel(canvas, 86,25, 0,255,0,255); + }); + + it("2d.line.width.scaledefault", function () { + // Default lineWidth strokes are affected by scale transformations + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(50, 50); + ctx.strokeStyle = '#0f0'; + ctx.moveTo(0, 0.5); + ctx.lineTo(2, 0.5); + ctx.stroke(); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + _assertPixel(canvas, 50,5, 0,255,0,255); + _assertPixel(canvas, 50,45, 0,255,0,255); + }); + + it("2d.line.width.valid", function () { + // Setting lineWidth to valid values works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineWidth = 1.5; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = "1e1"; + assert.strictEqual(ctx.lineWidth, 10, "ctx.lineWidth", "10") + + ctx.lineWidth = 1/1024; + assert.strictEqual(ctx.lineWidth, 1/1024, "ctx.lineWidth", "1/1024") + + ctx.lineWidth = 1000; + assert.strictEqual(ctx.lineWidth, 1000, "ctx.lineWidth", "1000") + }); + + it("2d.line.width.invalid", function () { + // Setting lineWidth to invalid values is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineWidth = 1.5; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = 0; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = -1; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = Infinity; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = -Infinity; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = NaN; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = 'string'; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + + ctx.lineWidth = 1.5; + ctx.lineWidth = true; + assert.strictEqual(ctx.lineWidth, 1, "ctx.lineWidth", "1") + + ctx.lineWidth = 1.5; + ctx.lineWidth = false; + assert.strictEqual(ctx.lineWidth, 1.5, "ctx.lineWidth", "1.5") + }); + + it("2d.line.cap.butt", function () { + // lineCap 'butt' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineCap = 'butt'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 15, 20, 20); + + _assertPixel(canvas, 25,14, 0,255,0,255); + _assertPixel(canvas, 25,15, 0,255,0,255); + _assertPixel(canvas, 25,16, 0,255,0,255); + _assertPixel(canvas, 25,34, 0,255,0,255); + _assertPixel(canvas, 25,35, 0,255,0,255); + _assertPixel(canvas, 25,36, 0,255,0,255); + + _assertPixel(canvas, 75,14, 0,255,0,255); + _assertPixel(canvas, 75,15, 0,255,0,255); + _assertPixel(canvas, 75,16, 0,255,0,255); + _assertPixel(canvas, 75,34, 0,255,0,255); + _assertPixel(canvas, 75,35, 0,255,0,255); + _assertPixel(canvas, 75,36, 0,255,0,255); + }); + + it("2d.line.cap.round", function () { + // lineCap 'round' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineCap = 'round'; + ctx.lineWidth = 20; + + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.beginPath(); + ctx.moveTo(35-tol, 15); + ctx.arc(25, 15, 10-tol, 0, Math.PI, true); + ctx.arc(25, 35, 10-tol, Math.PI, 0, true); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(85+tol, 15); + ctx.arc(75, 15, 10+tol, 0, Math.PI, true); + ctx.arc(75, 35, 10+tol, Math.PI, 0, true); + ctx.fill(); + + _assertPixel(canvas, 17,6, 0,255,0,255); + _assertPixel(canvas, 25,6, 0,255,0,255); + _assertPixel(canvas, 32,6, 0,255,0,255); + _assertPixel(canvas, 17,43, 0,255,0,255); + _assertPixel(canvas, 25,43, 0,255,0,255); + _assertPixel(canvas, 32,43, 0,255,0,255); + + _assertPixel(canvas, 67,6, 0,255,0,255); + _assertPixel(canvas, 75,6, 0,255,0,255); + _assertPixel(canvas, 82,6, 0,255,0,255); + _assertPixel(canvas, 67,43, 0,255,0,255); + _assertPixel(canvas, 75,43, 0,255,0,255); + _assertPixel(canvas, 82,43, 0,255,0,255); + }); + + it("2d.line.cap.square", function () { + // lineCap 'square' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineCap = 'square'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 5, 20, 40); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 5, 20, 40); + + _assertPixel(canvas, 25,4, 0,255,0,255); + _assertPixel(canvas, 25,5, 0,255,0,255); + _assertPixel(canvas, 25,6, 0,255,0,255); + _assertPixel(canvas, 25,44, 0,255,0,255); + _assertPixel(canvas, 25,45, 0,255,0,255); + _assertPixel(canvas, 25,46, 0,255,0,255); + + _assertPixel(canvas, 75,4, 0,255,0,255); + _assertPixel(canvas, 75,5, 0,255,0,255); + _assertPixel(canvas, 75,6, 0,255,0,255); + _assertPixel(canvas, 75,44, 0,255,0,255); + _assertPixel(canvas, 75,45, 0,255,0,255); + _assertPixel(canvas, 75,46, 0,255,0,255); + }); + + it("2d.line.cap.open", function () { + // Line caps are drawn at the corners of an unclosed rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'bevel'; + ctx.lineCap = 'square'; + ctx.lineWidth = 400; + + ctx.beginPath(); + ctx.moveTo(200, 200); + ctx.lineTo(200, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 200); + ctx.lineTo(200, 200); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.cap.closed", function () { + // Line caps are not drawn at the corners of an unclosed rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'bevel'; + ctx.lineCap = 'square'; + ctx.lineWidth = 400; + + ctx.beginPath(); + ctx.moveTo(200, 200); + ctx.lineTo(200, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 200); + ctx.closePath(); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.cap.valid", function () { + // Setting lineCap to valid values works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineCap = 'butt' + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'round'; + assert.strictEqual(ctx.lineCap, 'round', "ctx.lineCap", "'round'") + + ctx.lineCap = 'square'; + assert.strictEqual(ctx.lineCap, 'square', "ctx.lineCap", "'square'") + }); + + it("2d.line.cap.invalid", function () { + // Setting lineCap to invalid values is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineCap = 'butt' + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'invalid'; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'ROUND'; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'round\0'; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'round '; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = ""; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + + ctx.lineCap = 'butt'; + ctx.lineCap = 'bevel'; + assert.strictEqual(ctx.lineCap, 'butt', "ctx.lineCap", "'butt'") + }); + + it("2d.line.join.bevel", function () { + // lineJoin 'bevel' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineJoin = 'bevel'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 20, 20); + ctx.fillRect(20, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(30, 20); + ctx.lineTo(40-tol, 20); + ctx.lineTo(30, 10+tol); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 20, 20); + ctx.fillRect(70, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(80, 20); + ctx.lineTo(90+tol, 20); + ctx.lineTo(80, 10-tol); + ctx.fill(); + + _assertPixel(canvas, 34,16, 0,255,0,255); + _assertPixel(canvas, 34,15, 0,255,0,255); + _assertPixel(canvas, 35,15, 0,255,0,255); + _assertPixel(canvas, 36,15, 0,255,0,255); + _assertPixel(canvas, 36,14, 0,255,0,255); + + _assertPixel(canvas, 84,16, 0,255,0,255); + _assertPixel(canvas, 84,15, 0,255,0,255); + _assertPixel(canvas, 85,15, 0,255,0,255); + _assertPixel(canvas, 86,15, 0,255,0,255); + _assertPixel(canvas, 86,14, 0,255,0,255); + }); + + it("2d.line.join.round", function () { + // lineJoin 'round' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineJoin = 'round'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 20, 20); + ctx.fillRect(20, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(30, 20); + ctx.arc(30, 20, 10-tol, 0, 2*Math.PI, true); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 20, 20); + ctx.fillRect(70, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(80, 20); + ctx.arc(80, 20, 10+tol, 0, 2*Math.PI, true); + ctx.fill(); + + _assertPixel(canvas, 36,14, 0,255,0,255); + _assertPixel(canvas, 36,13, 0,255,0,255); + _assertPixel(canvas, 37,13, 0,255,0,255); + _assertPixel(canvas, 38,13, 0,255,0,255); + _assertPixel(canvas, 38,12, 0,255,0,255); + + _assertPixel(canvas, 86,14, 0,255,0,255); + _assertPixel(canvas, 86,13, 0,255,0,255); + _assertPixel(canvas, 87,13, 0,255,0,255); + _assertPixel(canvas, 88,13, 0,255,0,255); + _assertPixel(canvas, 88,12, 0,255,0,255); + }); + + it("2d.line.join.miter", function () { + // lineJoin 'miter' is rendered correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 30, 20); + ctx.fillRect(20, 10, 20, 30); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 30, 20); + ctx.fillRect(70, 10, 20, 30); + + _assertPixel(canvas, 38,12, 0,255,0,255); + _assertPixel(canvas, 39,11, 0,255,0,255); + _assertPixel(canvas, 40,10, 0,255,0,255); + _assertPixel(canvas, 41,9, 0,255,0,255); + _assertPixel(canvas, 42,8, 0,255,0,255); + + _assertPixel(canvas, 88,12, 0,255,0,255); + _assertPixel(canvas, 89,11, 0,255,0,255); + _assertPixel(canvas, 90,10, 0,255,0,255); + _assertPixel(canvas, 91,9, 0,255,0,255); + _assertPixel(canvas, 92,8, 0,255,0,255); + }); + + it("2d.line.join.open", function () { + // Line joins are not drawn at the corner of an unclosed rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 200; + + ctx.beginPath(); + ctx.moveTo(100, 50); + ctx.lineTo(100, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 50); + ctx.lineTo(100, 50); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.join.closed", function () { + // Line joins are drawn at the corner of a closed rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 200; + + ctx.beginPath(); + ctx.moveTo(100, 50); + ctx.lineTo(100, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 50); + ctx.closePath(); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.join.parallel", function () { + // Line joins are drawn at 180-degree joins + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 300; + ctx.lineJoin = 'round'; + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.lineTo(0, 25); + ctx.lineTo(-100, 25); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.join.valid", function () { + // Setting lineJoin to valid values works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineJoin = 'bevel' + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'round'; + assert.strictEqual(ctx.lineJoin, 'round', "ctx.lineJoin", "'round'") + + ctx.lineJoin = 'miter'; + assert.strictEqual(ctx.lineJoin, 'miter', "ctx.lineJoin", "'miter'") + }); + + it("2d.line.join.invalid", function () { + // Setting lineJoin to invalid values is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineJoin = 'bevel' + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'invalid'; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'ROUND'; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'round\0'; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'round '; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = ""; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'butt'; + assert.strictEqual(ctx.lineJoin, 'bevel', "ctx.lineJoin", "'bevel'") + }); + + it("2d.line.miter.exceeded", function () { + // Miter joins are not drawn when the miter limit is exceeded + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 201); // slightly non-right-angle to avoid being a special case + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.acute", function () { + // Miter joins are drawn correctly with acute angles + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 2.614; + ctx.beginPath(); + ctx.moveTo(100, 1000); + ctx.lineTo(100, 100); + ctx.lineTo(1000, 1000); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 2.613; + ctx.beginPath(); + ctx.moveTo(100, 1000); + ctx.lineTo(100, 100); + ctx.lineTo(1000, 1000); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.obtuse", function () { + // Miter joins are drawn correctly with obtuse angles + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 1600; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 1.083; + ctx.beginPath(); + ctx.moveTo(800, 10000); + ctx.lineTo(800, 300); + ctx.lineTo(10000, -8900); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.082; + ctx.beginPath(); + ctx.moveTo(800, 10000); + ctx.lineTo(800, 300); + ctx.lineTo(10000, -8900); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.rightangle", function () { + // Miter joins are not drawn when the miter limit is exceeded, on exact right angles + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 200); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.lineedge", function () { + // Miter joins are not drawn when the miter limit is exceeded at the corners of a zero-height rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.strokeRect(100, 25, 200, 0); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.within", function () { + // Miter joins are drawn when the miter limit is not quite exceeded + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 1.416; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 201); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.miter.valid", function () { + // Setting miterLimit to valid values works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.miterLimit = 1.5; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = "1e1"; + assert.strictEqual(ctx.miterLimit, 10, "ctx.miterLimit", "10") + + ctx.miterLimit = 1/1024; + assert.strictEqual(ctx.miterLimit, 1/1024, "ctx.miterLimit", "1/1024") + + ctx.miterLimit = 1000; + assert.strictEqual(ctx.miterLimit, 1000, "ctx.miterLimit", "1000") + }); + + it("2d.line.miter.invalid", function () { + // Setting miterLimit to invalid values is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.miterLimit = 1.5; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = 0; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = -1; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = Infinity; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = -Infinity; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = NaN; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = 'string'; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + + ctx.miterLimit = 1.5; + ctx.miterLimit = true; + assert.strictEqual(ctx.miterLimit, 1, "ctx.miterLimit", "1") + + ctx.miterLimit = 1.5; + ctx.miterLimit = false; + assert.strictEqual(ctx.miterLimit, 1.5, "ctx.miterLimit", "1.5") + }); + + it("2d.line.cross", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'bevel'; + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(110, 50); + ctx.lineTo(110, 60); + ctx.lineTo(100, 60); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.line.union", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 24); + ctx.lineTo(100, 25); + ctx.lineTo(0, 26); + ctx.closePath(); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 25,1, 0,255,0,255); + _assertPixel(canvas, 48,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 25,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + }); + + it("2d.line.invalid.strokestyle", function () { + // Verify correct behavior of canvas on an invalid strokeStyle() + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.strokeStyle = 'rgb(0, 255, 0)'; + ctx.strokeStyle = 'nonsense'; + ctx.lineWidth = 200; + ctx.moveTo(0,100); + ctx.lineTo(200,100); + ctx.stroke(); + var imageData = ctx.getImageData(0, 0, 200, 200); + var imgdata = imageData.data; + assert(imgdata[4] == 0, "imgdata[\""+(4)+"\"] == 0"); + assert(imgdata[5] == 255, "imgdata[\""+(5)+"\"] == 255"); + assert(imgdata[6] == 0, "imgdata[\""+(6)+"\"] == 0"); + }); +}); diff --git a/test/wpt/generated/meta.js b/test/wpt/generated/meta.js new file mode 100644 index 000000000..9e15857b3 --- /dev/null +++ b/test/wpt/generated/meta.js @@ -0,0 +1,92 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: meta", function () { +}); diff --git a/test/wpt/generated/path-objects.js b/test/wpt/generated/path-objects.js new file mode 100644 index 000000000..397ec76f6 --- /dev/null +++ b/test/wpt/generated/path-objects.js @@ -0,0 +1,4352 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: path-objects", function () { + + it("2d.path.initial", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.closePath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.beginPath", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.rect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.moveTo.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.rect(0, 0, 10, 50); + ctx.moveTo(100, 0); + ctx.lineTo(10, 0); + ctx.lineTo(10, 50); + ctx.lineTo(100, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 90,25, 0,255,0,255); + }); + + it("2d.path.moveTo.newsubpath", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.moveTo(100, 0); + ctx.moveTo(100, 50); + ctx.moveTo(0, 50); + ctx.fillStyle = '#f00'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.moveTo.multiple", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.moveTo(0, 25); + ctx.moveTo(100, 25); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.moveTo.nonfinite", function () { + // moveTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.moveTo(Infinity, 50); + ctx.moveTo(-Infinity, 50); + ctx.moveTo(NaN, 50); + ctx.moveTo(0, Infinity); + ctx.moveTo(0, -Infinity); + ctx.moveTo(0, NaN); + ctx.moveTo(Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.closePath.empty", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.closePath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.closePath.newline", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -100); + ctx.lineTo(200, -100); + ctx.lineTo(200, 25); + ctx.closePath(); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.closePath.nextpoint", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -1000); + ctx.closePath(); + ctx.lineTo(1000, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.ensuresubpath.1", function () { + // If there is no subpath, the point is added and nothing is drawn + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.lineTo(100, 50); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.ensuresubpath.2", function () { + // If there is no subpath, the point is added and used for subsequent drawing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.lineTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.nextpoint", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(-100, -100); + ctx.lineTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.lineTo.nonfinite", function () { + // lineTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(Infinity, 50); + ctx.lineTo(-Infinity, 50); + ctx.lineTo(NaN, 50); + ctx.lineTo(0, Infinity); + ctx.lineTo(0, -Infinity); + ctx.lineTo(0, NaN); + ctx.lineTo(Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.lineTo.nonfinite.details", function () { + // lineTo() with Infinity/NaN for first arg still converts the second arg + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + for (var arg1 of [Infinity, -Infinity, NaN]) { + var converted = false; + ctx.lineTo(arg1, { valueOf: function() { converted = true; return 0; } }); + assert(converted, "converted"); + } + }); + + it("2d.path.quadraticCurveTo.ensuresubpath.1", function () { + // If there is no subpath, the first control point is added (and nothing is drawn up to it) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.quadraticCurveTo(100, 50, 200, 50); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 95,45, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.ensuresubpath.2", function () { + // If there is no subpath, the first control point is added + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.quadraticCurveTo(0, 25, 100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 5,45, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.quadraticCurveTo(100, 25, 100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.shape", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 55; + ctx.beginPath(); + ctx.moveTo(-1000, 1050); + ctx.quadraticCurveTo(0, -1000, 1200, 1050); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.scaled", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(1000, 1000); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 0.055; + ctx.beginPath(); + ctx.moveTo(-1, 1.05); + ctx.quadraticCurveTo(0, -1, 1.2, 1.05); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.quadraticCurveTo.nonfinite", function () { + // quadraticCurveTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.quadraticCurveTo(Infinity, 50, 0, 50); + ctx.quadraticCurveTo(-Infinity, 50, 0, 50); + ctx.quadraticCurveTo(NaN, 50, 0, 50); + ctx.quadraticCurveTo(0, Infinity, 0, 50); + ctx.quadraticCurveTo(0, -Infinity, 0, 50); + ctx.quadraticCurveTo(0, NaN, 0, 50); + ctx.quadraticCurveTo(0, 50, Infinity, 50); + ctx.quadraticCurveTo(0, 50, -Infinity, 50); + ctx.quadraticCurveTo(0, 50, NaN, 50); + ctx.quadraticCurveTo(0, 50, 0, Infinity); + ctx.quadraticCurveTo(0, 50, 0, -Infinity); + ctx.quadraticCurveTo(0, 50, 0, NaN); + ctx.quadraticCurveTo(Infinity, Infinity, 0, 50); + ctx.quadraticCurveTo(Infinity, Infinity, Infinity, 50); + ctx.quadraticCurveTo(Infinity, Infinity, Infinity, Infinity); + ctx.quadraticCurveTo(Infinity, Infinity, 0, Infinity); + ctx.quadraticCurveTo(Infinity, 50, Infinity, 50); + ctx.quadraticCurveTo(Infinity, 50, Infinity, Infinity); + ctx.quadraticCurveTo(Infinity, 50, 0, Infinity); + ctx.quadraticCurveTo(0, Infinity, Infinity, 50); + ctx.quadraticCurveTo(0, Infinity, Infinity, Infinity); + ctx.quadraticCurveTo(0, Infinity, 0, Infinity); + ctx.quadraticCurveTo(0, 50, Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.ensuresubpath.1", function () { + // If there is no subpath, the first control point is added (and nothing is drawn up to it) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.bezierCurveTo(100, 50, 200, 50, 200, 50); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 95,45, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.ensuresubpath.2", function () { + // If there is no subpath, the first control point is added + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.bezierCurveTo(0, 25, 100, 25, 100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 5,45, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.bezierCurveTo(100, 25, 100, 25, 100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.shape", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 55; + ctx.beginPath(); + ctx.moveTo(-2000, 3100); + ctx.bezierCurveTo(-2000, -1000, 2100, -1000, 2100, 3100); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.scaled", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(1000, 1000); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 0.055; + ctx.beginPath(); + ctx.moveTo(-2, 3.1); + ctx.bezierCurveTo(-2, -1, 2.1, -1, 2.1, 3.1); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.bezierCurveTo.nonfinite", function () { + // bezierCurveTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.bezierCurveTo(Infinity, 50, 0, 50, 0, 50); + ctx.bezierCurveTo(-Infinity, 50, 0, 50, 0, 50); + ctx.bezierCurveTo(NaN, 50, 0, 50, 0, 50); + ctx.bezierCurveTo(0, Infinity, 0, 50, 0, 50); + ctx.bezierCurveTo(0, -Infinity, 0, 50, 0, 50); + ctx.bezierCurveTo(0, NaN, 0, 50, 0, 50); + ctx.bezierCurveTo(0, 50, Infinity, 50, 0, 50); + ctx.bezierCurveTo(0, 50, -Infinity, 50, 0, 50); + ctx.bezierCurveTo(0, 50, NaN, 50, 0, 50); + ctx.bezierCurveTo(0, 50, 0, Infinity, 0, 50); + ctx.bezierCurveTo(0, 50, 0, -Infinity, 0, 50); + ctx.bezierCurveTo(0, 50, 0, NaN, 0, 50); + ctx.bezierCurveTo(0, 50, 0, 50, Infinity, 50); + ctx.bezierCurveTo(0, 50, 0, 50, -Infinity, 50); + ctx.bezierCurveTo(0, 50, 0, 50, NaN, 50); + ctx.bezierCurveTo(0, 50, 0, 50, 0, Infinity); + ctx.bezierCurveTo(0, 50, 0, 50, 0, -Infinity); + ctx.bezierCurveTo(0, 50, 0, 50, 0, NaN); + ctx.bezierCurveTo(Infinity, Infinity, 0, 50, 0, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, 50, 0, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, Infinity, 0, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, Infinity, Infinity, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, Infinity, 0, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, 50, Infinity, 50); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, 50, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, Infinity, 50, 0, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, 0, Infinity, 0, 50); + ctx.bezierCurveTo(Infinity, Infinity, 0, Infinity, Infinity, 50); + ctx.bezierCurveTo(Infinity, Infinity, 0, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, 0, Infinity, 0, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, 0, 50, Infinity, 50); + ctx.bezierCurveTo(Infinity, Infinity, 0, 50, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, Infinity, 0, 50, 0, Infinity); + ctx.bezierCurveTo(Infinity, 50, Infinity, 50, 0, 50); + ctx.bezierCurveTo(Infinity, 50, Infinity, Infinity, 0, 50); + ctx.bezierCurveTo(Infinity, 50, Infinity, Infinity, Infinity, 50); + ctx.bezierCurveTo(Infinity, 50, Infinity, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, 50, Infinity, Infinity, 0, Infinity); + ctx.bezierCurveTo(Infinity, 50, Infinity, 50, Infinity, 50); + ctx.bezierCurveTo(Infinity, 50, Infinity, 50, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, 50, Infinity, 50, 0, Infinity); + ctx.bezierCurveTo(Infinity, 50, 0, Infinity, 0, 50); + ctx.bezierCurveTo(Infinity, 50, 0, Infinity, Infinity, 50); + ctx.bezierCurveTo(Infinity, 50, 0, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, 50, 0, Infinity, 0, Infinity); + ctx.bezierCurveTo(Infinity, 50, 0, 50, Infinity, 50); + ctx.bezierCurveTo(Infinity, 50, 0, 50, Infinity, Infinity); + ctx.bezierCurveTo(Infinity, 50, 0, 50, 0, Infinity); + ctx.bezierCurveTo(0, Infinity, Infinity, 50, 0, 50); + ctx.bezierCurveTo(0, Infinity, Infinity, Infinity, 0, 50); + ctx.bezierCurveTo(0, Infinity, Infinity, Infinity, Infinity, 50); + ctx.bezierCurveTo(0, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(0, Infinity, Infinity, Infinity, 0, Infinity); + ctx.bezierCurveTo(0, Infinity, Infinity, 50, Infinity, 50); + ctx.bezierCurveTo(0, Infinity, Infinity, 50, Infinity, Infinity); + ctx.bezierCurveTo(0, Infinity, Infinity, 50, 0, Infinity); + ctx.bezierCurveTo(0, Infinity, 0, Infinity, 0, 50); + ctx.bezierCurveTo(0, Infinity, 0, Infinity, Infinity, 50); + ctx.bezierCurveTo(0, Infinity, 0, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(0, Infinity, 0, Infinity, 0, Infinity); + ctx.bezierCurveTo(0, Infinity, 0, 50, Infinity, 50); + ctx.bezierCurveTo(0, Infinity, 0, 50, Infinity, Infinity); + ctx.bezierCurveTo(0, Infinity, 0, 50, 0, Infinity); + ctx.bezierCurveTo(0, 50, Infinity, Infinity, 0, 50); + ctx.bezierCurveTo(0, 50, Infinity, Infinity, Infinity, 50); + ctx.bezierCurveTo(0, 50, Infinity, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(0, 50, Infinity, Infinity, 0, Infinity); + ctx.bezierCurveTo(0, 50, Infinity, 50, Infinity, 50); + ctx.bezierCurveTo(0, 50, Infinity, 50, Infinity, Infinity); + ctx.bezierCurveTo(0, 50, Infinity, 50, 0, Infinity); + ctx.bezierCurveTo(0, 50, 0, Infinity, Infinity, 50); + ctx.bezierCurveTo(0, 50, 0, Infinity, Infinity, Infinity); + ctx.bezierCurveTo(0, 50, 0, Infinity, 0, Infinity); + ctx.bezierCurveTo(0, 50, 0, 50, Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.arcTo.ensuresubpath.1", function () { + // If there is no subpath, the first control point is added (and nothing is drawn up to it) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arcTo(100, 50, 200, 50, 0.1); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.ensuresubpath.2", function () { + // If there is no subpath, the first control point is added + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arcTo(0, 25, 50, 250, 0.1); // adds (x1,y1), draws nothing + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.coincide.1", function () { + // arcTo() has no effect if P0 = P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(0, 25, 50, 1000, 1); + ctx.lineTo(100, 25); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arcTo(50, 25, 100, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,1, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 50,48, 0,255,0,255); + }); + + it("2d.path.arcTo.coincide.2", function () { + // arcTo() draws a straight line to P1 if P1 = P2 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 100, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.collinear.1", function () { + // arcTo() with all points on a line, and P1 between P0/P2, draws a straight line to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 200, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.arcTo(0, 25, 100, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.collinear.2", function () { + // arcTo() with all points on a line, and P2 between P0/P1, draws a straight line to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 10, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 110, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.collinear.3", function () { + // arcTo() with all points on a line, and P0 between P1/P2, draws a straight line to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, -100, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 0, 25, 1); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.arcTo(0, 25, -200, 25, 1); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.shape.curve1", function () { + // arcTo() curves in the right kind of shape + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var tol = 1.5; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.moveTo(10, 25); + ctx.arcTo(75, 25, 75, 60, 20); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.rect(10, 20, 45, 10); + ctx.moveTo(80, 45); + ctx.arc(55, 45, 25+tol, 0, -Math.PI/2, true); + ctx.arc(55, 45, 15-tol, -Math.PI/2, 0, false); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 55,19, 0,255,0,255); + _assertPixel(canvas, 55,20, 0,255,0,255); + _assertPixel(canvas, 55,21, 0,255,0,255); + _assertPixel(canvas, 64,22, 0,255,0,255); + _assertPixel(canvas, 65,21, 0,255,0,255); + _assertPixel(canvas, 72,28, 0,255,0,255); + _assertPixel(canvas, 73,27, 0,255,0,255); + _assertPixel(canvas, 78,36, 0,255,0,255); + _assertPixel(canvas, 79,35, 0,255,0,255); + _assertPixel(canvas, 80,44, 0,255,0,255); + _assertPixel(canvas, 80,45, 0,255,0,255); + _assertPixel(canvas, 80,46, 0,255,0,255); + _assertPixel(canvas, 65,45, 0,255,0,255); + }); + + it("2d.path.arcTo.shape.curve2", function () { + // arcTo() curves in the right kind of shape + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var tol = 1.5; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.rect(10, 20, 45, 10); + ctx.moveTo(80, 45); + ctx.arc(55, 45, 25-tol, 0, -Math.PI/2, true); + ctx.arc(55, 45, 15+tol, -Math.PI/2, 0, false); + ctx.fill(); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.moveTo(10, 25); + ctx.arcTo(75, 25, 75, 60, 20); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 55,19, 0,255,0,255); + _assertPixel(canvas, 55,20, 0,255,0,255); + _assertPixel(canvas, 55,21, 0,255,0,255); + _assertPixel(canvas, 64,22, 0,255,0,255); + _assertPixel(canvas, 65,21, 0,255,0,255); + _assertPixel(canvas, 72,28, 0,255,0,255); + _assertPixel(canvas, 73,27, 0,255,0,255); + _assertPixel(canvas, 78,36, 0,255,0,255); + _assertPixel(canvas, 79,35, 0,255,0,255); + _assertPixel(canvas, 80,44, 0,255,0,255); + _assertPixel(canvas, 80,45, 0,255,0,255); + _assertPixel(canvas, 80,46, 0,255,0,255); + }); + + it("2d.path.arcTo.shape.start", function () { + // arcTo() draws a straight line from P0 to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(200, 25, 200, 50, 10); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arcTo.shape.end", function () { + // arcTo() does not draw anything from P1 to P2 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(-100, -100); + ctx.arcTo(-100, 25, 200, 25, 10); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arcTo.negative", function () { + // arcTo() with negative radius throws an exception + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.arcTo(0, 0, 0, 0, -1); }, /INDEX_SIZE_ERR/); + var path = new Path2D(); + assert.throws(function() { path.arcTo(10, 10, 20, 20, -5); }, /INDEX_SIZE_ERR/); + }); + + it("2d.path.arcTo.zero.1", function () { + // arcTo() with zero radius draws a straight line from P0 to P1 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 100, 100, 0); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(0, -25); + ctx.arcTo(50, -25, 50, 50, 0); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.zero.2", function () { + // arcTo() with zero radius draws a straight line from P0 to P1, even when all points are collinear + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, -100, 25, 0); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 50, 25, 0); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arcTo.transformation", function () { + // arcTo joins up to the last subpath point correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 50); + ctx.translate(100, 0); + ctx.arcTo(50, 50, 50, 0, 50); + ctx.lineTo(-100, 0); + ctx.fill(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.arcTo.scale", function () { + // arcTo scales the curve, not just the control points + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 50); + ctx.translate(100, 0); + ctx.scale(0.1, 1); + ctx.arcTo(50, 50, 50, 0, 50); + ctx.lineTo(-1000, 0); + ctx.fill(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.arcTo.nonfinite", function () { + // arcTo() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.arcTo(Infinity, 50, 0, 50, 0); + ctx.arcTo(-Infinity, 50, 0, 50, 0); + ctx.arcTo(NaN, 50, 0, 50, 0); + ctx.arcTo(0, Infinity, 0, 50, 0); + ctx.arcTo(0, -Infinity, 0, 50, 0); + ctx.arcTo(0, NaN, 0, 50, 0); + ctx.arcTo(0, 50, Infinity, 50, 0); + ctx.arcTo(0, 50, -Infinity, 50, 0); + ctx.arcTo(0, 50, NaN, 50, 0); + ctx.arcTo(0, 50, 0, Infinity, 0); + ctx.arcTo(0, 50, 0, -Infinity, 0); + ctx.arcTo(0, 50, 0, NaN, 0); + ctx.arcTo(0, 50, 0, 50, Infinity); + ctx.arcTo(0, 50, 0, 50, -Infinity); + ctx.arcTo(0, 50, 0, 50, NaN); + ctx.arcTo(Infinity, Infinity, 0, 50, 0); + ctx.arcTo(Infinity, Infinity, Infinity, 50, 0); + ctx.arcTo(Infinity, Infinity, Infinity, Infinity, 0); + ctx.arcTo(Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.arcTo(Infinity, Infinity, Infinity, 50, Infinity); + ctx.arcTo(Infinity, Infinity, 0, Infinity, 0); + ctx.arcTo(Infinity, Infinity, 0, Infinity, Infinity); + ctx.arcTo(Infinity, Infinity, 0, 50, Infinity); + ctx.arcTo(Infinity, 50, Infinity, 50, 0); + ctx.arcTo(Infinity, 50, Infinity, Infinity, 0); + ctx.arcTo(Infinity, 50, Infinity, Infinity, Infinity); + ctx.arcTo(Infinity, 50, Infinity, 50, Infinity); + ctx.arcTo(Infinity, 50, 0, Infinity, 0); + ctx.arcTo(Infinity, 50, 0, Infinity, Infinity); + ctx.arcTo(Infinity, 50, 0, 50, Infinity); + ctx.arcTo(0, Infinity, Infinity, 50, 0); + ctx.arcTo(0, Infinity, Infinity, Infinity, 0); + ctx.arcTo(0, Infinity, Infinity, Infinity, Infinity); + ctx.arcTo(0, Infinity, Infinity, 50, Infinity); + ctx.arcTo(0, Infinity, 0, Infinity, 0); + ctx.arcTo(0, Infinity, 0, Infinity, Infinity); + ctx.arcTo(0, Infinity, 0, 50, Infinity); + ctx.arcTo(0, 50, Infinity, Infinity, 0); + ctx.arcTo(0, 50, Infinity, Infinity, Infinity); + ctx.arcTo(0, 50, Infinity, 50, Infinity); + ctx.arcTo(0, 50, 0, Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.arc.empty", function () { + // arc() with an empty path does not draw a straight line to the start point + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(200, 25, 5, 0, 2*Math.PI, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.nonempty", function () { + // arc() with a non-empty path does draw a straight line to the start point + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arc(200, 25, 5, 0, 2*Math.PI, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.end", function () { + // arc() adds the end point of the arc to the subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(-100, 0); + ctx.arc(-100, 0, 25, -Math.PI/2, Math.PI/2, true); + ctx.lineTo(100, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.default", function () { + // arc() with missing last argument defaults to clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, -Math.PI, Math.PI/2); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.1", function () { + // arc() draws pi/2 .. -pi anticlockwise correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, Math.PI/2, -Math.PI, true); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.2", function () { + // arc() draws -3pi/2 .. -pi anticlockwise correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, -3*Math.PI/2, -Math.PI, true); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.3", function () { + // arc() wraps angles mod 2pi when anticlockwise and end > start+2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, (512+1/2)*Math.PI, (1024-1)*Math.PI, true); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.4", function () { + // arc() draws a full circle when clockwise and end > start+2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arc(50, 25, 60, (512+1/2)*Math.PI, (1024-1)*Math.PI, false); + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.angle.5", function () { + // arc() wraps angles mod 2pi when clockwise and start > end+2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, (1024-1)*Math.PI, (512+1/2)*Math.PI, false); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.angle.6", function () { + // arc() draws a full circle when anticlockwise and start > end+2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arc(50, 25, 60, (1024-1)*Math.PI, (512+1/2)*Math.PI, true); + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.zero.1", function () { + // arc() draws nothing when startAngle = endAngle and anticlockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 0, true); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.zero.2", function () { + // arc() draws nothing when startAngle = endAngle and clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 0, false); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.twopie.1", function () { + // arc() draws nothing when end = start + 2pi-e and anticlockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI - 1e-4, true); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.twopie.2", function () { + // arc() draws a full circle when end = start + 2pi-e and clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI - 1e-4, false); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.twopie.3", function () { + // arc() draws a full circle when end = start + 2pi+e and anticlockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI + 1e-4, true); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.twopie.4", function () { + // arc() draws nothing when end = start + 2pi+e and clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI + 1e-4, false); + ctx.stroke(); + _assertPixel(canvas, 50,20, 0,255,0,255); + }); + + it("2d.path.arc.shape.1", function () { + // arc() from 0 to pi does not draw anything in the wrong half + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI, false); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 20,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.shape.2", function () { + // arc() from 0 to pi draws stuff in the right half + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 100; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 20,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.shape.3", function () { + // arc() from 0 to -pi/2 does not draw anything in the wrong quadrant + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 100; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(0, 50, 50, 0, -Math.PI/2, false); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.shape.4", function () { + // arc() from 0 to -pi/2 draws stuff in the right quadrant + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 150; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(-50, 50, 100, 0, -Math.PI/2, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.shape.5", function () { + // arc() from 0 to 5pi does not draw crazy things + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 200; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(300, 0, 100, 0, 5*Math.PI, false); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.selfintersect.1", function () { + // arc() with lineWidth > 2*radius is drawn sensibly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 200; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(100, 50, 25, 0, -Math.PI/2, true); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(0, 0, 25, 0, -Math.PI/2, true); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.selfintersect.2", function () { + // arc() with lineWidth > 2*radius is drawn sensibly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 180; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(-50, 50, 25, 0, -Math.PI/2, true); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(100, 0, 25, 0, -Math.PI/2, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,10, 0,255,0,255); + _assertPixel(canvas, 97,1, 0,255,0,255); + _assertPixel(canvas, 97,2, 0,255,0,255); + _assertPixel(canvas, 97,3, 0,255,0,255); + _assertPixel(canvas, 2,48, 0,255,0,255); + }); + + it("2d.path.arc.negative", function () { + // arc() with negative radius throws INDEX_SIZE_ERR + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.arc(0, 0, -1, 0, 0, true); }, /INDEX_SIZE_ERR/); + var path = new Path2D(); + assert.throws(function() { path.arc(10, 10, -5, 0, 1, false); }, /INDEX_SIZE_ERR/); + }); + + it("2d.path.arc.zeroradius", function () { + // arc() with zero radius draws a line to the start point + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00' + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arc(200, 25, 0, 0, Math.PI, true); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.arc.scale.1", function () { + // Non-uniformly scaled arcs are the right shape + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(2, 0.5); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(25, 50, 56, 0, 2*Math.PI, false); + ctx.fill(); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(-25, 50); + ctx.arc(-25, 50, 24, 0, 2*Math.PI, false); + ctx.moveTo(75, 50); + ctx.arc(75, 50, 24, 0, 2*Math.PI, false); + ctx.moveTo(25, -25); + ctx.arc(25, -25, 24, 0, 2*Math.PI, false); + ctx.moveTo(25, 125); + ctx.arc(25, 125, 24, 0, 2*Math.PI, false); + ctx.fill(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.arc.scale.2", function () { + // Highly scaled arcs are the right shape + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(100, 100); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 1.2; + ctx.beginPath(); + ctx.arc(0, 0, 0.6, 0, Math.PI/2, false); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 50,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,25, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 50,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.arc.nonfinite", function () { + // arc() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.arc(Infinity, 0, 50, 0, 2*Math.PI, true); + ctx.arc(-Infinity, 0, 50, 0, 2*Math.PI, true); + ctx.arc(NaN, 0, 50, 0, 2*Math.PI, true); + ctx.arc(0, Infinity, 50, 0, 2*Math.PI, true); + ctx.arc(0, -Infinity, 50, 0, 2*Math.PI, true); + ctx.arc(0, NaN, 50, 0, 2*Math.PI, true); + ctx.arc(0, 0, Infinity, 0, 2*Math.PI, true); + ctx.arc(0, 0, -Infinity, 0, 2*Math.PI, true); + ctx.arc(0, 0, NaN, 0, 2*Math.PI, true); + ctx.arc(0, 0, 50, Infinity, 2*Math.PI, true); + ctx.arc(0, 0, 50, -Infinity, 2*Math.PI, true); + ctx.arc(0, 0, 50, NaN, 2*Math.PI, true); + ctx.arc(0, 0, 50, 0, Infinity, true); + ctx.arc(0, 0, 50, 0, -Infinity, true); + ctx.arc(0, 0, 50, 0, NaN, true); + ctx.arc(Infinity, Infinity, 50, 0, 2*Math.PI, true); + ctx.arc(Infinity, Infinity, Infinity, 0, 2*Math.PI, true); + ctx.arc(Infinity, Infinity, Infinity, Infinity, 2*Math.PI, true); + ctx.arc(Infinity, Infinity, Infinity, Infinity, Infinity, true); + ctx.arc(Infinity, Infinity, Infinity, 0, Infinity, true); + ctx.arc(Infinity, Infinity, 50, Infinity, 2*Math.PI, true); + ctx.arc(Infinity, Infinity, 50, Infinity, Infinity, true); + ctx.arc(Infinity, Infinity, 50, 0, Infinity, true); + ctx.arc(Infinity, 0, Infinity, 0, 2*Math.PI, true); + ctx.arc(Infinity, 0, Infinity, Infinity, 2*Math.PI, true); + ctx.arc(Infinity, 0, Infinity, Infinity, Infinity, true); + ctx.arc(Infinity, 0, Infinity, 0, Infinity, true); + ctx.arc(Infinity, 0, 50, Infinity, 2*Math.PI, true); + ctx.arc(Infinity, 0, 50, Infinity, Infinity, true); + ctx.arc(Infinity, 0, 50, 0, Infinity, true); + ctx.arc(0, Infinity, Infinity, 0, 2*Math.PI, true); + ctx.arc(0, Infinity, Infinity, Infinity, 2*Math.PI, true); + ctx.arc(0, Infinity, Infinity, Infinity, Infinity, true); + ctx.arc(0, Infinity, Infinity, 0, Infinity, true); + ctx.arc(0, Infinity, 50, Infinity, 2*Math.PI, true); + ctx.arc(0, Infinity, 50, Infinity, Infinity, true); + ctx.arc(0, Infinity, 50, 0, Infinity, true); + ctx.arc(0, 0, Infinity, Infinity, 2*Math.PI, true); + ctx.arc(0, 0, Infinity, Infinity, Infinity, true); + ctx.arc(0, 0, Infinity, 0, Infinity, true); + ctx.arc(0, 0, 50, Infinity, Infinity, true); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.rect.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.rect(0, 0, 100, 50); + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.newsubpath", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-50, 25); + ctx.rect(200, 25, 1, 1); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.closed", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.rect(100, 50, 100, 100); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.end.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.rect(200, 100, 400, 1000); + ctx.lineTo(-2000, -1000); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.end.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 450; + ctx.lineCap = 'round'; + ctx.lineJoin = 'bevel'; + ctx.rect(150, 150, 2000, 2000); + ctx.lineTo(160, 160); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.rect.zero.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(0, 50, 100, 0); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(50, -100, 0, 250); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.3", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(50, 25, 0, 0); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.4", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.rect(100, 25, 0, 0); + ctx.lineTo(0, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.5", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(0, 0); + ctx.rect(100, 25, 0, 0); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.zero.6", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.5; + ctx.lineWidth = 200; + ctx.beginPath(); + ctx.rect(100, 25, 1000, 0); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.negative", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#0f0'; + ctx.rect(0, 0, 50, 25); + ctx.rect(100, 0, -50, 25); + ctx.rect(0, 50, 50, -25); + ctx.rect(100, 50, -50, -25); + ctx.fill(); + _assertPixel(canvas, 25,12, 0,255,0,255); + _assertPixel(canvas, 75,12, 0,255,0,255); + _assertPixel(canvas, 25,37, 0,255,0,255); + _assertPixel(canvas, 75,37, 0,255,0,255); + }); + + it("2d.path.rect.winding", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.rect(0, 0, 50, 50); + ctx.rect(100, 50, -50, -50); + ctx.rect(0, 25, 100, -25); + ctx.rect(100, 25, -100, 25); + ctx.fill(); + _assertPixel(canvas, 25,12, 0,255,0,255); + _assertPixel(canvas, 75,12, 0,255,0,255); + _assertPixel(canvas, 25,37, 0,255,0,255); + _assertPixel(canvas, 75,37, 0,255,0,255); + }); + + it("2d.path.rect.selfintersect", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 90; + ctx.beginPath(); + ctx.rect(45, 20, 10, 10); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.rect.nonfinite", function () { + // rect() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.rect(Infinity, 50, 1, 1); + ctx.rect(-Infinity, 50, 1, 1); + ctx.rect(NaN, 50, 1, 1); + ctx.rect(0, Infinity, 1, 1); + ctx.rect(0, -Infinity, 1, 1); + ctx.rect(0, NaN, 1, 1); + ctx.rect(0, 50, Infinity, 1); + ctx.rect(0, 50, -Infinity, 1); + ctx.rect(0, 50, NaN, 1); + ctx.rect(0, 50, 1, Infinity); + ctx.rect(0, 50, 1, -Infinity); + ctx.rect(0, 50, 1, NaN); + ctx.rect(Infinity, Infinity, 1, 1); + ctx.rect(Infinity, Infinity, Infinity, 1); + ctx.rect(Infinity, Infinity, Infinity, Infinity); + ctx.rect(Infinity, Infinity, 1, Infinity); + ctx.rect(Infinity, 50, Infinity, 1); + ctx.rect(Infinity, 50, Infinity, Infinity); + ctx.rect(Infinity, 50, 1, Infinity); + ctx.rect(0, Infinity, Infinity, 1); + ctx.rect(0, Infinity, Infinity, Infinity); + ctx.rect(0, Infinity, 1, Infinity); + ctx.rect(0, 50, Infinity, Infinity); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.roundrect.newsubpath", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-50, 25); + ctx.roundRect(200, 25, 1, 1, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.closed", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.roundRect(100, 50, 100, 100, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.end.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.roundRect(200, 100, 400, 1000, [0]); + ctx.lineTo(-2000, -1000); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.end.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 450; + ctx.lineCap = 'round'; + ctx.lineJoin = 'bevel'; + ctx.roundRect(150, 150, 2000, 2000, [0]); + ctx.lineTo(160, 160); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.end.3", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.roundRect(101, 51, 2000, 2000, [500, 500, 500, 500]); + ctx.lineTo(-1, -1); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.end.4", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 10; + ctx.roundRect(-1, -1, 2000, 2000, [1000, 1000, 1000, 1000]); + ctx.lineTo(-150, -150); + ctx.stroke(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(0, 50, 100, 0, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(50, -100, 0, 250, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.3", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(50, 25, 0, 0, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.4", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.roundRect(100, 25, 0, 0, [0]); + ctx.lineTo(0, 25); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.5", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(0, 0); + ctx.roundRect(100, 25, 0, 0, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.zero.6", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.5; + ctx.lineWidth = 200; + ctx.beginPath(); + ctx.roundRect(100, 25, 1000, 0, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.negative", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#0f0'; + ctx.roundRect(0, 0, 50, 25, [10, 0, 0, 0]); + ctx.roundRect(100, 0, -50, 25, [10, 0, 0, 0]); + ctx.roundRect(0, 50, 50, -25, [10, 0, 0, 0]); + ctx.roundRect(100, 50, -50, -25, [10, 0, 0, 0]); + ctx.fill(); + // All rects drawn + _assertPixel(canvas, 25,12, 0,255,0,255); + _assertPixel(canvas, 75,12, 0,255,0,255); + _assertPixel(canvas, 25,37, 0,255,0,255); + _assertPixel(canvas, 75,37, 0,255,0,255); + // Correct corners are rounded. + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + }); + + it("2d.path.roundrect.winding", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.roundRect(0, 0, 50, 50, [0]); + ctx.roundRect(100, 50, -50, -50, [0]); + ctx.roundRect(0, 25, 100, -25, [0]); + ctx.roundRect(100, 25, -100, 25, [0]); + ctx.fill(); + _assertPixel(canvas, 25,12, 0,255,0,255); + _assertPixel(canvas, 75,12, 0,255,0,255); + _assertPixel(canvas, 25,37, 0,255,0,255); + _assertPixel(canvas, 75,37, 0,255,0,255); + }); + + it("2d.path.roundrect.selfintersect", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.roundRect(0, 0, 100, 50, [0]); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 90; + ctx.beginPath(); + ctx.roundRect(45, 20, 10, 10, [0]); + ctx.stroke(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.roundrect.nonfinite", function () { + // roundRect() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.roundRect(Infinity, 50, 1, 1, [0]); + ctx.roundRect(-Infinity, 50, 1, 1, [0]); + ctx.roundRect(NaN, 50, 1, 1, [0]); + ctx.roundRect(0, Infinity, 1, 1, [0]); + ctx.roundRect(0, -Infinity, 1, 1, [0]); + ctx.roundRect(0, NaN, 1, 1, [0]); + ctx.roundRect(0, 50, Infinity, 1, [0]); + ctx.roundRect(0, 50, -Infinity, 1, [0]); + ctx.roundRect(0, 50, NaN, 1, [0]); + ctx.roundRect(0, 50, 1, Infinity, [0]); + ctx.roundRect(0, 50, 1, -Infinity, [0]); + ctx.roundRect(0, 50, 1, NaN, [0]); + ctx.roundRect(0, 50, 1, 1, [Infinity]); + ctx.roundRect(0, 50, 1, 1, [-Infinity]); + ctx.roundRect(0, 50, 1, 1, [NaN]); + ctx.roundRect(0, 50, 1, 1, [Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [-Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [NaN,0]); + ctx.roundRect(0, 50, 1, 1, [0,Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,-Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,NaN]); + ctx.roundRect(0, 50, 1, 1, [Infinity,0,0]); + ctx.roundRect(0, 50, 1, 1, [-Infinity,0,0]); + ctx.roundRect(0, 50, 1, 1, [NaN,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [0,-Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [0,NaN,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,0,-Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,0,NaN]); + ctx.roundRect(0, 50, 1, 1, [Infinity,0,0,0]); + ctx.roundRect(0, 50, 1, 1, [-Infinity,0,0,0]); + ctx.roundRect(0, 50, 1, 1, [NaN,0,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,Infinity,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,-Infinity,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,NaN,0,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,-Infinity,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,NaN,0]); + ctx.roundRect(0, 50, 1, 1, [0,0,0,Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,0,0,-Infinity]); + ctx.roundRect(0, 50, 1, 1, [0,0,0,NaN]); + ctx.roundRect(Infinity, Infinity, 1, 1, [0]); + ctx.roundRect(Infinity, Infinity, Infinity, 1, [0]); + ctx.roundRect(Infinity, Infinity, Infinity, Infinity, [0]); + ctx.roundRect(Infinity, Infinity, Infinity, Infinity, [Infinity]); + ctx.roundRect(Infinity, Infinity, Infinity, 1, [Infinity]); + ctx.roundRect(Infinity, Infinity, 1, Infinity, [0]); + ctx.roundRect(Infinity, Infinity, 1, Infinity, [Infinity]); + ctx.roundRect(Infinity, Infinity, 1, 1, [Infinity]); + ctx.roundRect(Infinity, 50, Infinity, 1, [0]); + ctx.roundRect(Infinity, 50, Infinity, Infinity, [0]); + ctx.roundRect(Infinity, 50, Infinity, Infinity, [Infinity]); + ctx.roundRect(Infinity, 50, Infinity, 1, [Infinity]); + ctx.roundRect(Infinity, 50, 1, Infinity, [0]); + ctx.roundRect(Infinity, 50, 1, Infinity, [Infinity]); + ctx.roundRect(Infinity, 50, 1, 1, [Infinity]); + ctx.roundRect(0, Infinity, Infinity, 1, [0]); + ctx.roundRect(0, Infinity, Infinity, Infinity, [0]); + ctx.roundRect(0, Infinity, Infinity, Infinity, [Infinity]); + ctx.roundRect(0, Infinity, Infinity, 1, [Infinity]); + ctx.roundRect(0, Infinity, 1, Infinity, [0]); + ctx.roundRect(0, Infinity, 1, Infinity, [Infinity]); + ctx.roundRect(0, Infinity, 1, 1, [Infinity]); + ctx.roundRect(0, 50, Infinity, Infinity, [0]); + ctx.roundRect(0, 50, Infinity, Infinity, [Infinity]); + ctx.roundRect(0, 50, Infinity, 1, [Infinity]); + ctx.roundRect(0, 50, 1, Infinity, [Infinity]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, Infinity)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, -Infinity)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, NaN)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(Infinity, 10)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(-Infinity, 10)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(NaN, 10)]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: Infinity}]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: -Infinity}]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: NaN}]); + ctx.roundRect(0, 0, 100, 100, [{x: Infinity, y: 10}]); + ctx.roundRect(0, 0, 100, 100, [{x: -Infinity, y: 10}]); + ctx.roundRect(0, 0, 100, 100, [{x: NaN, y: 10}]); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 90,45, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.1.double", function () { + // Verify that when four radii are given to roundRect(), the first radius, specified as a double, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.1.dompoint", function () { + // Verify that when four radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.1.dompointinit", function () { + // Verify that when four radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.2.double", function () { + // Verify that when four radii are given to roundRect(), the second radius, specified as a double, applies to the top-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.2.dompoint", function () { + // Verify that when four radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20), 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.2.dompointinit", function () { + // Verify that when four radii are given to roundRect(), the second radius, specified as a DOMPointInit, applies to the top-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.3.double", function () { + // Verify that when four radii are given to roundRect(), the third radius, specified as a double, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.3.dompoint", function () { + // Verify that when four radii are given to roundRect(), the third radius, specified as a DOMPoint, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.3.dompointinit", function () { + // Verify that when four radii are given to roundRect(), the third radius, specified as a DOMPointInit, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, {x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.4.double", function () { + // Verify that when four radii are given to roundRect(), the fourth radius, specified as a double, applies to the bottom-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.4.radii.4.dompoint", function () { + // Verify that when four radii are given to roundRect(), the fourth radius, specified as a DOMPoint, applies to the bottom-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.4.radii.4.dompointinit", function () { + // Verify that when four radii are given to roundRect(), the fourth radius, specified as a DOMPointInit, applies to the bottom-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.1.double", function () { + // Verify that when three radii are given to roundRect(), the first radius, specified as a double, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.1.dompoint", function () { + // Verify that when three radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.1.dompointinit", function () { + // Verify that when three radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.2.double", function () { + // Verify that when three radii are given to roundRect(), the second radius, specified as a double, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.3.radii.2.dompoint", function () { + // Verify that when three radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.2.dompointinit", function () { + // Verify that when three radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.3.double", function () { + // Verify that when three radii are given to roundRect(), the third radius, specified as a double, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.3.dompoint", function () { + // Verify that when three radii are given to roundRect(), the third radius, specified as a DOMPoint, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.3.radii.3.dompointinit", function () { + // Verify that when three radii are given to roundRect(), the third radius, specified as a DOMPointInit, applies to the bottom-right corner. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.1.double", function () { + // Verify that when two radii are given to roundRect(), the first radius, specified as a double, applies to the top-left and bottom-right corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.1.dompoint", function () { + // Verify that when two radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left and bottom-right corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.1.dompointinit", function () { + // Verify that when two radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left and bottom-right corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 98,1, 0,255,0,255); + _assertPixel(canvas, 1,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.2.double", function () { + // Verify that when two radii are given to roundRect(), the second radius, specified as a double, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.2.radii.2.dompoint", function () { + // Verify that when two radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.2.radii.2.dompointinit", function () { + // Verify that when two radii are given to roundRect(), the second radius, specified as a DOMPointInit, applies to the top-right and bottom-left corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + + // other corners + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.path.roundrect.1.radius.double", function () { + // Verify that when one radius is given to roundRect(), specified as a double, it applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.1.radius.double.single.argument", function () { + // Verify that when one radius is given to roundRect() as a non-array argument, specified as a double, it applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, 20); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + }); + + it("2d.path.roundrect.1.radius.dompoint", function () { + // Verify that when one radius is given to roundRect(), specified as a DOMPoint, it applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + }); + + it("2d.path.roundrect.1.radius.dompoint.single argument", function () { + // Verify that when one radius is given to roundRect() as a non-array argument, specified as a DOMPoint, it applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, new DOMPoint(40, 20)); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + }); + + it("2d.path.roundrect.1.radius.dompointinit", function () { + // Verify that when one radius is given to roundRect(), specified as a DOMPointInit, applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + }); + + it("2d.path.roundrect.1.radius.dompointinit.single.argument", function () { + // Verify that when one radius is given to roundRect() as a non-array argument, specified as a DOMPointInit, applies to all corners. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, {x: 40, y: 20}); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + _assertPixel(canvas, 20,1, 255,0,0,255); + _assertPixel(canvas, 41,1, 0,255,0,255); + _assertPixel(canvas, 1,10, 255,0,0,255); + _assertPixel(canvas, 1,21, 0,255,0,255); + + // top-right corner + _assertPixel(canvas, 79,1, 255,0,0,255); + _assertPixel(canvas, 58,1, 0,255,0,255); + _assertPixel(canvas, 98,10, 255,0,0,255); + _assertPixel(canvas, 98,21, 0,255,0,255); + + // bottom-right corner + _assertPixel(canvas, 79,48, 255,0,0,255); + _assertPixel(canvas, 58,48, 0,255,0,255); + _assertPixel(canvas, 98,39, 255,0,0,255); + _assertPixel(canvas, 98,28, 0,255,0,255); + + // bottom-left corner + _assertPixel(canvas, 20,48, 255,0,0,255); + _assertPixel(canvas, 41,48, 0,255,0,255); + _assertPixel(canvas, 1,39, 255,0,0,255); + _assertPixel(canvas, 1,28, 0,255,0,255); + }); + + it("2d.path.roundrect.radius.intersecting.1", function () { + // Check that roundRects with intersecting corner arcs are rendered correctly. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [40, 40, 40, 40]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 2,25, 0,255,0,255); + _assertPixel(canvas, 50,1, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 50,48, 0,255,0,255); + _assertPixel(canvas, 97,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + }); + + it("2d.path.roundrect.radius.intersecting.2", function () { + // Check that roundRects with intersecting corner arcs are rendered correctly. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [1000, 1000, 1000, 1000]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 2,25, 0,255,0,255); + _assertPixel(canvas, 50,1, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 50,48, 0,255,0,255); + _assertPixel(canvas, 97,25, 0,255,0,255); + _assertPixel(canvas, 1,1, 255,0,0,255); + _assertPixel(canvas, 98,1, 255,0,0,255); + _assertPixel(canvas, 1,48, 255,0,0,255); + _assertPixel(canvas, 98,48, 255,0,0,255); + }); + + it("2d.path.roundrect.radius.none", function () { + // Check that roundRect throws an RangeError if radii is an empty array. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 100, 50, [])}); + }); + + it("2d.path.roundrect.radius.noargument", function () { + // Check that roundRect draws a rectangle when no radii are provided. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(10, 10, 80, 30); + ctx.fillStyle = '#0f0'; + ctx.fill(); + // upper left corner (10, 10) + _assertPixel(canvas, 10,9, 255,0,0,255); + _assertPixel(canvas, 9,10, 255,0,0,255); + _assertPixel(canvas, 10,10, 0,255,0,255); + + // upper right corner (89, 10) + _assertPixel(canvas, 90,10, 255,0,0,255); + _assertPixel(canvas, 89,9, 255,0,0,255); + _assertPixel(canvas, 89,10, 0,255,0,255); + + // lower right corner (89, 39) + _assertPixel(canvas, 89,40, 255,0,0,255); + _assertPixel(canvas, 90,39, 255,0,0,255); + _assertPixel(canvas, 89,39, 0,255,0,255); + + // lower left corner (10, 30) + _assertPixel(canvas, 9,39, 255,0,0,255); + _assertPixel(canvas, 10,40, 255,0,0,255); + _assertPixel(canvas, 10,39, 0,255,0,255); + }); + + it("2d.path.roundrect.radius.toomany", function () { + // Check that roundRect throws an IndeSizeError if radii has more than four items. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 100, 50, [0, 0, 0, 0, 0])}); + }); + + it("2d.path.roundrect.radius.negative", function () { + // roundRect() with negative radius throws an exception + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [-1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [1, -1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [new DOMPoint(-1, 1), 1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [new DOMPoint(1, -1)])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [{x: -1, y: 1}, 1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [{x: 1, y: -1}])}); + }); + + it("2d.path.ellipse.basics", function () { + // Verify canvas throws error when drawing ellipse with negative radii. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.ellipse(10, 10, 10, 5, 0, 0, 1, false); + ctx.ellipse(10, 10, 10, 0, 0, 0, 1, false); + ctx.ellipse(10, 10, -0, 5, 0, 0, 1, false); + assert.throws(function() { ctx.ellipse(10, 10, -2, 5, 0, 0, 1, false); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.ellipse(10, 10, 0, -1.5, 0, 0, 1, false); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.ellipse(10, 10, -2, -5, 0, 0, 1, false); }, /INDEX_SIZE_ERR/); + ctx.ellipse(80, 0, 10, 4294967277, Math.PI / -84, -Math.PI / 2147483436, false); + }); + + it("2d.path.fill.overlap", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; + ctx.rect(0, 0, 100, 50); + ctx.closePath(); + ctx.rect(10, 10, 80, 30); + ctx.fill(); + + _assertPixelApprox(canvas, 50,25, 0,127,0,255, 1); + }); + + it("2d.path.fill.winding.add", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.winding.subtract.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.winding.subtract.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.winding.subtract.3", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(-20, -20); + ctx.lineTo(120, -20); + ctx.lineTo(120, 70); + ctx.lineTo(-20, 70); + ctx.lineTo(-20, -20); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.closed.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.fill.closed.unaffected", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.fillStyle = '#f00'; + ctx.fill(); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + _assertPixel(canvas, 90,10, 0,255,0,255); + _assertPixel(canvas, 10,40, 0,255,0,255); + }); + + it("2d.path.stroke.overlap", function () { + // Stroked subpaths are combined before being drawn + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)'; + ctx.lineWidth = 50; + ctx.moveTo(0, 20); + ctx.lineTo(100, 20); + ctx.moveTo(0, 30); + ctx.lineTo(100, 30); + ctx.stroke(); + + _assertPixelApprox(canvas, 50,25, 0,127,0,255, 1); + }); + + it("2d.path.stroke.union", function () { + // Strokes in opposite directions are unioned, not subtracted + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 40; + ctx.moveTo(0, 10); + ctx.lineTo(100, 10); + ctx.moveTo(100, 40); + ctx.lineTo(0, 40); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.unaffected", function () { + // Stroking does not start a new path or subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -100); + ctx.lineTo(200, -100); + ctx.lineTo(200, 25); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + + ctx.closePath(); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.scale1", function () { + // Stroke line widths are scaled by the current transformation matrix + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(25, 12.5, 50, 25); + ctx.save(); + ctx.scale(50, 25); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.beginPath(); + ctx.rect(-25, -12.5, 150, 75); + ctx.save(); + ctx.scale(50, 25); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.stroke.scale2", function () { + // Stroke line widths are scaled by the current transformation matrix + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(25, 12.5, 50, 25); + ctx.save(); + ctx.rotate(Math.PI/2); + ctx.scale(25, 50); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.beginPath(); + ctx.rect(-25, -12.5, 150, 75); + ctx.save(); + ctx.rotate(Math.PI/2); + ctx.scale(25, 50); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.stroke.skew", function () { + // Strokes lines are skewed by the current transformation matrix + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(49, -50); + ctx.lineTo(201, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 283); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.save(); + ctx.beginPath(); + ctx.translate(-150, 0); + ctx.moveTo(49, -50); + ctx.lineTo(199, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 142); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + ctx.save(); + ctx.beginPath(); + ctx.translate(-150, 0); + ctx.moveTo(49, -50); + ctx.lineTo(199, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 142); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + _assertPixel(canvas, 0,0, 0,255,0,255); + _assertPixel(canvas, 50,0, 0,255,0,255); + _assertPixel(canvas, 99,0, 0,255,0,255); + _assertPixel(canvas, 0,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 99,25, 0,255,0,255); + _assertPixel(canvas, 0,49, 0,255,0,255); + _assertPixel(canvas, 50,49, 0,255,0,255); + _assertPixel(canvas, 99,49, 0,255,0,255); + }); + + it("2d.path.stroke.empty", function () { + // Empty subpaths are not stroked + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(40, 25); + ctx.moveTo(60, 25); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.line", function () { + // Zero-length line segments from lineTo are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.lineTo(50, 25); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.closed", function () { + // Zero-length line segments from closed paths are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.lineTo(50, 25); + ctx.closePath(); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.curve", function () { + // Zero-length line segments from quadraticCurveTo and bezierCurveTo are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.quadraticCurveTo(50, 25, 50, 25); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.bezierCurveTo(50, 25, 50, 25, 50, 25); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.arc", function () { + // Zero-length line segments from arcTo and arc are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arcTo(50, 25, 150, 25, 10); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(60, 25); + ctx.arc(50, 25, 10, 0, 0, false); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.rect", function () { + // Zero-length line segments from rect and strokeRect are removed before stroking + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.rect(50, 25, 0, 0); + ctx.stroke(); + + ctx.strokeRect(50, 25, 0, 0); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.stroke.prune.corner", function () { + // Zero-length line segments are removed before stroking with miters + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.4; + + ctx.beginPath(); + ctx.moveTo(-1000, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 1000); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.transformation.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(-100, 0); + ctx.rect(100, 0, 100, 50); + ctx.translate(0, -100); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.transformation.multiple", function () { + // Transformations are applied while building paths, not when drawing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.translate(-100, 0); + ctx.rect(0, 0, 100, 50); + ctx.fill(); + ctx.translate(100, 0); + ctx.fill(); + + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.translate(0, -50); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + ctx.translate(0, 50); + ctx.stroke(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.transformation.changing", function () { + // Transformations are applied while building paths, not when drawing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.moveTo(0, 0); + ctx.translate(100, 0); + ctx.lineTo(0, 0); + ctx.translate(0, 50); + ctx.lineTo(0, 0); + ctx.translate(-100, 0); + ctx.lineTo(0, 0); + ctx.translate(1000, 1000); + ctx.rotate(Math.PI/2); + ctx.scale(0.1, 0.1); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.empty", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.basic.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(0, 0, 100, 50); + ctx.clip(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.basic.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(-100, 0, 100, 50); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.intersect", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.beginPath(); + ctx.rect(50, 0, 50, 50) + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.winding.1", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.winding.2", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.clip(); + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.lineTo(0, 0); + ctx.clip(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.clip.unaffected", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.clip(); + + ctx.lineTo(0, 0); + ctx.fill(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.path.isPointInPath.basic.1", function () { + // isPointInPath() detects whether the point is inside the path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(0, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(10, 10), true, "ctx.isPointInPath(10, 10)", "true") + assert.strictEqual(ctx.isPointInPath(30, 10), false, "ctx.isPointInPath(30, 10)", "false") + }); + + it("2d.path.isPointInPath.basic.2", function () { + // isPointInPath() detects whether the point is inside the path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(20, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(30, 10), true, "ctx.isPointInPath(30, 10)", "true") + }); + + it("2d.path.isPointInPath.edge", function () { + // isPointInPath() counts points on the path as being inside + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(0, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(0, 0), true, "ctx.isPointInPath(0, 0)", "true") + assert.strictEqual(ctx.isPointInPath(10, 0), true, "ctx.isPointInPath(10, 0)", "true") + assert.strictEqual(ctx.isPointInPath(20, 0), true, "ctx.isPointInPath(20, 0)", "true") + assert.strictEqual(ctx.isPointInPath(20, 10), true, "ctx.isPointInPath(20, 10)", "true") + assert.strictEqual(ctx.isPointInPath(20, 20), true, "ctx.isPointInPath(20, 20)", "true") + assert.strictEqual(ctx.isPointInPath(10, 20), true, "ctx.isPointInPath(10, 20)", "true") + assert.strictEqual(ctx.isPointInPath(0, 20), true, "ctx.isPointInPath(0, 20)", "true") + assert.strictEqual(ctx.isPointInPath(0, 10), true, "ctx.isPointInPath(0, 10)", "true") + assert.strictEqual(ctx.isPointInPath(10, -0.01), false, "ctx.isPointInPath(10, -0.01)", "false") + assert.strictEqual(ctx.isPointInPath(10, 20.01), false, "ctx.isPointInPath(10, 20.01)", "false") + assert.strictEqual(ctx.isPointInPath(-0.01, 10), false, "ctx.isPointInPath(-0.01, 10)", "false") + assert.strictEqual(ctx.isPointInPath(20.01, 10), false, "ctx.isPointInPath(20.01, 10)", "false") + }); + + it("2d.path.isPointInPath.empty", function () { + // isPointInPath() works when there is no path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.isPointInPath(0, 0), false, "ctx.isPointInPath(0, 0)", "false") + }); + + it("2d.path.isPointInPath.subpath", function () { + // isPointInPath() uses the current path, not just the subpath + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(0, 0, 20, 20); + ctx.beginPath(); + ctx.rect(20, 0, 20, 20); + ctx.closePath(); + ctx.rect(40, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(30, 10), true, "ctx.isPointInPath(30, 10)", "true") + assert.strictEqual(ctx.isPointInPath(50, 10), true, "ctx.isPointInPath(50, 10)", "true") + }); + + it("2d.path.isPointInPath.outside", function () { + // isPointInPath() works on paths outside the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(0, -100, 20, 20); + ctx.rect(20, -10, 20, 20); + assert.strictEqual(ctx.isPointInPath(10, -110), false, "ctx.isPointInPath(10, -110)", "false") + assert.strictEqual(ctx.isPointInPath(10, -90), true, "ctx.isPointInPath(10, -90)", "true") + assert.strictEqual(ctx.isPointInPath(10, -70), false, "ctx.isPointInPath(10, -70)", "false") + assert.strictEqual(ctx.isPointInPath(30, -20), false, "ctx.isPointInPath(30, -20)", "false") + assert.strictEqual(ctx.isPointInPath(30, 0), true, "ctx.isPointInPath(30, 0)", "true") + assert.strictEqual(ctx.isPointInPath(30, 20), false, "ctx.isPointInPath(30, 20)", "false") + }); + + it("2d.path.isPointInPath.unclosed", function () { + // isPointInPath() works on unclosed subpaths + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(0, 0); + ctx.lineTo(20, 0); + ctx.lineTo(20, 20); + ctx.lineTo(0, 20); + assert.strictEqual(ctx.isPointInPath(10, 10), true, "ctx.isPointInPath(10, 10)", "true") + assert.strictEqual(ctx.isPointInPath(30, 10), false, "ctx.isPointInPath(30, 10)", "false") + }); + + it("2d.path.isPointInPath.arc", function () { + // isPointInPath() works on arcs + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.arc(50, 25, 10, 0, Math.PI, false); + assert.strictEqual(ctx.isPointInPath(50, 10), false, "ctx.isPointInPath(50, 10)", "false") + assert.strictEqual(ctx.isPointInPath(50, 20), false, "ctx.isPointInPath(50, 20)", "false") + assert.strictEqual(ctx.isPointInPath(50, 30), true, "ctx.isPointInPath(50, 30)", "true") + assert.strictEqual(ctx.isPointInPath(50, 40), false, "ctx.isPointInPath(50, 40)", "false") + assert.strictEqual(ctx.isPointInPath(30, 20), false, "ctx.isPointInPath(30, 20)", "false") + assert.strictEqual(ctx.isPointInPath(70, 20), false, "ctx.isPointInPath(70, 20)", "false") + assert.strictEqual(ctx.isPointInPath(30, 30), false, "ctx.isPointInPath(30, 30)", "false") + assert.strictEqual(ctx.isPointInPath(70, 30), false, "ctx.isPointInPath(70, 30)", "false") + }); + + it("2d.path.isPointInPath.bigarc", function () { + // isPointInPath() works on unclosed arcs larger than 2pi + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.arc(50, 25, 10, 0, 7, false); + assert.strictEqual(ctx.isPointInPath(50, 10), false, "ctx.isPointInPath(50, 10)", "false") + assert.strictEqual(ctx.isPointInPath(50, 20), true, "ctx.isPointInPath(50, 20)", "true") + assert.strictEqual(ctx.isPointInPath(50, 30), true, "ctx.isPointInPath(50, 30)", "true") + assert.strictEqual(ctx.isPointInPath(50, 40), false, "ctx.isPointInPath(50, 40)", "false") + assert.strictEqual(ctx.isPointInPath(30, 20), false, "ctx.isPointInPath(30, 20)", "false") + assert.strictEqual(ctx.isPointInPath(70, 20), false, "ctx.isPointInPath(70, 20)", "false") + assert.strictEqual(ctx.isPointInPath(30, 30), false, "ctx.isPointInPath(30, 30)", "false") + assert.strictEqual(ctx.isPointInPath(70, 30), false, "ctx.isPointInPath(70, 30)", "false") + }); + + it("2d.path.isPointInPath.bezier", function () { + // isPointInPath() works on Bezier curves + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.moveTo(25, 25); + ctx.bezierCurveTo(50, -50, 50, 100, 75, 25); + assert.strictEqual(ctx.isPointInPath(25, 20), false, "ctx.isPointInPath(25, 20)", "false") + assert.strictEqual(ctx.isPointInPath(25, 30), false, "ctx.isPointInPath(25, 30)", "false") + assert.strictEqual(ctx.isPointInPath(30, 20), true, "ctx.isPointInPath(30, 20)", "true") + assert.strictEqual(ctx.isPointInPath(30, 30), false, "ctx.isPointInPath(30, 30)", "false") + assert.strictEqual(ctx.isPointInPath(40, 2), false, "ctx.isPointInPath(40, 2)", "false") + assert.strictEqual(ctx.isPointInPath(40, 20), true, "ctx.isPointInPath(40, 20)", "true") + assert.strictEqual(ctx.isPointInPath(40, 30), false, "ctx.isPointInPath(40, 30)", "false") + assert.strictEqual(ctx.isPointInPath(40, 47), false, "ctx.isPointInPath(40, 47)", "false") + assert.strictEqual(ctx.isPointInPath(45, 20), true, "ctx.isPointInPath(45, 20)", "true") + assert.strictEqual(ctx.isPointInPath(45, 30), false, "ctx.isPointInPath(45, 30)", "false") + assert.strictEqual(ctx.isPointInPath(55, 20), false, "ctx.isPointInPath(55, 20)", "false") + assert.strictEqual(ctx.isPointInPath(55, 30), true, "ctx.isPointInPath(55, 30)", "true") + assert.strictEqual(ctx.isPointInPath(60, 2), false, "ctx.isPointInPath(60, 2)", "false") + assert.strictEqual(ctx.isPointInPath(60, 20), false, "ctx.isPointInPath(60, 20)", "false") + assert.strictEqual(ctx.isPointInPath(60, 30), true, "ctx.isPointInPath(60, 30)", "true") + assert.strictEqual(ctx.isPointInPath(60, 47), false, "ctx.isPointInPath(60, 47)", "false") + assert.strictEqual(ctx.isPointInPath(70, 20), false, "ctx.isPointInPath(70, 20)", "false") + assert.strictEqual(ctx.isPointInPath(70, 30), true, "ctx.isPointInPath(70, 30)", "true") + assert.strictEqual(ctx.isPointInPath(75, 20), false, "ctx.isPointInPath(75, 20)", "false") + assert.strictEqual(ctx.isPointInPath(75, 30), false, "ctx.isPointInPath(75, 30)", "false") + }); + + it("2d.path.isPointInPath.winding", function () { + // isPointInPath() uses the non-zero winding number rule + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Create a square ring, using opposite windings to make a hole in the centre + ctx.moveTo(0, 0); + ctx.lineTo(50, 0); + ctx.lineTo(50, 50); + ctx.lineTo(0, 50); + ctx.lineTo(0, 0); + ctx.lineTo(10, 10); + ctx.lineTo(10, 40); + ctx.lineTo(40, 40); + ctx.lineTo(40, 10); + ctx.lineTo(10, 10); + + assert.strictEqual(ctx.isPointInPath(5, 5), true, "ctx.isPointInPath(5, 5)", "true") + assert.strictEqual(ctx.isPointInPath(25, 5), true, "ctx.isPointInPath(25, 5)", "true") + assert.strictEqual(ctx.isPointInPath(45, 5), true, "ctx.isPointInPath(45, 5)", "true") + assert.strictEqual(ctx.isPointInPath(5, 25), true, "ctx.isPointInPath(5, 25)", "true") + assert.strictEqual(ctx.isPointInPath(25, 25), false, "ctx.isPointInPath(25, 25)", "false") + assert.strictEqual(ctx.isPointInPath(45, 25), true, "ctx.isPointInPath(45, 25)", "true") + assert.strictEqual(ctx.isPointInPath(5, 45), true, "ctx.isPointInPath(5, 45)", "true") + assert.strictEqual(ctx.isPointInPath(25, 45), true, "ctx.isPointInPath(25, 45)", "true") + assert.strictEqual(ctx.isPointInPath(45, 45), true, "ctx.isPointInPath(45, 45)", "true") + }); + + it("2d.path.isPointInPath.transform.1", function () { + // isPointInPath() handles transformations correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.translate(50, 0); + ctx.rect(0, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(-40, 10), false, "ctx.isPointInPath(-40, 10)", "false") + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(49, 10), false, "ctx.isPointInPath(49, 10)", "false") + assert.strictEqual(ctx.isPointInPath(51, 10), true, "ctx.isPointInPath(51, 10)", "true") + assert.strictEqual(ctx.isPointInPath(69, 10), true, "ctx.isPointInPath(69, 10)", "true") + assert.strictEqual(ctx.isPointInPath(71, 10), false, "ctx.isPointInPath(71, 10)", "false") + }); + + it("2d.path.isPointInPath.transform.2", function () { + // isPointInPath() handles transformations correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(50, 0, 20, 20); + ctx.translate(50, 0); + assert.strictEqual(ctx.isPointInPath(-40, 10), false, "ctx.isPointInPath(-40, 10)", "false") + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(49, 10), false, "ctx.isPointInPath(49, 10)", "false") + assert.strictEqual(ctx.isPointInPath(51, 10), true, "ctx.isPointInPath(51, 10)", "true") + assert.strictEqual(ctx.isPointInPath(69, 10), true, "ctx.isPointInPath(69, 10)", "true") + assert.strictEqual(ctx.isPointInPath(71, 10), false, "ctx.isPointInPath(71, 10)", "false") + }); + + it("2d.path.isPointInPath.transform.3", function () { + // isPointInPath() handles transformations correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.scale(-1, 1); + ctx.rect(-70, 0, 20, 20); + assert.strictEqual(ctx.isPointInPath(-40, 10), false, "ctx.isPointInPath(-40, 10)", "false") + assert.strictEqual(ctx.isPointInPath(10, 10), false, "ctx.isPointInPath(10, 10)", "false") + assert.strictEqual(ctx.isPointInPath(49, 10), false, "ctx.isPointInPath(49, 10)", "false") + assert.strictEqual(ctx.isPointInPath(51, 10), true, "ctx.isPointInPath(51, 10)", "true") + assert.strictEqual(ctx.isPointInPath(69, 10), true, "ctx.isPointInPath(69, 10)", "true") + assert.strictEqual(ctx.isPointInPath(71, 10), false, "ctx.isPointInPath(71, 10)", "false") + }); + + it("2d.path.isPointInPath.transform.4", function () { + // isPointInPath() handles transformations correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.translate(50, 0); + ctx.rect(50, 0, 20, 20); + ctx.translate(0, 50); + assert.strictEqual(ctx.isPointInPath(60, 10), false, "ctx.isPointInPath(60, 10)", "false") + assert.strictEqual(ctx.isPointInPath(110, 10), true, "ctx.isPointInPath(110, 10)", "true") + assert.strictEqual(ctx.isPointInPath(110, 60), false, "ctx.isPointInPath(110, 60)", "false") + }); + + it("2d.path.isPointInPath.nonfinite", function () { + // isPointInPath() returns false for non-finite arguments + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.rect(-100, -50, 200, 100); + assert.strictEqual(ctx.isPointInPath(Infinity, 0), false, "ctx.isPointInPath(Infinity, 0)", "false") + assert.strictEqual(ctx.isPointInPath(-Infinity, 0), false, "ctx.isPointInPath(-Infinity, 0)", "false") + assert.strictEqual(ctx.isPointInPath(NaN, 0), false, "ctx.isPointInPath(NaN, 0)", "false") + assert.strictEqual(ctx.isPointInPath(0, Infinity), false, "ctx.isPointInPath(0, Infinity)", "false") + assert.strictEqual(ctx.isPointInPath(0, -Infinity), false, "ctx.isPointInPath(0, -Infinity)", "false") + assert.strictEqual(ctx.isPointInPath(0, NaN), false, "ctx.isPointInPath(0, NaN)", "false") + assert.strictEqual(ctx.isPointInPath(NaN, NaN), false, "ctx.isPointInPath(NaN, NaN)", "false") + }); + + it("2d.path.isPointInStroke.scaleddashes", function () { + // isPointInStroke() should return correct results on dashed paths at high scale factors + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var scale = 20; + ctx.setLineDash([10, 21.4159]); // dash from t=0 to t=10 along the circle + ctx.scale(scale, scale); + ctx.ellipse(6, 10, 5, 5, 0, 2*Math.PI, false); + ctx.stroke(); + + // hit-test the beginning of the dash (t=0) + assert.strictEqual(ctx.isPointInStroke(11*scale, 10*scale), true, "ctx.isPointInStroke(11*scale, 10*scale)", "true") + // hit-test the middle of the dash (t=5) + assert.strictEqual(ctx.isPointInStroke(8.70*scale, 14.21*scale), true, "ctx.isPointInStroke(8.70*scale, 14.21*scale)", "true") + // hit-test the end of the dash (t=9.8) + assert.strictEqual(ctx.isPointInStroke(4.10*scale, 14.63*scale), true, "ctx.isPointInStroke(4.10*scale, 14.63*scale)", "true") + // hit-test past the end of the dash (t=10.2) + assert.strictEqual(ctx.isPointInStroke(3.74*scale, 14.46*scale), false, "ctx.isPointInStroke(3.74*scale, 14.46*scale)", "false") + }); + + it("2d.path.isPointInPath.basic", function () { + // Verify the winding rule in isPointInPath works for for rect path. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + canvas.width = 200; + canvas.height = 200; + + // Testing default isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(50, 50), true, "ctx.isPointInPath(50, 50)", "true") + assert.strictEqual(ctx.isPointInPath(NaN, 50), false, "ctx.isPointInPath(NaN, 50)", "false") + assert.strictEqual(ctx.isPointInPath(50, NaN), false, "ctx.isPointInPath(50, NaN)", "false") + + // Testing nonzero isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(50, 50, 'nonzero'), true, "ctx.isPointInPath(50, 50, 'nonzero')", "true") + + // Testing evenodd isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(50, 50, 'evenodd'), false, "ctx.isPointInPath(50, 50, 'evenodd')", "false") + + // Testing extremely large scale + ctx.save(); + ctx.scale(Number.MAX_VALUE, Number.MAX_VALUE); + ctx.beginPath(); + ctx.rect(-10, -10, 20, 20); + assert.strictEqual(ctx.isPointInPath(0, 0, 'nonzero'), true, "ctx.isPointInPath(0, 0, 'nonzero')", "true") + assert.strictEqual(ctx.isPointInPath(0, 0, 'evenodd'), true, "ctx.isPointInPath(0, 0, 'evenodd')", "true") + ctx.restore(); + + // Check with non-invertible ctm. + ctx.save(); + ctx.scale(0, 0); + ctx.beginPath(); + ctx.rect(-10, -10, 20, 20); + assert.strictEqual(ctx.isPointInPath(0, 0, 'nonzero'), false, "ctx.isPointInPath(0, 0, 'nonzero')", "false") + assert.strictEqual(ctx.isPointInPath(0, 0, 'evenodd'), false, "ctx.isPointInPath(0, 0, 'evenodd')", "false") + ctx.restore(); + }); + + it("2d.path.isPointInpath.multi.path", function () { + // Verify the winding rule in isPointInPath works for path object. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + canvas.width = 200; + canvas.height = 200; + + // Testing default isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(path, 50, 50), true, "ctx.isPointInPath(path, 50, 50)", "true") + assert.strictEqual(ctx.isPointInPath(path, 50, 50, undefined), true, "ctx.isPointInPath(path, 50, 50, undefined)", "true") + assert.strictEqual(ctx.isPointInPath(path, NaN, 50), false, "ctx.isPointInPath(path, NaN, 50)", "false") + assert.strictEqual(ctx.isPointInPath(path, 50, NaN), false, "ctx.isPointInPath(path, 50, NaN)", "false") + + // Testing nonzero isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + assert.strictEqual(ctx.isPointInPath(path, 50, 50, 'nonzero'), true, "ctx.isPointInPath(path, 50, 50, 'nonzero')", "true") + + // Testing evenodd isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + assert_false(ctx.isPointInPath(path, 50, 50, 'evenodd')); + }); + + it("2d.path.isPointInpath.invalid", function () { + // Verify isPointInPath throws exceptions with invalid inputs. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + canvas.width = 200; + canvas.height = 200; + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + // Testing invalid enumeration isPointInPath (w/ and w/o Path object'); + assert.throws(function() { ctx.isPointInPath(path, 50, 50, 'gazonk'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(50, 50, 'gazonk'); }, TypeError); + + // Testing invalid type isPointInPath with Path object'); + assert.throws(function() { ctx.isPointInPath(null, 50, 50); }, TypeError); + assert.throws(function() { ctx.isPointInPath(null, 50, 50, 'nonzero'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(null, 50, 50, 'evenodd'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(null, 50, 50, null); }, TypeError); + assert.throws(function() { ctx.isPointInPath(path, 50, 50, null); }, TypeError); + assert.throws(function() { ctx.isPointInPath(undefined, 50, 50); }, TypeError); + assert.throws(function() { ctx.isPointInPath(undefined, 50, 50, 'nonzero'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(undefined, 50, 50, 'evenodd'); }, TypeError); + assert.throws(function() { ctx.isPointInPath(undefined, 50, 50, undefined); }, TypeError); + assert.throws(function() { ctx.isPointInPath([], 50, 50); }, TypeError); + assert.throws(function() { ctx.isPointInPath([], 50, 50, 'nonzero'); }, TypeError); + assert.throws(function() { ctx.isPointInPath([], 50, 50, 'evenodd'); }, TypeError); + assert.throws(function() { ctx.isPointInPath({}, 50, 50); }, TypeError); + assert.throws(function() { ctx.isPointInPath({}, 50, 50, 'nonzero'); }, TypeError); + assert.throws(function() { ctx.isPointInPath({}, 50, 50, 'evenodd'); }, TypeError); + }); +}); diff --git a/test/wpt/generated/pixel-manipulation.js b/test/wpt/generated/pixel-manipulation.js new file mode 100644 index 000000000..453572d0c --- /dev/null +++ b/test/wpt/generated/pixel-manipulation.js @@ -0,0 +1,1448 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: pixel-manipulation", function () { + + it("2d.imageData.create2.basic", function () { + // createImageData(sw, sh) exists and returns something + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(ctx.createImageData(1, 1), null, "ctx.createImageData(1, 1)", "null"); + }); + + it("2d.imageData.create1.basic", function () { + // createImageData(imgdata) exists and returns something + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(ctx.createImageData(ctx.createImageData(1, 1)), null, "ctx.createImageData(ctx.createImageData(1, 1))", "null"); + }); + + it("2d.imageData.create2.type", function () { + // createImageData(sw, sh) returns an ImageData object containing a Uint8ClampedArray object + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + assert.notStrictEqual(window.Uint8ClampedArray, undefined, "window.Uint8ClampedArray", "undefined"); + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.createImageData(1, 1); + assert(imgdata.thisImplementsImageData, "imgdata.thisImplementsImageData"); + assert(imgdata.data.thisImplementsUint8ClampedArray, "imgdata.data.thisImplementsUint8ClampedArray"); + }); + + it("2d.imageData.create1.type", function () { + // createImageData(imgdata) returns an ImageData object containing a Uint8ClampedArray object + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + assert.notStrictEqual(window.Uint8ClampedArray, undefined, "window.Uint8ClampedArray", "undefined"); + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.createImageData(ctx.createImageData(1, 1)); + assert(imgdata.thisImplementsImageData, "imgdata.thisImplementsImageData"); + assert(imgdata.data.thisImplementsUint8ClampedArray, "imgdata.data.thisImplementsUint8ClampedArray"); + }); + + it("2d.imageData.create2.this", function () { + // createImageData(sw, sh) should throw when called with the wrong |this| + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call(null, 1, 1); }, TypeError); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call(undefined, 1, 1); }, TypeError); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call({}, 1, 1); }, TypeError); + }); + + it("2d.imageData.create1.this", function () { + // createImageData(imgdata) should throw when called with the wrong |this| + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.createImageData(1, 1); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call(null, imgdata); }, TypeError); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call(undefined, imgdata); }, TypeError); + assert.throws(function() { CanvasRenderingContext2D.prototype.createImageData.call({}, imgdata); }, TypeError); + }); + + it("2d.imageData.create2.initial", function () { + // createImageData(sw, sh) returns transparent black data of the right size + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.createImageData(10, 20); + assert.strictEqual(imgdata.data.length, imgdata.width*imgdata.height*4, "imgdata.data.length", "imgdata.width*imgdata.height*4") + assert(imgdata.width < imgdata.height, "imgdata.width < imgdata.height"); + assert(imgdata.width > 0, "imgdata.width > 0"); + var isTransparentBlack = true; + for (var i = 0; i < imgdata.data.length; ++i) + if (imgdata.data[i] !== 0) + isTransparentBlack = false; + assert(isTransparentBlack, "isTransparentBlack"); + }); + + it("2d.imageData.create1.initial", function () { + // createImageData(imgdata) returns transparent black data of the right size + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + var imgdata1 = ctx.getImageData(0, 0, 10, 20); + var imgdata2 = ctx.createImageData(imgdata1); + assert.strictEqual(imgdata2.data.length, imgdata1.data.length, "imgdata2.data.length", "imgdata1.data.length") + assert.strictEqual(imgdata2.width, imgdata1.width, "imgdata2.width", "imgdata1.width") + assert.strictEqual(imgdata2.height, imgdata1.height, "imgdata2.height", "imgdata1.height") + var isTransparentBlack = true; + for (var i = 0; i < imgdata2.data.length; ++i) + if (imgdata2.data[i] !== 0) + isTransparentBlack = false; + assert(isTransparentBlack, "isTransparentBlack"); + }); + + it("2d.imageData.create2.large", function () { + // createImageData(sw, sh) works for sizes much larger than the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.createImageData(1000, 2000); + assert.strictEqual(imgdata.data.length, imgdata.width*imgdata.height*4, "imgdata.data.length", "imgdata.width*imgdata.height*4") + assert(imgdata.width < imgdata.height, "imgdata.width < imgdata.height"); + assert(imgdata.width > 0, "imgdata.width > 0"); + var isTransparentBlack = true; + for (var i = 0; i < imgdata.data.length; i += 7813) // check ~1024 points (assuming normal scaling) + if (imgdata.data[i] !== 0) + isTransparentBlack = false; + assert(isTransparentBlack, "isTransparentBlack"); + }); + + it.skip("2d.imageData.create2.negative", function () { + // createImageData(sw, sh) takes the absolute magnitude of the size arguments + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata1 = ctx.createImageData(10, 20); + var imgdata2 = ctx.createImageData(-10, 20); + var imgdata3 = ctx.createImageData(10, -20); + var imgdata4 = ctx.createImageData(-10, -20); + assert.strictEqual(imgdata1.data.length, imgdata2.data.length, "imgdata1.data.length", "imgdata2.data.length") + assert.strictEqual(imgdata2.data.length, imgdata3.data.length, "imgdata2.data.length", "imgdata3.data.length") + assert.strictEqual(imgdata3.data.length, imgdata4.data.length, "imgdata3.data.length", "imgdata4.data.length") + }); + + it.skip("2d.imageData.create2.zero", function () { + // createImageData(sw, sh) throws INDEX_SIZE_ERR if size is zero + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.createImageData(10, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.createImageData(0, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.createImageData(0, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.createImageData(0.99, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.createImageData(10, 0.1); }, /INDEX_SIZE_ERR/); + }); + + it.skip("2d.imageData.create2.nonfinite", function () { + // createImageData() throws TypeError if arguments are not finite + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.createImageData(Infinity, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(-Infinity, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(NaN, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(10, Infinity); }, TypeError); + assert.throws(function() { ctx.createImageData(10, -Infinity); }, TypeError); + assert.throws(function() { ctx.createImageData(10, NaN); }, TypeError); + assert.throws(function() { ctx.createImageData(Infinity, Infinity); }, TypeError); + var posinfobj = { valueOf: function() { return Infinity; } }, + neginfobj = { valueOf: function() { return -Infinity; } }, + nanobj = { valueOf: function() { return -Infinity; } }; + assert.throws(function() { ctx.createImageData(posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(neginfobj, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(nanobj, 10); }, TypeError); + assert.throws(function() { ctx.createImageData(10, posinfobj); }, TypeError); + assert.throws(function() { ctx.createImageData(10, neginfobj); }, TypeError); + assert.throws(function() { ctx.createImageData(10, nanobj); }, TypeError); + assert.throws(function() { ctx.createImageData(posinfobj, posinfobj); }, TypeError); + }); + + it.skip("2d.imageData.create1.zero", function () { + // createImageData(null) throws TypeError + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.createImageData(null); }, TypeError); + }); + + it.skip("2d.imageData.create2.double", function () { + // createImageData(w, h) double is converted to long + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata1 = ctx.createImageData(10.01, 10.99); + var imgdata2 = ctx.createImageData(-10.01, -10.99); + assert.strictEqual(imgdata1.width, 10, "imgdata1.width", "10") + assert.strictEqual(imgdata1.height, 10, "imgdata1.height", "10") + assert.strictEqual(imgdata2.width, 10, "imgdata2.width", "10") + assert.strictEqual(imgdata2.height, 10, "imgdata2.height", "10") + }); + + it("2d.imageData.create.and.resize", function () { + // Verify no crash when resizing an image bitmap to zero. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var image = new Image(); + image.onload = t.step_func(function() { + var options = { resizeHeight: 0 }; + var p1 = createImageBitmap(image, options); + p1.catch(function(error){}); + t.done(); + }); + image.src = 'red.png'; + }); + + it("2d.imageData.get.basic", function () { + // getImageData() exists and returns something + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(ctx.getImageData(0, 0, 100, 50), null, "ctx.getImageData(0, 0, 100, 50)", "null"); + }); + + it("2d.imageData.get.type", function () { + // getImageData() returns an ImageData object containing a Uint8ClampedArray object + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + assert.notStrictEqual(window.Uint8ClampedArray, undefined, "window.Uint8ClampedArray", "undefined"); + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.getImageData(0, 0, 1, 1); + assert(imgdata.thisImplementsImageData, "imgdata.thisImplementsImageData"); + assert(imgdata.data.thisImplementsUint8ClampedArray, "imgdata.data.thisImplementsUint8ClampedArray"); + }); + + it("2d.imageData.get.zero", function () { + // getImageData() throws INDEX_SIZE_ERR if size is zero + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.getImageData(1, 1, 10, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 0, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 0, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 0.1, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 10, 0.99); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, -0.1, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { ctx.getImageData(1, 1, 10, -0.99); }, /INDEX_SIZE_ERR/); + }); + + it("2d.imageData.get.nonfinite", function () { + // getImageData() throws TypeError if arguments are not finite + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.getImageData(Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(-Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(NaN, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, -Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, NaN, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, -Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, NaN, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, -Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, NaN); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, Infinity, Infinity); }, TypeError); + var posinfobj = { valueOf: function() { return Infinity; } }, + neginfobj = { valueOf: function() { return -Infinity; } }, + nanobj = { valueOf: function() { return -Infinity; } }; + assert.throws(function() { ctx.getImageData(posinfobj, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(neginfobj, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(nanobj, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, posinfobj, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, neginfobj, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, nanobj, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, neginfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, nanobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, neginfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, 10, nanobj); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, posinfobj, 10, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, posinfobj, posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, posinfobj, posinfobj, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, posinfobj, 10, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, 10, posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, 10, posinfobj, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(posinfobj, 10, 10, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, posinfobj, posinfobj, 10); }, TypeError); + assert.throws(function() { ctx.getImageData(10, posinfobj, posinfobj, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, posinfobj, 10, posinfobj); }, TypeError); + assert.throws(function() { ctx.getImageData(10, 10, posinfobj, posinfobj); }, TypeError); + }); + + it.skip("2d.imageData.get.source.outside", function () { + // getImageData() returns transparent black outside the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#08f'; + ctx.fillRect(0, 0, 100, 50); + + var imgdata1 = ctx.getImageData(-10, 5, 1, 1); + assert.strictEqual(imgdata1.data[0], 0, "imgdata1.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata1.data[1], 0, "imgdata1.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata1.data[2], 0, "imgdata1.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata1.data[3], 0, "imgdata1.data[\""+(3)+"\"]", "0") + + var imgdata2 = ctx.getImageData(10, -5, 1, 1); + assert.strictEqual(imgdata2.data[0], 0, "imgdata2.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata2.data[1], 0, "imgdata2.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata2.data[2], 0, "imgdata2.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata2.data[3], 0, "imgdata2.data[\""+(3)+"\"]", "0") + + var imgdata3 = ctx.getImageData(200, 5, 1, 1); + assert.strictEqual(imgdata3.data[0], 0, "imgdata3.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata3.data[1], 0, "imgdata3.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata3.data[2], 0, "imgdata3.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata3.data[3], 0, "imgdata3.data[\""+(3)+"\"]", "0") + + var imgdata4 = ctx.getImageData(10, 60, 1, 1); + assert.strictEqual(imgdata4.data[0], 0, "imgdata4.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata4.data[1], 0, "imgdata4.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata4.data[2], 0, "imgdata4.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata4.data[3], 0, "imgdata4.data[\""+(3)+"\"]", "0") + + var imgdata5 = ctx.getImageData(100, 10, 1, 1); + assert.strictEqual(imgdata5.data[0], 0, "imgdata5.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata5.data[1], 0, "imgdata5.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata5.data[2], 0, "imgdata5.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata5.data[3], 0, "imgdata5.data[\""+(3)+"\"]", "0") + + var imgdata6 = ctx.getImageData(0, 10, 1, 1); + assert.strictEqual(imgdata6.data[0], 0, "imgdata6.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata6.data[1], 136, "imgdata6.data[\""+(1)+"\"]", "136") + assert.strictEqual(imgdata6.data[2], 255, "imgdata6.data[\""+(2)+"\"]", "255") + assert.strictEqual(imgdata6.data[3], 255, "imgdata6.data[\""+(3)+"\"]", "255") + + var imgdata7 = ctx.getImageData(-10, 10, 20, 20); + assert.strictEqual(imgdata7.data[ 0*4+0], 0, "imgdata7.data[ 0*4+0]", "0") + assert.strictEqual(imgdata7.data[ 0*4+1], 0, "imgdata7.data[ 0*4+1]", "0") + assert.strictEqual(imgdata7.data[ 0*4+2], 0, "imgdata7.data[ 0*4+2]", "0") + assert.strictEqual(imgdata7.data[ 0*4+3], 0, "imgdata7.data[ 0*4+3]", "0") + assert.strictEqual(imgdata7.data[ 9*4+0], 0, "imgdata7.data[ 9*4+0]", "0") + assert.strictEqual(imgdata7.data[ 9*4+1], 0, "imgdata7.data[ 9*4+1]", "0") + assert.strictEqual(imgdata7.data[ 9*4+2], 0, "imgdata7.data[ 9*4+2]", "0") + assert.strictEqual(imgdata7.data[ 9*4+3], 0, "imgdata7.data[ 9*4+3]", "0") + assert.strictEqual(imgdata7.data[10*4+0], 0, "imgdata7.data[10*4+0]", "0") + assert.strictEqual(imgdata7.data[10*4+1], 136, "imgdata7.data[10*4+1]", "136") + assert.strictEqual(imgdata7.data[10*4+2], 255, "imgdata7.data[10*4+2]", "255") + assert.strictEqual(imgdata7.data[10*4+3], 255, "imgdata7.data[10*4+3]", "255") + assert.strictEqual(imgdata7.data[19*4+0], 0, "imgdata7.data[19*4+0]", "0") + assert.strictEqual(imgdata7.data[19*4+1], 136, "imgdata7.data[19*4+1]", "136") + assert.strictEqual(imgdata7.data[19*4+2], 255, "imgdata7.data[19*4+2]", "255") + assert.strictEqual(imgdata7.data[19*4+3], 255, "imgdata7.data[19*4+3]", "255") + assert.strictEqual(imgdata7.data[20*4+0], 0, "imgdata7.data[20*4+0]", "0") + assert.strictEqual(imgdata7.data[20*4+1], 0, "imgdata7.data[20*4+1]", "0") + assert.strictEqual(imgdata7.data[20*4+2], 0, "imgdata7.data[20*4+2]", "0") + assert.strictEqual(imgdata7.data[20*4+3], 0, "imgdata7.data[20*4+3]", "0") + }); + + it.skip("2d.imageData.get.source.negative", function () { + // getImageData() works with negative width and height, and returns top-to-bottom left-to-right + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#fff'; + ctx.fillRect(20, 10, 60, 10); + + var imgdata1 = ctx.getImageData(85, 25, -10, -10); + assert.strictEqual(imgdata1.data[0], 255, "imgdata1.data[\""+(0)+"\"]", "255") + assert.strictEqual(imgdata1.data[1], 255, "imgdata1.data[\""+(1)+"\"]", "255") + assert.strictEqual(imgdata1.data[2], 255, "imgdata1.data[\""+(2)+"\"]", "255") + assert.strictEqual(imgdata1.data[3], 255, "imgdata1.data[\""+(3)+"\"]", "255") + assert.strictEqual(imgdata1.data[imgdata1.data.length-4+0], 0, "imgdata1.data[imgdata1.data.length-4+0]", "0") + assert.strictEqual(imgdata1.data[imgdata1.data.length-4+1], 0, "imgdata1.data[imgdata1.data.length-4+1]", "0") + assert.strictEqual(imgdata1.data[imgdata1.data.length-4+2], 0, "imgdata1.data[imgdata1.data.length-4+2]", "0") + assert.strictEqual(imgdata1.data[imgdata1.data.length-4+3], 255, "imgdata1.data[imgdata1.data.length-4+3]", "255") + + var imgdata2 = ctx.getImageData(0, 0, -1, -1); + assert.strictEqual(imgdata2.data[0], 0, "imgdata2.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata2.data[1], 0, "imgdata2.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata2.data[2], 0, "imgdata2.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata2.data[3], 0, "imgdata2.data[\""+(3)+"\"]", "0") + }); + + it("2d.imageData.get.source.size", function () { + // getImageData() returns bigger ImageData for bigger source rectangle + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata1 = ctx.getImageData(0, 0, 10, 10); + var imgdata2 = ctx.getImageData(0, 0, 20, 20); + assert(imgdata2.width > imgdata1.width, "imgdata2.width > imgdata1.width"); + assert(imgdata2.height > imgdata1.height, "imgdata2.height > imgdata1.height"); + }); + + it.skip("2d.imageData.get.double", function () { + // createImageData(w, h) double is converted to long + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata1 = ctx.getImageData(0, 0, 10.01, 10.99); + var imgdata2 = ctx.getImageData(0, 0, -10.01, -10.99); + assert.strictEqual(imgdata1.width, 10, "imgdata1.width", "10") + assert.strictEqual(imgdata1.height, 10, "imgdata1.height", "10") + assert.strictEqual(imgdata2.width, 10, "imgdata2.width", "10") + assert.strictEqual(imgdata2.height, 10, "imgdata2.height", "10") + }); + + it("2d.imageData.get.nonpremul", function () { + // getImageData() returns non-premultiplied colors + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(10, 10, 10, 10); + assert(imgdata.data[0] > 200, "imgdata.data[\""+(0)+"\"] > 200"); + assert(imgdata.data[1] > 200, "imgdata.data[\""+(1)+"\"] > 200"); + assert(imgdata.data[2] > 200, "imgdata.data[\""+(2)+"\"] > 200"); + assert(imgdata.data[3] > 100, "imgdata.data[\""+(3)+"\"] > 100"); + assert(imgdata.data[3] < 200, "imgdata.data[\""+(3)+"\"] < 200"); + }); + + it("2d.imageData.get.range", function () { + // getImageData() returns values in the range [0, 255] + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#fff'; + ctx.fillRect(20, 10, 60, 10); + var imgdata1 = ctx.getImageData(10, 5, 1, 1); + assert.strictEqual(imgdata1.data[0], 0, "imgdata1.data[\""+(0)+"\"]", "0") + var imgdata2 = ctx.getImageData(30, 15, 1, 1); + assert.strictEqual(imgdata2.data[0], 255, "imgdata2.data[\""+(0)+"\"]", "255") + }); + + it("2d.imageData.get.clamp", function () { + // getImageData() clamps colors to the range [0, 255] + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = 'rgb(-100, -200, -300)'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = 'rgb(256, 300, 400)'; + ctx.fillRect(20, 10, 60, 10); + var imgdata1 = ctx.getImageData(10, 5, 1, 1); + assert.strictEqual(imgdata1.data[0], 0, "imgdata1.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata1.data[1], 0, "imgdata1.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata1.data[2], 0, "imgdata1.data[\""+(2)+"\"]", "0") + var imgdata2 = ctx.getImageData(30, 15, 1, 1); + assert.strictEqual(imgdata2.data[0], 255, "imgdata2.data[\""+(0)+"\"]", "255") + assert.strictEqual(imgdata2.data[1], 255, "imgdata2.data[\""+(1)+"\"]", "255") + assert.strictEqual(imgdata2.data[2], 255, "imgdata2.data[\""+(2)+"\"]", "255") + }); + + it("2d.imageData.get.length", function () { + // getImageData() returns a correctly-sized Uint8ClampedArray + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(imgdata.data.length, imgdata.width*imgdata.height*4, "imgdata.data.length", "imgdata.width*imgdata.height*4") + }); + + it("2d.imageData.get.order.cols", function () { + // getImageData() returns leftmost columns first + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 2, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata.data[Math.round(imgdata.width/2*4)], 255, "imgdata.data[Math.round(imgdata.width/2*4)]", "255") + assert.strictEqual(imgdata.data[Math.round((imgdata.height/2)*imgdata.width*4)], 0, "imgdata.data[Math.round((imgdata.height/2)*imgdata.width*4)]", "0") + }); + + it("2d.imageData.get.order.rows", function () { + // getImageData() returns topmost rows first + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 2); + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata.data[Math.floor(imgdata.width/2*4)], 0, "imgdata.data[Math.floor(imgdata.width/2*4)]", "0") + assert.strictEqual(imgdata.data[(imgdata.height/2)*imgdata.width*4], 255, "imgdata.data[(imgdata.height/2)*imgdata.width*4]", "255") + }); + + it("2d.imageData.get.order.rgb", function () { + // getImageData() returns R then G then B + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#48c'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(imgdata.data[0], 0x44, "imgdata.data[\""+(0)+"\"]", "0x44") + assert.strictEqual(imgdata.data[1], 0x88, "imgdata.data[\""+(1)+"\"]", "0x88") + assert.strictEqual(imgdata.data[2], 0xCC, "imgdata.data[\""+(2)+"\"]", "0xCC") + assert.strictEqual(imgdata.data[3], 255, "imgdata.data[\""+(3)+"\"]", "255") + assert.strictEqual(imgdata.data[4], 0x44, "imgdata.data[\""+(4)+"\"]", "0x44") + assert.strictEqual(imgdata.data[5], 0x88, "imgdata.data[\""+(5)+"\"]", "0x88") + assert.strictEqual(imgdata.data[6], 0xCC, "imgdata.data[\""+(6)+"\"]", "0xCC") + assert.strictEqual(imgdata.data[7], 255, "imgdata.data[\""+(7)+"\"]", "255") + }); + + it("2d.imageData.get.order.alpha", function () { + // getImageData() returns A in the fourth component + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert(imgdata.data[3] < 200, "imgdata.data[\""+(3)+"\"] < 200"); + assert(imgdata.data[3] > 100, "imgdata.data[\""+(3)+"\"] > 100"); + }); + + it("2d.imageData.get.unaffected", function () { + // getImageData() is not affected by context state + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50) + ctx.save(); + ctx.translate(50, 0); + ctx.globalAlpha = 0.1; + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.rect(0, 0, 5, 5); + ctx.clip(); + var imgdata = ctx.getImageData(0, 0, 50, 50); + ctx.restore(); + ctx.putImageData(imgdata, 50, 0); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }); + + it.skip("2d.imageData.get.large.crash", function () { + // Test that canvas crash when image data cannot be allocated. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.getImageData(10, 0xffffffff, 2147483647, 10); }, TypeError); + }); + + it("2d.imageData.get.rounding", function () { + // Test the handling of non-integer source coordinates in getImageData(). + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + function testDimensions(sx, sy, sw, sh, width, height) + { + imageData = ctx.getImageData(sx, sy, sw, sh); + assert(imageData.width == width, "imageData.width == width"); + assert(imageData.height == height, "imageData.height == height"); + } + + testDimensions(0, 0, 20, 10, 20, 10); + + testDimensions(.1, .2, 20, 10, 20, 10); + testDimensions(.9, .8, 20, 10, 20, 10); + + testDimensions(0, 0, 20.9, 10.9, 20, 10); + testDimensions(0, 0, 20.1, 10.1, 20, 10); + + testDimensions(-1, -1, 20, 10, 20, 10); + + testDimensions(-1.1, 0, 20, 10, 20, 10); + testDimensions(-1.9, 0, 20, 10, 20, 10); + }); + + it("2d.imageData.get.invalid", function () { + // Verify getImageData() behavior in invalid cases. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + imageData = ctx.getImageData(0,0,2,2); + var testValues = [NaN, true, false, "\"garbage\"", "-1", + "0", "1", "2", Infinity, -Infinity, + -5, -0.5, 0, 0.5, 5, + 5.4, 255, 256, null, undefined]; + var testResults = [0, 1, 0, 0, 0, + 0, 1, 2, 255, 0, + 0, 0, 0, 0, 5, + 5, 255, 255, 0, 0]; + for (var i = 0; i < testValues.length; i++) { + imageData.data[0] = testValues[i]; + assert(imageData.data[0] == testResults[i], "imageData.data[\""+(0)+"\"] == testResults[\""+(i)+"\"]"); + } + imageData.data['foo']='garbage'; + assert(imageData.data['foo'] == 'garbage', "imageData.data['foo'] == 'garbage'"); + imageData.data[-1]='garbage'; + assert(imageData.data[-1] == undefined, "imageData.data[-1] == undefined"); + imageData.data[17]='garbage'; + assert(imageData.data[17] == undefined, "imageData.data[\""+(17)+"\"] == undefined"); + }); + + it("2d.imageData.object.properties", function () { + // ImageData objects have the right properties + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.strictEqual(typeof(imgdata.width), 'number', "typeof(imgdata.width)", "'number'") + assert.strictEqual(typeof(imgdata.height), 'number', "typeof(imgdata.height)", "'number'") + assert.strictEqual(typeof(imgdata.data), 'object', "typeof(imgdata.data)", "'object'") + }); + + it("2d.imageData.object.readonly", function () { + // ImageData objects properties are read-only + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + var w = imgdata.width; + var h = imgdata.height; + var d = imgdata.data; + imgdata.width = 123; + imgdata.height = 123; + imgdata.data = [100,100,100,100]; + assert.strictEqual(imgdata.width, w, "imgdata.width", "w") + assert.strictEqual(imgdata.height, h, "imgdata.height", "h") + assert.strictEqual(imgdata.data, d, "imgdata.data", "d") + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + assert.strictEqual(imgdata.data[1], 0, "imgdata.data[\""+(1)+"\"]", "0") + assert.strictEqual(imgdata.data[2], 0, "imgdata.data[\""+(2)+"\"]", "0") + assert.strictEqual(imgdata.data[3], 0, "imgdata.data[\""+(3)+"\"]", "0") + }); + + it("2d.imageData.object.ctor.size", function () { + // ImageData has a usable constructor + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + + var imgdata = new window.ImageData(2, 3); + assert.strictEqual(imgdata.width, 2, "imgdata.width", "2") + assert.strictEqual(imgdata.height, 3, "imgdata.height", "3") + assert.strictEqual(imgdata.data.length, 2 * 3 * 4, "imgdata.data.length", "2 * 3 * 4") + for (var i = 0; i < imgdata.data.length; ++i) { + assert.strictEqual(imgdata.data[i], 0, "imgdata.data[\""+(i)+"\"]", "0") + } + }); + + it("2d.imageData.object.ctor.basics", function () { + // Testing different type of ImageData constructor + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + function setRGBA(imageData, i, rgba) + { + var s = i * 4; + imageData[s] = rgba[0]; + imageData[s + 1] = rgba[1]; + imageData[s + 2] = rgba[2]; + imageData[s + 3] = rgba[3]; + } + + function getRGBA(imageData, i) + { + var result = []; + var s = i * 4; + for (var j = 0; j < 4; j++) { + result[j] = imageData[s + j]; + } + return result; + } + + function assertArrayEquals(actual, expected) + { + assert.strictEqual(typeof actual, "object", "typeof actual", "\"object\"") + assert.notStrictEqual(actual, null, "actual", "null"); + assert.strictEqual("length" in actual, true, "\"length\" in actual", "true") + assert.strictEqual(actual.length, expected.length, "actual.length", "expected.length") + for (var i = 0; i < actual.length; i++) { + assert.strictEqual(actual.hasOwnProperty(i), expected.hasOwnProperty(i), "actual.hasOwnProperty(i)", "expected.hasOwnProperty(i)") + assert.strictEqual(actual[i], expected[i], "actual[\""+(i)+"\"]", "expected[\""+(i)+"\"]") + } + } + + assert.notStrictEqual(ImageData, undefined, "ImageData", "undefined"); + imageData = new ImageData(100, 50); + + assert.notStrictEqual(imageData, null, "imageData", "null"); + assert.notStrictEqual(imageData.data, null, "imageData.data", "null"); + assert.strictEqual(imageData.width, 100, "imageData.width", "100") + assert.strictEqual(imageData.height, 50, "imageData.height", "50") + assertArrayEquals(getRGBA(imageData.data, 4), [0, 0, 0, 0]); + + var testColor = [0, 255, 255, 128]; + setRGBA(imageData.data, 4, testColor); + assertArrayEquals(getRGBA(imageData.data, 4), testColor); + + assert.throws(function() { new ImageData(10); }, TypeError); + assert.throws(function() { new ImageData(0, 10); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(10, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData('width', 'height'); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(1 << 31, 1 << 31); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(0)); }, TypeError); + assert.throws(function() { new ImageData(new Uint8Array(100), 25); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(27), 2); }, /INVALID_STATE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(28), 7, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(104), 14); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray([12, 34, 168, 65328]), 1, 151); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(self, 4, 4); }, TypeError); + assert.throws(function() { new ImageData(null, 4, 4); }, TypeError); + assert.throws(function() { new ImageData(imageData.data, 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(imageData.data, 13); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(imageData.data, 1 << 31); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(imageData.data, 'biggish'); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(imageData.data, 1 << 24, 1 << 31); }, /INDEX_SIZE_ERR/); + assert.strictEqual(new ImageData(new Uint8ClampedArray(28), 7).height, 1, "new ImageData(new Uint8ClampedArray(28), 7).height", "1") + + imageDataFromData = new ImageData(imageData.data, 100); + assert.strictEqual(imageDataFromData.width, 100, "imageDataFromData.width", "100") + assert.strictEqual(imageDataFromData.height, 50, "imageDataFromData.height", "50") + assert.strictEqual(imageDataFromData.data, imageData.data, "imageDataFromData.data", "imageData.data") + assertArrayEquals(getRGBA(imageDataFromData.data, 10), getRGBA(imageData.data, 10)); + setRGBA(imageData.data, 10, testColor); + assertArrayEquals(getRGBA(imageDataFromData.data, 10), getRGBA(imageData.data, 10)); + + var data = new Uint8ClampedArray(400); + data[22] = 129; + imageDataFromData = new ImageData(data, 20, 5); + assert.strictEqual(imageDataFromData.width, 20, "imageDataFromData.width", "20") + assert.strictEqual(imageDataFromData.height, 5, "imageDataFromData.height", "5") + assert.strictEqual(imageDataFromData.data, data, "imageDataFromData.data", "data") + assertArrayEquals(getRGBA(imageDataFromData.data, 2), getRGBA(data, 2)); + setRGBA(imageDataFromData.data, 2, testColor); + assertArrayEquals(getRGBA(imageDataFromData.data, 2), getRGBA(data, 2)); + + if (window.SharedArrayBuffer) { + assert.throws(function() { new ImageData(new Uint16Array(new SharedArrayBuffer(32)), 4, 2); }, TypeError); + } + }); + + it("2d.imageData.object.ctor.array", function () { + // ImageData has a usable constructor + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + + var array = new Uint8ClampedArray(8); + var imgdata = new window.ImageData(array, 1, 2); + assert.strictEqual(imgdata.width, 1, "imgdata.width", "1") + assert.strictEqual(imgdata.height, 2, "imgdata.height", "2") + assert.strictEqual(imgdata.data, array, "imgdata.data", "array") + }); + + it("2d.imageData.object.ctor.array.bounds", function () { + // ImageData has a usable constructor + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(window.ImageData, undefined, "window.ImageData", "undefined"); + + assert.throws(function() { new ImageData(new Uint8ClampedArray(0), 1); }, /INVALID_STATE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(3), 1); }, /INVALID_STATE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(4), 0); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8ClampedArray(4), 1, 2); }, /INDEX_SIZE_ERR/); + assert.throws(function() { new ImageData(new Uint8Array(8), 1, 2); }, TypeError); + assert.throws(function() { new ImageData(new Int8Array(8), 1, 2); }, TypeError); + }); + + it("2d.imageData.object.set", function () { + // ImageData.data can be modified + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + assert.strictEqual(imgdata.data[0], 100, "imgdata.data[\""+(0)+"\"]", "100") + imgdata.data[0] = 200; + assert.strictEqual(imgdata.data[0], 200, "imgdata.data[\""+(0)+"\"]", "200") + }); + + it("2d.imageData.object.undefined", function () { + // ImageData.data converts undefined to 0 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = undefined; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + }); + + it("2d.imageData.object.nan", function () { + // ImageData.data converts NaN to 0 + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = NaN; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = 100; + imgdata.data[0] = "cheese"; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + }); + + it("2d.imageData.object.string", function () { + // ImageData.data converts strings to numbers with ToNumber + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = "110"; + assert.strictEqual(imgdata.data[0], 110, "imgdata.data[\""+(0)+"\"]", "110") + imgdata.data[0] = 100; + imgdata.data[0] = "0x78"; + assert.strictEqual(imgdata.data[0], 120, "imgdata.data[\""+(0)+"\"]", "120") + imgdata.data[0] = 100; + imgdata.data[0] = " +130e0 "; + assert.strictEqual(imgdata.data[0], 130, "imgdata.data[\""+(0)+"\"]", "130") + }); + + it("2d.imageData.object.clamp", function () { + // ImageData.data clamps numbers to [0, 255] + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + + imgdata.data[0] = 100; + imgdata.data[0] = 300; + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + imgdata.data[0] = 100; + imgdata.data[0] = -100; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + + imgdata.data[0] = 100; + imgdata.data[0] = 200+Math.pow(2, 32); + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + imgdata.data[0] = 100; + imgdata.data[0] = -200-Math.pow(2, 32); + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + + imgdata.data[0] = 100; + imgdata.data[0] = Math.pow(10, 39); + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + imgdata.data[0] = 100; + imgdata.data[0] = -Math.pow(10, 39); + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + + imgdata.data[0] = 100; + imgdata.data[0] = -Infinity; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = 100; + imgdata.data[0] = Infinity; + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + }); + + it("2d.imageData.object.round", function () { + // ImageData.data rounds numbers with round-to-zero + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 0.499; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = 0.5; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = 0.501; + assert.strictEqual(imgdata.data[0], 1, "imgdata.data[\""+(0)+"\"]", "1") + imgdata.data[0] = 1.499; + assert.strictEqual(imgdata.data[0], 1, "imgdata.data[\""+(0)+"\"]", "1") + imgdata.data[0] = 1.5; + assert.strictEqual(imgdata.data[0], 2, "imgdata.data[\""+(0)+"\"]", "2") + imgdata.data[0] = 1.501; + assert.strictEqual(imgdata.data[0], 2, "imgdata.data[\""+(0)+"\"]", "2") + imgdata.data[0] = 2.5; + assert.strictEqual(imgdata.data[0], 2, "imgdata.data[\""+(0)+"\"]", "2") + imgdata.data[0] = 3.5; + assert.strictEqual(imgdata.data[0], 4, "imgdata.data[\""+(0)+"\"]", "4") + imgdata.data[0] = 252.5; + assert.strictEqual(imgdata.data[0], 252, "imgdata.data[\""+(0)+"\"]", "252") + imgdata.data[0] = 253.5; + assert.strictEqual(imgdata.data[0], 254, "imgdata.data[\""+(0)+"\"]", "254") + imgdata.data[0] = 254.5; + assert.strictEqual(imgdata.data[0], 254, "imgdata.data[\""+(0)+"\"]", "254") + imgdata.data[0] = 256.5; + assert.strictEqual(imgdata.data[0], 255, "imgdata.data[\""+(0)+"\"]", "255") + imgdata.data[0] = -0.5; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + imgdata.data[0] = -1.5; + assert.strictEqual(imgdata.data[0], 0, "imgdata.data[\""+(0)+"\"]", "0") + }); + + it("2d.imageData.put.null", function () { + // putImageData() with null imagedata throws TypeError + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.throws(function() { ctx.putImageData(null, 0, 0); }, TypeError); + }); + + it("2d.imageData.put.nonfinite", function () { + // putImageData() throws TypeError if arguments are not finite + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.getImageData(0, 0, 10, 10); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, -Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, NaN, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, -Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, NaN); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, -Infinity, 10, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, NaN, 10, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, -Infinity, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, NaN, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, -Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, NaN, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, -Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, NaN, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, -Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, NaN, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, 10, -Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, 10, NaN); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, Infinity, 10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, Infinity, 10, 10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, 10, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, Infinity, 10, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, Infinity, 10, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, 10, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, 10, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, Infinity, 10, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, Infinity, Infinity, 10); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, Infinity, Infinity, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, Infinity, 10, Infinity); }, TypeError); + assert.throws(function() { ctx.putImageData(imgdata, 10, 10, 10, 10, Infinity, Infinity); }, TypeError); + }); + + it("2d.imageData.put.basic", function () { + // putImageData() puts image data from getImageData() onto the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.created", function () { + // putImageData() puts image data from createImageData() onto the canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = ctx.createImageData(100, 50); + for (var i = 0; i < imgdata.data.length; i += 4) { + imgdata.data[i] = 0; + imgdata.data[i+1] = 255; + imgdata.data[i+2] = 0; + imgdata.data[i+3] = 255; + } + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.wrongtype", function () { + // putImageData() does not accept non-ImageData objects + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var imgdata = { width: 1, height: 1, data: [255, 0, 0, 255] }; + assert.throws(function() { ctx.putImageData(imgdata, 0, 0); }, TypeError); + assert.throws(function() { ctx.putImageData("cheese", 0, 0); }, TypeError); + assert.throws(function() { ctx.putImageData(42, 0, 0); }, TypeError); + }); + + it("2d.imageData.put.cross", function () { + // putImageData() accepts image data got from a different canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50) + var imgdata = ctx2.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.alpha", function () { + // putImageData() puts non-solid image data correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = 'rgba(0, 255, 0, 0.25)'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,64); + }); + + it("2d.imageData.put.modified", function () { + // putImageData() puts modified image data correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(45, 20, 10, 10) + var imgdata = ctx.getImageData(45, 20, 10, 10); + for (var i = 0, len = imgdata.width*imgdata.height*4; i < len; i += 4) + { + imgdata.data[i] = 0; + imgdata.data[i+1] = 255; + } + ctx.putImageData(imgdata, 45, 20); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.zero", function () { + // putImageData() with zero-sized dirty rectangle puts nothing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0, 0, 0, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.rect1", function () { + // putImageData() only modifies areas inside the dirty rectangle, using width and height + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, 40, 20, 0, 0, 20, 20); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 35,25, 0,255,0,255); + _assertPixelApprox(canvas, 65,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,15, 0,255,0,255); + _assertPixelApprox(canvas, 50,45, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.rect2", function () { + // putImageData() only modifies areas inside the dirty rectangle, using x and y + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(60, 30, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, -20, -10, 60, 30, 20, 20); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 35,25, 0,255,0,255); + _assertPixelApprox(canvas, 65,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,15, 0,255,0,255); + _assertPixelApprox(canvas, 50,45, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.negative", function () { + // putImageData() handles negative-sized dirty rectangles correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, 40, 20, 20, 20, -20, -20); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 35,25, 0,255,0,255); + _assertPixelApprox(canvas, 65,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,15, 0,255,0,255); + _assertPixelApprox(canvas, 50,45, 0,255,0,255); + }); + + it("2d.imageData.put.dirty.outside", function () { + // putImageData() handles dirty rectangles outside the canvas correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + + ctx.putImageData(imgdata, 100, 20, 20, 20, -20, -20); + ctx.putImageData(imgdata, 200, 200, 0, 0, 100, 50); + ctx.putImageData(imgdata, 40, 20, -30, -20, 30, 20); + ctx.putImageData(imgdata, -30, 20, 0, 0, 30, 20); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 98,15, 0,255,0,255); + _assertPixelApprox(canvas, 98,25, 0,255,0,255); + _assertPixelApprox(canvas, 98,45, 0,255,0,255); + _assertPixelApprox(canvas, 1,5, 0,255,0,255); + _assertPixelApprox(canvas, 1,25, 0,255,0,255); + _assertPixelApprox(canvas, 1,45, 0,255,0,255); + }); + + it("2d.imageData.put.unchanged", function () { + // putImageData(getImageData(...), ...) has no effect + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var i = 0; + for (var y = 0; y < 16; ++y) { + for (var x = 0; x < 16; ++x, ++i) { + ctx.fillStyle = 'rgba(' + i + ',' + (Math.floor(i*1.5) % 256) + ',' + (Math.floor(i*23.3) % 256) + ',' + (i/256) + ')'; + ctx.fillRect(x, y, 1, 1); + } + } + var imgdata1 = ctx.getImageData(0.1, 0.2, 15.8, 15.9); + var olddata = []; + for (var i = 0; i < imgdata1.data.length; ++i) + olddata[i] = imgdata1.data[i]; + + ctx.putImageData(imgdata1, 0.1, 0.2); + + var imgdata2 = ctx.getImageData(0.1, 0.2, 15.8, 15.9); + for (var i = 0; i < imgdata2.data.length; ++i) { + assert.strictEqual(olddata[i], imgdata2.data[i], "olddata[\""+(i)+"\"]", "imgdata2.data[\""+(i)+"\"]") + } + }); + + it("2d.imageData.put.unaffected", function () { + // putImageData() is not affected by context state + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.globalAlpha = 0.1; + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 1; + ctx.translate(100, 50); + ctx.scale(0.1, 0.1); + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.imageData.put.clip", function () { + // putImageData() is not affected by clipping regions + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.putImageData(imgdata, 0, 0); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }); + + it("2d.imageData.put.path", function () { + // putImageData() does not affect the current path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.rect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.putImageData(imgdata, 0, 0); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); +}); diff --git a/test/wpt/generated/shadows.js b/test/wpt/generated/shadows.js new file mode 100644 index 000000000..91a138519 --- /dev/null +++ b/test/wpt/generated/shadows.js @@ -0,0 +1,1203 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: shadows", function () { + + it("2d.shadow.attributes.shadowBlur.initial", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.shadowBlur, 0, "ctx.shadowBlur", "0") + }); + + it("2d.shadow.attributes.shadowBlur.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowBlur = 1; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 0.5; + assert.strictEqual(ctx.shadowBlur, 0.5, "ctx.shadowBlur", "0.5") + + ctx.shadowBlur = 1e6; + assert.strictEqual(ctx.shadowBlur, 1e6, "ctx.shadowBlur", "1e6") + + ctx.shadowBlur = 0; + assert.strictEqual(ctx.shadowBlur, 0, "ctx.shadowBlur", "0") + }); + + it("2d.shadow.attributes.shadowBlur.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowBlur = 1; + ctx.shadowBlur = -2; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = Infinity; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = -Infinity; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = NaN; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = 'string'; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = true; + assert.strictEqual(ctx.shadowBlur, 1, "ctx.shadowBlur", "1") + + ctx.shadowBlur = 1; + ctx.shadowBlur = false; + assert.strictEqual(ctx.shadowBlur, 0, "ctx.shadowBlur", "0") + }); + + it("2d.shadow.attributes.shadowOffset.initial", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.shadowOffsetX, 0, "ctx.shadowOffsetX", "0") + assert.strictEqual(ctx.shadowOffsetY, 0, "ctx.shadowOffsetY", "0") + }); + + it("2d.shadow.attributes.shadowOffset.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 0.5; + ctx.shadowOffsetY = 0.25; + assert.strictEqual(ctx.shadowOffsetX, 0.5, "ctx.shadowOffsetX", "0.5") + assert.strictEqual(ctx.shadowOffsetY, 0.25, "ctx.shadowOffsetY", "0.25") + + ctx.shadowOffsetX = -0.5; + ctx.shadowOffsetY = -0.25; + assert.strictEqual(ctx.shadowOffsetX, -0.5, "ctx.shadowOffsetX", "-0.5") + assert.strictEqual(ctx.shadowOffsetY, -0.25, "ctx.shadowOffsetY", "-0.25") + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + assert.strictEqual(ctx.shadowOffsetX, 0, "ctx.shadowOffsetX", "0") + assert.strictEqual(ctx.shadowOffsetY, 0, "ctx.shadowOffsetY", "0") + + ctx.shadowOffsetX = 1e6; + ctx.shadowOffsetY = 1e6; + assert.strictEqual(ctx.shadowOffsetX, 1e6, "ctx.shadowOffsetX", "1e6") + assert.strictEqual(ctx.shadowOffsetY, 1e6, "ctx.shadowOffsetY", "1e6") + }); + + it("2d.shadow.attributes.shadowOffset.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = Infinity; + ctx.shadowOffsetY = Infinity; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = -Infinity; + ctx.shadowOffsetY = -Infinity; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = NaN; + ctx.shadowOffsetY = NaN; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = 'string'; + ctx.shadowOffsetY = 'string'; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 2, "ctx.shadowOffsetY", "2") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = true; + ctx.shadowOffsetY = true; + assert.strictEqual(ctx.shadowOffsetX, 1, "ctx.shadowOffsetX", "1") + assert.strictEqual(ctx.shadowOffsetY, 1, "ctx.shadowOffsetY", "1") + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = false; + ctx.shadowOffsetY = false; + assert.strictEqual(ctx.shadowOffsetX, 0, "ctx.shadowOffsetX", "0") + assert.strictEqual(ctx.shadowOffsetY, 0, "ctx.shadowOffsetY", "0") + }); + + it("2d.shadow.attributes.shadowColor.initial", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.shadowColor, 'rgba(0, 0, 0, 0)', "ctx.shadowColor", "'rgba(0, 0, 0, 0)'") + }); + + it("2d.shadow.attributes.shadowColor.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowColor = 'lime'; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + + ctx.shadowColor = 'RGBA(0,255, 0,0)'; + assert.strictEqual(ctx.shadowColor, 'rgba(0, 255, 0, 0)', "ctx.shadowColor", "'rgba(0, 255, 0, 0)'") + }); + + it("2d.shadow.attributes.shadowColor.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = 'bogus'; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = 'red bogus'; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = ctx; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = undefined; + assert.strictEqual(ctx.shadowColor, '#00ff00', "ctx.shadowColor", "'#00ff00'") + }); + + it("2d.shadow.enable.off.1", function () { + // Shadows are not drawn when only shadowColor is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.shadowColor = '#f00'; + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.enable.off.2", function () { + // Shadows are not drawn when only shadowColor is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.enable.blur", function () { + // Shadows are drawn if shadowBlur is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowBlur = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.enable.x", function () { + // Shadows are drawn if shadowOffsetX is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.enable.y", function () { + // Shadows are drawn if shadowOffsetY is set + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.offset.positiveX", function () { + // Shadows can be offset with positive x + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.offset.negativeX", function () { + // Shadows can be offset with negative x + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = -50; + ctx.fillRect(50, 0, 50, 50); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.offset.positiveY", function () { + // Shadows can be offset with positive y + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 25; + ctx.fillRect(0, 0, 100, 25); + _assertPixel(canvas, 50,12, 0,255,0,255); + _assertPixel(canvas, 50,37, 0,255,0,255); + }); + + it("2d.shadow.offset.negativeY", function () { + // Shadows can be offset with negative y + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = -25; + ctx.fillRect(0, 25, 100, 25); + _assertPixel(canvas, 50,12, 0,255,0,255); + _assertPixel(canvas, 50,37, 0,255,0,255); + }); + + it("2d.shadow.outside", function () { + // Shadows of shapes outside the visible area can be offset onto the visible area + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 100; + ctx.fillRect(-100, 0, 25, 50); + ctx.shadowOffsetX = -100; + ctx.fillRect(175, 0, 25, 50); + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 100; + ctx.fillRect(25, -100, 50, 25); + ctx.shadowOffsetY = -100; + ctx.fillRect(25, 125, 50, 25); + _assertPixel(canvas, 12,25, 0,255,0,255); + _assertPixel(canvas, 87,25, 0,255,0,255); + _assertPixel(canvas, 50,12, 0,255,0,255); + _assertPixel(canvas, 50,37, 0,255,0,255); + }); + + it("2d.shadow.clip.1", function () { + // Shadows of clipped shapes are still drawn within the clipping region + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(50, 0, 50, 50); + ctx.clip(); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + ctx.restore(); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.clip.2", function () { + // Shadows are not drawn outside the clipping region + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + ctx.restore(); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.clip.3", function () { + // Shadows of clipped shapes are still drawn within the clipping region + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.fillStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(-50, 0, 50, 50); + ctx.restore(); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.stroke.basic", function () { + // Shadows are drawn for strokes + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.moveTo(0, -25); + ctx.lineTo(100, -25); + ctx.stroke(); + + _assertPixel(canvas, 1,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,25, 0,255,0,255); + }); + + it("2d.shadow.stroke.cap.1", function () { + // Shadows are not drawn for areas outside stroke caps + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.lineCap = 'butt'; + ctx.moveTo(-50, -25); + ctx.lineTo(0, -25); + ctx.moveTo(100, -25); + ctx.lineTo(150, -25); + ctx.stroke(); + + _assertPixel(canvas, 1,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,25, 0,255,0,255); + }); + + it("2d.shadow.stroke.cap.2", function () { + // Shadows are drawn for stroke caps + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.lineCap = 'square'; + ctx.moveTo(25, -25); + ctx.lineTo(75, -25); + ctx.stroke(); + + _assertPixel(canvas, 1,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,25, 0,255,0,255); + }); + + it("2d.shadow.stroke.join.1", function () { + // Shadows are not drawn for areas outside stroke joins + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'bevel'; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.shadow.stroke.join.2", function () { + // Shadows are drawn for stroke joins + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.shadow.stroke.join.3", function () { + // Shadows are drawn for stroke joins respecting miter limit + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 0.1; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); // (not an exact right angle, to avoid some other bug in Firefox 3) + ctx.stroke(); + + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,48, 0,255,0,255); + }); + + it("2d.shadow.image.basic", function () { + // Shadows are drawn for images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.drawImage(document.getElementById('red.png'), 0, -50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.image.transparent.1", function () { + // Shadows are not drawn for transparent images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.drawImage(document.getElementById('transparent.png'), 0, -50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.image.transparent.2", function () { + // Shadows are not drawn for transparent parts of images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(document.getElementById('redtransparent.png'), 50, -50); + ctx.shadowColor = '#f00'; + ctx.drawImage(document.getElementById('redtransparent.png'), -50, -50); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.image.alpha", function () { + // Shadows are drawn correctly for partially-transparent images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.drawImage(document.getElementById('transparent50.png'), 0, -50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.image.section", function () { + // Shadows are not drawn for areas outside image source rectangles + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#f00'; + ctx.drawImage(document.getElementById('redtransparent.png'), 50, 0, 50, 50, 0, -50, 50, 50); + + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.image.scale", function () { + // Shadows are drawn correctly for scaled images + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(document.getElementById('redtransparent.png'), 0, 0, 100, 50, -10, -50, 240, 50); + + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.canvas.basic", function () { + // Shadows are drawn for canvases + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.drawImage(canvas2, 0, -50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.canvas.transparent.1", function () { + // Shadows are not drawn for transparent canvases + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.drawImage(canvas2, 0, -50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.canvas.transparent.2", function () { + // Shadows are not drawn for transparent parts of canvases + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 50, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(canvas2, 50, -50); + ctx.shadowColor = '#f00'; + ctx.drawImage(canvas2, -50, -50); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.canvas.alpha", function () { + // Shadows are drawn correctly for partially-transparent canvases + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = 'rgba(255, 0, 0, 0.5)'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.drawImage(canvas2, 0, -50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.pattern.basic", function () { + // Shadows are drawn for fill patterns + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var pattern = ctx.createPattern(document.getElementById('red.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.pattern.transparent.1", function () { + // Shadows are not drawn for transparent fill patterns + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var pattern = ctx.createPattern(document.getElementById('transparent.png'), 'repeat'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.pattern.transparent.2", function () { + // Shadows are not drawn for transparent parts of fill patterns + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var pattern = ctx.createPattern(document.getElementById('redtransparent.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.pattern.alpha", function () { + // Shadows are drawn correctly for partially-transparent fill patterns + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var pattern = ctx.createPattern(document.getElementById('transparent50.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.gradient.basic", function () { + // Shadows are drawn for gradient fills + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, '#f00'); + gradient.addColorStop(1, '#f00'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.gradient.transparent.1", function () { + // Shadows are not drawn for transparent gradient fills + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, 'rgba(0,0,0,0)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.gradient.transparent.2", function () { + // Shadows are not drawn for transparent parts of gradient fills + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, '#f00'); + gradient.addColorStop(0.499, '#f00'); + gradient.addColorStop(0.5, 'rgba(0,0,0,0)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.shadow.gradient.alpha", function () { + // Shadows are drawn correctly for partially-transparent gradient fills + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, 'rgba(255,0,0,0.5)'); + gradient.addColorStop(1, 'rgba(255,0,0,0.5)'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.transform.1", function () { + // Shadows take account of transformations + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.translate(100, 100); + ctx.fillRect(-100, -150, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.transform.2", function () { + // Shadow offsets are not affected by transformations + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.rotate(Math.PI) + ctx.fillRect(-100, 0, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.blur.low", function () { + // Shadows look correct for small blurs + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 25; + for (var x = 0; x < 100; ++x) { + ctx.save(); + ctx.beginPath(); + ctx.rect(x, 0, 1, 50); + ctx.clip(); + ctx.shadowBlur = x; + ctx.fillRect(-200, -200, 500, 200); + ctx.restore(); + } + }); + + it("2d.shadow.blur.high", function () { + // Shadows look correct for large blurs + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 100; + ctx.fillRect(-200, -200, 200, 400); + }); + + it("2d.shadow.alpha.1", function () { + // Shadow color alpha components are used + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = 'rgba(255, 0, 0, 0.01)'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255, 4); + }); + + it("2d.shadow.alpha.2", function () { + // Shadow color alpha components are used + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = 'rgba(0, 0, 255, 0.5)'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.alpha.3", function () { + // Shadows are affected by globalAlpha + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; // (work around broken Firefox globalAlpha caching) + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 50; + ctx.globalAlpha = 0.5; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.alpha.4", function () { + // Shadows with alpha components are correctly affected by globalAlpha + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; // (work around broken Firefox globalAlpha caching) + ctx.shadowColor = 'rgba(0, 0, 255, 0.707)'; + ctx.shadowOffsetY = 50; + ctx.globalAlpha = 0.707; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.alpha.5", function () { + // Shadows of shapes with alpha components are drawn correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = 'rgba(64, 0, 0, 0.5)'; + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + _assertPixelApprox(canvas, 50,25, 127,0,127,255); + }); + + it("2d.shadow.composite.1", function () { + // Shadows are drawn using globalCompositeOperation + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'xor'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, 0, 200, 50); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.composite.2", function () { + // Shadows are drawn using globalCompositeOperation + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'xor'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 1; + ctx.fillStyle = '#0f0'; + ctx.fillRect(-10, -10, 120, 70); + + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); + + it("2d.shadow.composite.3", function () { + // Areas outside shadows are drawn correctly with destination-out + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'destination-out'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 10; + ctx.fillStyle = '#f00'; + ctx.fillRect(200, 0, 100, 50); + + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 50,25, 0,255,0,255); + }); +}); diff --git a/test/wpt/generated/text-styles.js b/test/wpt/generated/text-styles.js new file mode 100644 index 000000000..3c227841e --- /dev/null +++ b/test/wpt/generated/text-styles.js @@ -0,0 +1,614 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: text-styles", function () { + + it("2d.text.font.parse.basic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '20px serif'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20PX SERIF'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + }); + + it("2d.text.font.parse.tiny", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '1px sans-serif'; + assert.strictEqual(ctx.font, '1px sans-serif', "ctx.font", "'1px sans-serif'") + }); + + it("2d.text.font.parse.complex", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = 'small-caps italic 400 12px/2 Unknown Font, sans-serif'; + assert.strictEqual(ctx.font, 'italic small-caps 12px "Unknown Font", sans-serif', "ctx.font", "'italic small-caps 12px \"Unknown Font\", sans-serif'") + }); + + it("2d.text.font.parse.family", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '20px cursive,fantasy,monospace,sans-serif,serif,UnquotedFont,"QuotedFont\\\\\\","'; + assert.strictEqual(ctx.font, '20px cursive, fantasy, monospace, sans-serif, serif, UnquotedFont, "QuotedFont\\\\\\","', "ctx.font", "'20px cursive, fantasy, monospace, sans-serif, serif, UnquotedFont, \"QuotedFont\\\\\\\\\\\\\",\"'") + }); + + it("2d.text.font.parse.size.percentage", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50% serif'; + assert.strictEqual(ctx.font, '72px serif', "ctx.font", "'72px serif'") + canvas.setAttribute('style', 'font-size: 100px'); + assert.strictEqual(ctx.font, '72px serif', "ctx.font", "'72px serif'") + }); + + it("2d.text.font.parse.size.percentage.default", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.font = '1000% serif'; + assert.strictEqual(ctx2.font, '100px serif', "ctx2.font", "'100px serif'") + }); + + it("2d.text.font.parse.system", function () { + // System fonts must be computed to explicit values + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = 'message-box'; + assert.notStrictEqual(ctx.font, 'message-box', "ctx.font", "'message-box'"); + }); + + it("2d.text.font.parse.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '20px serif'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = ''; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = 'bogus'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = 'inherit'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px {bogus}'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px initial'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px default'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px inherit'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '10px revert'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = 'var(--x)'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = 'var(--x, 10px serif)'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + + ctx.font = '20px serif'; + ctx.font = '1em serif; background: green; margin: 10px'; + assert.strictEqual(ctx.font, '20px serif', "ctx.font", "'20px serif'") + }); + + it("2d.text.font.default", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.font, '10px sans-serif', "ctx.font", "'10px sans-serif'") + }); + + it("2d.text.font.relative_size", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.font = '1em sans-serif'; + assert.strictEqual(ctx2.font, '10px sans-serif', "ctx2.font", "'10px sans-serif'") + }); + + it("2d.text.align.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textAlign = 'start'; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + + ctx.textAlign = 'end'; + assert.strictEqual(ctx.textAlign, 'end', "ctx.textAlign", "'end'") + + ctx.textAlign = 'left'; + assert.strictEqual(ctx.textAlign, 'left', "ctx.textAlign", "'left'") + + ctx.textAlign = 'right'; + assert.strictEqual(ctx.textAlign, 'right', "ctx.textAlign", "'right'") + + ctx.textAlign = 'center'; + assert.strictEqual(ctx.textAlign, 'center', "ctx.textAlign", "'center'") + }); + + it("2d.text.align.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textAlign = 'start'; + ctx.textAlign = 'bogus'; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + + ctx.textAlign = 'start'; + ctx.textAlign = 'END'; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + + ctx.textAlign = 'start'; + ctx.textAlign = 'end '; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + + ctx.textAlign = 'start'; + ctx.textAlign = 'end\0'; + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + }); + + it("2d.text.align.default", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.textAlign, 'start', "ctx.textAlign", "'start'") + }); + + it("2d.text.baseline.valid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textBaseline = 'top'; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + + ctx.textBaseline = 'hanging'; + assert.strictEqual(ctx.textBaseline, 'hanging', "ctx.textBaseline", "'hanging'") + + ctx.textBaseline = 'middle'; + assert.strictEqual(ctx.textBaseline, 'middle', "ctx.textBaseline", "'middle'") + + ctx.textBaseline = 'alphabetic'; + assert.strictEqual(ctx.textBaseline, 'alphabetic', "ctx.textBaseline", "'alphabetic'") + + ctx.textBaseline = 'ideographic'; + assert.strictEqual(ctx.textBaseline, 'ideographic', "ctx.textBaseline", "'ideographic'") + + ctx.textBaseline = 'bottom'; + assert.strictEqual(ctx.textBaseline, 'bottom', "ctx.textBaseline", "'bottom'") + }); + + it("2d.text.baseline.invalid", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'bogus'; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'MIDDLE'; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle '; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle\0'; + assert.strictEqual(ctx.textBaseline, 'top', "ctx.textBaseline", "'top'") + }); + + it("2d.text.baseline.default", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.textBaseline, 'alphabetic', "ctx.textBaseline", "'alphabetic'") + }); + + it("2d.text.draw.baseline.top", function () { + // textBaseline top is the top of the em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'top'; + ctx.fillText('CC', 0, 0); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.bottom", function () { + // textBaseline bottom is the bottom of the em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'bottom'; + ctx.fillText('CC', 0, 50); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.middle", function () { + // textBaseline middle is the middle of the em square (not the bounding box) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'middle'; + ctx.fillText('CC', 0, 25); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.alphabetic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'alphabetic'; + ctx.fillText('CC', 0, 37.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.ideographic", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'ideographic'; + ctx.fillText('CC', 0, 31.25); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.baseline.hanging", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'hanging'; + ctx.fillText('CC', 0, 12.5); + _assertPixelApprox(canvas, 5,5, 0,255,0,255); + _assertPixelApprox(canvas, 95,5, 0,255,0,255); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + _assertPixelApprox(canvas, 5,45, 0,255,0,255); + _assertPixelApprox(canvas, 95,45, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.space", function () { + // Space characters are converted to U+0020, and collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E EE', -100, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.other", function () { + // Space characters are converted to U+0020, and collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dEE', -100, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.start", function () { + // Space characters at the start of a line are collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText(' EE', 0, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.draw.space.collapse.end", function () { + // Space characters at the end of a line are collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'right'; + ctx.fillText('EE ', 100, 37.5); + _assertPixelApprox(canvas, 25,25, 0,255,0,255); + _assertPixelApprox(canvas, 75,25, 0,255,0,255); + }), 500); + }); + + it("2d.text.measure.width.space", function () { + // Space characters are converted to U+0020 and collapsed (per CSS) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + assert.strictEqual(ctx.measureText('A B').width, 150, "ctx.measureText('A B').width", "150") + assert.strictEqual(ctx.measureText('A B').width, 200, "ctx.measureText('A B').width", "200") + assert.strictEqual(ctx.measureText('A \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dB').width, 150, "ctx.measureText('A \\x09\\x0a\\x0c\\x0d \\x09\\x0a\\x0c\\x0dB').width", "150") + assert(ctx.measureText('A \x0b B').width >= 200, "ctx.measureText('A \\x0b B').width >= 200"); + + assert.strictEqual(ctx.measureText(' AB').width, 100, "ctx.measureText(' AB').width", "100") + assert.strictEqual(ctx.measureText('AB ').width, 100, "ctx.measureText('AB ').width", "100") + }), 500); + }); + }); + + it("2d.text.measure.rtl.text", function () { + // Measurement should follow canvas direction instead text direction + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + metrics = ctx.measureText('اَلْعَرَبِيَّةُ'); + assert(metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight"); + + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight"); + }); + + it("2d.text.measure.boundingBox.textAlign", function () { + // Measurement should be related to textAlignment + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.textAlign = "right"; + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight"); + + ctx.textAlign = "left" + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight"); + }); + + it("2d.text.measure.boundingBox.direction", function () { + // Measurement should follow text direction + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.direction = "ltr"; + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight"); + + ctx.direction = "rtl"; + metrics = ctx.measureText('hello'); + assert(metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight, "metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight"); + }); +}); diff --git a/test/wpt/generated/the-canvas-element.js b/test/wpt/generated/the-canvas-element.js new file mode 100644 index 000000000..cea4fd9b4 --- /dev/null +++ b/test/wpt/generated/the-canvas-element.js @@ -0,0 +1,279 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: the-canvas-element", function () { + + it("2d.getcontext.exists", function () { + // The 2D context is implemented + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(canvas.getContext('2d'), null, "canvas.getContext('2d')", "null"); + }); + + it("2d.getcontext.invalid.args", function () { + // Calling getContext with invalid arguments. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(canvas.getContext(''), null, "canvas.getContext('')", "null") + assert.strictEqual(canvas.getContext('2d#'), null, "canvas.getContext('2d#')", "null") + assert.strictEqual(canvas.getContext('This is clearly not a valid context name.'), null, "canvas.getContext('This is clearly not a valid context name.')", "null") + assert.strictEqual(canvas.getContext('2d\0'), null, "canvas.getContext('2d\\0')", "null") + assert.strictEqual(canvas.getContext('2\uFF44'), null, "canvas.getContext('2\\uFF44')", "null") + assert.strictEqual(canvas.getContext('2D'), null, "canvas.getContext('2D')", "null") + assert.throws(function() { canvas.getContext(); }, TypeError); + assert.strictEqual(canvas.getContext('null'), null, "canvas.getContext('null')", "null") + assert.strictEqual(canvas.getContext('undefined'), null, "canvas.getContext('undefined')", "null") + }); + + it("2d.getcontext.extraargs.create", function () { + // The 2D context doesn't throw with extra getContext arguments (new context) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(document.createElement("canvas").getContext('2d', false, {}, [], 1, "2"), null, "document.createElement(\"canvas\").getContext('2d', false, {}, [], 1, \"2\")", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', 123), null, "document.createElement(\"canvas\").getContext('2d', 123)", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', "test"), null, "document.createElement(\"canvas\").getContext('2d', \"test\")", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', undefined), null, "document.createElement(\"canvas\").getContext('2d', undefined)", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', null), null, "document.createElement(\"canvas\").getContext('2d', null)", "null"); + assert.notStrictEqual(document.createElement("canvas").getContext('2d', Symbol.hasInstance), null, "document.createElement(\"canvas\").getContext('2d', Symbol.hasInstance)", "null"); + }); + + it("2d.getcontext.extraargs.cache", function () { + // The 2D context doesn't throw with extra getContext arguments (cached) + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.notStrictEqual(canvas.getContext('2d', false, {}, [], 1, "2"), null, "canvas.getContext('2d', false, {}, [], 1, \"2\")", "null"); + assert.notStrictEqual(canvas.getContext('2d', 123), null, "canvas.getContext('2d', 123)", "null"); + assert.notStrictEqual(canvas.getContext('2d', "test"), null, "canvas.getContext('2d', \"test\")", "null"); + assert.notStrictEqual(canvas.getContext('2d', undefined), null, "canvas.getContext('2d', undefined)", "null"); + assert.notStrictEqual(canvas.getContext('2d', null), null, "canvas.getContext('2d', null)", "null"); + assert.notStrictEqual(canvas.getContext('2d', Symbol.hasInstance), null, "canvas.getContext('2d', Symbol.hasInstance)", "null"); + }); + + it("2d.type.exists", function () { + // The 2D context interface is a property of 'window' + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert(window.CanvasRenderingContext2D, "window.CanvasRenderingContext2D"); + }); + + it("2d.type.prototype", function () { + // window.CanvasRenderingContext2D.prototype are not [[Writable]] and not [[Configurable]], and its methods are [[Configurable]]. + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert(window.CanvasRenderingContext2D.prototype, "window.CanvasRenderingContext2D.prototype"); + assert(window.CanvasRenderingContext2D.prototype.fill, "window.CanvasRenderingContext2D.prototype.fill"); + window.CanvasRenderingContext2D.prototype = null; + assert(window.CanvasRenderingContext2D.prototype, "window.CanvasRenderingContext2D.prototype"); + delete window.CanvasRenderingContext2D.prototype; + assert(window.CanvasRenderingContext2D.prototype, "window.CanvasRenderingContext2D.prototype"); + window.CanvasRenderingContext2D.prototype.fill = 1; + assert.strictEqual(window.CanvasRenderingContext2D.prototype.fill, 1, "window.CanvasRenderingContext2D.prototype.fill", "1") + delete window.CanvasRenderingContext2D.prototype.fill; + assert.strictEqual(window.CanvasRenderingContext2D.prototype.fill, undefined, "window.CanvasRenderingContext2D.prototype.fill", "undefined") + }); + + it("2d.type class string", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + assert.strictEqual(Object.prototype.toString.call(ctx), '[object CanvasRenderingContext2D]') + }) + + it("2d.type.replace", function () { + // Interface methods can be overridden + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var fillRect = window.CanvasRenderingContext2D.prototype.fillRect; + window.CanvasRenderingContext2D.prototype.fillRect = function (x, y, w, h) + { + this.fillStyle = '#0f0'; + fillRect.call(this, x, y, w, h); + }; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.type.extend", function () { + // Interface methods can be added + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + window.CanvasRenderingContext2D.prototype.fillRectGreen = function (x, y, w, h) + { + this.fillStyle = '#0f0'; + this.fillRect(x, y, w, h); + }; + ctx.fillStyle = '#f00'; + ctx.fillRectGreen(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.getcontext.unique", function () { + // getContext('2d') returns the same object + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(canvas.getContext('2d'), canvas.getContext('2d'), "canvas.getContext('2d')", "canvas.getContext('2d')") + }); + + it("2d.getcontext.shared", function () { + // getContext('2d') returns objects which share canvas state + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var ctx2 = canvas.getContext('2d'); + ctx.fillStyle = '#f00'; + ctx2.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.scaled", function () { + // CSS-scaled canvases get drawn correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 50, 25); + ctx.fillStyle = '#0ff'; + ctx.fillRect(0, 0, 25, 10); + }); + + it("2d.canvas.reference", function () { + // CanvasRenderingContext2D.canvas refers back to its canvas + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(ctx.canvas, canvas, "ctx.canvas", "canvas") + }); + + it("2d.canvas.readonly", function () { + // CanvasRenderingContext2D.canvas is readonly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var c = document.createElement('canvas'); + var d = ctx.canvas; + assert.notStrictEqual(c, d, "c", "d"); + ctx.canvas = c; + assert.strictEqual(ctx.canvas, d, "ctx.canvas", "d") + }); + + it("2d.canvas.context", function () { + // checks CanvasRenderingContext2D prototype + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + assert.strictEqual(Object.getPrototypeOf(CanvasRenderingContext2D.prototype), Object.prototype, "Object.getPrototypeOf(CanvasRenderingContext2D.prototype)", "Object.prototype") + assert.strictEqual(Object.getPrototypeOf(ctx), CanvasRenderingContext2D.prototype, "Object.getPrototypeOf(ctx)", "CanvasRenderingContext2D.prototype") + t.done(); + }); +}); diff --git a/test/wpt/generated/the-canvas-state.js b/test/wpt/generated/the-canvas-state.js new file mode 100644 index 000000000..393bc6cd2 --- /dev/null +++ b/test/wpt/generated/the-canvas-state.js @@ -0,0 +1,206 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: the-canvas-state", function () { + + it("2d.state.saverestore.transformation", function () { + // save()/restore() affects the current transformation matrix + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.translate(200, 0); + ctx.restore(); + ctx.fillStyle = '#f00'; + ctx.fillRect(-200, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.state.saverestore.clip", function () { + // save()/restore() affects the clipping path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.rect(0, 0, 1, 1); + ctx.clip(); + ctx.restore(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.state.saverestore.path", function () { + // save()/restore() does not affect the current path + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.rect(0, 0, 100, 50); + ctx.restore(); + ctx.fillStyle = '#0f0'; + ctx.fill(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.state.saverestore.bitmap", function () { + // save()/restore() does not affect the current bitmap + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.state.saverestore.stack", function () { + // save()/restore() can be nested as a stack + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.lineWidth = 1; + ctx.save(); + ctx.lineWidth = 2; + ctx.save(); + ctx.lineWidth = 3; + assert.strictEqual(ctx.lineWidth, 3, "ctx.lineWidth", "3") + ctx.restore(); + assert.strictEqual(ctx.lineWidth, 2, "ctx.lineWidth", "2") + ctx.restore(); + assert.strictEqual(ctx.lineWidth, 1, "ctx.lineWidth", "1") + }); + + it("2d.state.saverestore.stackdepth", function () { + // save()/restore() stack depth is not unreasonably limited + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + var limit = 512; + for (var i = 1; i < limit; ++i) + { + ctx.save(); + ctx.lineWidth = i; + } + for (var i = limit-1; i > 0; --i) + { + assert.strictEqual(ctx.lineWidth, i, "ctx.lineWidth", "i") + ctx.restore(); + } + }); + + it("2d.state.saverestore.underflow", function () { + // restore() with an empty stack has no effect + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + for (var i = 0; i < 16; ++i) + ctx.restore(); + ctx.lineWidth = 0.5; + ctx.restore(); + assert.strictEqual(ctx.lineWidth, 0.5, "ctx.lineWidth", "0.5") + }); +}); diff --git a/test/wpt/generated/transformations.js b/test/wpt/generated/transformations.js new file mode 100644 index 000000000..a4b5f2fb6 --- /dev/null +++ b/test/wpt/generated/transformations.js @@ -0,0 +1,675 @@ +// THIS FILE WAS AUTO-GENERATED. DO NOT EDIT BY HAND. + +const assert = require('assert'); +const path = require('path'); + +const { + createCanvas, + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + CanvasPattern, + CanvasGradient +} = require('../../..'); + +const window = { + CanvasRenderingContext2D, + ImageData, + Image, + DOMMatrix, + DOMPoint, + Uint8ClampedArray, + CanvasPattern, + CanvasGradient +}; + +const document = { + createElement(type, ...args) { + if (type !== "canvas") + throw new Error(`createElement(${type}) not supported`); + return createCanvas(...args); + } +}; + +function _getPixel(canvas, x, y) { + const ctx = canvas.getContext('2d'); + const imgdata = ctx.getImageData(x, y, 1, 1); + return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ]; +} + +function _assertApprox(actual, expected, epsilon=0, msg="") { + assert(typeof actual === "number", "actual should be a number but got a ${typeof type_actual}"); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + `expected ${actual} to equal ${expected} +/- ${epsilon}. ${msg}`); + } else { + assert.strictEqual(actual, expected); + } +} + +function _assertPixel(canvas, x, y, r, g, b, a, pos, color) { + const c = _getPixel(canvas, x,y); + assert.strictEqual(c[0], r, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[1], g, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[2], b, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + assert.strictEqual(c[3], a, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function _assertPixelApprox(canvas, x, y, r, g, b, a, pos, color, tolerance) { + const c = _getPixel(canvas, x,y); + _assertApprox(c[0], r, tolerance, 'Red channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[1], g, tolerance, 'Green channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[2], b, tolerance, 'Blue channel of the pixel at (' + x + ', ' + y + ')'); + _assertApprox(c[3], a, tolerance, 'Alpha channel of the pixel at (' + x + ', ' + y + ')'); +} + +function assert_throws_js(Type, fn) { + assert.throws(fn, Type); +} + +// Used by font tests to allow fonts to load. +function deferTest() {} + +class Test { + // Two cases of this in the tests, look unnecessary. + done() {} + // Used by font tests to allow fonts to load. + step_func_done(func) { func(); } + // Used for image onload callback. + step_func(func) { func(); } +} + +function step_timeout(result, time) { + // Nothing; code needs to be converted for this to work. +} + +describe("WPT: transformations", function () { + + it("2d.transformation.order", function () { + // Transformations are applied in the right order + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(2, 1); + ctx.rotate(Math.PI / 2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, -50, 50, 50); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.transformation.scale.basic", function () { + // scale() works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(2, 4); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 12.5); + _assertPixel(canvas, 90,40, 0,255,0,255); + }); + + it("2d.transformation.scale.zero", function () { + // scale() with a scale factor of zero works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.translate(50, 0); + ctx.scale(0, 1); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + + ctx.save(); + ctx.translate(0, 25); + ctx.scale(1, 0); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + + canvas.toDataURL(); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.scale.negative", function () { + // scale() with negative scale factors works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.scale(-1, 1); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-50, 0, 50, 50); + ctx.restore(); + + ctx.save(); + ctx.scale(1, -1); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, -50, 50, 50); + ctx.restore(); + _assertPixel(canvas, 25,25, 0,255,0,255); + _assertPixel(canvas, 75,25, 0,255,0,255); + }); + + it("2d.transformation.scale.large", function () { + // scale() with large scale factors works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(1e5, 1e5); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 1, 1); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.scale.nonfinite", function () { + // scale() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.scale(Infinity, 0.1); + ctx.scale(-Infinity, 0.1); + ctx.scale(NaN, 0.1); + ctx.scale(0.1, Infinity); + ctx.scale(0.1, -Infinity); + ctx.scale(0.1, NaN); + ctx.scale(Infinity, Infinity); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.scale.multiple", function () { + // Multiple scale()s combine + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(Math.sqrt(2), Math.sqrt(2)); + ctx.scale(Math.sqrt(2), Math.sqrt(2)); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + _assertPixel(canvas, 90,40, 0,255,0,255); + }); + + it("2d.transformation.rotate.zero", function () { + // rotate() by 0 does nothing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.rotate.radians", function () { + // rotate() uses radians + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI); // should fail obviously if this is 3.1 degrees + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.rotate.direction", function () { + // rotate() is clockwise + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI / 2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, -100, 50, 100); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.rotate.wrap", function () { + // rotate() wraps large positive values correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI * (1 + 4096)); // == pi (mod 2*pi) + // We need about pi +/- 0.001 in order to get correct-looking results + // 32-bit floats can store pi*4097 with precision 2^-10, so that should + // be safe enough on reasonable implementations + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,2, 0,255,0,255); + _assertPixel(canvas, 98,47, 0,255,0,255); + }); + + it("2d.transformation.rotate.wrapnegative", function () { + // rotate() wraps large negative values correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(-Math.PI * (1 + 4096)); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + _assertPixel(canvas, 98,2, 0,255,0,255); + _assertPixel(canvas, 98,47, 0,255,0,255); + }); + + it("2d.transformation.rotate.nonfinite", function () { + // rotate() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.rotate(Infinity); + ctx.rotate(-Infinity); + ctx.rotate(NaN); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.translate.basic", function () { + // translate() works + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + _assertPixel(canvas, 90,40, 0,255,0,255); + }); + + it("2d.transformation.translate.nonfinite", function () { + // translate() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.translate(Infinity, 0.1); + ctx.translate(-Infinity, 0.1); + ctx.translate(NaN, 0.1); + ctx.translate(0.1, Infinity); + ctx.translate(0.1, -Infinity); + ctx.translate(0.1, NaN); + ctx.translate(Infinity, Infinity); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.transform.identity", function () { + // transform() with the identity matrix does nothing + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.transform(1,0, 0,1, 0,0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.transform.skewed", function () { + // transform() with skewy matrix transforms correctly + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Create green with a red square ring inside it + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(20, 10, 60, 30); + ctx.fillStyle = '#0f0'; + ctx.fillRect(40, 20, 20, 10); + + // Draw a skewed shape to fill that gap, to make sure it is aligned correctly + ctx.transform(1,4, 2,3, 5,6); + // Post-transform coordinates: + // [[20,10],[80,10],[80,40],[20,40],[20,10],[40,20],[40,30],[60,30],[60,20],[40,20],[20,10]]; + // Hence pre-transform coordinates: + var pts=[[-7.4,11.2],[-43.4,59.2],[-31.4,53.2],[4.6,5.2],[-7.4,11.2], + [-15.4,25.2],[-11.4,23.2],[-23.4,39.2],[-27.4,41.2],[-15.4,25.2], + [-7.4,11.2]]; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var i = 0; i < pts.length; ++i) + ctx.lineTo(pts[i][0], pts[i][1]); + ctx.fill(); + _assertPixel(canvas, 21,11, 0,255,0,255); + _assertPixel(canvas, 79,11, 0,255,0,255); + _assertPixel(canvas, 21,39, 0,255,0,255); + _assertPixel(canvas, 79,39, 0,255,0,255); + _assertPixel(canvas, 39,19, 0,255,0,255); + _assertPixel(canvas, 61,19, 0,255,0,255); + _assertPixel(canvas, 39,31, 0,255,0,255); + _assertPixel(canvas, 61,31, 0,255,0,255); + }); + + it("2d.transformation.transform.multiply", function () { + // transform() multiplies the CTM + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.transform(1,2, 3,4, 5,6); + ctx.transform(-2,1, 3/2,-1/2, 1,-2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.transform.nonfinite", function () { + // transform() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.transform(Infinity, 0, 0, 0, 0, 0); + ctx.transform(-Infinity, 0, 0, 0, 0, 0); + ctx.transform(NaN, 0, 0, 0, 0, 0); + ctx.transform(0, Infinity, 0, 0, 0, 0); + ctx.transform(0, -Infinity, 0, 0, 0, 0); + ctx.transform(0, NaN, 0, 0, 0, 0); + ctx.transform(0, 0, Infinity, 0, 0, 0); + ctx.transform(0, 0, -Infinity, 0, 0, 0); + ctx.transform(0, 0, NaN, 0, 0, 0); + ctx.transform(0, 0, 0, Infinity, 0, 0); + ctx.transform(0, 0, 0, -Infinity, 0, 0); + ctx.transform(0, 0, 0, NaN, 0, 0); + ctx.transform(0, 0, 0, 0, Infinity, 0); + ctx.transform(0, 0, 0, 0, -Infinity, 0); + ctx.transform(0, 0, 0, 0, NaN, 0); + ctx.transform(0, 0, 0, 0, 0, Infinity); + ctx.transform(0, 0, 0, 0, 0, -Infinity); + ctx.transform(0, 0, 0, 0, 0, NaN); + ctx.transform(Infinity, Infinity, 0, 0, 0, 0); + ctx.transform(Infinity, Infinity, Infinity, 0, 0, 0); + ctx.transform(Infinity, Infinity, Infinity, Infinity, 0, 0); + ctx.transform(Infinity, Infinity, Infinity, Infinity, Infinity, 0); + ctx.transform(Infinity, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.transform(Infinity, Infinity, Infinity, Infinity, 0, Infinity); + ctx.transform(Infinity, Infinity, Infinity, 0, Infinity, 0); + ctx.transform(Infinity, Infinity, Infinity, 0, Infinity, Infinity); + ctx.transform(Infinity, Infinity, Infinity, 0, 0, Infinity); + ctx.transform(Infinity, Infinity, 0, Infinity, 0, 0); + ctx.transform(Infinity, Infinity, 0, Infinity, Infinity, 0); + ctx.transform(Infinity, Infinity, 0, Infinity, Infinity, Infinity); + ctx.transform(Infinity, Infinity, 0, Infinity, 0, Infinity); + ctx.transform(Infinity, Infinity, 0, 0, Infinity, 0); + ctx.transform(Infinity, Infinity, 0, 0, Infinity, Infinity); + ctx.transform(Infinity, Infinity, 0, 0, 0, Infinity); + ctx.transform(Infinity, 0, Infinity, 0, 0, 0); + ctx.transform(Infinity, 0, Infinity, Infinity, 0, 0); + ctx.transform(Infinity, 0, Infinity, Infinity, Infinity, 0); + ctx.transform(Infinity, 0, Infinity, Infinity, Infinity, Infinity); + ctx.transform(Infinity, 0, Infinity, Infinity, 0, Infinity); + ctx.transform(Infinity, 0, Infinity, 0, Infinity, 0); + ctx.transform(Infinity, 0, Infinity, 0, Infinity, Infinity); + ctx.transform(Infinity, 0, Infinity, 0, 0, Infinity); + ctx.transform(Infinity, 0, 0, Infinity, 0, 0); + ctx.transform(Infinity, 0, 0, Infinity, Infinity, 0); + ctx.transform(Infinity, 0, 0, Infinity, Infinity, Infinity); + ctx.transform(Infinity, 0, 0, Infinity, 0, Infinity); + ctx.transform(Infinity, 0, 0, 0, Infinity, 0); + ctx.transform(Infinity, 0, 0, 0, Infinity, Infinity); + ctx.transform(Infinity, 0, 0, 0, 0, Infinity); + ctx.transform(0, Infinity, Infinity, 0, 0, 0); + ctx.transform(0, Infinity, Infinity, Infinity, 0, 0); + ctx.transform(0, Infinity, Infinity, Infinity, Infinity, 0); + ctx.transform(0, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.transform(0, Infinity, Infinity, Infinity, 0, Infinity); + ctx.transform(0, Infinity, Infinity, 0, Infinity, 0); + ctx.transform(0, Infinity, Infinity, 0, Infinity, Infinity); + ctx.transform(0, Infinity, Infinity, 0, 0, Infinity); + ctx.transform(0, Infinity, 0, Infinity, 0, 0); + ctx.transform(0, Infinity, 0, Infinity, Infinity, 0); + ctx.transform(0, Infinity, 0, Infinity, Infinity, Infinity); + ctx.transform(0, Infinity, 0, Infinity, 0, Infinity); + ctx.transform(0, Infinity, 0, 0, Infinity, 0); + ctx.transform(0, Infinity, 0, 0, Infinity, Infinity); + ctx.transform(0, Infinity, 0, 0, 0, Infinity); + ctx.transform(0, 0, Infinity, Infinity, 0, 0); + ctx.transform(0, 0, Infinity, Infinity, Infinity, 0); + ctx.transform(0, 0, Infinity, Infinity, Infinity, Infinity); + ctx.transform(0, 0, Infinity, Infinity, 0, Infinity); + ctx.transform(0, 0, Infinity, 0, Infinity, 0); + ctx.transform(0, 0, Infinity, 0, Infinity, Infinity); + ctx.transform(0, 0, Infinity, 0, 0, Infinity); + ctx.transform(0, 0, 0, Infinity, Infinity, 0); + ctx.transform(0, 0, 0, Infinity, Infinity, Infinity); + ctx.transform(0, 0, 0, Infinity, 0, Infinity); + ctx.transform(0, 0, 0, 0, Infinity, Infinity); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); + + it("2d.transformation.setTransform.skewed", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + // Create green with a red square ring inside it + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(20, 10, 60, 30); + ctx.fillStyle = '#0f0'; + ctx.fillRect(40, 20, 20, 10); + + // Draw a skewed shape to fill that gap, to make sure it is aligned correctly + ctx.setTransform(1,4, 2,3, 5,6); + // Post-transform coordinates: + // [[20,10],[80,10],[80,40],[20,40],[20,10],[40,20],[40,30],[60,30],[60,20],[40,20],[20,10]]; + // Hence pre-transform coordinates: + var pts=[[-7.4,11.2],[-43.4,59.2],[-31.4,53.2],[4.6,5.2],[-7.4,11.2], + [-15.4,25.2],[-11.4,23.2],[-23.4,39.2],[-27.4,41.2],[-15.4,25.2], + [-7.4,11.2]]; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var i = 0; i < pts.length; ++i) + ctx.lineTo(pts[i][0], pts[i][1]); + ctx.fill(); + _assertPixel(canvas, 21,11, 0,255,0,255); + _assertPixel(canvas, 79,11, 0,255,0,255); + _assertPixel(canvas, 21,39, 0,255,0,255); + _assertPixel(canvas, 79,39, 0,255,0,255); + _assertPixel(canvas, 39,19, 0,255,0,255); + _assertPixel(canvas, 61,19, 0,255,0,255); + _assertPixel(canvas, 39,31, 0,255,0,255); + _assertPixel(canvas, 61,31, 0,255,0,255); + }); + + it("2d.transformation.setTransform.multiple", function () { + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.setTransform(1/2,0, 0,1/2, 0,0); + ctx.setTransform(); + ctx.setTransform(2,0, 0,2, 0,0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + _assertPixel(canvas, 75,35, 0,255,0,255); + }); + + it("2d.transformation.setTransform.nonfinite", function () { + // setTransform() with Infinity/NaN is ignored + const canvas = createCanvas(100, 50); + const ctx = canvas.getContext("2d"); + const t = new Test(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + ctx.setTransform(Infinity, 0, 0, 0, 0, 0); + ctx.setTransform(-Infinity, 0, 0, 0, 0, 0); + ctx.setTransform(NaN, 0, 0, 0, 0, 0); + ctx.setTransform(0, Infinity, 0, 0, 0, 0); + ctx.setTransform(0, -Infinity, 0, 0, 0, 0); + ctx.setTransform(0, NaN, 0, 0, 0, 0); + ctx.setTransform(0, 0, Infinity, 0, 0, 0); + ctx.setTransform(0, 0, -Infinity, 0, 0, 0); + ctx.setTransform(0, 0, NaN, 0, 0, 0); + ctx.setTransform(0, 0, 0, Infinity, 0, 0); + ctx.setTransform(0, 0, 0, -Infinity, 0, 0); + ctx.setTransform(0, 0, 0, NaN, 0, 0); + ctx.setTransform(0, 0, 0, 0, Infinity, 0); + ctx.setTransform(0, 0, 0, 0, -Infinity, 0); + ctx.setTransform(0, 0, 0, 0, NaN, 0); + ctx.setTransform(0, 0, 0, 0, 0, Infinity); + ctx.setTransform(0, 0, 0, 0, 0, -Infinity); + ctx.setTransform(0, 0, 0, 0, 0, NaN); + ctx.setTransform(Infinity, Infinity, 0, 0, 0, 0); + ctx.setTransform(Infinity, Infinity, Infinity, 0, 0, 0); + ctx.setTransform(Infinity, Infinity, Infinity, Infinity, 0, 0); + ctx.setTransform(Infinity, Infinity, Infinity, Infinity, Infinity, 0); + ctx.setTransform(Infinity, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.setTransform(Infinity, Infinity, Infinity, Infinity, 0, Infinity); + ctx.setTransform(Infinity, Infinity, Infinity, 0, Infinity, 0); + ctx.setTransform(Infinity, Infinity, Infinity, 0, Infinity, Infinity); + ctx.setTransform(Infinity, Infinity, Infinity, 0, 0, Infinity); + ctx.setTransform(Infinity, Infinity, 0, Infinity, 0, 0); + ctx.setTransform(Infinity, Infinity, 0, Infinity, Infinity, 0); + ctx.setTransform(Infinity, Infinity, 0, Infinity, Infinity, Infinity); + ctx.setTransform(Infinity, Infinity, 0, Infinity, 0, Infinity); + ctx.setTransform(Infinity, Infinity, 0, 0, Infinity, 0); + ctx.setTransform(Infinity, Infinity, 0, 0, Infinity, Infinity); + ctx.setTransform(Infinity, Infinity, 0, 0, 0, Infinity); + ctx.setTransform(Infinity, 0, Infinity, 0, 0, 0); + ctx.setTransform(Infinity, 0, Infinity, Infinity, 0, 0); + ctx.setTransform(Infinity, 0, Infinity, Infinity, Infinity, 0); + ctx.setTransform(Infinity, 0, Infinity, Infinity, Infinity, Infinity); + ctx.setTransform(Infinity, 0, Infinity, Infinity, 0, Infinity); + ctx.setTransform(Infinity, 0, Infinity, 0, Infinity, 0); + ctx.setTransform(Infinity, 0, Infinity, 0, Infinity, Infinity); + ctx.setTransform(Infinity, 0, Infinity, 0, 0, Infinity); + ctx.setTransform(Infinity, 0, 0, Infinity, 0, 0); + ctx.setTransform(Infinity, 0, 0, Infinity, Infinity, 0); + ctx.setTransform(Infinity, 0, 0, Infinity, Infinity, Infinity); + ctx.setTransform(Infinity, 0, 0, Infinity, 0, Infinity); + ctx.setTransform(Infinity, 0, 0, 0, Infinity, 0); + ctx.setTransform(Infinity, 0, 0, 0, Infinity, Infinity); + ctx.setTransform(Infinity, 0, 0, 0, 0, Infinity); + ctx.setTransform(0, Infinity, Infinity, 0, 0, 0); + ctx.setTransform(0, Infinity, Infinity, Infinity, 0, 0); + ctx.setTransform(0, Infinity, Infinity, Infinity, Infinity, 0); + ctx.setTransform(0, Infinity, Infinity, Infinity, Infinity, Infinity); + ctx.setTransform(0, Infinity, Infinity, Infinity, 0, Infinity); + ctx.setTransform(0, Infinity, Infinity, 0, Infinity, 0); + ctx.setTransform(0, Infinity, Infinity, 0, Infinity, Infinity); + ctx.setTransform(0, Infinity, Infinity, 0, 0, Infinity); + ctx.setTransform(0, Infinity, 0, Infinity, 0, 0); + ctx.setTransform(0, Infinity, 0, Infinity, Infinity, 0); + ctx.setTransform(0, Infinity, 0, Infinity, Infinity, Infinity); + ctx.setTransform(0, Infinity, 0, Infinity, 0, Infinity); + ctx.setTransform(0, Infinity, 0, 0, Infinity, 0); + ctx.setTransform(0, Infinity, 0, 0, Infinity, Infinity); + ctx.setTransform(0, Infinity, 0, 0, 0, Infinity); + ctx.setTransform(0, 0, Infinity, Infinity, 0, 0); + ctx.setTransform(0, 0, Infinity, Infinity, Infinity, 0); + ctx.setTransform(0, 0, Infinity, Infinity, Infinity, Infinity); + ctx.setTransform(0, 0, Infinity, Infinity, 0, Infinity); + ctx.setTransform(0, 0, Infinity, 0, Infinity, 0); + ctx.setTransform(0, 0, Infinity, 0, Infinity, Infinity); + ctx.setTransform(0, 0, Infinity, 0, 0, Infinity); + ctx.setTransform(0, 0, 0, Infinity, Infinity, 0); + ctx.setTransform(0, 0, 0, Infinity, Infinity, Infinity); + ctx.setTransform(0, 0, 0, Infinity, 0, Infinity); + ctx.setTransform(0, 0, 0, 0, Infinity, Infinity); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + _assertPixel(canvas, 50,25, 0,255,0,255); + }); +}); diff --git a/test/wpt/line-styles.yaml b/test/wpt/line-styles.yaml new file mode 100644 index 000000000..e6dc3205e --- /dev/null +++ b/test/wpt/line-styles.yaml @@ -0,0 +1,1017 @@ +- name: 2d.line.defaults + testing: + - 2d.lineWidth.default + - 2d.lineCap.default + - 2d.lineJoin.default + - 2d.miterLimit.default + code: | + @assert ctx.lineWidth === 1; + @assert ctx.lineCap === 'butt'; + @assert ctx.lineJoin === 'miter'; + @assert ctx.miterLimit === 10; + +- name: 2d.line.width.basic + desc: lineWidth determines the width of line strokes + testing: + - 2d.lineWidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 20; + // Draw a green line over a red box, to check the line is not too small + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + // Draw a green box over a red line, to check the line is not too large + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 15, 20, 20); + + @assert pixel 14,25 == 0,255,0,255; + @assert pixel 15,25 == 0,255,0,255; + @assert pixel 16,25 == 0,255,0,255; + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 34,25 == 0,255,0,255; + @assert pixel 35,25 == 0,255,0,255; + @assert pixel 36,25 == 0,255,0,255; + + @assert pixel 64,25 == 0,255,0,255; + @assert pixel 65,25 == 0,255,0,255; + @assert pixel 66,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + @assert pixel 84,25 == 0,255,0,255; + @assert pixel 85,25 == 0,255,0,255; + @assert pixel 86,25 == 0,255,0,255; + expected: green + +- name: 2d.line.width.transformed + desc: Line stroke widths are affected by scale transformations + testing: + - 2d.lineWidth + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 4; + // Draw a green line over a red box, to check the line is not too small + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.save(); + ctx.scale(5, 1); + ctx.beginPath(); + ctx.moveTo(5, 15); + ctx.lineTo(5, 35); + ctx.stroke(); + ctx.restore(); + + // Draw a green box over a red line, to check the line is not too large + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.save(); + ctx.scale(-5, 1); + ctx.beginPath(); + ctx.moveTo(-15, 15); + ctx.lineTo(-15, 35); + ctx.stroke(); + ctx.restore(); + ctx.fillRect(65, 15, 20, 20); + + @assert pixel 14,25 == 0,255,0,255; + @assert pixel 15,25 == 0,255,0,255; + @assert pixel 16,25 == 0,255,0,255; + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 34,25 == 0,255,0,255; + @assert pixel 35,25 == 0,255,0,255; + @assert pixel 36,25 == 0,255,0,255; + + @assert pixel 64,25 == 0,255,0,255; + @assert pixel 65,25 == 0,255,0,255; + @assert pixel 66,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + @assert pixel 84,25 == 0,255,0,255; + @assert pixel 85,25 == 0,255,0,255; + @assert pixel 86,25 == 0,255,0,255; + expected: green + +- name: 2d.line.width.scaledefault + desc: Default lineWidth strokes are affected by scale transformations + testing: + - 2d.lineWidth + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(50, 50); + ctx.strokeStyle = '#0f0'; + ctx.moveTo(0, 0.5); + ctx.lineTo(2, 0.5); + ctx.stroke(); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + @assert pixel 50,5 == 0,255,0,255; + @assert pixel 50,45 == 0,255,0,255; + expected: green + +- name: 2d.line.width.valid + desc: Setting lineWidth to valid values works + testing: + - 2d.lineWidth.set + - 2d.lineWidth.get + code: | + ctx.lineWidth = 1.5; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = "1e1"; + @assert ctx.lineWidth === 10; + + ctx.lineWidth = 1/1024; + @assert ctx.lineWidth === 1/1024; + + ctx.lineWidth = 1000; + @assert ctx.lineWidth === 1000; + +- name: 2d.line.width.invalid + desc: Setting lineWidth to invalid values is ignored + testing: + - 2d.lineWidth.invalid + code: | + ctx.lineWidth = 1.5; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = 0; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = -1; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = Infinity; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = -Infinity; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = NaN; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = 'string'; + @assert ctx.lineWidth === 1.5; + + ctx.lineWidth = 1.5; + ctx.lineWidth = true; + @assert ctx.lineWidth === 1; + + ctx.lineWidth = 1.5; + ctx.lineWidth = false; + @assert ctx.lineWidth === 1.5; + +- name: 2d.line.cap.butt + desc: lineCap 'butt' is rendered correctly + testing: + - 2d.lineCap.butt + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineCap = 'butt'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 15, 20, 20); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 15, 20, 20); + + @assert pixel 25,14 == 0,255,0,255; + @assert pixel 25,15 == 0,255,0,255; + @assert pixel 25,16 == 0,255,0,255; + @assert pixel 25,34 == 0,255,0,255; + @assert pixel 25,35 == 0,255,0,255; + @assert pixel 25,36 == 0,255,0,255; + + @assert pixel 75,14 == 0,255,0,255; + @assert pixel 75,15 == 0,255,0,255; + @assert pixel 75,16 == 0,255,0,255; + @assert pixel 75,34 == 0,255,0,255; + @assert pixel 75,35 == 0,255,0,255; + @assert pixel 75,36 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.round + desc: lineCap 'round' is rendered correctly + testing: + - 2d.lineCap.round + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineCap = 'round'; + ctx.lineWidth = 20; + + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.beginPath(); + ctx.moveTo(35-tol, 15); + ctx.arc(25, 15, 10-tol, 0, Math.PI, true); + ctx.arc(25, 35, 10-tol, Math.PI, 0, true); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(85+tol, 15); + ctx.arc(75, 15, 10+tol, 0, Math.PI, true); + ctx.arc(75, 35, 10+tol, Math.PI, 0, true); + ctx.fill(); + + @assert pixel 17,6 == 0,255,0,255; + @assert pixel 25,6 == 0,255,0,255; + @assert pixel 32,6 == 0,255,0,255; + @assert pixel 17,43 == 0,255,0,255; + @assert pixel 25,43 == 0,255,0,255; + @assert pixel 32,43 == 0,255,0,255; + + @assert pixel 67,6 == 0,255,0,255; + @assert pixel 75,6 == 0,255,0,255; + @assert pixel 82,6 == 0,255,0,255; + @assert pixel 67,43 == 0,255,0,255; + @assert pixel 75,43 == 0,255,0,255; + @assert pixel 82,43 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.square + desc: lineCap 'square' is rendered correctly + testing: + - 2d.lineCap.square + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineCap = 'square'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(15, 5, 20, 40); + ctx.beginPath(); + ctx.moveTo(25, 15); + ctx.lineTo(25, 35); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(75, 15); + ctx.lineTo(75, 35); + ctx.stroke(); + ctx.fillRect(65, 5, 20, 40); + + @assert pixel 25,4 == 0,255,0,255; + @assert pixel 25,5 == 0,255,0,255; + @assert pixel 25,6 == 0,255,0,255; + @assert pixel 25,44 == 0,255,0,255; + @assert pixel 25,45 == 0,255,0,255; + @assert pixel 25,46 == 0,255,0,255; + + @assert pixel 75,4 == 0,255,0,255; + @assert pixel 75,5 == 0,255,0,255; + @assert pixel 75,6 == 0,255,0,255; + @assert pixel 75,44 == 0,255,0,255; + @assert pixel 75,45 == 0,255,0,255; + @assert pixel 75,46 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.open + desc: Line caps are drawn at the corners of an unclosed rectangle + testing: + - 2d.lineCap.end + code: | + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'bevel'; + ctx.lineCap = 'square'; + ctx.lineWidth = 400; + + ctx.beginPath(); + ctx.moveTo(200, 200); + ctx.lineTo(200, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 200); + ctx.lineTo(200, 200); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.closed + desc: Line caps are not drawn at the corners of an unclosed rectangle + testing: + - 2d.lineCap.end + code: | + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'bevel'; + ctx.lineCap = 'square'; + ctx.lineWidth = 400; + + ctx.beginPath(); + ctx.moveTo(200, 200); + ctx.lineTo(200, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 200); + ctx.closePath(); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.cap.valid + desc: Setting lineCap to valid values works + testing: + - 2d.lineCap.set + - 2d.lineCap.get + code: | + ctx.lineCap = 'butt' + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'round'; + @assert ctx.lineCap === 'round'; + + ctx.lineCap = 'square'; + @assert ctx.lineCap === 'square'; + +- name: 2d.line.cap.invalid + desc: Setting lineCap to invalid values is ignored + testing: + - 2d.lineCap.invalid + code: | + ctx.lineCap = 'butt' + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'invalid'; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'ROUND'; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'round\0'; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'round '; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = ""; + @assert ctx.lineCap === 'butt'; + + ctx.lineCap = 'butt'; + ctx.lineCap = 'bevel'; + @assert ctx.lineCap === 'butt'; + +- name: 2d.line.join.bevel + desc: lineJoin 'bevel' is rendered correctly + testing: + - 2d.lineJoin.common + - 2d.lineJoin.bevel + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineJoin = 'bevel'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 20, 20); + ctx.fillRect(20, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(30, 20); + ctx.lineTo(40-tol, 20); + ctx.lineTo(30, 10+tol); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 20, 20); + ctx.fillRect(70, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(80, 20); + ctx.lineTo(90+tol, 20); + ctx.lineTo(80, 10-tol); + ctx.fill(); + + @assert pixel 34,16 == 0,255,0,255; + @assert pixel 34,15 == 0,255,0,255; + @assert pixel 35,15 == 0,255,0,255; + @assert pixel 36,15 == 0,255,0,255; + @assert pixel 36,14 == 0,255,0,255; + + @assert pixel 84,16 == 0,255,0,255; + @assert pixel 84,15 == 0,255,0,255; + @assert pixel 85,15 == 0,255,0,255; + @assert pixel 86,15 == 0,255,0,255; + @assert pixel 86,14 == 0,255,0,255; + expected: green + +- name: 2d.line.join.round + desc: lineJoin 'round' is rendered correctly + testing: + - 2d.lineJoin.round + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + var tol = 1; // tolerance to avoid antialiasing artifacts + + ctx.lineJoin = 'round'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 20, 20); + ctx.fillRect(20, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(30, 20); + ctx.arc(30, 20, 10-tol, 0, 2*Math.PI, true); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 20, 20); + ctx.fillRect(70, 20, 20, 20); + ctx.beginPath(); + ctx.moveTo(80, 20); + ctx.arc(80, 20, 10+tol, 0, 2*Math.PI, true); + ctx.fill(); + + @assert pixel 36,14 == 0,255,0,255; + @assert pixel 36,13 == 0,255,0,255; + @assert pixel 37,13 == 0,255,0,255; + @assert pixel 38,13 == 0,255,0,255; + @assert pixel 38,12 == 0,255,0,255; + + @assert pixel 86,14 == 0,255,0,255; + @assert pixel 86,13 == 0,255,0,255; + @assert pixel 87,13 == 0,255,0,255; + @assert pixel 88,13 == 0,255,0,255; + @assert pixel 88,12 == 0,255,0,255; + expected: green + +- name: 2d.line.join.miter + desc: lineJoin 'miter' is rendered correctly + testing: + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 20; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + + ctx.fillRect(10, 10, 30, 20); + ctx.fillRect(20, 10, 20, 30); + + ctx.beginPath(); + ctx.moveTo(10, 20); + ctx.lineTo(30, 20); + ctx.lineTo(30, 40); + ctx.stroke(); + + + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + + ctx.beginPath(); + ctx.moveTo(60, 20); + ctx.lineTo(80, 20); + ctx.lineTo(80, 40); + ctx.stroke(); + + ctx.fillRect(60, 10, 30, 20); + ctx.fillRect(70, 10, 20, 30); + + @assert pixel 38,12 == 0,255,0,255; + @assert pixel 39,11 == 0,255,0,255; + @assert pixel 40,10 == 0,255,0,255; + @assert pixel 41,9 == 0,255,0,255; + @assert pixel 42,8 == 0,255,0,255; + + @assert pixel 88,12 == 0,255,0,255; + @assert pixel 89,11 == 0,255,0,255; + @assert pixel 90,10 == 0,255,0,255; + @assert pixel 91,9 == 0,255,0,255; + @assert pixel 92,8 == 0,255,0,255; + expected: green + +- name: 2d.line.join.open + desc: Line joins are not drawn at the corner of an unclosed rectangle + testing: + - 2d.lineJoin.joins + code: | + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 200; + + ctx.beginPath(); + ctx.moveTo(100, 50); + ctx.lineTo(100, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 50); + ctx.lineTo(100, 50); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.join.closed + desc: Line joins are drawn at the corner of a closed rectangle + testing: + - 2d.lineJoin.joinclosed + code: | + ctx.fillStyle = '#f00'; + ctx.strokeStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineJoin = 'miter'; + ctx.lineWidth = 200; + + ctx.beginPath(); + ctx.moveTo(100, 50); + ctx.lineTo(100, 1000); + ctx.lineTo(1000, 1000); + ctx.lineTo(1000, 50); + ctx.closePath(); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.join.parallel + desc: Line joins are drawn at 180-degree joins + testing: + - 2d.lineJoin.joins + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 300; + ctx.lineJoin = 'round'; + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.lineTo(0, 25); + ctx.lineTo(-100, 25); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.join.valid + desc: Setting lineJoin to valid values works + testing: + - 2d.lineJoin.set + - 2d.lineJoin.get + code: | + ctx.lineJoin = 'bevel' + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'round'; + @assert ctx.lineJoin === 'round'; + + ctx.lineJoin = 'miter'; + @assert ctx.lineJoin === 'miter'; + +- name: 2d.line.join.invalid + desc: Setting lineJoin to invalid values is ignored + testing: + - 2d.lineJoin.invalid + code: | + ctx.lineJoin = 'bevel' + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'invalid'; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'ROUND'; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'round\0'; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'round '; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = ""; + @assert ctx.lineJoin === 'bevel'; + + ctx.lineJoin = 'bevel'; + ctx.lineJoin = 'butt'; + @assert ctx.lineJoin === 'bevel'; + +- name: 2d.line.miter.exceeded + desc: Miter joins are not drawn when the miter limit is exceeded + testing: + - 2d.lineJoin.miterLimit + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 201); // slightly non-right-angle to avoid being a special case + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.acute + desc: Miter joins are drawn correctly with acute angles + testing: + - 2d.lineJoin.miterLimit + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 2.614; + ctx.beginPath(); + ctx.moveTo(100, 1000); + ctx.lineTo(100, 100); + ctx.lineTo(1000, 1000); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 2.613; + ctx.beginPath(); + ctx.moveTo(100, 1000); + ctx.lineTo(100, 100); + ctx.lineTo(1000, 1000); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.obtuse + desc: Miter joins are drawn correctly with obtuse angles + testing: + - 2d.lineJoin.miterLimit + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 1600; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 1.083; + ctx.beginPath(); + ctx.moveTo(800, 10000); + ctx.lineTo(800, 300); + ctx.lineTo(10000, -8900); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.082; + ctx.beginPath(); + ctx.moveTo(800, 10000); + ctx.lineTo(800, 300); + ctx.lineTo(10000, -8900); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.rightangle + desc: Miter joins are not drawn when the miter limit is exceeded, on exact right + angles + testing: + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 200); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.lineedge + desc: Miter joins are not drawn when the miter limit is exceeded at the corners + of a zero-height rectangle + testing: + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#f00'; + ctx.miterLimit = 1.414; + ctx.beginPath(); + ctx.strokeRect(100, 25, 200, 0); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.within + desc: Miter joins are drawn when the miter limit is not quite exceeded + testing: + - 2d.lineJoin.miter + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + + ctx.strokeStyle = '#0f0'; + ctx.miterLimit = 1.416; + ctx.beginPath(); + ctx.moveTo(200, 1000); + ctx.lineTo(200, 200); + ctx.lineTo(1000, 201); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.miter.valid + desc: Setting miterLimit to valid values works + testing: + - 2d.miterLimit.set + - 2d.miterLimit.get + code: | + ctx.miterLimit = 1.5; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = "1e1"; + @assert ctx.miterLimit === 10; + + ctx.miterLimit = 1/1024; + @assert ctx.miterLimit === 1/1024; + + ctx.miterLimit = 1000; + @assert ctx.miterLimit === 1000; + +- name: 2d.line.miter.invalid + desc: Setting miterLimit to invalid values is ignored + testing: + - 2d.miterLimit.invalid + code: | + ctx.miterLimit = 1.5; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = 0; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = -1; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = Infinity; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = -Infinity; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = NaN; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = 'string'; + @assert ctx.miterLimit === 1.5; + + ctx.miterLimit = 1.5; + ctx.miterLimit = true; + @assert ctx.miterLimit === 1; + + ctx.miterLimit = 1.5; + ctx.miterLimit = false; + @assert ctx.miterLimit === 1.5; + +- name: 2d.line.cross + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 200; + ctx.lineJoin = 'bevel'; + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(110, 50); + ctx.lineTo(110, 60); + ctx.lineTo(100, 60); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + expected: green + +- name: 2d.line.union + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 24); + ctx.lineTo(100, 25); + ctx.lineTo(0, 26); + ctx.closePath(); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 25,1 == 0,255,0,255; + @assert pixel 48,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 25,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + expected: green + + + + + + + +- name: 2d.line.invalid.strokestyle + desc: Verify correct behavior of canvas on an invalid strokeStyle() + testing: + - 2d.strokestyle.invalid + code: | + ctx.strokeStyle = 'rgb(0, 255, 0)'; + ctx.strokeStyle = 'nonsense'; + ctx.lineWidth = 200; + ctx.moveTo(0,100); + ctx.lineTo(200,100); + ctx.stroke(); + var imageData = ctx.getImageData(0, 0, 200, 200); + var imgdata = imageData.data; + @assert imgdata[4] == 0; + @assert imgdata[5] == 255; + @assert imgdata[6] == 0; + diff --git a/test/wpt/meta.yaml b/test/wpt/meta.yaml new file mode 100644 index 000000000..f6902d078 --- /dev/null +++ b/test/wpt/meta.yaml @@ -0,0 +1,555 @@ +- meta: | + cases = [ + ("zero", "0", 0), + ("empty", "", None), + ("onlyspace", " ", None), + ("space", " 100", 100), + ("whitespace", "\r\n\t\f100", 100), + ("plus", "+100", 100), + ("minus", "-100", None), + ("octal", "0100", 100), + ("hex", "0x100", 0), + ("exp", "100e1", 100), + ("decimal", "100.999", 100), + ("percent", "100%", 100), + ("em", "100em", 100), + ("junk", "#!?", None), + ("trailingjunk", "100#!?", 100), + ] + def gen(name, string, exp, code): + testing = ["size.nonnegativeinteger"] + if exp is None: + testing.append("size.error") + code += "@assert canvas.width === 300;\n@assert canvas.height === 150;\n" + expected = "size 300 150" + else: + code += "@assert canvas.width === %s;\n@assert canvas.height === %s;\n" % (exp, exp) + expected = "size %s %s" % (exp, exp) + + # With "100%", Opera gets canvas.width = 100 but renders at 100% of the frame width, + # so check the CSS display width + code += '@assert window.getComputedStyle(canvas, null).getPropertyValue("width") === "%spx";\n' % (exp, ) + + code += "@assert canvas.getAttribute('width') === %r;\n" % string + code += "@assert canvas.getAttribute('height') === %r;\n" % string + + if exp == 0: + expected = None # can't generate zero-sized PNGs for the expected image + + return code, testing, expected + + for name, string, exp in cases: + code = "" + code, testing, expected = gen(name, string, exp, code) + # We need to replace \r with because \r\n gets converted to \n in the HTML parser. + htmlString = string.replace('\r', ' ') + tests.append( { + "name": "size.attributes.parse.%s" % name, + "desc": "Parsing of non-negative integers", + "testing": testing, + "canvas": 'width="%s" height="%s"' % (htmlString, htmlString), + "code": code, + "expected": expected + } ) + + for name, string, exp in cases: + code = "canvas.setAttribute('width', %r);\ncanvas.setAttribute('height', %r);\n" % (string, string) + code, testing, expected = gen(name, string, exp, code) + tests.append( { + "name": "size.attributes.setAttribute.%s" % name, + "desc": "Parsing of non-negative integers in setAttribute", + "testing": testing, + "canvas": 'width="50" height="50"', + "code": code, + "expected": expected + } ) + +- meta: | + state = [ # some non-default values to test with + ('strokeStyle', '"#ff0000"'), + ('fillStyle', '"#ff0000"'), + ('globalAlpha', 0.5), + ('lineWidth', 0.5), + ('lineCap', '"round"'), + ('lineJoin', '"round"'), + ('miterLimit', 0.5), + ('shadowOffsetX', 5), + ('shadowOffsetY', 5), + ('shadowBlur', 5), + ('shadowColor', '"#ff0000"'), + ('globalCompositeOperation', '"copy"'), + ('font', '"25px serif"'), + ('textAlign', '"center"'), + ('textBaseline', '"bottom"'), + ] + for key,value in state: + tests.append( { + 'name': '2d.state.saverestore.%s' % key, + 'desc': 'save()/restore() works for %s' % key, + 'testing': [ '2d.state.%s' % key ], + 'code': + """// Test that restore() undoes any modifications + var old = ctx.%(key)s; + ctx.save(); + ctx.%(key)s = %(value)s; + ctx.restore(); + @assert ctx.%(key)s === old; + + // Also test that save() doesn't modify the values + ctx.%(key)s = %(value)s; + old = ctx.%(key)s; + // we're not interested in failures caused by get(set(x)) != x (e.g. + // from rounding), so compare against 'old' instead of against %(value)s + ctx.save(); + @assert ctx.%(key)s === old; + ctx.restore(); + """ % { 'key':key, 'value':value } + } ) + + tests.append( { + 'name': 'initial.reset.2dstate', + 'desc': 'Resetting the canvas state resets 2D state variables', + 'testing': [ 'initial.reset' ], + 'code': + """canvas.width = 100; + var default_val; + """ + "".join( + """ + default_val = ctx.%(key)s; + ctx.%(key)s = %(value)s; + canvas.width = 100; + @assert ctx.%(key)s === default_val; + """ % { 'key':key, 'value':value } + for key,value in state), + } ) + +- meta: | + # Composite operation tests + # + ops = [ + # name FA FB + ('source-over', '1', '1-aA'), + ('destination-over', '1-aB', '1'), + ('source-in', 'aB', '0'), + ('destination-in', '0', 'aA'), + ('source-out', '1-aB', '0'), + ('destination-out', '0', '1-aA'), + ('source-atop', 'aB', '1-aA'), + ('destination-atop', '1-aB', 'aA'), + ('xor', '1-aB', '1-aA'), + ('copy', '1', '0'), + ('lighter', '1', '1'), + ] + + # The ones that change the output when src = (0,0,0,0): + ops_trans = [ 'source-in', 'destination-in', 'source-out', 'destination-atop', 'copy' ]; + + def calc_output(A, B, FA_code, FB_code): + (RA, GA, BA, aA) = A + (RB, GB, BB, aB) = B + rA, gA, bA = RA*aA, GA*aA, BA*aA + rB, gB, bB = RB*aB, GB*aB, BB*aB + + FA = eval(FA_code) + FB = eval(FB_code) + + rO = rA*FA + rB*FB + gO = gA*FA + gB*FB + bO = bA*FA + bB*FB + aO = aA*FA + aB*FB + + rO = min(255, rO) + gO = min(255, gO) + bO = min(255, bO) + aO = min(1, aO) + + if aO: + RO = rO / aO + GO = gO / aO + BO = bO / aO + else: RO = GO = BO = 0 + + return (RO, GO, BO, aO) + + def to_test(color): + r, g, b, a = color + return '%d,%d,%d,%d' % (round(r), round(g), round(b), round(a*255)) + def to_cairo(color): + r, g, b, a = color + return '%f,%f,%f,%f' % (r/255., g/255., b/255., a) + + for (name, src, dest) in [ + ('solid', (255, 255, 0, 1.0), (0, 255, 255, 1.0)), + ('transparent', (0, 0, 255, 0.75), (0, 255, 0, 0.5)), + # catches the atop, xor and lighter bugs in Opera 9.10 + ]: + for op, FA_code, FB_code in ops: + expected = calc_output(src, dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'testing': [ '2d.composite.%s' % op ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, src, to_test(expected)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % to_cairo(expected), + } ) + + for (name, src, dest) in [ ('image', (255, 255, 0, 0.75), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + expected = calc_output(src, dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'testing': [ '2d.composite.%s' % op ], + 'images': [ 'yellow75.png' ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.drawImage(document.getElementById('yellow75.png'), 0, 0); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % to_cairo(expected), + } ) + + for (name, src, dest) in [ ('canvas', (255, 255, 0, 0.75), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + expected = calc_output(src, dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'testing': [ '2d.composite.%s' % op ], + 'images': [ 'yellow75.png' ], + 'code': """ + var canvas2 = document.createElement('canvas'); + canvas2.width = canvas.width; + canvas2.height = canvas.height; + var ctx2 = canvas2.getContext('2d'); + ctx2.drawImage(document.getElementById('yellow75.png'), 0, 0); + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.drawImage(canvas2, 0, 0); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % to_cairo(expected), + } ) + + + for (name, src, dest) in [ ('uncovered.fill', (0, 0, 255, 0.75), (0, 255, 0, 0.5)) ]: + for op, FA_code, FB_code in ops: + if op not in ops_trans: continue + expected0 = calc_output((0,0,0,0.0), dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'desc': 'fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.', + 'testing': [ '2d.composite.%s' % op ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.fillStyle = 'rgba%s'; + ctx.translate(0, 25); + ctx.fillRect(0, 50, 100, 50); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, src, to_test(expected0)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (to_cairo(expected0)), + } ) + + for (name, src, dest) in [ ('uncovered.image', (255, 255, 0, 1.0), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + if op not in ops_trans: continue + expected0 = calc_output((0,0,0,0.0), dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'desc': 'drawImage() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.', + 'testing': [ '2d.composite.%s' % op ], + 'images': [ 'yellow.png' ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.drawImage(document.getElementById('yellow.png'), 40, 40, 10, 10, 40, 50, 10, 10); + @assert pixel 15,15 ==~ %s +/- 5; + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected0), to_test(expected0)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (to_cairo(expected0)), + } ) + + for (name, src, dest) in [ ('uncovered.nocontext', (255, 255, 0, 1.0), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + if op not in ops_trans: continue + expected0 = calc_output((0,0,0,0.0), dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'desc': 'drawImage() of a canvas with no context draws pixels as (0,0,0,0), and does not leave the pixels unchanged.', + 'testing': [ '2d.composite.%s' % op ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + var canvas2 = document.createElement('canvas'); + ctx.drawImage(canvas2, 0, 0); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected0)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (to_cairo(expected0)), + } ) + + for (name, src, dest) in [ ('uncovered.pattern', (255, 255, 0, 1.0), (0, 255, 255, 0.5)) ]: + for op, FA_code, FB_code in ops: + if op not in ops_trans: continue + expected0 = calc_output((0,0,0,0.0), dest, FA_code, FB_code) + tests.append( { + 'name': '2d.composite.%s.%s' % (name, op), + 'desc': 'Pattern fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.', + 'testing': [ '2d.composite.%s' % op ], + 'images': [ 'yellow.png' ], + 'code': """ + ctx.fillStyle = 'rgba%s'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.fillStyle = ctx.createPattern(document.getElementById('yellow.png'), 'no-repeat'); + ctx.fillRect(0, 50, 100, 50); + @assert pixel 50,25 ==~ %s +/- 5; + """ % (dest, op, to_test(expected0)), + 'expected': """size 100 50 + cr.set_source_rgba(%s) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (to_cairo(expected0)), + } ) + + for op, FA_code, FB_code in ops: + tests.append( { + 'name': '2d.composite.clip.%s' % (op), + 'desc': 'fill() does not affect pixels outside the clip region.', + 'testing': [ '2d.composite.%s' % op ], + 'code': """ + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = '%s'; + ctx.rect(-20, -20, 10, 10); + ctx.clip(); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + """ % (op), + 'expected': 'green' + } ) + +- meta: | + # Color parsing tests + + # Try most of the CSS3 Color values - http://www.w3.org/TR/css3-color/#colorunits + big_float = '1' + ('0' * 39) + big_double = '1' + ('0' * 310) + for name, string, r,g,b,a, notes in [ + ('html4', 'limE', 0,255,0,255, ""), + ('hex3', '#0f0', 0,255,0,255, ""), + ('hex4', '#0f0f', 0,255,0,255, ""), + ('hex6', '#00fF00', 0,255,0,255, ""), + ('hex8', '#00ff00ff', 0,255,0,255, ""), + ('rgb-num', 'rgb(0,255,0)', 0,255,0,255, ""), + ('rgb-clamp-1', 'rgb(-1000, 1000, -1000)', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-clamp-2', 'rgb(-200%, 200%, -200%)', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-clamp-3', 'rgb(-2147483649, 4294967298, -18446744073709551619)', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-clamp-4', 'rgb(-'+big_float+', '+big_float+', -'+big_float+')', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-clamp-5', 'rgb(-'+big_double+', '+big_double+', -'+big_double+')', 0,255,0,255, 'Assumes colors are clamped to [0,255].'), + ('rgb-percent', 'rgb(0% ,100% ,0%)', 0,255,0,255, 'CSS3 Color says "The integer value 255 corresponds to 100%". (In particular, it is not 254...)'), + ('rgb-eof', 'rgb(0, 255, 0', 0,255,0,255, ""), # see CSS2.1 4.2 "Unexpected end of style sheet" + ('rgba-solid-1', 'rgba( 0 , 255 , 0 , 1 )', 0,255,0,255, ""), + ('rgba-solid-2', 'rgba( 0 , 255 , 0 , 1.0 )', 0,255,0,255, ""), + ('rgba-solid-3', 'rgba( 0 , 255 , 0 , +1 )', 0,255,0,255, ""), + ('rgba-solid-4', 'rgba( -0 , 255 , +0 , 1 )', 0,255,0,255, ""), + ('rgba-num-1', 'rgba( 0 , 255 , 0 , .499 )', 0,255,0,127, ""), + ('rgba-num-2', 'rgba( 0 , 255 , 0 , 0.499 )', 0,255,0,127, ""), + ('rgba-percent', 'rgba(0%,100%,0%,0.499)', 0,255,0,127, ""), # 0.499*255 rounds to 127, both down and nearest, so it should be safe + ('rgba-clamp-1', 'rgba(0, 255, 0, -2)', 0,0,0,0, ""), + ('rgba-clamp-2', 'rgba(0, 255, 0, 2)', 0,255,0,255, ""), + ('rgba-eof', 'rgba(0, 255, 0, 1', 0,255,0,255, ""), + ('transparent-1', 'transparent', 0,0,0,0, ""), + ('transparent-2', 'TrAnSpArEnT', 0,0,0,0, ""), + ('hsl-1', 'hsl(120, 100%, 50%)', 0,255,0,255, ""), + ('hsl-2', 'hsl( -240 , 100% , 50% )', 0,255,0,255, ""), + ('hsl-3', 'hsl(360120, 100%, 50%)', 0,255,0,255, ""), + ('hsl-4', 'hsl(-360240, 100%, 50%)', 0,255,0,255, ""), + ('hsl-5', 'hsl(120.0, 100.0%, 50.0%)', 0,255,0,255, ""), + ('hsl-6', 'hsl(+120, +100%, +50%)', 0,255,0,255, ""), + ('hsl-clamp-1', 'hsl(120, 200%, 50%)', 0,255,0,255, ""), + ('hsl-clamp-2', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""), + ('hsl-clamp-3', 'hsl(120, 100%, 200%)', 255,255,255,255, ""), + ('hsl-clamp-4', 'hsl(120, 100%, -200%)', 0,0,0,255, ""), + ('hsla-1', 'hsla(120, 100%, 50%, 0.499)', 0,255,0,127, ""), + ('hsla-2', 'hsla( 120.0 , 100.0% , 50.0% , 1 )', 0,255,0,255, ""), + ('hsla-clamp-1', 'hsla(120, 200%, 50%, 1)', 0,255,0,255, ""), + ('hsla-clamp-2', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""), + ('hsla-clamp-3', 'hsla(120, 100%, 200%, 1)', 255,255,255,255, ""), + ('hsla-clamp-4', 'hsla(120, 100%, -200%, 1)', 0,0,0,255, ""), + ('hsla-clamp-5', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""), + ('hsla-clamp-6', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""), + ('svg-1', 'gray', 128,128,128,255, ""), + ('svg-2', 'grey', 128,128,128,255, ""), + # css-color-4 rgb() color function + # https://drafts.csswg.org/css-color/#numeric-rgb + ('css-color-4-rgb-1', 'rgb(0, 255.0, 0)', 0,255,0,255, ""), + ('css-color-4-rgb-2', 'rgb(0, 255, 0, 0.2)', 0,255,0,51, ""), + ('css-color-4-rgb-3', 'rgb(0, 255, 0, 20%)', 0,255,0,51, ""), + ('css-color-4-rgb-4', 'rgb(0 255 0)', 0,255,0,255, ""), + ('css-color-4-rgb-5', 'rgb(0 255 0 / 0.2)', 0,255,0,51, ""), + ('css-color-4-rgb-6', 'rgb(0 255 0 / 20%)', 0,255,0,51, ""), + ('css-color-4-rgba-1', 'rgba(0, 255.0, 0)', 0,255,0,255, ""), + ('css-color-4-rgba-2', 'rgba(0, 255, 0, 0.2)', 0,255,0,51, ""), + ('css-color-4-rgba-3', 'rgba(0, 255, 0, 20%)', 0,255,0,51, ""), + ('css-color-4-rgba-4', 'rgba(0 255 0)', 0,255,0,255, ""), + ('css-color-4-rgba-5', 'rgba(0 255 0 / 0.2)', 0,255,0,51, ""), + ('css-color-4-rgba-6', 'rgba(0 255 0 / 20%)', 0,255,0,51, ""), + # css-color-4 hsl() color function + # https://drafts.csswg.org/css-color/#the-hsl-notation + ('css-color-4-hsl-1', 'hsl(120 100.0% 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsl-2', 'hsl(120 100.0% 50.0% / 0.2)', 0,255,0,51, ""), + ('css-color-4-hsl-3', 'hsl(120.0, 100.0%, 50.0%, 0.2)', 0,255,0,51, ""), + ('css-color-4-hsl-4', 'hsl(120.0, 100.0%, 50.0%, 20%)', 0,255,0,51, ""), + ('css-color-4-hsl-5', 'hsl(120deg, 100.0%, 50.0%, 0.2)', 0,255,0,51, ""), + ('css-color-4-hsl-6', 'hsl(120deg, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsl-7', 'hsl(133.33333333grad, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsl-8', 'hsl(2.0943951024rad, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsl-9', 'hsl(0.3333333333turn, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-1', 'hsl(120 100.0% 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-2', 'hsl(120 100.0% 50.0% / 0.2)', 0,255,0,51, ""), + ('css-color-4-hsla-3', 'hsl(120.0, 100.0%, 50.0%, 0.2)', 0,255,0,51, ""), + ('css-color-4-hsla-4', 'hsl(120.0, 100.0%, 50.0%, 20%)', 0,255,0,51, ""), + ('css-color-4-hsla-5', 'hsl(120deg, 100.0%, 50.0%, 0.2)', 0,255,0,51, ""), + ('css-color-4-hsla-6', 'hsl(120deg, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-7', 'hsl(133.33333333grad, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-8', 'hsl(2.0943951024rad, 100.0%, 50.0%)', 0,255,0,255, ""), + ('css-color-4-hsla-9', 'hsl(0.3333333333turn, 100.0%, 50.0%)', 0,255,0,255, ""), + # currentColor is handled later + ]: + # TODO: test by retrieving fillStyle, instead of actually drawing? + # TODO: test strokeStyle, shadowColor in the same way + test = { + 'name': '2d.fillStyle.parse.%s' % name, + 'testing': [ '2d.colors.parse' ], + 'notes': notes, + 'code': """ + ctx.fillStyle = '#f00'; + ctx.fillStyle = '%s'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == %d,%d,%d,%d; + """ % (string, r,g,b,a), + 'expected': """size 100 50 + cr.set_source_rgba(%f, %f, %f, %f) + cr.rectangle(0, 0, 100, 50) + cr.fill() + """ % (r/255., g/255., b/255., a/255.), + } + tests.append(test) + + # Also test that invalid colors are ignored + for name, string in [ + ('hex1', '#f'), + ('hex2', '#f0'), + ('hex3', '#g00'), + ('hex4', '#fg00'), + ('hex5', '#ff000'), + ('hex6', '#fg0000'), + ('hex7', '#ff0000f'), + ('hex8', '#fg0000ff'), + ('rgb-1', 'rgb(255.0, 0, 0,)'), + ('rgb-2', 'rgb(100%, 0, 0)'), + ('rgb-3', 'rgb(255, - 1, 0)'), + ('rgba-1', 'rgba(100%, 0, 0, 1)'), + ('rgba-2', 'rgba(255, 0, 0, 1. 0)'), + ('rgba-3', 'rgba(255, 0, 0, 1.)'), + ('rgba-4', 'rgba(255, 0, 0, '), + ('rgba-5', 'rgba(255, 0, 0, 1,)'), + ('hsl-1', 'hsl(0%, 100%, 50%)'), + ('hsl-2', 'hsl(z, 100%, 50%)'), + ('hsl-3', 'hsl(0, 0, 50%)'), + ('hsl-4', 'hsl(0, 100%, 0)'), + ('hsl-5', 'hsl(0, 100.%, 50%)'), + ('hsl-6', 'hsl(0, 100%, 50%,)'), + ('hsla-1', 'hsla(0%, 100%, 50%, 1)'), + ('hsla-2', 'hsla(0, 0, 50%, 1)'), + ('hsla-3', 'hsla(0, 0, 50%, 1,)'), + ('name-1', 'darkbrown'), + ('name-2', 'firebrick1'), + ('name-3', 'red blue'), + ('name-4', '"red"'), + ('name-5', '"red'), + # css-color-4 color function + # comma and comma-less expressions should not mix together. + ('css-color-4-rgb-1', 'rgb(255, 0, 0 / 1)'), + ('css-color-4-rgb-2', 'rgb(255 0 0, 1)'), + ('css-color-4-rgb-3', 'rgb(255, 0 0)'), + ('css-color-4-rgba-1', 'rgba(255, 0, 0 / 1)'), + ('css-color-4-rgba-2', 'rgba(255 0 0, 1)'), + ('css-color-4-rgba-3', 'rgba(255, 0 0)'), + ('css-color-4-hsl-1', 'hsl(0, 100%, 50% / 1)'), + ('css-color-4-hsl-2', 'hsl(0 100% 50%, 1)'), + ('css-color-4-hsl-3', 'hsl(0, 100% 50%)'), + ('css-color-4-hsla-1', 'hsla(0, 100%, 50% / 1)'), + ('css-color-4-hsla-2', 'hsla(0 100% 50%, 1)'), + ('css-color-4-hsla-3', 'hsla(0, 100% 50%)'), + # trailing slash + ('css-color-4-rgb-4', 'rgb(0 0 0 /)'), + ('css-color-4-rgb-5', 'rgb(0, 0, 0 /)'), + ('css-color-4-hsl-4', 'hsl(0 100% 50% /)'), + ('css-color-4-hsl-5', 'hsl(0, 100%, 50% /)'), + ]: + test = { + 'name': '2d.fillStyle.parse.invalid.%s' % name, + 'testing': [ '2d.colors.parse' ], + 'code': """ + ctx.fillStyle = '#0f0'; + try { ctx.fillStyle = '%s'; } catch (e) { } // this shouldn't throw, but it shouldn't matter here if it does + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + """ % string, + 'expected': 'green' + } + tests.append(test) + + # Some can't have positive tests, only negative tests, because we don't know what color they're meant to be + for name, string in [ + ('system', 'ThreeDDarkShadow'), + #('flavor', 'flavor'), # removed from latest CSS3 Color drafts + ]: + test = { + 'name': '2d.fillStyle.parse.%s' % name, + 'testing': [ '2d.colors.parse' ], + 'code': """ + ctx.fillStyle = '#f00'; + ctx.fillStyle = '%s'; + @assert ctx.fillStyle =~ /^#(?!(FF0000|ff0000|f00)$)/; // test that it's not red + """ % (string,), + } + tests.append(test) diff --git a/test/wpt/path-objects.yaml b/test/wpt/path-objects.yaml new file mode 100644 index 000000000..0ca97e762 --- /dev/null +++ b/test/wpt/path-objects.yaml @@ -0,0 +1,3646 @@ +- name: 2d.path.initial + testing: + - 2d.path.initial + #mozilla: { bug: TODO } + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.closePath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.beginPath + testing: + - 2d.path.beginPath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.rect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.moveTo.basic + testing: + - 2d.path.moveTo + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.rect(0, 0, 10, 50); + ctx.moveTo(100, 0); + ctx.lineTo(10, 0); + ctx.lineTo(10, 50); + ctx.lineTo(100, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 90,25 == 0,255,0,255; + expected: green + +- name: 2d.path.moveTo.newsubpath + testing: + - 2d.path.moveTo + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.moveTo(100, 0); + ctx.moveTo(100, 50); + ctx.moveTo(0, 50); + ctx.fillStyle = '#f00'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.moveTo.multiple + testing: + - 2d.path.moveTo + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.moveTo(0, 25); + ctx.moveTo(100, 25); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.moveTo.nonfinite + desc: moveTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.moveTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.closePath.empty + testing: + - 2d.path.closePath.empty + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.closePath(); + ctx.fillStyle = '#f00'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.closePath.newline + testing: + - 2d.path.closePath.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -100); + ctx.lineTo(200, -100); + ctx.lineTo(200, 25); + ctx.closePath(); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.closePath.nextpoint + testing: + - 2d.path.closePath.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -1000); + ctx.closePath(); + ctx.lineTo(1000, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.ensuresubpath.1 + desc: If there is no subpath, the point is added and nothing is drawn + testing: + - 2d.path.lineTo.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.lineTo(100, 50); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.ensuresubpath.2 + desc: If there is no subpath, the point is added and used for subsequent drawing + testing: + - 2d.path.lineTo.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.lineTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.basic + testing: + - 2d.path.lineTo.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.nextpoint + testing: + - 2d.path.lineTo.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(-100, -100); + ctx.lineTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.nonfinite + desc: lineTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.lineTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.lineTo.nonfinite.details + desc: lineTo() with Infinity/NaN for first arg still converts the second arg + testing: + - 2d.nonfinite + code: | + for (var arg1 of [Infinity, -Infinity, NaN]) { + var converted = false; + ctx.lineTo(arg1, { valueOf: function() { converted = true; return 0; } }); + @assert converted; + } + expected: clear + +- name: 2d.path.quadraticCurveTo.ensuresubpath.1 + desc: If there is no subpath, the first control point is added (and nothing is drawn + up to it) + testing: + - 2d.path.quadratic.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.quadraticCurveTo(100, 50, 200, 50); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 95,45 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.quadraticCurveTo.ensuresubpath.2 + desc: If there is no subpath, the first control point is added + testing: + - 2d.path.quadratic.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.quadraticCurveTo(0, 25, 100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 5,45 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.quadraticCurveTo.basic + testing: + - 2d.path.quadratic.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.quadraticCurveTo(100, 25, 100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.quadraticCurveTo.shape + testing: + - 2d.path.quadratic.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 55; + ctx.beginPath(); + ctx.moveTo(-1000, 1050); + ctx.quadraticCurveTo(0, -1000, 1200, 1050); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.quadraticCurveTo.scaled + testing: + - 2d.path.quadratic.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(1000, 1000); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 0.055; + ctx.beginPath(); + ctx.moveTo(-1, 1.05); + ctx.quadraticCurveTo(0, -1, 1.2, 1.05); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.quadraticCurveTo.nonfinite + desc: quadraticCurveTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.quadraticCurveTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.ensuresubpath.1 + desc: If there is no subpath, the first control point is added (and nothing is drawn + up to it) + testing: + - 2d.path.bezier.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.bezierCurveTo(100, 50, 200, 50, 200, 50); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 95,45 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.ensuresubpath.2 + desc: If there is no subpath, the first control point is added + testing: + - 2d.path.bezier.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.bezierCurveTo(0, 25, 100, 25, 100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 5,45 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.basic + testing: + - 2d.path.bezier.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.bezierCurveTo(100, 25, 100, 25, 100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.shape + testing: + - 2d.path.bezier.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 55; + ctx.beginPath(); + ctx.moveTo(-2000, 3100); + ctx.bezierCurveTo(-2000, -1000, 2100, -1000, 2100, 3100); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.scaled + testing: + - 2d.path.bezier.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(1000, 1000); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 0.055; + ctx.beginPath(); + ctx.moveTo(-2, 3.1); + ctx.bezierCurveTo(-2, -1, 2.1, -1, 2.1, 3.1); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.bezierCurveTo.nonfinite + desc: bezierCurveTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.bezierCurveTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.ensuresubpath.1 + desc: If there is no subpath, the first control point is added (and nothing is drawn + up to it) + testing: + - 2d.path.arcTo.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arcTo(100, 50, 200, 50, 0.1); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.ensuresubpath.2 + desc: If there is no subpath, the first control point is added + testing: + - 2d.path.arcTo.empty + - 2d.path.ensure + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arcTo(0, 25, 50, 250, 0.1); // adds (x1,y1), draws nothing + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.coincide.1 + desc: arcTo() has no effect if P0 = P1 + testing: + - 2d.path.arcTo.coincide.01 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(0, 25, 50, 1000, 1); + ctx.lineTo(100, 25); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arcTo(50, 25, 100, 25, 1); + ctx.stroke(); + + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.coincide.2 + desc: arcTo() draws a straight line to P1 if P1 = P2 + testing: + - 2d.path.arcTo.coincide.12 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 100, 25, 1); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.collinear.1 + desc: arcTo() with all points on a line, and P1 between P0/P2, draws a straight + line to P1 + testing: + - 2d.path.arcTo.collinear + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 200, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.arcTo(0, 25, 100, 25, 1); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.collinear.2 + desc: arcTo() with all points on a line, and P2 between P0/P1, draws a straight + line to P1 + testing: + - 2d.path.arcTo.collinear + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 10, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 110, 25, 1); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.collinear.3 + desc: arcTo() with all points on a line, and P0 between P1/P2, draws a straight + line to P1 + testing: + - 2d.path.arcTo.collinear + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, -100, 25, 1); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 0, 25, 1); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(-100, 25); + ctx.arcTo(0, 25, -200, 25, 1); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.shape.curve1 + desc: arcTo() curves in the right kind of shape + testing: + - 2d.path.arcTo.shape + code: | + var tol = 1.5; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.moveTo(10, 25); + ctx.arcTo(75, 25, 75, 60, 20); + ctx.stroke(); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.rect(10, 20, 45, 10); + ctx.moveTo(80, 45); + ctx.arc(55, 45, 25+tol, 0, -Math.PI/2, true); + ctx.arc(55, 45, 15-tol, -Math.PI/2, 0, false); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 55,19 == 0,255,0,255; + @assert pixel 55,20 == 0,255,0,255; + @assert pixel 55,21 == 0,255,0,255; + @assert pixel 64,22 == 0,255,0,255; + @assert pixel 65,21 == 0,255,0,255; + @assert pixel 72,28 == 0,255,0,255; + @assert pixel 73,27 == 0,255,0,255; + @assert pixel 78,36 == 0,255,0,255; + @assert pixel 79,35 == 0,255,0,255; + @assert pixel 80,44 == 0,255,0,255; + @assert pixel 80,45 == 0,255,0,255; + @assert pixel 80,46 == 0,255,0,255; + @assert pixel 65,45 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.shape.curve2 + desc: arcTo() curves in the right kind of shape + testing: + - 2d.path.arcTo.shape + code: | + var tol = 1.5; // tolerance to avoid antialiasing artifacts + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.rect(10, 20, 45, 10); + ctx.moveTo(80, 45); + ctx.arc(55, 45, 25-tol, 0, -Math.PI/2, true); + ctx.arc(55, 45, 15+tol, -Math.PI/2, 0, false); + ctx.fill(); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.moveTo(10, 25); + ctx.arcTo(75, 25, 75, 60, 20); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 55,19 == 0,255,0,255; + @assert pixel 55,20 == 0,255,0,255; + @assert pixel 55,21 == 0,255,0,255; + @assert pixel 64,22 == 0,255,0,255; + @assert pixel 65,21 == 0,255,0,255; + @assert pixel 72,28 == 0,255,0,255; + @assert pixel 73,27 == 0,255,0,255; + @assert pixel 78,36 == 0,255,0,255; + @assert pixel 79,35 == 0,255,0,255; + @assert pixel 80,44 == 0,255,0,255; + @assert pixel 80,45 == 0,255,0,255; + @assert pixel 80,46 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.shape.start + desc: arcTo() draws a straight line from P0 to P1 + testing: + - 2d.path.arcTo.shape + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(200, 25, 200, 50, 10); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.shape.end + desc: arcTo() does not draw anything from P1 to P2 + testing: + - 2d.path.arcTo.shape + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.beginPath(); + ctx.moveTo(-100, -100); + ctx.arcTo(-100, 25, 200, 25, 10); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.negative + desc: arcTo() with negative radius throws an exception + testing: + - 2d.path.arcTo.negative + code: | + @assert throws INDEX_SIZE_ERR ctx.arcTo(0, 0, 0, 0, -1); + var path = new Path2D(); + @assert throws INDEX_SIZE_ERR path.arcTo(10, 10, 20, 20, -5); + +- name: 2d.path.arcTo.zero.1 + desc: arcTo() with zero radius draws a straight line from P0 to P1 + testing: + - 2d.path.arcTo.zeroradius + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, 100, 100, 0); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(0, -25); + ctx.arcTo(50, -25, 50, 50, 0); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.zero.2 + desc: arcTo() with zero radius draws a straight line from P0 to P1, even when all + points are collinear + testing: + - 2d.path.arcTo.zeroradius + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arcTo(100, 25, -100, 25, 0); + ctx.stroke(); + + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 25); + ctx.arcTo(200, 25, 50, 25, 0); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.transformation + desc: arcTo joins up to the last subpath point correctly + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 50); + ctx.translate(100, 0); + ctx.arcTo(50, 50, 50, 0, 50); + ctx.lineTo(-100, 0); + ctx.fill(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.scale + desc: arcTo scales the curve, not just the control points + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 50); + ctx.translate(100, 0); + ctx.scale(0.1, 1); + ctx.arcTo(50, 50, 50, 0, 50); + ctx.lineTo(-1000, 0); + ctx.fill(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.arcTo.nonfinite + desc: arcTo() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.arcTo(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + + +- name: 2d.path.arc.empty + desc: arc() with an empty path does not draw a straight line to the start point + testing: + - 2d.path.arc.nonempty + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(200, 25, 5, 0, 2*Math.PI, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.nonempty + desc: arc() with a non-empty path does draw a straight line to the start point + testing: + - 2d.path.arc.nonempty + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arc(200, 25, 5, 0, 2*Math.PI, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.end + desc: arc() adds the end point of the arc to the subpath + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(-100, 0); + ctx.arc(-100, 0, 25, -Math.PI/2, Math.PI/2, true); + ctx.lineTo(100, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.default + desc: arc() with missing last argument defaults to clockwise + testing: + - 2d.path.arc.omitted + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, -Math.PI, Math.PI/2); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.1 + desc: arc() draws pi/2 .. -pi anticlockwise correctly + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, Math.PI/2, -Math.PI, true); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.2 + desc: arc() draws -3pi/2 .. -pi anticlockwise correctly + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, -3*Math.PI/2, -Math.PI, true); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.3 + desc: arc() wraps angles mod 2pi when anticlockwise and end > start+2pi + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, (512+1/2)*Math.PI, (1024-1)*Math.PI, true); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.4 + desc: arc() draws a full circle when clockwise and end > start+2pi + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arc(50, 25, 60, (512+1/2)*Math.PI, (1024-1)*Math.PI, false); + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.5 + desc: arc() wraps angles mod 2pi when clockwise and start > end+2pi + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(100, 0); + ctx.arc(100, 0, 150, (1024-1)*Math.PI, (512+1/2)*Math.PI, false); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.angle.6 + desc: arc() draws a full circle when anticlockwise and start > end+2pi + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arc(50, 25, 60, (1024-1)*Math.PI, (512+1/2)*Math.PI, true); + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.zero.1 + desc: arc() draws nothing when startAngle = endAngle and anticlockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 0, true); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.zero.2 + desc: arc() draws nothing when startAngle = endAngle and clockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 0, false); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.twopie.1 + desc: arc() draws nothing when end = start + 2pi-e and anticlockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI - 1e-4, true); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.twopie.2 + desc: arc() draws a full circle when end = start + 2pi-e and clockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI - 1e-4, false); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.twopie.3 + desc: arc() draws a full circle when end = start + 2pi+e and anticlockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI + 1e-4, true); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.twopie.4 + desc: arc() draws nothing when end = start + 2pi+e and clockwise + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.arc(50, 25, 50, 0, 2*Math.PI + 1e-4, false); + ctx.stroke(); + @assert pixel 50,20 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.1 + desc: arc() from 0 to pi does not draw anything in the wrong half + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI, false); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 20,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.2 + desc: arc() from 0 to pi draws stuff in the right half + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 100; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 20,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.3 + desc: arc() from 0 to -pi/2 does not draw anything in the wrong quadrant + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 100; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(0, 50, 50, 0, -Math.PI/2, false); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; @moz-todo + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.4 + desc: arc() from 0 to -pi/2 draws stuff in the right quadrant + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 150; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(-50, 50, 100, 0, -Math.PI/2, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.shape.5 + desc: arc() from 0 to 5pi does not draw crazy things + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 200; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(300, 0, 100, 0, 5*Math.PI, false); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.selfintersect.1 + desc: arc() with lineWidth > 2*radius is drawn sensibly + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 200; + ctx.strokeStyle = '#f00'; + ctx.beginPath(); + ctx.arc(100, 50, 25, 0, -Math.PI/2, true); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(0, 0, 25, 0, -Math.PI/2, true); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; @moz-todo + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.selfintersect.2 + desc: arc() with lineWidth > 2*radius is drawn sensibly + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 180; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(-50, 50, 25, 0, -Math.PI/2, true); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(100, 0, 25, 0, -Math.PI/2, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,10 == 0,255,0,255; + @assert pixel 97,1 == 0,255,0,255; + @assert pixel 97,2 == 0,255,0,255; + @assert pixel 97,3 == 0,255,0,255; + @assert pixel 2,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.negative + desc: arc() with negative radius throws INDEX_SIZE_ERR + testing: + - 2d.path.arc.negative + code: | + @assert throws INDEX_SIZE_ERR ctx.arc(0, 0, -1, 0, 0, true); + var path = new Path2D(); + @assert throws INDEX_SIZE_ERR path.arc(10, 10, -5, 0, 1, false); + +- name: 2d.path.arc.zeroradius + desc: arc() with zero radius draws a line to the start point + testing: + - 2d.path.arc.zero + code: | + ctx.fillStyle = '#f00' + ctx.fillRect(0, 0, 100, 50); + ctx.lineWidth = 50; + ctx.strokeStyle = '#0f0'; + ctx.beginPath(); + ctx.moveTo(0, 25); + ctx.arc(200, 25, 0, 0, Math.PI, true); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.scale.1 + desc: Non-uniformly scaled arcs are the right shape + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(2, 0.5); + ctx.fillStyle = '#0f0'; + ctx.beginPath(); + ctx.arc(25, 50, 56, 0, 2*Math.PI, false); + ctx.fill(); + ctx.fillStyle = '#f00'; + ctx.beginPath(); + ctx.moveTo(-25, 50); + ctx.arc(-25, 50, 24, 0, 2*Math.PI, false); + ctx.moveTo(75, 50); + ctx.arc(75, 50, 24, 0, 2*Math.PI, false); + ctx.moveTo(25, -25); + ctx.arc(25, -25, 24, 0, 2*Math.PI, false); + ctx.moveTo(25, 125); + ctx.arc(25, 125, 24, 0, 2*Math.PI, false); + ctx.fill(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.scale.2 + desc: Highly scaled arcs are the right shape + testing: + - 2d.path.arc.draw + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.scale(100, 100); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 1.2; + ctx.beginPath(); + ctx.arc(0, 0, 0.6, 0, Math.PI/2, false); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.arc.nonfinite + desc: arc() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.arc(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <2*Math.PI Infinity -Infinity NaN>, ); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + + +- name: 2d.path.rect.basic + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.rect(0, 0, 100, 50); + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.newsubpath + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-50, 25); + ctx.rect(200, 25, 1, 1); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.closed + testing: + - 2d.path.rect.closed + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.rect(100, 50, 100, 100); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.end.1 + testing: + - 2d.path.rect.newsubpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.rect(200, 100, 400, 1000); + ctx.lineTo(-2000, -1000); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.end.2 + testing: + - 2d.path.rect.newsubpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 450; + ctx.lineCap = 'round'; + ctx.lineJoin = 'bevel'; + ctx.rect(150, 150, 2000, 2000); + ctx.lineTo(160, 160); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.1 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(0, 50, 100, 0); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.2 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(50, -100, 0, 250); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.3 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.rect(50, 25, 0, 0); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.4 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.rect(100, 25, 0, 0); + ctx.lineTo(0, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.5 + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(0, 0); + ctx.rect(100, 25, 0, 0); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.rect.zero.6 + testing: + - 2d.path.rect.subpath + #mozilla: { bug: TODO } + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.5; + ctx.lineWidth = 200; + ctx.beginPath(); + ctx.rect(100, 25, 1000, 0); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.rect.negative + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#0f0'; + ctx.rect(0, 0, 50, 25); + ctx.rect(100, 0, -50, 25); + ctx.rect(0, 50, 50, -25); + ctx.rect(100, 50, -50, -25); + ctx.fill(); + @assert pixel 25,12 == 0,255,0,255; + @assert pixel 75,12 == 0,255,0,255; + @assert pixel 25,37 == 0,255,0,255; + @assert pixel 75,37 == 0,255,0,255; + +- name: 2d.path.rect.winding + testing: + - 2d.path.rect.subpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.rect(0, 0, 50, 50); + ctx.rect(100, 50, -50, -50); + ctx.rect(0, 25, 100, -25); + ctx.rect(100, 25, -100, 25); + ctx.fill(); + @assert pixel 25,12 == 0,255,0,255; + @assert pixel 75,12 == 0,255,0,255; + @assert pixel 25,37 == 0,255,0,255; + @assert pixel 75,37 == 0,255,0,255; + +- name: 2d.path.rect.selfintersect + #mozilla: { bug: TODO } + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 90; + ctx.beginPath(); + ctx.rect(45, 20, 10, 10); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.rect.nonfinite + desc: rect() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.rect(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.newsubpath + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-50, 25); + ctx.roundRect(200, 25, 1, 1, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.closed + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.roundRect(100, 50, 100, 100, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.end.1 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.roundRect(200, 100, 400, 1000, [0]); + ctx.lineTo(-2000, -1000); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.end.2 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 450; + ctx.lineCap = 'round'; + ctx.lineJoin = 'bevel'; + ctx.roundRect(150, 150, 2000, 2000, [0]); + ctx.lineTo(160, 160); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.end.3 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.roundRect(101, 51, 2000, 2000, [500, 500, 500, 500]); + ctx.lineTo(-1, -1); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.end.4 + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 10; + ctx.roundRect(-1, -1, 2000, 2000, [1000, 1000, 1000, 1000]); + ctx.lineTo(-150, -150); + ctx.stroke(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.1 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(0, 50, 100, 0, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.2 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(50, -100, 0, 250, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.3 + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.beginPath(); + ctx.roundRect(50, 25, 0, 0, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.4 + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 50; + ctx.roundRect(100, 25, 0, 0, [0]); + ctx.lineTo(0, 25); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.5 + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.moveTo(0, 0); + ctx.roundRect(100, 25, 0, 0, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.zero.6 + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.5; + ctx.lineWidth = 200; + ctx.beginPath(); + ctx.roundRect(100, 25, 1000, 0, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.negative + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#0f0'; + ctx.roundRect(0, 0, 50, 25, [10, 0, 0, 0]); + ctx.roundRect(100, 0, -50, 25, [10, 0, 0, 0]); + ctx.roundRect(0, 50, 50, -25, [10, 0, 0, 0]); + ctx.roundRect(100, 50, -50, -25, [10, 0, 0, 0]); + ctx.fill(); + // All rects drawn + @assert pixel 25,12 == 0,255,0,255; + @assert pixel 75,12 == 0,255,0,255; + @assert pixel 25,37 == 0,255,0,255; + @assert pixel 75,37 == 0,255,0,255; + // Correct corners are rounded. + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + +- name: 2d.path.roundrect.winding + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.beginPath(); + ctx.fillStyle = '#f00'; + ctx.roundRect(0, 0, 50, 50, [0]); + ctx.roundRect(100, 50, -50, -50, [0]); + ctx.roundRect(0, 25, 100, -25, [0]); + ctx.roundRect(100, 25, -100, 25, [0]); + ctx.fill(); + @assert pixel 25,12 == 0,255,0,255; + @assert pixel 75,12 == 0,255,0,255; + @assert pixel 25,37 == 0,255,0,255; + @assert pixel 75,37 == 0,255,0,255; + +- name: 2d.path.roundrect.selfintersect + code: | + ctx.fillStyle = '#f00'; + ctx.roundRect(0, 0, 100, 50, [0]); + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 90; + ctx.beginPath(); + ctx.roundRect(45, 20, 10, 10, [0]); + ctx.stroke(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.nonfinite + desc: roundRect() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + @nonfinite ctx.roundRect(<0 Infinity -Infinity NaN>, <50 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <1 Infinity -Infinity NaN>, <[0] [Infinity] [-Infinity] [NaN] [Infinity,0] [-Infinity,0] [NaN,0] [0,Infinity] [0,-Infinity] [0,NaN] [Infinity,0,0] [-Infinity,0,0] [NaN,0,0] [0,Infinity,0] [0,-Infinity,0] [0,NaN,0] [0,0,Infinity] [0,0,-Infinity] [0,0,NaN] [Infinity,0,0,0] [-Infinity,0,0,0] [NaN,0,0,0] [0,Infinity,0,0] [0,-Infinity,0,0] [0,NaN,0,0] [0,0,Infinity,0] [0,0,-Infinity,0] [0,0,NaN,0] [0,0,0,Infinity] [0,0,0,-Infinity] [0,0,0,NaN]>); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, Infinity)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, -Infinity)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(10, NaN)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(Infinity, 10)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(-Infinity, 10)]); + ctx.roundRect(0, 0, 100, 100, [new DOMPoint(NaN, 10)]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: Infinity}]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: -Infinity}]); + ctx.roundRect(0, 0, 100, 100, [{x: 10, y: NaN}]); + ctx.roundRect(0, 0, 100, 100, [{x: Infinity, y: 10}]); + ctx.roundRect(0, 0, 100, 100, [{x: -Infinity, y: 10}]); + ctx.roundRect(0, 0, 100, 100, [{x: NaN, y: 10}]); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 90,45 == 0,255,0,255; + expected: green + +- name: 2d.path.roundrect.4.radii.1.double + desc: Verify that when four radii are given to roundRect(), the first radius, specified as a double, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.1.dompoint + desc: Verify that when four radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.1.dompointinit + desc: Verify that when four radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.2.double + desc: Verify that when four radii are given to roundRect(), the second radius, specified as a double, applies to the top-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.2.dompoint + desc: Verify that when four radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20), 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.2.dompointinit + desc: Verify that when four radii are given to roundRect(), the second radius, specified as a DOMPointInit, applies to the top-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.3.double + desc: Verify that when four radii are given to roundRect(), the third radius, specified as a double, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.3.dompoint + desc: Verify that when four radii are given to roundRect(), the third radius, specified as a DOMPoint, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.3.dompointinit + desc: Verify that when four radii are given to roundRect(), the third radius, specified as a DOMPointInit, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, {x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.4.double + desc: Verify that when four radii are given to roundRect(), the fourth radius, specified as a double, applies to the bottom-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.4.radii.4.dompoint + desc: Verify that when four radii are given to roundRect(), the fourth radius, specified as a DOMPoint, applies to the bottom-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.4.radii.4.dompointinit + desc: Verify that when four radii are given to roundRect(), the fourth radius, specified as a DOMPointInit, applies to the bottom-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.1.double + desc: Verify that when three radii are given to roundRect(), the first radius, specified as a double, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.1.dompoint + desc: Verify that when three radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.1.dompointinit + desc: Verify that when three radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.2.double + desc: Verify that when three radii are given to roundRect(), the second radius, specified as a double, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.3.radii.2.dompoint + desc: Verify that when three radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.2.dompointinit + desc: Verify that when three radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.3.double + desc: Verify that when three radii are given to roundRect(), the third radius, specified as a double, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.3.dompoint + desc: Verify that when three radii are given to roundRect(), the third radius, specified as a DOMPoint, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.3.radii.3.dompointinit + desc: Verify that when three radii are given to roundRect(), the third radius, specified as a DOMPointInit, applies to the bottom-right corner. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.1.double + desc: Verify that when two radii are given to roundRect(), the first radius, specified as a double, applies to the top-left and bottom-right corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.1.dompoint + desc: Verify that when two radii are given to roundRect(), the first radius, specified as a DOMPoint, applies to the top-left and bottom-right corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20), 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.1.dompointinit + desc: Verify that when two radii are given to roundRect(), the first radius, specified as a DOMPointInit, applies to the top-left and bottom-right corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}, 0]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // other corners + @assert pixel 98,1 == 0,255,0,255; + @assert pixel 1,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.2.double + desc: Verify that when two radii are given to roundRect(), the second radius, specified as a double, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, 20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 0,255,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.2.radii.2.dompoint + desc: Verify that when two radii are given to roundRect(), the second radius, specified as a DOMPoint, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.2.radii.2.dompointinit + desc: Verify that when two radii are given to roundRect(), the second radius, specified as a DOMPointInit, applies to the top-right and bottom-left corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [0, {x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + + // other corners + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + +- name: 2d.path.roundrect.1.radius.double + desc: Verify that when one radius is given to roundRect(), specified as a double, it applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [20]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.1.radius.double.single.argument + desc: Verify that when one radius is given to roundRect() as a non-array argument, specified as a double, it applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, 20); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + +- name: 2d.path.roundrect.1.radius.dompoint + desc: Verify that when one radius is given to roundRect(), specified as a DOMPoint, it applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [new DOMPoint(40, 20)]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + +- name: 2d.path.roundrect.1.radius.dompoint.single argument + desc: Verify that when one radius is given to roundRect() as a non-array argument, specified as a DOMPoint, it applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, new DOMPoint(40, 20)); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + +- name: 2d.path.roundrect.1.radius.dompointinit + desc: Verify that when one radius is given to roundRect(), specified as a DOMPointInit, applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [{x: 40, y: 20}]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + +- name: 2d.path.roundrect.1.radius.dompointinit.single.argument + desc: Verify that when one radius is given to roundRect() as a non-array argument, specified as a DOMPointInit, applies to all corners. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, {x: 40, y: 20}); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + // top-left corner + @assert pixel 20,1 == 255,0,0,255; + @assert pixel 41,1 == 0,255,0,255; + @assert pixel 1,10 == 255,0,0,255; + @assert pixel 1,21 == 0,255,0,255; + + // top-right corner + @assert pixel 79,1 == 255,0,0,255; + @assert pixel 58,1 == 0,255,0,255; + @assert pixel 98,10 == 255,0,0,255; + @assert pixel 98,21 == 0,255,0,255; + + // bottom-right corner + @assert pixel 79,48 == 255,0,0,255; + @assert pixel 58,48 == 0,255,0,255; + @assert pixel 98,39 == 255,0,0,255; + @assert pixel 98,28 == 0,255,0,255; + + // bottom-left corner + @assert pixel 20,48 == 255,0,0,255; + @assert pixel 41,48 == 0,255,0,255; + @assert pixel 1,39 == 255,0,0,255; + @assert pixel 1,28 == 0,255,0,255; + +- name: 2d.path.roundrect.radius.intersecting.1 + desc: Check that roundRects with intersecting corner arcs are rendered correctly. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [40, 40, 40, 40]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 2,25 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 97,25 == 0,255,0,255; + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + +- name: 2d.path.roundrect.radius.intersecting.2 + desc: Check that roundRects with intersecting corner arcs are rendered correctly. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(0, 0, 100, 50, [1000, 1000, 1000, 1000]); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 2,25 == 0,255,0,255; + @assert pixel 50,1 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 50,48 == 0,255,0,255; + @assert pixel 97,25 == 0,255,0,255; + @assert pixel 1,1 == 255,0,0,255; + @assert pixel 98,1 == 255,0,0,255; + @assert pixel 1,48 == 255,0,0,255; + @assert pixel 98,48 == 255,0,0,255; + +- name: 2d.path.roundrect.radius.none + desc: Check that roundRect throws an RangeError if radii is an empty array. + code: | + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 100, 50, [])}); + +- name: 2d.path.roundrect.radius.noargument + desc: Check that roundRect draws a rectangle when no radii are provided. + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.roundRect(10, 10, 80, 30); + ctx.fillStyle = '#0f0'; + ctx.fill(); + // upper left corner (10, 10) + @assert pixel 10,9 == 255,0,0,255; + @assert pixel 9,10 == 255,0,0,255; + @assert pixel 10,10 == 0,255,0,255; + + // upper right corner (89, 10) + @assert pixel 90,10 == 255,0,0,255; + @assert pixel 89,9 == 255,0,0,255; + @assert pixel 89,10 == 0,255,0,255; + + // lower right corner (89, 39) + @assert pixel 89,40 == 255,0,0,255; + @assert pixel 90,39 == 255,0,0,255; + @assert pixel 89,39 == 0,255,0,255; + + // lower left corner (10, 30) + @assert pixel 9,39 == 255,0,0,255; + @assert pixel 10,40 == 255,0,0,255; + @assert pixel 10,39 == 0,255,0,255; + +- name: 2d.path.roundrect.radius.toomany + desc: Check that roundRect throws an IndeSizeError if radii has more than four items. + code: | + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 100, 50, [0, 0, 0, 0, 0])}); + +- name: 2d.path.roundrect.radius.negative + desc: roundRect() with negative radius throws an exception + code: | + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [-1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [1, -1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [new DOMPoint(-1, 1), 1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [new DOMPoint(1, -1)])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [{x: -1, y: 1}, 1])}); + assert_throws_js(RangeError, () => { ctx.roundRect(0, 0, 0, 0, [{x: 1, y: -1}])}); + +- name: 2d.path.ellipse.basics + desc: Verify canvas throws error when drawing ellipse with negative radii. + testing: + - 2d.ellipse.basics + code: | + ctx.ellipse(10, 10, 10, 5, 0, 0, 1, false); + ctx.ellipse(10, 10, 10, 0, 0, 0, 1, false); + ctx.ellipse(10, 10, -0, 5, 0, 0, 1, false); + @assert throws INDEX_SIZE_ERR ctx.ellipse(10, 10, -2, 5, 0, 0, 1, false); + @assert throws INDEX_SIZE_ERR ctx.ellipse(10, 10, 0, -1.5, 0, 0, 1, false); + @assert throws INDEX_SIZE_ERR ctx.ellipse(10, 10, -2, -5, 0, 0, 1, false); + ctx.ellipse(80, 0, 10, 4294967277, Math.PI / -84, -Math.PI / 2147483436, false); + +- name: 2d.path.fill.overlap + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; + ctx.rect(0, 0, 100, 50); + ctx.closePath(); + ctx.rect(10, 10, 80, 30); + ctx.fill(); + + @assert pixel 50,25 ==~ 0,127,0,255 +/- 1; + expected: | + size 100 50 + cr.set_source_rgb(0, 0.5, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.path.fill.winding.add + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.winding.subtract.1 + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.winding.subtract.2 + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.winding.subtract.3 + testing: + - 2d.path.fill.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(-20, -20); + ctx.lineTo(120, -20); + ctx.lineTo(120, 70); + ctx.lineTo(-20, 70); + ctx.lineTo(-20, -20); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.closed.basic + testing: + - 2d.path.fill.closed + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.lineTo(0, 50); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.fill.closed.unaffected + testing: + - 2d.path.fill.closed + code: | + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 100, 50); + + ctx.moveTo(0, 0); + ctx.lineTo(100, 0); + ctx.lineTo(100, 50); + ctx.fillStyle = '#f00'; + ctx.fill(); + ctx.lineTo(0, 50); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + @assert pixel 90,10 == 0,255,0,255; + @assert pixel 10,40 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.overlap + desc: Stroked subpaths are combined before being drawn + testing: + - 2d.path.stroke.basic + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)'; + ctx.lineWidth = 50; + ctx.moveTo(0, 20); + ctx.lineTo(100, 20); + ctx.moveTo(0, 30); + ctx.lineTo(100, 30); + ctx.stroke(); + + @assert pixel 50,25 ==~ 0,127,0,255 +/- 1; + expected: | + size 100 50 + cr.set_source_rgb(0, 0.5, 0) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.path.stroke.union + desc: Strokes in opposite directions are unioned, not subtracted + testing: + - 2d.path.stroke.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#0f0'; + ctx.lineWidth = 40; + ctx.moveTo(0, 10); + ctx.lineTo(100, 10); + ctx.moveTo(100, 40); + ctx.lineTo(0, 40); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.unaffected + desc: Stroking does not start a new path or subpath + testing: + - 2d.path.stroke.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.lineWidth = 50; + ctx.moveTo(-100, 25); + ctx.lineTo(-100, -100); + ctx.lineTo(200, -100); + ctx.lineTo(200, 25); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + + ctx.closePath(); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.scale1 + desc: Stroke line widths are scaled by the current transformation matrix + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(25, 12.5, 50, 25); + ctx.save(); + ctx.scale(50, 25); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.beginPath(); + ctx.rect(-25, -12.5, 150, 75); + ctx.save(); + ctx.scale(50, 25); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.scale2 + desc: Stroke line widths are scaled by the current transformation matrix + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(25, 12.5, 50, 25); + ctx.save(); + ctx.rotate(Math.PI/2); + ctx.scale(25, 50); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.beginPath(); + ctx.rect(-25, -12.5, 150, 75); + ctx.save(); + ctx.rotate(Math.PI/2); + ctx.scale(25, 50); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.skew + desc: Strokes lines are skewed by the current transformation matrix + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(49, -50); + ctx.lineTo(201, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 283); + ctx.strokeStyle = '#0f0'; + ctx.stroke(); + ctx.restore(); + + ctx.save(); + ctx.beginPath(); + ctx.translate(-150, 0); + ctx.moveTo(49, -50); + ctx.lineTo(199, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 142); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + ctx.save(); + ctx.beginPath(); + ctx.translate(-150, 0); + ctx.moveTo(49, -50); + ctx.lineTo(199, -50); + ctx.rotate(Math.PI/4); + ctx.scale(1, 142); + ctx.strokeStyle = '#f00'; + ctx.stroke(); + ctx.restore(); + + @assert pixel 0,0 == 0,255,0,255; + @assert pixel 50,0 == 0,255,0,255; + @assert pixel 99,0 == 0,255,0,255; + @assert pixel 0,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 99,25 == 0,255,0,255; + @assert pixel 0,49 == 0,255,0,255; + @assert pixel 50,49 == 0,255,0,255; + @assert pixel 99,49 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.empty + desc: Empty subpaths are not stroked + testing: + - 2d.path.stroke.empty + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(40, 25); + ctx.moveTo(60, 25); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.stroke.prune.line + desc: Zero-length line segments from lineTo are removed before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.lineTo(50, 25); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.closed + desc: Zero-length line segments from closed paths are removed before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.lineTo(50, 25); + ctx.closePath(); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.curve + desc: Zero-length line segments from quadraticCurveTo and bezierCurveTo are removed + before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.quadraticCurveTo(50, 25, 50, 25); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.bezierCurveTo(50, 25, 50, 25, 50, 25); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.arc + desc: Zero-length line segments from arcTo and arc are removed before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.moveTo(50, 25); + ctx.arcTo(50, 25, 150, 25, 10); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(60, 25); + ctx.arc(50, 25, 10, 0, 0, false); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.rect + desc: Zero-length line segments from rect and strokeRect are removed before stroking + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 100; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + ctx.beginPath(); + ctx.rect(50, 25, 0, 0); + ctx.stroke(); + + ctx.strokeRect(50, 25, 0, 0); + + @assert pixel 50,25 == 0,255,0,255; @moz-todo + expected: green + +- name: 2d.path.stroke.prune.corner + desc: Zero-length line segments are removed before stroking with miters + testing: + - 2d.path.stroke.prune + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 400; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 1.4; + + ctx.beginPath(); + ctx.moveTo(-1000, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 200); + ctx.lineTo(-100, 1000); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + + +- name: 2d.path.transformation.basic + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(-100, 0); + ctx.rect(100, 0, 100, 50); + ctx.translate(0, -100); + ctx.fillStyle = '#0f0'; + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.transformation.multiple + # TODO: change this name + desc: Transformations are applied while building paths, not when drawing + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.translate(-100, 0); + ctx.rect(0, 0, 100, 50); + ctx.fill(); + ctx.translate(100, 0); + ctx.fill(); + + ctx.beginPath(); + ctx.strokeStyle = '#f00'; + ctx.lineWidth = 50; + ctx.translate(0, -50); + ctx.moveTo(0, 25); + ctx.lineTo(100, 25); + ctx.stroke(); + ctx.translate(0, 50); + ctx.stroke(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.transformation.changing + desc: Transformations are applied while building paths, not when drawing + testing: + - 2d.path.transformation + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.moveTo(0, 0); + ctx.translate(100, 0); + ctx.lineTo(0, 0); + ctx.translate(0, 50); + ctx.lineTo(0, 0); + ctx.translate(-100, 0); + ctx.lineTo(0, 0); + ctx.translate(1000, 1000); + ctx.rotate(Math.PI/2); + ctx.scale(0.1, 0.1); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + + +- name: 2d.path.clip.empty + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.basic.1 + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(0, 0, 100, 50); + ctx.clip(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.basic.2 + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(-100, 0, 100, 50); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.intersect + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.beginPath(); + ctx.rect(50, 0, 50, 50) + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.winding.1 + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.lineTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.clip(); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.winding.2 + testing: + - 2d.path.clip.basic + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.beginPath(); + ctx.moveTo(-10, -10); + ctx.lineTo(110, -10); + ctx.lineTo(110, 60); + ctx.lineTo(-10, 60); + ctx.lineTo(-10, -10); + ctx.clip(); + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.lineTo(0, 0); + ctx.clip(); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.path.clip.unaffected + testing: + - 2d.path.clip.closed + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 50); + ctx.lineTo(100, 50); + ctx.lineTo(100, 0); + ctx.clip(); + + ctx.lineTo(0, 0); + ctx.fill(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + + + +- name: 2d.path.isPointInPath.basic.1 + desc: isPointInPath() detects whether the point is inside the path + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(0, 0, 20, 20); + @assert ctx.isPointInPath(10, 10) === true; + @assert ctx.isPointInPath(30, 10) === false; + +- name: 2d.path.isPointInPath.basic.2 + desc: isPointInPath() detects whether the point is inside the path + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(20, 0, 20, 20); + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(30, 10) === true; + +- name: 2d.path.isPointInPath.edge + desc: isPointInPath() counts points on the path as being inside + testing: + - 2d.path.isPointInPath.edge + code: | + ctx.rect(0, 0, 20, 20); + @assert ctx.isPointInPath(0, 0) === true; + @assert ctx.isPointInPath(10, 0) === true; + @assert ctx.isPointInPath(20, 0) === true; + @assert ctx.isPointInPath(20, 10) === true; + @assert ctx.isPointInPath(20, 20) === true; + @assert ctx.isPointInPath(10, 20) === true; + @assert ctx.isPointInPath(0, 20) === true; + @assert ctx.isPointInPath(0, 10) === true; + @assert ctx.isPointInPath(10, -0.01) === false; + @assert ctx.isPointInPath(10, 20.01) === false; + @assert ctx.isPointInPath(-0.01, 10) === false; + @assert ctx.isPointInPath(20.01, 10) === false; + +- name: 2d.path.isPointInPath.empty + desc: isPointInPath() works when there is no path + testing: + - 2d.path.isPointInPath + code: | + @assert ctx.isPointInPath(0, 0) === false; + +- name: 2d.path.isPointInPath.subpath + desc: isPointInPath() uses the current path, not just the subpath + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(0, 0, 20, 20); + ctx.beginPath(); + ctx.rect(20, 0, 20, 20); + ctx.closePath(); + ctx.rect(40, 0, 20, 20); + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(30, 10) === true; + @assert ctx.isPointInPath(50, 10) === true; + +- name: 2d.path.isPointInPath.outside + desc: isPointInPath() works on paths outside the canvas + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(0, -100, 20, 20); + ctx.rect(20, -10, 20, 20); + @assert ctx.isPointInPath(10, -110) === false; + @assert ctx.isPointInPath(10, -90) === true; + @assert ctx.isPointInPath(10, -70) === false; + @assert ctx.isPointInPath(30, -20) === false; + @assert ctx.isPointInPath(30, 0) === true; + @assert ctx.isPointInPath(30, 20) === false; + +- name: 2d.path.isPointInPath.unclosed + desc: isPointInPath() works on unclosed subpaths + testing: + - 2d.path.isPointInPath + code: | + ctx.moveTo(0, 0); + ctx.lineTo(20, 0); + ctx.lineTo(20, 20); + ctx.lineTo(0, 20); + @assert ctx.isPointInPath(10, 10) === true; + @assert ctx.isPointInPath(30, 10) === false; + +- name: 2d.path.isPointInPath.arc + desc: isPointInPath() works on arcs + testing: + - 2d.path.isPointInPath + code: | + ctx.arc(50, 25, 10, 0, Math.PI, false); + @assert ctx.isPointInPath(50, 10) === false; + @assert ctx.isPointInPath(50, 20) === false; + @assert ctx.isPointInPath(50, 30) === true; + @assert ctx.isPointInPath(50, 40) === false; + @assert ctx.isPointInPath(30, 20) === false; + @assert ctx.isPointInPath(70, 20) === false; + @assert ctx.isPointInPath(30, 30) === false; + @assert ctx.isPointInPath(70, 30) === false; + +- name: 2d.path.isPointInPath.bigarc + desc: isPointInPath() works on unclosed arcs larger than 2pi + opera: {bug: 320937} + testing: + - 2d.path.isPointInPath + code: | + ctx.arc(50, 25, 10, 0, 7, false); + @assert ctx.isPointInPath(50, 10) === false; + @assert ctx.isPointInPath(50, 20) === true; + @assert ctx.isPointInPath(50, 30) === true; + @assert ctx.isPointInPath(50, 40) === false; + @assert ctx.isPointInPath(30, 20) === false; + @assert ctx.isPointInPath(70, 20) === false; + @assert ctx.isPointInPath(30, 30) === false; + @assert ctx.isPointInPath(70, 30) === false; + +- name: 2d.path.isPointInPath.bezier + desc: isPointInPath() works on Bezier curves + testing: + - 2d.path.isPointInPath + code: | + ctx.moveTo(25, 25); + ctx.bezierCurveTo(50, -50, 50, 100, 75, 25); + @assert ctx.isPointInPath(25, 20) === false; + @assert ctx.isPointInPath(25, 30) === false; + @assert ctx.isPointInPath(30, 20) === true; + @assert ctx.isPointInPath(30, 30) === false; + @assert ctx.isPointInPath(40, 2) === false; + @assert ctx.isPointInPath(40, 20) === true; + @assert ctx.isPointInPath(40, 30) === false; + @assert ctx.isPointInPath(40, 47) === false; + @assert ctx.isPointInPath(45, 20) === true; + @assert ctx.isPointInPath(45, 30) === false; + @assert ctx.isPointInPath(55, 20) === false; + @assert ctx.isPointInPath(55, 30) === true; + @assert ctx.isPointInPath(60, 2) === false; + @assert ctx.isPointInPath(60, 20) === false; + @assert ctx.isPointInPath(60, 30) === true; + @assert ctx.isPointInPath(60, 47) === false; + @assert ctx.isPointInPath(70, 20) === false; + @assert ctx.isPointInPath(70, 30) === true; + @assert ctx.isPointInPath(75, 20) === false; + @assert ctx.isPointInPath(75, 30) === false; + +- name: 2d.path.isPointInPath.winding + desc: isPointInPath() uses the non-zero winding number rule + testing: + - 2d.path.isPointInPath + code: | + // Create a square ring, using opposite windings to make a hole in the centre + ctx.moveTo(0, 0); + ctx.lineTo(50, 0); + ctx.lineTo(50, 50); + ctx.lineTo(0, 50); + ctx.lineTo(0, 0); + ctx.lineTo(10, 10); + ctx.lineTo(10, 40); + ctx.lineTo(40, 40); + ctx.lineTo(40, 10); + ctx.lineTo(10, 10); + + @assert ctx.isPointInPath(5, 5) === true; + @assert ctx.isPointInPath(25, 5) === true; + @assert ctx.isPointInPath(45, 5) === true; + @assert ctx.isPointInPath(5, 25) === true; + @assert ctx.isPointInPath(25, 25) === false; + @assert ctx.isPointInPath(45, 25) === true; + @assert ctx.isPointInPath(5, 45) === true; + @assert ctx.isPointInPath(25, 45) === true; + @assert ctx.isPointInPath(45, 45) === true; + +- name: 2d.path.isPointInPath.transform.1 + desc: isPointInPath() handles transformations correctly + testing: + - 2d.path.isPointInPath + code: | + ctx.translate(50, 0); + ctx.rect(0, 0, 20, 20); + @assert ctx.isPointInPath(-40, 10) === false; + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(49, 10) === false; + @assert ctx.isPointInPath(51, 10) === true; + @assert ctx.isPointInPath(69, 10) === true; + @assert ctx.isPointInPath(71, 10) === false; + +- name: 2d.path.isPointInPath.transform.2 + desc: isPointInPath() handles transformations correctly + testing: + - 2d.path.isPointInPath + code: | + ctx.rect(50, 0, 20, 20); + ctx.translate(50, 0); + @assert ctx.isPointInPath(-40, 10) === false; + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(49, 10) === false; + @assert ctx.isPointInPath(51, 10) === true; + @assert ctx.isPointInPath(69, 10) === true; + @assert ctx.isPointInPath(71, 10) === false; + +- name: 2d.path.isPointInPath.transform.3 + desc: isPointInPath() handles transformations correctly + testing: + - 2d.path.isPointInPath + code: | + ctx.scale(-1, 1); + ctx.rect(-70, 0, 20, 20); + @assert ctx.isPointInPath(-40, 10) === false; + @assert ctx.isPointInPath(10, 10) === false; + @assert ctx.isPointInPath(49, 10) === false; + @assert ctx.isPointInPath(51, 10) === true; + @assert ctx.isPointInPath(69, 10) === true; + @assert ctx.isPointInPath(71, 10) === false; + +- name: 2d.path.isPointInPath.transform.4 + desc: isPointInPath() handles transformations correctly + testing: + - 2d.path.isPointInPath + code: | + ctx.translate(50, 0); + ctx.rect(50, 0, 20, 20); + ctx.translate(0, 50); + @assert ctx.isPointInPath(60, 10) === false; + @assert ctx.isPointInPath(110, 10) === true; + @assert ctx.isPointInPath(110, 60) === false; + +- name: 2d.path.isPointInPath.nonfinite + desc: isPointInPath() returns false for non-finite arguments + testing: + - 2d.path.isPointInPath.nonfinite + code: | + ctx.rect(-100, -50, 200, 100); + @assert ctx.isPointInPath(Infinity, 0) === false; + @assert ctx.isPointInPath(-Infinity, 0) === false; + @assert ctx.isPointInPath(NaN, 0) === false; + @assert ctx.isPointInPath(0, Infinity) === false; + @assert ctx.isPointInPath(0, -Infinity) === false; + @assert ctx.isPointInPath(0, NaN) === false; + @assert ctx.isPointInPath(NaN, NaN) === false; + + +- name: 2d.path.isPointInStroke.scaleddashes + desc: isPointInStroke() should return correct results on dashed paths at high scale + factors + testing: + - 2d.path.isPointInStroke + code: | + var scale = 20; + ctx.setLineDash([10, 21.4159]); // dash from t=0 to t=10 along the circle + ctx.scale(scale, scale); + ctx.ellipse(6, 10, 5, 5, 0, 2*Math.PI, false); + ctx.stroke(); + + // hit-test the beginning of the dash (t=0) + @assert ctx.isPointInStroke(11*scale, 10*scale) === true; + // hit-test the middle of the dash (t=5) + @assert ctx.isPointInStroke(8.70*scale, 14.21*scale) === true; + // hit-test the end of the dash (t=9.8) + @assert ctx.isPointInStroke(4.10*scale, 14.63*scale) === true; + // hit-test past the end of the dash (t=10.2) + @assert ctx.isPointInStroke(3.74*scale, 14.46*scale) === false; + +- name: 2d.path.isPointInPath.basic + desc: Verify the winding rule in isPointInPath works for for rect path. + testing: + - 2d.isPointInPath.basic + code: | + canvas.width = 200; + canvas.height = 200; + + // Testing default isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(50, 50) === true; + @assert ctx.isPointInPath(NaN, 50) === false; + @assert ctx.isPointInPath(50, NaN) === false; + + // Testing nonzero isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(50, 50, 'nonzero') === true; + + // Testing evenodd isPointInPath + ctx.beginPath(); + ctx.rect(0, 0, 100, 100); + ctx.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(50, 50, 'evenodd') === false; + + // Testing extremely large scale + ctx.save(); + ctx.scale(Number.MAX_VALUE, Number.MAX_VALUE); + ctx.beginPath(); + ctx.rect(-10, -10, 20, 20); + @assert ctx.isPointInPath(0, 0, 'nonzero') === true; + @assert ctx.isPointInPath(0, 0, 'evenodd') === true; + ctx.restore(); + + // Check with non-invertible ctm. + ctx.save(); + ctx.scale(0, 0); + ctx.beginPath(); + ctx.rect(-10, -10, 20, 20); + @assert ctx.isPointInPath(0, 0, 'nonzero') === false; + @assert ctx.isPointInPath(0, 0, 'evenodd') === false; + ctx.restore(); + +- name: 2d.path.isPointInpath.multi.path + desc: Verify the winding rule in isPointInPath works for path object. + testing: + - 2d.isPointInPath.basic + code: | + canvas.width = 200; + canvas.height = 200; + + // Testing default isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(path, 50, 50) === true; + @assert ctx.isPointInPath(path, 50, 50, undefined) === true; + @assert ctx.isPointInPath(path, NaN, 50) === false; + @assert ctx.isPointInPath(path, 50, NaN) === false; + + // Testing nonzero isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + @assert ctx.isPointInPath(path, 50, 50, 'nonzero') === true; + + // Testing evenodd isPointInPath with Path object'); + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + assert_false(ctx.isPointInPath(path, 50, 50, 'evenodd')); + +- name: 2d.path.isPointInpath.invalid + desc: Verify isPointInPath throws exceptions with invalid inputs. + testing: + - 2d.isPointInPath.basic + code: | + canvas.width = 200; + canvas.height = 200; + path = new Path2D(); + path.rect(0, 0, 100, 100); + path.rect(25, 25, 50, 50); + // Testing invalid enumeration isPointInPath (w/ and w/o Path object'); + @assert throws TypeError ctx.isPointInPath(path, 50, 50, 'gazonk'); + @assert throws TypeError ctx.isPointInPath(50, 50, 'gazonk'); + + // Testing invalid type isPointInPath with Path object'); + @assert throws TypeError ctx.isPointInPath(null, 50, 50); + @assert throws TypeError ctx.isPointInPath(null, 50, 50, 'nonzero'); + @assert throws TypeError ctx.isPointInPath(null, 50, 50, 'evenodd'); + @assert throws TypeError ctx.isPointInPath(null, 50, 50, null); + @assert throws TypeError ctx.isPointInPath(path, 50, 50, null); + @assert throws TypeError ctx.isPointInPath(undefined, 50, 50); + @assert throws TypeError ctx.isPointInPath(undefined, 50, 50, 'nonzero'); + @assert throws TypeError ctx.isPointInPath(undefined, 50, 50, 'evenodd'); + @assert throws TypeError ctx.isPointInPath(undefined, 50, 50, undefined); + @assert throws TypeError ctx.isPointInPath([], 50, 50); + @assert throws TypeError ctx.isPointInPath([], 50, 50, 'nonzero'); + @assert throws TypeError ctx.isPointInPath([], 50, 50, 'evenodd'); + @assert throws TypeError ctx.isPointInPath({}, 50, 50); + @assert throws TypeError ctx.isPointInPath({}, 50, 50, 'nonzero'); + @assert throws TypeError ctx.isPointInPath({}, 50, 50, 'evenodd'); diff --git a/test/wpt/pixel-manipulation.yaml b/test/wpt/pixel-manipulation.yaml new file mode 100644 index 000000000..ddacaf441 --- /dev/null +++ b/test/wpt/pixel-manipulation.yaml @@ -0,0 +1,1145 @@ +- name: 2d.imageData.create2.basic + desc: createImageData(sw, sh) exists and returns something + testing: + - 2d.imageData.create2.object + code: | + @assert ctx.createImageData(1, 1) !== null; + +- name: 2d.imageData.create1.basic + desc: createImageData(imgdata) exists and returns something + testing: + - 2d.imageData.create1.object + code: | + @assert ctx.createImageData(ctx.createImageData(1, 1)) !== null; + +- name: 2d.imageData.create2.type + desc: createImageData(sw, sh) returns an ImageData object containing a Uint8ClampedArray + object + testing: + - 2d.imageData.create2.object + code: | + @assert window.ImageData !== undefined; + @assert window.Uint8ClampedArray !== undefined; + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.createImageData(1, 1); + @assert imgdata.thisImplementsImageData; + @assert imgdata.data.thisImplementsUint8ClampedArray; + +- name: 2d.imageData.create1.type + desc: createImageData(imgdata) returns an ImageData object containing a Uint8ClampedArray + object + testing: + - 2d.imageData.create1.object + code: | + @assert window.ImageData !== undefined; + @assert window.Uint8ClampedArray !== undefined; + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.createImageData(ctx.createImageData(1, 1)); + @assert imgdata.thisImplementsImageData; + @assert imgdata.data.thisImplementsUint8ClampedArray; + +- name: 2d.imageData.create2.this + desc: createImageData(sw, sh) should throw when called with the wrong |this| + notes: &bindings Defined in "Web IDL" (draft) + testing: + - 2d.imageData.create2.object + code: | + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call(null, 1, 1); @moz-todo + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call(undefined, 1, 1); @moz-todo + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call({}, 1, 1); @moz-todo + +- name: 2d.imageData.create1.this + desc: createImageData(imgdata) should throw when called with the wrong |this| + notes: *bindings + testing: + - 2d.imageData.create2.object + code: | + var imgdata = ctx.createImageData(1, 1); + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call(null, imgdata); @moz-todo + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call(undefined, imgdata); @moz-todo + @assert throws TypeError CanvasRenderingContext2D.prototype.createImageData.call({}, imgdata); @moz-todo + +- name: 2d.imageData.create2.initial + desc: createImageData(sw, sh) returns transparent black data of the right size + testing: + - 2d.imageData.create2.size + - 2d.imageData.create.initial + - 2d.imageData.initial + code: | + var imgdata = ctx.createImageData(10, 20); + @assert imgdata.data.length === imgdata.width*imgdata.height*4; + @assert imgdata.width < imgdata.height; + @assert imgdata.width > 0; + var isTransparentBlack = true; + for (var i = 0; i < imgdata.data.length; ++i) + if (imgdata.data[i] !== 0) + isTransparentBlack = false; + @assert isTransparentBlack; + +- name: 2d.imageData.create1.initial + desc: createImageData(imgdata) returns transparent black data of the right size + testing: + - 2d.imageData.create1.size + - 2d.imageData.create.initial + - 2d.imageData.initial + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + var imgdata1 = ctx.getImageData(0, 0, 10, 20); + var imgdata2 = ctx.createImageData(imgdata1); + @assert imgdata2.data.length === imgdata1.data.length; + @assert imgdata2.width === imgdata1.width; + @assert imgdata2.height === imgdata1.height; + var isTransparentBlack = true; + for (var i = 0; i < imgdata2.data.length; ++i) + if (imgdata2.data[i] !== 0) + isTransparentBlack = false; + @assert isTransparentBlack; + +- name: 2d.imageData.create2.large + desc: createImageData(sw, sh) works for sizes much larger than the canvas + testing: + - 2d.imageData.create2.size + code: | + var imgdata = ctx.createImageData(1000, 2000); + @assert imgdata.data.length === imgdata.width*imgdata.height*4; + @assert imgdata.width < imgdata.height; + @assert imgdata.width > 0; + var isTransparentBlack = true; + for (var i = 0; i < imgdata.data.length; i += 7813) // check ~1024 points (assuming normal scaling) + if (imgdata.data[i] !== 0) + isTransparentBlack = false; + @assert isTransparentBlack; + +- name: 2d.imageData.create2.negative + desc: createImageData(sw, sh) takes the absolute magnitude of the size arguments + testing: + - 2d.imageData.create2.size + code: | + var imgdata1 = ctx.createImageData(10, 20); + var imgdata2 = ctx.createImageData(-10, 20); + var imgdata3 = ctx.createImageData(10, -20); + var imgdata4 = ctx.createImageData(-10, -20); + @assert imgdata1.data.length === imgdata2.data.length; + @assert imgdata2.data.length === imgdata3.data.length; + @assert imgdata3.data.length === imgdata4.data.length; + +- name: 2d.imageData.create2.zero + desc: createImageData(sw, sh) throws INDEX_SIZE_ERR if size is zero + testing: + - 2d.imageData.getcreate.zero + code: | + @assert throws INDEX_SIZE_ERR ctx.createImageData(10, 0); + @assert throws INDEX_SIZE_ERR ctx.createImageData(0, 10); + @assert throws INDEX_SIZE_ERR ctx.createImageData(0, 0); + @assert throws INDEX_SIZE_ERR ctx.createImageData(0.99, 10); + @assert throws INDEX_SIZE_ERR ctx.createImageData(10, 0.1); + +- name: 2d.imageData.create2.nonfinite + desc: createImageData() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.imageData.getcreate.nonfinite + code: | + @nonfinite @assert throws TypeError ctx.createImageData(<10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>); + var posinfobj = { valueOf: function() { return Infinity; } }, + neginfobj = { valueOf: function() { return -Infinity; } }, + nanobj = { valueOf: function() { return -Infinity; } }; + @nonfinite @assert throws TypeError ctx.createImageData(<10 posinfobj neginfobj nanobj>, <10 posinfobj neginfobj nanobj>); + +- name: 2d.imageData.create1.zero + desc: createImageData(null) throws TypeError + testing: + - 2d.imageData.create.null + code: | + @assert throws TypeError ctx.createImageData(null); + +- name: 2d.imageData.create2.double + desc: createImageData(w, h) double is converted to long + testing: + - 2d.imageData.create2.size + code: | + var imgdata1 = ctx.createImageData(10.01, 10.99); + var imgdata2 = ctx.createImageData(-10.01, -10.99); + @assert imgdata1.width === 10; + @assert imgdata1.height === 10; + @assert imgdata2.width === 10; + @assert imgdata2.height === 10; + +- name: 2d.imageData.create.and.resize + desc: Verify no crash when resizing an image bitmap to zero. + testing: + - 2d.imageData.resize + images: + - red.png + code: | + var image = new Image(); + image.onload = t.step_func(function() { + var options = { resizeHeight: 0 }; + var p1 = createImageBitmap(image, options); + p1.catch(function(error){}); + t.done(); + }); + image.src = 'red.png'; + +- name: 2d.imageData.get.basic + desc: getImageData() exists and returns something + testing: + - 2d.imageData.get.basic + code: | + @assert ctx.getImageData(0, 0, 100, 50) !== null; + +- name: 2d.imageData.get.type + desc: getImageData() returns an ImageData object containing a Uint8ClampedArray + object + testing: + - 2d.imageData.get.object + code: | + @assert window.ImageData !== undefined; + @assert window.Uint8ClampedArray !== undefined; + window.ImageData.prototype.thisImplementsImageData = true; + window.Uint8ClampedArray.prototype.thisImplementsUint8ClampedArray = true; + var imgdata = ctx.getImageData(0, 0, 1, 1); + @assert imgdata.thisImplementsImageData; + @assert imgdata.data.thisImplementsUint8ClampedArray; + +- name: 2d.imageData.get.zero + desc: getImageData() throws INDEX_SIZE_ERR if size is zero + testing: + - 2d.imageData.getcreate.zero + code: | + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 10, 0); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 0, 10); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 0, 0); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 0.1, 10); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 10, 0.99); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, -0.1, 10); + @assert throws INDEX_SIZE_ERR ctx.getImageData(1, 1, 10, -0.99); + +- name: 2d.imageData.get.nonfinite + desc: getImageData() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.imageData.getcreate.nonfinite + code: | + @nonfinite @assert throws TypeError ctx.getImageData(<10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>); + var posinfobj = { valueOf: function() { return Infinity; } }, + neginfobj = { valueOf: function() { return -Infinity; } }, + nanobj = { valueOf: function() { return -Infinity; } }; + @nonfinite @assert throws TypeError ctx.getImageData(<10 posinfobj neginfobj nanobj>, <10 posinfobj neginfobj nanobj>, <10 posinfobj neginfobj nanobj>, <10 posinfobj neginfobj nanobj>); + +- name: 2d.imageData.get.source.outside + desc: getImageData() returns transparent black outside the canvas + testing: + - 2d.imageData.get.basic + - 2d.imageData.get.outside + code: | + ctx.fillStyle = '#08f'; + ctx.fillRect(0, 0, 100, 50); + + var imgdata1 = ctx.getImageData(-10, 5, 1, 1); + @assert imgdata1.data[0] === 0; + @assert imgdata1.data[1] === 0; + @assert imgdata1.data[2] === 0; + @assert imgdata1.data[3] === 0; + + var imgdata2 = ctx.getImageData(10, -5, 1, 1); + @assert imgdata2.data[0] === 0; + @assert imgdata2.data[1] === 0; + @assert imgdata2.data[2] === 0; + @assert imgdata2.data[3] === 0; + + var imgdata3 = ctx.getImageData(200, 5, 1, 1); + @assert imgdata3.data[0] === 0; + @assert imgdata3.data[1] === 0; + @assert imgdata3.data[2] === 0; + @assert imgdata3.data[3] === 0; + + var imgdata4 = ctx.getImageData(10, 60, 1, 1); + @assert imgdata4.data[0] === 0; + @assert imgdata4.data[1] === 0; + @assert imgdata4.data[2] === 0; + @assert imgdata4.data[3] === 0; + + var imgdata5 = ctx.getImageData(100, 10, 1, 1); + @assert imgdata5.data[0] === 0; + @assert imgdata5.data[1] === 0; + @assert imgdata5.data[2] === 0; + @assert imgdata5.data[3] === 0; + + var imgdata6 = ctx.getImageData(0, 10, 1, 1); + @assert imgdata6.data[0] === 0; + @assert imgdata6.data[1] === 136; + @assert imgdata6.data[2] === 255; + @assert imgdata6.data[3] === 255; + + var imgdata7 = ctx.getImageData(-10, 10, 20, 20); + @assert imgdata7.data[ 0*4+0] === 0; + @assert imgdata7.data[ 0*4+1] === 0; + @assert imgdata7.data[ 0*4+2] === 0; + @assert imgdata7.data[ 0*4+3] === 0; + @assert imgdata7.data[ 9*4+0] === 0; + @assert imgdata7.data[ 9*4+1] === 0; + @assert imgdata7.data[ 9*4+2] === 0; + @assert imgdata7.data[ 9*4+3] === 0; + @assert imgdata7.data[10*4+0] === 0; + @assert imgdata7.data[10*4+1] === 136; + @assert imgdata7.data[10*4+2] === 255; + @assert imgdata7.data[10*4+3] === 255; + @assert imgdata7.data[19*4+0] === 0; + @assert imgdata7.data[19*4+1] === 136; + @assert imgdata7.data[19*4+2] === 255; + @assert imgdata7.data[19*4+3] === 255; + @assert imgdata7.data[20*4+0] === 0; + @assert imgdata7.data[20*4+1] === 0; + @assert imgdata7.data[20*4+2] === 0; + @assert imgdata7.data[20*4+3] === 0; + +- name: 2d.imageData.get.source.negative + desc: getImageData() works with negative width and height, and returns top-to-bottom + left-to-right + testing: + - 2d.imageData.get.basic + - 2d.pixelarray.order + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#fff'; + ctx.fillRect(20, 10, 60, 10); + + var imgdata1 = ctx.getImageData(85, 25, -10, -10); + @assert imgdata1.data[0] === 255; + @assert imgdata1.data[1] === 255; + @assert imgdata1.data[2] === 255; + @assert imgdata1.data[3] === 255; + @assert imgdata1.data[imgdata1.data.length-4+0] === 0; + @assert imgdata1.data[imgdata1.data.length-4+1] === 0; + @assert imgdata1.data[imgdata1.data.length-4+2] === 0; + @assert imgdata1.data[imgdata1.data.length-4+3] === 255; + + var imgdata2 = ctx.getImageData(0, 0, -1, -1); + @assert imgdata2.data[0] === 0; + @assert imgdata2.data[1] === 0; + @assert imgdata2.data[2] === 0; + @assert imgdata2.data[3] === 0; + +- name: 2d.imageData.get.source.size + desc: getImageData() returns bigger ImageData for bigger source rectangle + testing: + - 2d.imageData.get.basic + code: | + var imgdata1 = ctx.getImageData(0, 0, 10, 10); + var imgdata2 = ctx.getImageData(0, 0, 20, 20); + @assert imgdata2.width > imgdata1.width; + @assert imgdata2.height > imgdata1.height; + +- name: 2d.imageData.get.double + desc: createImageData(w, h) double is converted to long + testing: + - 2d.imageData.get.basic + code: | + var imgdata1 = ctx.getImageData(0, 0, 10.01, 10.99); + var imgdata2 = ctx.getImageData(0, 0, -10.01, -10.99); + @assert imgdata1.width === 10; + @assert imgdata1.height === 10; + @assert imgdata2.width === 10; + @assert imgdata2.height === 10; + +- name: 2d.imageData.get.nonpremul + desc: getImageData() returns non-premultiplied colors + testing: + - 2d.imageData.get.premul + code: | + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(10, 10, 10, 10); + @assert imgdata.data[0] > 200; + @assert imgdata.data[1] > 200; + @assert imgdata.data[2] > 200; + @assert imgdata.data[3] > 100; + @assert imgdata.data[3] < 200; + +- name: 2d.imageData.get.range + desc: getImageData() returns values in the range [0, 255] + testing: + - 2d.pixelarray.range + - 2d.pixelarray.retrieve + code: | + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#fff'; + ctx.fillRect(20, 10, 60, 10); + var imgdata1 = ctx.getImageData(10, 5, 1, 1); + @assert imgdata1.data[0] === 0; + var imgdata2 = ctx.getImageData(30, 15, 1, 1); + @assert imgdata2.data[0] === 255; + +- name: 2d.imageData.get.clamp + desc: getImageData() clamps colors to the range [0, 255] + testing: + - 2d.pixelarray.range + code: | + ctx.fillStyle = 'rgb(-100, -200, -300)'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = 'rgb(256, 300, 400)'; + ctx.fillRect(20, 10, 60, 10); + var imgdata1 = ctx.getImageData(10, 5, 1, 1); + @assert imgdata1.data[0] === 0; + @assert imgdata1.data[1] === 0; + @assert imgdata1.data[2] === 0; + var imgdata2 = ctx.getImageData(30, 15, 1, 1); + @assert imgdata2.data[0] === 255; + @assert imgdata2.data[1] === 255; + @assert imgdata2.data[2] === 255; + +- name: 2d.imageData.get.length + desc: getImageData() returns a correctly-sized Uint8ClampedArray + testing: + - 2d.pixelarray.length + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data.length === imgdata.width*imgdata.height*4; + +- name: 2d.imageData.get.order.cols + desc: getImageData() returns leftmost columns first + testing: + - 2d.pixelarray.order + code: | + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 2, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data[0] === 0; + @assert imgdata.data[Math.round(imgdata.width/2*4)] === 255; + @assert imgdata.data[Math.round((imgdata.height/2)*imgdata.width*4)] === 0; + +- name: 2d.imageData.get.order.rows + desc: getImageData() returns topmost rows first + testing: + - 2d.pixelarray.order + code: | + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 100, 2); + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data[0] === 0; + @assert imgdata.data[Math.floor(imgdata.width/2*4)] === 0; + @assert imgdata.data[(imgdata.height/2)*imgdata.width*4] === 255; + +- name: 2d.imageData.get.order.rgb + desc: getImageData() returns R then G then B + testing: + - 2d.pixelarray.order + - 2d.pixelarray.indexes + code: | + ctx.fillStyle = '#48c'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data[0] === 0x44; + @assert imgdata.data[1] === 0x88; + @assert imgdata.data[2] === 0xCC; + @assert imgdata.data[3] === 255; + @assert imgdata.data[4] === 0x44; + @assert imgdata.data[5] === 0x88; + @assert imgdata.data[6] === 0xCC; + @assert imgdata.data[7] === 255; + +- name: 2d.imageData.get.order.alpha + desc: getImageData() returns A in the fourth component + testing: + - 2d.pixelarray.order + code: | + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert imgdata.data[3] < 200; + @assert imgdata.data[3] > 100; + +- name: 2d.imageData.get.unaffected + desc: getImageData() is not affected by context state + testing: + - 2d.imageData.unaffected + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50) + ctx.save(); + ctx.translate(50, 0); + ctx.globalAlpha = 0.1; + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.rect(0, 0, 5, 5); + ctx.clip(); + var imgdata = ctx.getImageData(0, 0, 50, 50); + ctx.restore(); + ctx.putImageData(imgdata, 50, 0); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + expected: green + + +- name: 2d.imageData.get.large.crash + desc: Test that canvas crash when image data cannot be allocated. + testing: + - 2d.getImageData + code: | + @assert throws TypeError ctx.getImageData(10, 0xffffffff, 2147483647, 10); + +- name: 2d.imageData.get.rounding + desc: Test the handling of non-integer source coordinates in getImageData(). + testing: + - 2d.getImageData + code: | + function testDimensions(sx, sy, sw, sh, width, height) + { + imageData = ctx.getImageData(sx, sy, sw, sh); + @assert imageData.width == width; + @assert imageData.height == height; + } + + testDimensions(0, 0, 20, 10, 20, 10); + + testDimensions(.1, .2, 20, 10, 20, 10); + testDimensions(.9, .8, 20, 10, 20, 10); + + testDimensions(0, 0, 20.9, 10.9, 20, 10); + testDimensions(0, 0, 20.1, 10.1, 20, 10); + + testDimensions(-1, -1, 20, 10, 20, 10); + + testDimensions(-1.1, 0, 20, 10, 20, 10); + testDimensions(-1.9, 0, 20, 10, 20, 10); + +- name: 2d.imageData.get.invalid + desc: Verify getImageData() behavior in invalid cases. + testing: + - 2d.imageData.get.invalid + code: | + imageData = ctx.getImageData(0,0,2,2); + var testValues = [NaN, true, false, "\"garbage\"", "-1", + "0", "1", "2", Infinity, -Infinity, + -5, -0.5, 0, 0.5, 5, + 5.4, 255, 256, null, undefined]; + var testResults = [0, 1, 0, 0, 0, + 0, 1, 2, 255, 0, + 0, 0, 0, 0, 5, + 5, 255, 255, 0, 0]; + for (var i = 0; i < testValues.length; i++) { + imageData.data[0] = testValues[i]; + @assert imageData.data[0] == testResults[i]; + } + imageData.data['foo']='garbage'; + @assert imageData.data['foo'] == 'garbage'; + imageData.data[-1]='garbage'; + @assert imageData.data[-1] == undefined; + imageData.data[17]='garbage'; + @assert imageData.data[17] == undefined; + +- name: 2d.imageData.object.properties + desc: ImageData objects have the right properties + testing: + - 2d.imageData.type + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + @assert typeof(imgdata.width) === 'number'; + @assert typeof(imgdata.height) === 'number'; + @assert typeof(imgdata.data) === 'object'; + +- name: 2d.imageData.object.readonly + desc: ImageData objects properties are read-only + testing: + - 2d.imageData.type + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + var w = imgdata.width; + var h = imgdata.height; + var d = imgdata.data; + imgdata.width = 123; + imgdata.height = 123; + imgdata.data = [100,100,100,100]; + @assert imgdata.width === w; + @assert imgdata.height === h; + @assert imgdata.data === d; + @assert imgdata.data[0] === 0; + @assert imgdata.data[1] === 0; + @assert imgdata.data[2] === 0; + @assert imgdata.data[3] === 0; + +- name: 2d.imageData.object.ctor.size + desc: ImageData has a usable constructor + testing: + - 2d.imageData.type + code: | + @assert window.ImageData !== undefined; + + var imgdata = new window.ImageData(2, 3); + @assert imgdata.width === 2; + @assert imgdata.height === 3; + @assert imgdata.data.length === 2 * 3 * 4; + for (var i = 0; i < imgdata.data.length; ++i) { + @assert imgdata.data[i] === 0; + } + +- name: 2d.imageData.object.ctor.basics + desc: Testing different type of ImageData constructor + testing: + - 2d.imageData.type + code: | + function setRGBA(imageData, i, rgba) + { + var s = i * 4; + imageData[s] = rgba[0]; + imageData[s + 1] = rgba[1]; + imageData[s + 2] = rgba[2]; + imageData[s + 3] = rgba[3]; + } + + function getRGBA(imageData, i) + { + var result = []; + var s = i * 4; + for (var j = 0; j < 4; j++) { + result[j] = imageData[s + j]; + } + return result; + } + + function assertArrayEquals(actual, expected) + { + @assert typeof actual === "object"; + @assert actual !== null; + @assert "length" in actual === true; + @assert actual.length === expected.length; + for (var i = 0; i < actual.length; i++) { + @assert actual.hasOwnProperty(i) === expected.hasOwnProperty(i); + @assert actual[i] === expected[i]; + } + } + + @assert ImageData !== undefined; + imageData = new ImageData(100, 50); + + @assert imageData !== null; + @assert imageData.data !== null; + @assert imageData.width === 100; + @assert imageData.height === 50; + assertArrayEquals(getRGBA(imageData.data, 4), [0, 0, 0, 0]); + + var testColor = [0, 255, 255, 128]; + setRGBA(imageData.data, 4, testColor); + assertArrayEquals(getRGBA(imageData.data, 4), testColor); + + @assert throws TypeError new ImageData(10); + @assert throws INDEX_SIZE_ERR new ImageData(0, 10); + @assert throws INDEX_SIZE_ERR new ImageData(10, 0); + @assert throws INDEX_SIZE_ERR new ImageData('width', 'height'); + @assert throws INDEX_SIZE_ERR new ImageData(1 << 31, 1 << 31); + @assert throws TypeError new ImageData(new Uint8ClampedArray(0)); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8Array(100), 25); + @assert throws INVALID_STATE_ERR new ImageData(new Uint8ClampedArray(27), 2); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray(28), 7, 0); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray(104), 14); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray([12, 34, 168, 65328]), 1, 151); + @assert throws TypeError new ImageData(self, 4, 4); + @assert throws TypeError new ImageData(null, 4, 4); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 0); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 13); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 1 << 31); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 'biggish'); + @assert throws INDEX_SIZE_ERR new ImageData(imageData.data, 1 << 24, 1 << 31); + @assert new ImageData(new Uint8ClampedArray(28), 7).height === 1; + + imageDataFromData = new ImageData(imageData.data, 100); + @assert imageDataFromData.width === 100; + @assert imageDataFromData.height === 50; + @assert imageDataFromData.data === imageData.data; + assertArrayEquals(getRGBA(imageDataFromData.data, 10), getRGBA(imageData.data, 10)); + setRGBA(imageData.data, 10, testColor); + assertArrayEquals(getRGBA(imageDataFromData.data, 10), getRGBA(imageData.data, 10)); + + var data = new Uint8ClampedArray(400); + data[22] = 129; + imageDataFromData = new ImageData(data, 20, 5); + @assert imageDataFromData.width === 20; + @assert imageDataFromData.height === 5; + @assert imageDataFromData.data === data; + assertArrayEquals(getRGBA(imageDataFromData.data, 2), getRGBA(data, 2)); + setRGBA(imageDataFromData.data, 2, testColor); + assertArrayEquals(getRGBA(imageDataFromData.data, 2), getRGBA(data, 2)); + + if (window.SharedArrayBuffer) { + @assert throws TypeError new ImageData(new Uint16Array(new SharedArrayBuffer(32)), 4, 2); + } + +- name: 2d.imageData.object.ctor.array + desc: ImageData has a usable constructor + testing: + - 2d.imageData.type + code: | + @assert window.ImageData !== undefined; + + var array = new Uint8ClampedArray(8); + var imgdata = new window.ImageData(array, 1, 2); + @assert imgdata.width === 1; + @assert imgdata.height === 2; + @assert imgdata.data === array; + +- name: 2d.imageData.object.ctor.array.bounds + desc: ImageData has a usable constructor + testing: + - 2d.imageData.type + code: | + @assert window.ImageData !== undefined; + + @assert throws INVALID_STATE_ERR new ImageData(new Uint8ClampedArray(0), 1); + @assert throws INVALID_STATE_ERR new ImageData(new Uint8ClampedArray(3), 1); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray(4), 0); + @assert throws INDEX_SIZE_ERR new ImageData(new Uint8ClampedArray(4), 1, 2); + @assert throws TypeError new ImageData(new Uint8Array(8), 1, 2); + @assert throws TypeError new ImageData(new Int8Array(8), 1, 2); + +- name: 2d.imageData.object.set + desc: ImageData.data can be modified + testing: + - 2d.pixelarray.modify + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + @assert imgdata.data[0] === 100; + imgdata.data[0] = 200; + @assert imgdata.data[0] === 200; + +- name: 2d.imageData.object.undefined + desc: ImageData.data converts undefined to 0 + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = undefined; + @assert imgdata.data[0] === 0; + +- name: 2d.imageData.object.nan + desc: ImageData.data converts NaN to 0 + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = NaN; + @assert imgdata.data[0] === 0; + imgdata.data[0] = 100; + imgdata.data[0] = "cheese"; + @assert imgdata.data[0] === 0; + +- name: 2d.imageData.object.string + desc: ImageData.data converts strings to numbers with ToNumber + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 100; + imgdata.data[0] = "110"; + @assert imgdata.data[0] === 110; + imgdata.data[0] = 100; + imgdata.data[0] = "0x78"; + @assert imgdata.data[0] === 120; + imgdata.data[0] = 100; + imgdata.data[0] = " +130e0 "; + @assert imgdata.data[0] === 130; + +- name: 2d.imageData.object.clamp + desc: ImageData.data clamps numbers to [0, 255] + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + + imgdata.data[0] = 100; + imgdata.data[0] = 300; + @assert imgdata.data[0] === 255; + imgdata.data[0] = 100; + imgdata.data[0] = -100; + @assert imgdata.data[0] === 0; + + imgdata.data[0] = 100; + imgdata.data[0] = 200+Math.pow(2, 32); + @assert imgdata.data[0] === 255; + imgdata.data[0] = 100; + imgdata.data[0] = -200-Math.pow(2, 32); + @assert imgdata.data[0] === 0; + + imgdata.data[0] = 100; + imgdata.data[0] = Math.pow(10, 39); + @assert imgdata.data[0] === 255; + imgdata.data[0] = 100; + imgdata.data[0] = -Math.pow(10, 39); + @assert imgdata.data[0] === 0; + + imgdata.data[0] = 100; + imgdata.data[0] = -Infinity; + @assert imgdata.data[0] === 0; + imgdata.data[0] = 100; + imgdata.data[0] = Infinity; + @assert imgdata.data[0] === 255; + +- name: 2d.imageData.object.round + desc: ImageData.data rounds numbers with round-to-zero + testing: + - 2d.pixelarray.modify + webidl: + - es-octet + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + imgdata.data[0] = 0.499; + @assert imgdata.data[0] === 0; + imgdata.data[0] = 0.5; + @assert imgdata.data[0] === 0; + imgdata.data[0] = 0.501; + @assert imgdata.data[0] === 1; + imgdata.data[0] = 1.499; + @assert imgdata.data[0] === 1; + imgdata.data[0] = 1.5; + @assert imgdata.data[0] === 2; + imgdata.data[0] = 1.501; + @assert imgdata.data[0] === 2; + imgdata.data[0] = 2.5; + @assert imgdata.data[0] === 2; + imgdata.data[0] = 3.5; + @assert imgdata.data[0] === 4; + imgdata.data[0] = 252.5; + @assert imgdata.data[0] === 252; + imgdata.data[0] = 253.5; + @assert imgdata.data[0] === 254; + imgdata.data[0] = 254.5; + @assert imgdata.data[0] === 254; + imgdata.data[0] = 256.5; + @assert imgdata.data[0] === 255; + imgdata.data[0] = -0.5; + @assert imgdata.data[0] === 0; + imgdata.data[0] = -1.5; + @assert imgdata.data[0] === 0; + + + +- name: 2d.imageData.put.null + desc: putImageData() with null imagedata throws TypeError + testing: + - 2d.imageData.put.wrongtype + code: | + @assert throws TypeError ctx.putImageData(null, 0, 0); + +- name: 2d.imageData.put.nonfinite + desc: putImageData() throws TypeError if arguments are not finite + notes: *bindings + testing: + - 2d.imageData.put.nonfinite + code: | + var imgdata = ctx.getImageData(0, 0, 10, 10); + @nonfinite @assert throws TypeError ctx.putImageData(, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>); + @nonfinite @assert throws TypeError ctx.putImageData(, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>, <10 Infinity -Infinity NaN>); + +- name: 2d.imageData.put.basic + desc: putImageData() puts image data from getImageData() onto the canvas + testing: + - 2d.imageData.put.normal + - 2d.imageData.put.3arg + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.created + desc: putImageData() puts image data from createImageData() onto the canvas + testing: + - 2d.imageData.put.normal + code: | + var imgdata = ctx.createImageData(100, 50); + for (var i = 0; i < imgdata.data.length; i += 4) { + imgdata.data[i] = 0; + imgdata.data[i+1] = 255; + imgdata.data[i+2] = 0; + imgdata.data[i+3] = 255; + } + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.wrongtype + desc: putImageData() does not accept non-ImageData objects + testing: + - 2d.imageData.put.wrongtype + code: | + var imgdata = { width: 1, height: 1, data: [255, 0, 0, 255] }; + @assert throws TypeError ctx.putImageData(imgdata, 0, 0); + @assert throws TypeError ctx.putImageData("cheese", 0, 0); + @assert throws TypeError ctx.putImageData(42, 0, 0); + expected: green + +- name: 2d.imageData.put.cross + desc: putImageData() accepts image data got from a different canvas + testing: + - 2d.imageData.put.normal + code: | + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#0f0'; + ctx2.fillRect(0, 0, 100, 50) + var imgdata = ctx2.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.alpha + desc: putImageData() puts non-solid image data correctly + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = 'rgba(0, 255, 0, 0.25)'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,64; + expected: | + size 100 50 + cr.set_source_rgba(0, 1, 0, 0.25) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.imageData.put.modified + desc: putImageData() puts modified image data correctly + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(45, 20, 10, 10) + var imgdata = ctx.getImageData(45, 20, 10, 10); + for (var i = 0, len = imgdata.width*imgdata.height*4; i < len; i += 4) + { + imgdata.data[i] = 0; + imgdata.data[i+1] = 255; + } + ctx.putImageData(imgdata, 45, 20); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.zero + desc: putImageData() with zero-sized dirty rectangle puts nothing + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.putImageData(imgdata, 0, 0, 0, 0, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.rect1 + desc: putImageData() only modifies areas inside the dirty rectangle, using width + and height + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, 40, 20, 0, 0, 20, 20); + + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 35,25 ==~ 0,255,0,255; + @assert pixel 65,25 ==~ 0,255,0,255; + @assert pixel 50,15 ==~ 0,255,0,255; + @assert pixel 50,45 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.rect2 + desc: putImageData() only modifies areas inside the dirty rectangle, using x and + y + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(60, 30, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, -20, -10, 60, 30, 20, 20); + + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 35,25 ==~ 0,255,0,255; + @assert pixel 65,25 ==~ 0,255,0,255; + @assert pixel 50,15 ==~ 0,255,0,255; + @assert pixel 50,45 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.negative + desc: putImageData() handles negative-sized dirty rectangles correctly + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 20, 20) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + ctx.fillStyle = '#f00'; + ctx.fillRect(40, 20, 20, 20) + ctx.putImageData(imgdata, 40, 20, 20, 20, -20, -20); + + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 35,25 ==~ 0,255,0,255; + @assert pixel 65,25 ==~ 0,255,0,255; + @assert pixel 50,15 ==~ 0,255,0,255; + @assert pixel 50,45 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.dirty.outside + desc: putImageData() handles dirty rectangles outside the canvas correctly + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + + var imgdata = ctx.getImageData(0, 0, 100, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + + ctx.putImageData(imgdata, 100, 20, 20, 20, -20, -20); + ctx.putImageData(imgdata, 200, 200, 0, 0, 100, 50); + ctx.putImageData(imgdata, 40, 20, -30, -20, 30, 20); + ctx.putImageData(imgdata, -30, 20, 0, 0, 30, 20); + + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 98,15 ==~ 0,255,0,255; + @assert pixel 98,25 ==~ 0,255,0,255; + @assert pixel 98,45 ==~ 0,255,0,255; + @assert pixel 1,5 ==~ 0,255,0,255; + @assert pixel 1,25 ==~ 0,255,0,255; + @assert pixel 1,45 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.unchanged + desc: putImageData(getImageData(...), ...) has no effect + testing: + - 2d.imageData.unchanged + code: | + var i = 0; + for (var y = 0; y < 16; ++y) { + for (var x = 0; x < 16; ++x, ++i) { + ctx.fillStyle = 'rgba(' + i + ',' + (Math.floor(i*1.5) % 256) + ',' + (Math.floor(i*23.3) % 256) + ',' + (i/256) + ')'; + ctx.fillRect(x, y, 1, 1); + } + } + var imgdata1 = ctx.getImageData(0.1, 0.2, 15.8, 15.9); + var olddata = []; + for (var i = 0; i < imgdata1.data.length; ++i) + olddata[i] = imgdata1.data[i]; + + ctx.putImageData(imgdata1, 0.1, 0.2); + + var imgdata2 = ctx.getImageData(0.1, 0.2, 15.8, 15.9); + for (var i = 0; i < imgdata2.data.length; ++i) { + @assert olddata[i] === imgdata2.data[i]; + } + +- name: 2d.imageData.put.unaffected + desc: putImageData() is not affected by context state + testing: + - 2d.imageData.unaffected + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.globalAlpha = 0.1; + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 1; + ctx.translate(100, 50); + ctx.scale(0.1, 0.1); + ctx.putImageData(imgdata, 0, 0); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.clip + desc: putImageData() is not affected by clipping regions + testing: + - 2d.imageData.unaffected + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50) + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.putImageData(imgdata, 0, 0); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.imageData.put.path + desc: putImageData() does not affect the current path + testing: + - 2d.imageData.put.normal + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50) + ctx.rect(0, 0, 100, 50); + var imgdata = ctx.getImageData(0, 0, 100, 50); + ctx.putImageData(imgdata, 0, 0); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green diff --git a/test/wpt/shadows.yaml b/test/wpt/shadows.yaml new file mode 100644 index 000000000..1d8da0ede --- /dev/null +++ b/test/wpt/shadows.yaml @@ -0,0 +1,1150 @@ +- name: 2d.shadow.attributes.shadowBlur.initial + testing: + - 2d.shadow.blur.get + - 2d.shadow.blur.initial + code: | + @assert ctx.shadowBlur === 0; + +- name: 2d.shadow.attributes.shadowBlur.valid + testing: + - 2d.shadow.blur.get + - 2d.shadow.blur.set + code: | + ctx.shadowBlur = 1; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 0.5; + @assert ctx.shadowBlur === 0.5; + + ctx.shadowBlur = 1e6; + @assert ctx.shadowBlur === 1e6; + + ctx.shadowBlur = 0; + @assert ctx.shadowBlur === 0; + +- name: 2d.shadow.attributes.shadowBlur.invalid + testing: + - 2d.shadow.blur.invalid + code: | + ctx.shadowBlur = 1; + ctx.shadowBlur = -2; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = Infinity; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = -Infinity; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = NaN; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = 'string'; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = true; + @assert ctx.shadowBlur === 1; + + ctx.shadowBlur = 1; + ctx.shadowBlur = false; + @assert ctx.shadowBlur === 0; + +- name: 2d.shadow.attributes.shadowOffset.initial + testing: + - 2d.shadow.offset.initial + code: | + @assert ctx.shadowOffsetX === 0; + @assert ctx.shadowOffsetY === 0; + +- name: 2d.shadow.attributes.shadowOffset.valid + testing: + - 2d.shadow.offset.get + - 2d.shadow.offset.set + code: | + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 0.5; + ctx.shadowOffsetY = 0.25; + @assert ctx.shadowOffsetX === 0.5; + @assert ctx.shadowOffsetY === 0.25; + + ctx.shadowOffsetX = -0.5; + ctx.shadowOffsetY = -0.25; + @assert ctx.shadowOffsetX === -0.5; + @assert ctx.shadowOffsetY === -0.25; + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + @assert ctx.shadowOffsetX === 0; + @assert ctx.shadowOffsetY === 0; + + ctx.shadowOffsetX = 1e6; + ctx.shadowOffsetY = 1e6; + @assert ctx.shadowOffsetX === 1e6; + @assert ctx.shadowOffsetY === 1e6; + +- name: 2d.shadow.attributes.shadowOffset.invalid + testing: + - 2d.shadow.offset.invalid + code: | + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = Infinity; + ctx.shadowOffsetY = Infinity; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = -Infinity; + ctx.shadowOffsetY = -Infinity; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = NaN; + ctx.shadowOffsetY = NaN; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = 'string'; + ctx.shadowOffsetY = 'string'; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 2; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = true; + ctx.shadowOffsetY = true; + @assert ctx.shadowOffsetX === 1; + @assert ctx.shadowOffsetY === 1; + + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 2; + ctx.shadowOffsetX = false; + ctx.shadowOffsetY = false; + @assert ctx.shadowOffsetX === 0; + @assert ctx.shadowOffsetY === 0; + +- name: 2d.shadow.attributes.shadowColor.initial + testing: + - 2d.shadow.color.initial + code: | + @assert ctx.shadowColor === 'rgba(0, 0, 0, 0)'; + +- name: 2d.shadow.attributes.shadowColor.valid + testing: + - 2d.shadow.color.get + - 2d.shadow.color.set + code: | + ctx.shadowColor = 'lime'; + @assert ctx.shadowColor === '#00ff00'; + + ctx.shadowColor = 'RGBA(0,255, 0,0)'; + @assert ctx.shadowColor === 'rgba(0, 255, 0, 0)'; + +- name: 2d.shadow.attributes.shadowColor.invalid + testing: + - 2d.shadow.color.invalid + code: | + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = 'bogus'; + @assert ctx.shadowColor === '#00ff00'; + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = 'red bogus'; + @assert ctx.shadowColor === '#00ff00'; + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = ctx; + @assert ctx.shadowColor === '#00ff00'; + + ctx.shadowColor = '#00ff00'; + ctx.shadowColor = undefined; + @assert ctx.shadowColor === '#00ff00'; + +- name: 2d.shadow.enable.off.1 + desc: Shadows are not drawn when only shadowColor is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.shadowColor = '#f00'; + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.enable.off.2 + desc: Shadows are not drawn when only shadowColor is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#f00'; + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.enable.blur + desc: Shadows are drawn if shadowBlur is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowBlur = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.enable.x + desc: Shadows are drawn if shadowOffsetX is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.enable.y + desc: Shadows are drawn if shadowOffsetY is set + testing: + - 2d.shadow.enable + - 2d.shadow.render + code: | + ctx.globalCompositeOperation = 'destination-atop'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 0.1; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.offset.positiveX + desc: Shadows can be offset with positive x + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.offset.negativeX + desc: Shadows can be offset with negative x + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = -50; + ctx.fillRect(50, 0, 50, 50); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.offset.positiveY + desc: Shadows can be offset with positive y + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 25; + ctx.fillRect(0, 0, 100, 25); + @assert pixel 50,12 == 0,255,0,255; + @assert pixel 50,37 == 0,255,0,255; + expected: green + +- name: 2d.shadow.offset.negativeY + desc: Shadows can be offset with negative y + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = -25; + ctx.fillRect(0, 25, 100, 25); + @assert pixel 50,12 == 0,255,0,255; + @assert pixel 50,37 == 0,255,0,255; + expected: green + +- name: 2d.shadow.outside + desc: Shadows of shapes outside the visible area can be offset onto the visible + area + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 100; + ctx.fillRect(-100, 0, 25, 50); + ctx.shadowOffsetX = -100; + ctx.fillRect(175, 0, 25, 50); + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 100; + ctx.fillRect(25, -100, 50, 25); + ctx.shadowOffsetY = -100; + ctx.fillRect(25, 125, 50, 25); + @assert pixel 12,25 == 0,255,0,255; + @assert pixel 87,25 == 0,255,0,255; + @assert pixel 50,12 == 0,255,0,255; + @assert pixel 50,37 == 0,255,0,255; + expected: green + +- name: 2d.shadow.clip.1 + desc: Shadows of clipped shapes are still drawn within the clipping region + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(50, 0, 50, 50); + ctx.clip(); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + ctx.restore(); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.clip.2 + desc: Shadows are not drawn outside the clipping region + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 50; + ctx.fillRect(0, 0, 50, 50); + ctx.restore(); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.clip.3 + desc: Shadows of clipped shapes are still drawn within the clipping region + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, 50, 50); + ctx.clip(); + ctx.fillStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 50; + ctx.fillRect(-50, 0, 50, 50); + ctx.restore(); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.basic + desc: Shadows are drawn for strokes + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.moveTo(0, -25); + ctx.lineTo(100, -25); + ctx.stroke(); + + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.cap.1 + desc: Shadows are not drawn for areas outside stroke caps + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.lineCap = 'butt'; + ctx.moveTo(-50, -25); + ctx.lineTo(0, -25); + ctx.moveTo(100, -25); + ctx.lineTo(150, -25); + ctx.stroke(); + + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.cap.2 + desc: Shadows are drawn for stroke caps + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.beginPath(); + ctx.lineWidth = 50; + ctx.lineCap = 'square'; + ctx.moveTo(25, -25); + ctx.lineTo(75, -25); + ctx.stroke(); + + @assert pixel 1,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.join.1 + desc: Shadows are not drawn for areas outside stroke joins + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'bevel'; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.join.2 + desc: Shadows are drawn for stroke joins + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.shadow.stroke.join.3 + desc: Shadows are drawn for stroke joins respecting miter limit + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.strokeStyle = '#f00'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.lineWidth = 200; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 0.1; + ctx.beginPath(); + ctx.moveTo(-200, -50); + ctx.lineTo(-150, -50); + ctx.lineTo(-151, -100); // (not an exact right angle, to avoid some other bug in Firefox 3) + ctx.stroke(); + + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,48 == 0,255,0,255; + expected: green + +- name: 2d.shadow.image.basic + desc: Shadows are drawn for images + testing: + - 2d.shadow.render + images: + - red.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.drawImage(document.getElementById('red.png'), 0, -50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.image.transparent.1 + desc: Shadows are not drawn for transparent images + testing: + - 2d.shadow.render + images: + - transparent.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.drawImage(document.getElementById('transparent.png'), 0, -50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.image.transparent.2 + desc: Shadows are not drawn for transparent parts of images + testing: + - 2d.shadow.render + images: + - redtransparent.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(document.getElementById('redtransparent.png'), 50, -50); + ctx.shadowColor = '#f00'; + ctx.drawImage(document.getElementById('redtransparent.png'), -50, -50); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.image.alpha + desc: Shadows are drawn correctly for partially-transparent images + testing: + - 2d.shadow.render + images: + - transparent50.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.drawImage(document.getElementById('transparent50.png'), 0, -50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.image.section + desc: Shadows are not drawn for areas outside image source rectangles + testing: + - 2d.shadow.render + images: + - redtransparent.png + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#f00'; + ctx.drawImage(document.getElementById('redtransparent.png'), 50, 0, 50, 50, 0, -50, 50, 50); + + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.shadow.image.scale + desc: Shadows are drawn correctly for scaled images + testing: + - 2d.shadow.render + images: + - redtransparent.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(document.getElementById('redtransparent.png'), 0, 0, 100, 50, -10, -50, 240, 50); + + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 50,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.shadow.canvas.basic + desc: Shadows are drawn for canvases + testing: + - 2d.shadow.render + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.drawImage(canvas2, 0, -50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.canvas.transparent.1 + desc: Shadows are not drawn for transparent canvases + testing: + - 2d.shadow.render + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.drawImage(canvas2, 0, -50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.canvas.transparent.2 + desc: Shadows are not drawn for transparent parts of canvases + testing: + - 2d.shadow.render + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = '#f00'; + ctx2.fillRect(0, 0, 50, 50); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.drawImage(canvas2, 50, -50); + ctx.shadowColor = '#f00'; + ctx.drawImage(canvas2, -50, -50); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.canvas.alpha + desc: Shadows are drawn correctly for partially-transparent canvases + testing: + - 2d.shadow.render + images: + - transparent50.png + code: | + var canvas2 = document.createElement('canvas'); + canvas2.width = 100; + canvas2.height = 50; + var ctx2 = canvas2.getContext('2d'); + ctx2.fillStyle = 'rgba(255, 0, 0, 0.5)'; + ctx2.fillRect(0, 0, 100, 50); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.drawImage(canvas2, 0, -50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.pattern.basic + desc: Shadows are drawn for fill patterns + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + images: + - red.png + code: | + var pattern = ctx.createPattern(document.getElementById('red.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.pattern.transparent.1 + desc: Shadows are not drawn for transparent fill patterns + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + images: + - transparent.png + code: | + var pattern = ctx.createPattern(document.getElementById('transparent.png'), 'repeat'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.pattern.transparent.2 + desc: Shadows are not drawn for transparent parts of fill patterns + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + images: + - redtransparent.png + code: | + var pattern = ctx.createPattern(document.getElementById('redtransparent.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.pattern.alpha + desc: Shadows are drawn correctly for partially-transparent fill patterns + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + images: + - transparent50.png + code: | + var pattern = ctx.createPattern(document.getElementById('transparent50.png'), 'repeat'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.fillStyle = pattern; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.gradient.basic + desc: Shadows are drawn for gradient fills + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + code: | + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, '#f00'); + gradient.addColorStop(1, '#f00'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#0f0'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.gradient.transparent.1 + desc: Shadows are not drawn for transparent gradient fills + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + code: | + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, 'rgba(0,0,0,0)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#f00'; + ctx.shadowOffsetY = 50; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.gradient.transparent.2 + desc: Shadows are not drawn for transparent parts of gradient fills + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + code: | + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, '#f00'); + gradient.addColorStop(0.499, '#f00'); + gradient.addColorStop(0.5, 'rgba(0,0,0,0)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 50, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, 0, 50, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.gradient.alpha + desc: Shadows are drawn correctly for partially-transparent gradient fills + testing: + - 2d.shadow.render + # http://bugs.webkit.org/show_bug.cgi?id=15266 + code: | + var gradient = ctx.createLinearGradient(0, 0, 100, 0); + gradient.addColorStop(0, 'rgba(255,0,0,0.5)'); + gradient.addColorStop(1, 'rgba(255,0,0,0.5)'); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#00f'; + ctx.fillStyle = gradient; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.transform.1 + desc: Shadows take account of transformations + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.translate(100, 100); + ctx.fillRect(-100, -150, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.transform.2 + desc: Shadow offsets are not affected by transformations + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowOffsetY = 50; + ctx.shadowColor = '#0f0'; + ctx.rotate(Math.PI) + ctx.fillRect(-100, 0, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.shadow.blur.low + desc: Shadows look correct for small blurs + manual: + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 25; + for (var x = 0; x < 100; ++x) { + ctx.save(); + ctx.beginPath(); + ctx.rect(x, 0, 1, 50); + ctx.clip(); + ctx.shadowBlur = x; + ctx.fillRect(-200, -200, 500, 200); + ctx.restore(); + } + expected: | + size 100 50 + import math + cr.set_source_rgb(0, 0, 1) + cr.rectangle(0, 0, 1, 25) + cr.fill() + cr.set_source_rgb(1, 1, 0) + cr.rectangle(0, 25, 1, 25) + cr.fill() + for x in range(1, 100): + sigma = x/2.0 + filter = [] + for i in range(-24, 26): + filter.append(math.exp(-i*i / (2*sigma*sigma)) / (math.sqrt(2*math.pi)*sigma)) + accum = [0] + for f in filter: + accum.append(accum[-1] + f) + for y in range(0, 50): + cr.set_source_rgb(accum[y], accum[y], 1-accum[y]) + cr.rectangle(x, y, 1, 1) + cr.fill() + +- name: 2d.shadow.blur.high + desc: Shadows look correct for large blurs + manual: + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#ff0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 100; + ctx.fillRect(-200, -200, 200, 400); + expected: | + size 100 50 + import math + sigma = 100.0/2 + filter = [] + for i in range(-200, 100): + filter.append(math.exp(-i*i / (2*sigma*sigma)) / (math.sqrt(2*math.pi)*sigma)) + accum = [0] + for f in filter: + accum.append(accum[-1] + f) + for x in range(0, 100): + cr.set_source_rgb(accum[x+200], accum[x+200], 1-accum[x+200]) + cr.rectangle(x, 0, 1, 50) + cr.fill() + +- name: 2d.shadow.alpha.1 + desc: Shadow color alpha components are used + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = 'rgba(255, 0, 0, 0.01)'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 0,255,0,255 +/- 4; + expected: green + +- name: 2d.shadow.alpha.2 + desc: Shadow color alpha components are used + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.shadowColor = 'rgba(0, 0, 255, 0.5)'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.alpha.3 + desc: Shadows are affected by globalAlpha + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; // (work around broken Firefox globalAlpha caching) + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 50; + ctx.globalAlpha = 0.5; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.alpha.4 + desc: Shadows with alpha components are correctly affected by globalAlpha + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; // (work around broken Firefox globalAlpha caching) + ctx.shadowColor = 'rgba(0, 0, 255, 0.707)'; + ctx.shadowOffsetY = 50; + ctx.globalAlpha = 0.707; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.alpha.5 + desc: Shadows of shapes with alpha components are drawn correctly + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = 'rgba(64, 0, 0, 0.5)'; + ctx.shadowColor = '#00f'; + ctx.shadowOffsetY = 50; + ctx.fillRect(0, -50, 100, 50); + + @assert pixel 50,25 ==~ 127,0,127,255; + expected: | + size 100 50 + cr.set_source_rgb(0.5, 0, 0.5) + cr.rectangle(0, 0, 100, 50) + cr.fill() + +- name: 2d.shadow.composite.1 + desc: Shadows are drawn using globalCompositeOperation + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'xor'; + ctx.shadowColor = '#f00'; + ctx.shadowOffsetX = 100; + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, 0, 200, 50); + + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.shadow.composite.2 + desc: Shadows are drawn using globalCompositeOperation + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'xor'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 1; + ctx.fillStyle = '#0f0'; + ctx.fillRect(-10, -10, 120, 70); + + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green + +- name: 2d.shadow.composite.3 + desc: Areas outside shadows are drawn correctly with destination-out + testing: + - 2d.shadow.render + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.globalCompositeOperation = 'destination-out'; + ctx.shadowColor = '#f00'; + ctx.shadowBlur = 10; + ctx.fillStyle = '#f00'; + ctx.fillRect(200, 0, 100, 50); + + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 50,25 ==~ 0,255,0,255; + expected: green diff --git a/test/wpt/text-styles.yaml b/test/wpt/text-styles.yaml new file mode 100644 index 000000000..c4d2caf00 --- /dev/null +++ b/test/wpt/text-styles.yaml @@ -0,0 +1,525 @@ +- name: 2d.text.font.parse.basic + testing: + - 2d.text.font.parse + - 2d.text.font.get + code: | + ctx.font = '20px serif'; + @assert ctx.font === '20px serif'; + + ctx.font = '20PX SERIF'; + @assert ctx.font === '20px serif'; @moz-todo + +- name: 2d.text.font.parse.tiny + testing: + - 2d.text.font.parse + - 2d.text.font.get + code: | + ctx.font = '1px sans-serif'; + @assert ctx.font === '1px sans-serif'; + +- name: 2d.text.font.parse.complex + testing: + - 2d.text.font.parse + - 2d.text.font.get + - 2d.text.font.lineheight + code: | + ctx.font = 'small-caps italic 400 12px/2 Unknown Font, sans-serif'; + @assert ctx.font === 'italic small-caps 12px "Unknown Font", sans-serif'; @moz-todo + +- name: 2d.text.font.parse.family + testing: + - 2d.text.font.parse + - 2d.text.font.get + - 2d.text.font.lineheight + code: | + ctx.font = '20px cursive,fantasy,monospace,sans-serif,serif,UnquotedFont,"QuotedFont\\\\\\","'; + @assert ctx.font === '20px cursive, fantasy, monospace, sans-serif, serif, UnquotedFont, "QuotedFont\\\\\\","'; + + # TODO: + # 2d.text.font.parse.size.absolute + # xx-small x-small small medium large x-large xx-large + # 2d.text.font.parse.size.relative + # smaller larger + # 2d.text.font.parse.size.length.relative + # em ex px + # 2d.text.font.parse.size.length.absolute + # in cm mm pt pc + +- name: 2d.text.font.parse.size.percentage + testing: + - 2d.text.font.parse + - 2d.text.font.get + - 2d.text.font.fontsize + - 2d.text.font.size + canvas: 'style="font-size: 144px" width="100" height="50"' + code: | + ctx.font = '50% serif'; + @assert ctx.font === '72px serif'; @moz-todo + canvas.setAttribute('style', 'font-size: 100px'); + @assert ctx.font === '72px serif'; @moz-todo + +- name: 2d.text.font.parse.size.percentage.default + testing: + - 2d.text.font.undefined + code: | + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.font = '1000% serif'; + @assert ctx2.font === '100px serif'; @moz-todo + +- name: 2d.text.font.parse.system + desc: System fonts must be computed to explicit values + testing: + - 2d.text.font.parse + - 2d.text.font.get + - 2d.text.font.systemfonts + code: | + ctx.font = 'message-box'; + @assert ctx.font !== 'message-box'; + +- name: 2d.text.font.parse.invalid + testing: + - 2d.text.font.invalid + code: | + ctx.font = '20px serif'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = ''; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = 'bogus'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = 'inherit'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = '10px {bogus}'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = '10px initial'; + @assert ctx.font === '20px serif'; @moz-todo + + ctx.font = '20px serif'; + ctx.font = '10px default'; + @assert ctx.font === '20px serif'; @moz-todo + + ctx.font = '20px serif'; + ctx.font = '10px inherit'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = '10px revert'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = 'var(--x)'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = 'var(--x, 10px serif)'; + @assert ctx.font === '20px serif'; + + ctx.font = '20px serif'; + ctx.font = '1em serif; background: green; margin: 10px'; + @assert ctx.font === '20px serif'; + +- name: 2d.text.font.default + testing: + - 2d.text.font.default + code: | + @assert ctx.font === '10px sans-serif'; + +- name: 2d.text.font.relative_size + testing: + - 2d.text.font.relative_size + code: | + var canvas2 = document.createElement('canvas'); + var ctx2 = canvas2.getContext('2d'); + ctx2.font = '1em sans-serif'; + @assert ctx2.font === '10px sans-serif'; + +- name: 2d.text.align.valid + testing: + - 2d.text.align.get + - 2d.text.align.set + code: | + ctx.textAlign = 'start'; + @assert ctx.textAlign === 'start'; + + ctx.textAlign = 'end'; + @assert ctx.textAlign === 'end'; + + ctx.textAlign = 'left'; + @assert ctx.textAlign === 'left'; + + ctx.textAlign = 'right'; + @assert ctx.textAlign === 'right'; + + ctx.textAlign = 'center'; + @assert ctx.textAlign === 'center'; + +- name: 2d.text.align.invalid + testing: + - 2d.text.align.invalid + code: | + ctx.textAlign = 'start'; + ctx.textAlign = 'bogus'; + @assert ctx.textAlign === 'start'; + + ctx.textAlign = 'start'; + ctx.textAlign = 'END'; + @assert ctx.textAlign === 'start'; + + ctx.textAlign = 'start'; + ctx.textAlign = 'end '; + @assert ctx.textAlign === 'start'; + + ctx.textAlign = 'start'; + ctx.textAlign = 'end\0'; + @assert ctx.textAlign === 'start'; + +- name: 2d.text.align.default + testing: + - 2d.text.align.default + code: | + @assert ctx.textAlign === 'start'; + + +- name: 2d.text.baseline.valid + testing: + - 2d.text.baseline.get + - 2d.text.baseline.set + code: | + ctx.textBaseline = 'top'; + @assert ctx.textBaseline === 'top'; + + ctx.textBaseline = 'hanging'; + @assert ctx.textBaseline === 'hanging'; + + ctx.textBaseline = 'middle'; + @assert ctx.textBaseline === 'middle'; + + ctx.textBaseline = 'alphabetic'; + @assert ctx.textBaseline === 'alphabetic'; + + ctx.textBaseline = 'ideographic'; + @assert ctx.textBaseline === 'ideographic'; + + ctx.textBaseline = 'bottom'; + @assert ctx.textBaseline === 'bottom'; + +- name: 2d.text.baseline.invalid + testing: + - 2d.text.baseline.invalid + code: | + ctx.textBaseline = 'top'; + ctx.textBaseline = 'bogus'; + @assert ctx.textBaseline === 'top'; + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'MIDDLE'; + @assert ctx.textBaseline === 'top'; + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle '; + @assert ctx.textBaseline === 'top'; + + ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle\0'; + @assert ctx.textBaseline === 'top'; + +- name: 2d.text.baseline.default + testing: + - 2d.text.baseline.default + code: | + @assert ctx.textBaseline === 'alphabetic'; + + + + + +- name: 2d.text.draw.baseline.top + desc: textBaseline top is the top of the em square (not the bounding box) + testing: + - 2d.text.baseline.top + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'top'; + ctx.fillText('CC', 0, 0); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.baseline.bottom + desc: textBaseline bottom is the bottom of the em square (not the bounding box) + testing: + - 2d.text.baseline.bottom + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'bottom'; + ctx.fillText('CC', 0, 50); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.baseline.middle + desc: textBaseline middle is the middle of the em square (not the bounding box) + testing: + - 2d.text.baseline.middle + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'middle'; + ctx.fillText('CC', 0, 25); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.baseline.alphabetic + testing: + - 2d.text.baseline.alphabetic + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'alphabetic'; + ctx.fillText('CC', 0, 37.5); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.baseline.ideographic + testing: + - 2d.text.baseline.ideographic + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'ideographic'; + ctx.fillText('CC', 0, 31.25); + @assert pixel 5,5 ==~ 0,255,0,255; + @assert pixel 95,5 ==~ 0,255,0,255; + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; @moz-todo + @assert pixel 95,45 ==~ 0,255,0,255; @moz-todo + }), 500); + expected: green + +- name: 2d.text.draw.baseline.hanging + testing: + - 2d.text.baseline.hanging + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textBaseline = 'hanging'; + ctx.fillText('CC', 0, 12.5); + @assert pixel 5,5 ==~ 0,255,0,255; @moz-todo + @assert pixel 95,5 ==~ 0,255,0,255; @moz-todo + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; + @assert pixel 5,45 ==~ 0,255,0,255; + @assert pixel 95,45 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.space + desc: Space characters are converted to U+0020, and collapsed (per CSS) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E EE', -100, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; @moz-todo + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.other + desc: Space characters are converted to U+0020, and collapsed (per CSS) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText('E \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dEE', -100, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; @moz-todo + @assert pixel 75,25 ==~ 0,255,0,255; @moz-todo + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.start + desc: Space characters at the start of a line are collapsed (per CSS) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillText(' EE', 0, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; @moz-todo + @assert pixel 75,25 ==~ 0,255,0,255; + }), 500); + expected: green + +- name: 2d.text.draw.space.collapse.end + desc: Space characters at the end of a line are collapsed (per CSS) + testing: + - 2d.text.draw.spaces + fonts: + - CanvasTest + code: | + ctx.font = '50px CanvasTest'; + deferTest(); + step_timeout(t.step_func_done(function () { + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#0f0'; + ctx.textAlign = 'right'; + ctx.fillText('EE ', 100, 37.5); + @assert pixel 25,25 ==~ 0,255,0,255; + @assert pixel 75,25 ==~ 0,255,0,255; @moz-todo + }), 500); + expected: green + + +- name: 2d.text.measure.width.space + desc: Space characters are converted to U+0020 and collapsed (per CSS) + testing: + - 2d.text.measure.spaces + fonts: + - CanvasTest + code: | + deferTest(); + var f = new FontFace("CanvasTest", "/fonts/CanvasTest.ttf"); + document.fonts.add(f); + document.fonts.ready.then(() => { + step_timeout(t.step_func_done(function () { + ctx.font = '50px CanvasTest'; + @assert ctx.measureText('A B').width === 150; + @assert ctx.measureText('A B').width === 200; + @assert ctx.measureText('A \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dB').width === 150; @moz-todo + @assert ctx.measureText('A \x0b B').width >= 200; + + @assert ctx.measureText(' AB').width === 100; @moz-todo + @assert ctx.measureText('AB ').width === 100; @moz-todo + }), 500); + }); + +- name: 2d.text.measure.rtl.text + desc: Measurement should follow canvas direction instead text direction + testing: + - 2d.text.measure.rtl.text + fonts: + - CanvasTest + code: | + metrics = ctx.measureText('اَلْعَرَبِيَّةُ'); + @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; + + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; + +- name: 2d.text.measure.boundingBox.textAlign + desc: Measurement should be related to textAlignment + testing: + - 2d.text.measure.boundingBox.textAlign + code: | + ctx.textAlign = "right"; + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight; + + ctx.textAlign = "left" + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; + +- name: 2d.text.measure.boundingBox.direction + desc: Measurement should follow text direction + testing: + - 2d.text.measure.boundingBox.direction + code: | + ctx.direction = "ltr"; + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; + + ctx.direction = "rtl"; + metrics = ctx.measureText('hello'); + @assert metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight; diff --git a/test/wpt/the-canvas-element.yaml b/test/wpt/the-canvas-element.yaml new file mode 100644 index 000000000..5abee0300 --- /dev/null +++ b/test/wpt/the-canvas-element.yaml @@ -0,0 +1,169 @@ +- name: 2d.getcontext.exists + desc: The 2D context is implemented + testing: + - context.2d + code: | + @assert canvas.getContext('2d') !== null; + +- name: 2d.getcontext.invalid.args + desc: Calling getContext with invalid arguments. + testing: + - context.2d + code: | + @assert canvas.getContext('') === null; + @assert canvas.getContext('2d#') === null; + @assert canvas.getContext('This is clearly not a valid context name.') === null; + @assert canvas.getContext('2d\0') === null; + @assert canvas.getContext('2\uFF44') === null; + @assert canvas.getContext('2D') === null; + @assert throws TypeError canvas.getContext(); + @assert canvas.getContext('null') === null; + @assert canvas.getContext('undefined') === null; + +- name: 2d.getcontext.extraargs.create + desc: The 2D context doesn't throw with extra getContext arguments (new context) + testing: + - context.2d.extraargs + code: | + @assert document.createElement("canvas").getContext('2d', false, {}, [], 1, "2") !== null; + @assert document.createElement("canvas").getContext('2d', 123) !== null; + @assert document.createElement("canvas").getContext('2d', "test") !== null; + @assert document.createElement("canvas").getContext('2d', undefined) !== null; + @assert document.createElement("canvas").getContext('2d', null) !== null; + @assert document.createElement("canvas").getContext('2d', Symbol.hasInstance) !== null; + +- name: 2d.getcontext.extraargs.cache + desc: The 2D context doesn't throw with extra getContext arguments (cached) + testing: + - context.2d.extraargs + code: | + @assert canvas.getContext('2d', false, {}, [], 1, "2") !== null; + @assert canvas.getContext('2d', 123) !== null; + @assert canvas.getContext('2d', "test") !== null; + @assert canvas.getContext('2d', undefined) !== null; + @assert canvas.getContext('2d', null) !== null; + @assert canvas.getContext('2d', Symbol.hasInstance) !== null; + +- name: 2d.type.exists + desc: The 2D context interface is a property of 'window' + notes: &bindings Defined in "Web IDL" (draft) + testing: + - context.2d.type + code: | + @assert window.CanvasRenderingContext2D; + +- name: 2d.type.prototype + desc: window.CanvasRenderingContext2D.prototype are not [[Writable]] and not [[Configurable]], + and its methods are [[Configurable]]. + notes: *bindings + testing: + - context.2d.type + code: | + @assert window.CanvasRenderingContext2D.prototype; + @assert window.CanvasRenderingContext2D.prototype.fill; + window.CanvasRenderingContext2D.prototype = null; + @assert window.CanvasRenderingContext2D.prototype; + delete window.CanvasRenderingContext2D.prototype; + @assert window.CanvasRenderingContext2D.prototype; + window.CanvasRenderingContext2D.prototype.fill = 1; + @assert window.CanvasRenderingContext2D.prototype.fill === 1; + delete window.CanvasRenderingContext2D.prototype.fill; + @assert window.CanvasRenderingContext2D.prototype.fill === undefined; + +- name: 2d.type.replace + desc: Interface methods can be overridden + notes: *bindings + testing: + - context.2d.type + code: | + var fillRect = window.CanvasRenderingContext2D.prototype.fillRect; + window.CanvasRenderingContext2D.prototype.fillRect = function (x, y, w, h) + { + this.fillStyle = '#0f0'; + fillRect.call(this, x, y, w, h); + }; + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.type.extend + desc: Interface methods can be added + notes: *bindings + testing: + - context.2d.type + code: | + window.CanvasRenderingContext2D.prototype.fillRectGreen = function (x, y, w, h) + { + this.fillStyle = '#0f0'; + this.fillRect(x, y, w, h); + }; + ctx.fillStyle = '#f00'; + ctx.fillRectGreen(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.getcontext.unique + desc: getContext('2d') returns the same object + testing: + - context.unique + code: | + @assert canvas.getContext('2d') === canvas.getContext('2d'); + +- name: 2d.getcontext.shared + desc: getContext('2d') returns objects which share canvas state + testing: + - context.unique + code: | + var ctx2 = canvas.getContext('2d'); + ctx.fillStyle = '#f00'; + ctx2.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.scaled + desc: CSS-scaled canvases get drawn correctly + canvas: 'width="50" height="25" style="width: 100px; height: 50px"' + manual: + code: | + ctx.fillStyle = '#00f'; + ctx.fillRect(0, 0, 50, 25); + ctx.fillStyle = '#0ff'; + ctx.fillRect(0, 0, 25, 10); + expected: | + size 100 50 + cr.set_source_rgb(0, 0, 1) + cr.rectangle(0, 0, 100, 50) + cr.fill() + cr.set_source_rgb(0, 1, 1) + cr.rectangle(0, 0, 50, 20) + cr.fill() + +- name: 2d.canvas.reference + desc: CanvasRenderingContext2D.canvas refers back to its canvas + testing: + - 2d.canvas + code: | + @assert ctx.canvas === canvas; + +- name: 2d.canvas.readonly + desc: CanvasRenderingContext2D.canvas is readonly + testing: + - 2d.canvas.attribute + code: | + var c = document.createElement('canvas'); + var d = ctx.canvas; + @assert c !== d; + ctx.canvas = c; + @assert ctx.canvas === d; + +- name: 2d.canvas.context + desc: checks CanvasRenderingContext2D prototype + testing: + - 2d.path.contexttypexxx.basic + code: | + @assert Object.getPrototypeOf(CanvasRenderingContext2D.prototype) === Object.prototype; + @assert Object.getPrototypeOf(ctx) === CanvasRenderingContext2D.prototype; + t.done(); + diff --git a/test/wpt/the-canvas-state.yaml b/test/wpt/the-canvas-state.yaml new file mode 100644 index 000000000..dda6dc314 --- /dev/null +++ b/test/wpt/the-canvas-state.yaml @@ -0,0 +1,107 @@ +- name: 2d.state.saverestore.transformation + desc: save()/restore() affects the current transformation matrix + testing: + - 2d.state.transformation + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.translate(200, 0); + ctx.restore(); + ctx.fillStyle = '#f00'; + ctx.fillRect(-200, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.state.saverestore.clip + desc: save()/restore() affects the clipping path + testing: + - 2d.state.clip + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.rect(0, 0, 1, 1); + ctx.clip(); + ctx.restore(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.state.saverestore.path + desc: save()/restore() does not affect the current path + testing: + - 2d.state.path + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.rect(0, 0, 100, 50); + ctx.restore(); + ctx.fillStyle = '#0f0'; + ctx.fill(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.state.saverestore.bitmap + desc: save()/restore() does not affect the current bitmap + testing: + - 2d.state.bitmap + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.save(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.state.saverestore.stack + desc: save()/restore() can be nested as a stack + testing: + - 2d.state.save + - 2d.state.restore + code: | + ctx.lineWidth = 1; + ctx.save(); + ctx.lineWidth = 2; + ctx.save(); + ctx.lineWidth = 3; + @assert ctx.lineWidth === 3; + ctx.restore(); + @assert ctx.lineWidth === 2; + ctx.restore(); + @assert ctx.lineWidth === 1; + +- name: 2d.state.saverestore.stackdepth + desc: save()/restore() stack depth is not unreasonably limited + testing: + - 2d.state.save + - 2d.state.restore + code: | + var limit = 512; + for (var i = 1; i < limit; ++i) + { + ctx.save(); + ctx.lineWidth = i; + } + for (var i = limit-1; i > 0; --i) + { + @assert ctx.lineWidth === i; + ctx.restore(); + } + +- name: 2d.state.saverestore.underflow + desc: restore() with an empty stack has no effect + testing: + - 2d.state.restore.underflow + code: | + for (var i = 0; i < 16; ++i) + ctx.restore(); + ctx.lineWidth = 0.5; + ctx.restore(); + @assert ctx.lineWidth === 0.5; + + diff --git a/test/wpt/transformations.yaml b/test/wpt/transformations.yaml new file mode 100644 index 000000000..b6aaec73c --- /dev/null +++ b/test/wpt/transformations.yaml @@ -0,0 +1,402 @@ +- name: 2d.transformation.order + desc: Transformations are applied in the right order + testing: + - 2d.transformation.order + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(2, 1); + ctx.rotate(Math.PI / 2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, -50, 50, 50); + @assert pixel 75,25 == 0,255,0,255; + expected: green + + +- name: 2d.transformation.scale.basic + desc: scale() works + testing: + - 2d.transformation.scale + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(2, 4); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 12.5); + @assert pixel 90,40 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.zero + desc: scale() with a scale factor of zero works + testing: + - 2d.transformation.scale + code: | + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.translate(50, 0); + ctx.scale(0, 1); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + + ctx.save(); + ctx.translate(0, 25); + ctx.scale(1, 0); + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + ctx.restore(); + + canvas.toDataURL(); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.negative + desc: scale() with negative scale factors works + testing: + - 2d.transformation.scale + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.save(); + ctx.scale(-1, 1); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-50, 0, 50, 50); + ctx.restore(); + + ctx.save(); + ctx.scale(1, -1); + ctx.fillStyle = '#0f0'; + ctx.fillRect(50, -50, 50, 50); + ctx.restore(); + @assert pixel 25,25 == 0,255,0,255; + @assert pixel 75,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.large + desc: scale() with large scale factors works + notes: Not really that large at all, but it hits the limits in Firefox. + testing: + - 2d.transformation.scale + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(1e5, 1e5); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 1, 1); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.nonfinite + desc: scale() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.scale(<0.1 Infinity -Infinity NaN>, <0.1 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.scale.multiple + desc: Multiple scale()s combine + testing: + - 2d.transformation.scale.multiple + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.scale(Math.sqrt(2), Math.sqrt(2)); + ctx.scale(Math.sqrt(2), Math.sqrt(2)); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + @assert pixel 90,40 == 0,255,0,255; + expected: green + + +- name: 2d.transformation.rotate.zero + desc: rotate() by 0 does nothing + testing: + - 2d.transformation.rotate + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.radians + desc: rotate() uses radians + testing: + - 2d.transformation.rotate.radians + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI); // should fail obviously if this is 3.1 degrees + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.direction + desc: rotate() is clockwise + testing: + - 2d.transformation.rotate.direction + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI / 2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, -100, 50, 100); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.wrap + desc: rotate() wraps large positive values correctly + testing: + - 2d.transformation.rotate + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(Math.PI * (1 + 4096)); // == pi (mod 2*pi) + // We need about pi +/- 0.001 in order to get correct-looking results + // 32-bit floats can store pi*4097 with precision 2^-10, so that should + // be safe enough on reasonable implementations + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,2 == 0,255,0,255; + @assert pixel 98,47 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.wrapnegative + desc: rotate() wraps large negative values correctly + testing: + - 2d.transformation.rotate + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.rotate(-Math.PI * (1 + 4096)); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + @assert pixel 98,2 == 0,255,0,255; + @assert pixel 98,47 == 0,255,0,255; + expected: green + +- name: 2d.transformation.rotate.nonfinite + desc: rotate() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.rotate(<0.1 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.translate.basic + desc: translate() works + testing: + - 2d.transformation.translate + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 50); + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -50, 100, 50); + @assert pixel 90,40 == 0,255,0,255; + expected: green + +- name: 2d.transformation.translate.nonfinite + desc: translate() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.translate(<0.1 Infinity -Infinity NaN>, <0.1 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + + +- name: 2d.transformation.transform.identity + desc: transform() with the identity matrix does nothing + testing: + - 2d.transformation.transform + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.transform(1,0, 0,1, 0,0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.transform.skewed + desc: transform() with skewy matrix transforms correctly + testing: + - 2d.transformation.transform + code: | + // Create green with a red square ring inside it + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(20, 10, 60, 30); + ctx.fillStyle = '#0f0'; + ctx.fillRect(40, 20, 20, 10); + + // Draw a skewed shape to fill that gap, to make sure it is aligned correctly + ctx.transform(1,4, 2,3, 5,6); + // Post-transform coordinates: + // [[20,10],[80,10],[80,40],[20,40],[20,10],[40,20],[40,30],[60,30],[60,20],[40,20],[20,10]]; + // Hence pre-transform coordinates: + var pts=[[-7.4,11.2],[-43.4,59.2],[-31.4,53.2],[4.6,5.2],[-7.4,11.2], + [-15.4,25.2],[-11.4,23.2],[-23.4,39.2],[-27.4,41.2],[-15.4,25.2], + [-7.4,11.2]]; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var i = 0; i < pts.length; ++i) + ctx.lineTo(pts[i][0], pts[i][1]); + ctx.fill(); + @assert pixel 21,11 == 0,255,0,255; + @assert pixel 79,11 == 0,255,0,255; + @assert pixel 21,39 == 0,255,0,255; + @assert pixel 79,39 == 0,255,0,255; + @assert pixel 39,19 == 0,255,0,255; + @assert pixel 61,19 == 0,255,0,255; + @assert pixel 39,31 == 0,255,0,255; + @assert pixel 61,31 == 0,255,0,255; + expected: green + +- name: 2d.transformation.transform.multiply + desc: transform() multiplies the CTM + testing: + - 2d.transformation.transform.multiply + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.transform(1,2, 3,4, 5,6); + ctx.transform(-2,1, 3/2,-1/2, 1,-2); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.transform.nonfinite + desc: transform() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.transform(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green + +- name: 2d.transformation.setTransform.skewed + testing: + - 2d.transformation.setTransform + code: | + // Create green with a red square ring inside it + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = '#f00'; + ctx.fillRect(20, 10, 60, 30); + ctx.fillStyle = '#0f0'; + ctx.fillRect(40, 20, 20, 10); + + // Draw a skewed shape to fill that gap, to make sure it is aligned correctly + ctx.setTransform(1,4, 2,3, 5,6); + // Post-transform coordinates: + // [[20,10],[80,10],[80,40],[20,40],[20,10],[40,20],[40,30],[60,30],[60,20],[40,20],[20,10]]; + // Hence pre-transform coordinates: + var pts=[[-7.4,11.2],[-43.4,59.2],[-31.4,53.2],[4.6,5.2],[-7.4,11.2], + [-15.4,25.2],[-11.4,23.2],[-23.4,39.2],[-27.4,41.2],[-15.4,25.2], + [-7.4,11.2]]; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var i = 0; i < pts.length; ++i) + ctx.lineTo(pts[i][0], pts[i][1]); + ctx.fill(); + @assert pixel 21,11 == 0,255,0,255; + @assert pixel 79,11 == 0,255,0,255; + @assert pixel 21,39 == 0,255,0,255; + @assert pixel 79,39 == 0,255,0,255; + @assert pixel 39,19 == 0,255,0,255; + @assert pixel 61,19 == 0,255,0,255; + @assert pixel 39,31 == 0,255,0,255; + @assert pixel 61,31 == 0,255,0,255; + expected: green + +- name: 2d.transformation.setTransform.multiple + testing: + - 2d.transformation.setTransform.identity + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.setTransform(1/2,0, 0,1/2, 0,0); + ctx.setTransform(); + ctx.setTransform(2,0, 0,2, 0,0); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 50, 25); + @assert pixel 75,35 == 0,255,0,255; + expected: green + +- name: 2d.transformation.setTransform.nonfinite + desc: setTransform() with Infinity/NaN is ignored + testing: + - 2d.nonfinite + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + + ctx.translate(100, 10); + @nonfinite ctx.setTransform(<0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>, <0 Infinity -Infinity NaN>); + + ctx.fillStyle = '#0f0'; + ctx.fillRect(-100, -10, 100, 50); + + @assert pixel 50,25 == 0,255,0,255; + expected: green \ No newline at end of file diff --git a/util/has_lib.js b/util/has_lib.js new file mode 100644 index 000000000..02d709064 --- /dev/null +++ b/util/has_lib.js @@ -0,0 +1,119 @@ +const query = process.argv[2] +const fs = require('fs') +const childProcess = require('child_process') + +const SYSTEM_PATHS = [ + '/lib', + '/usr/lib', + '/usr/lib64', + '/usr/local/lib', + '/opt/local/lib', + '/opt/homebrew/lib', + '/usr/lib/x86_64-linux-gnu', + '/usr/lib/i386-linux-gnu', + '/usr/lib/arm-linux-gnueabihf', + '/usr/lib/arm-linux-gnueabi', + '/usr/lib/aarch64-linux-gnu' +] + +/** + * Checks for lib using ldconfig if present, or searching SYSTEM_PATHS + * otherwise. + * @param {string} lib - library name, e.g. 'jpeg' in 'libjpeg64.so' (see first line) + * @return {boolean} exists + */ +function hasSystemLib (lib) { + const libName = 'lib' + lib + '.+(so|dylib)' + const libNameRegex = new RegExp(libName) + + // Try using ldconfig on linux systems + if (hasLdconfig()) { + try { + if (childProcess.execSync('ldconfig -p 2>/dev/null | grep -E "' + libName + '"').length) { + return true + } + } catch (err) { + // noop -- proceed to other search methods + } + } + + // Try checking common library locations + return SYSTEM_PATHS.some(function (systemPath) { + try { + const dirListing = fs.readdirSync(systemPath) + return dirListing.some(function (file) { + return libNameRegex.test(file) + }) + } catch (err) { + return false + } + }) +} + +/** + * Checks for ldconfig on the path and /sbin + * @return {boolean} exists + */ +function hasLdconfig () { + try { + // Add /sbin to path as ldconfig is located there on some systems -- e.g. + // Debian (and it can still be used by unprivileged users): + childProcess.execSync('export PATH="$PATH:/sbin"') + process.env.PATH = '...' + // execSync throws on nonzero exit + childProcess.execSync('hash ldconfig 2>/dev/null') + return true + } catch (err) { + return false + } +} + +/** + * Checks for freetype2 with --cflags-only-I + * @return Boolean exists + */ +function hasFreetype () { + try { + if (childProcess.execSync('pkg-config cairo --cflags-only-I 2>/dev/null | grep freetype2').length) { + return true + } + } catch (err) { + // noop + } + return false +} + +/** + * Checks for lib using pkg-config. + * @param {string} lib - library name + * @return {boolean} exists + */ +function hasPkgconfigLib (lib) { + try { + // execSync throws on nonzero exit + childProcess.execSync('pkg-config --exists "' + lib + '" 2>/dev/null') + return true + } catch (err) { + return false + } +} + +function main (query) { + switch (query) { + case 'gif': + case 'cairo': + return hasSystemLib(query) + case 'pango': + return hasPkgconfigLib(query) + case 'freetype': + return hasFreetype() + case 'jpeg': + return hasPkgconfigLib('libjpeg') + case 'rsvg': + return hasPkgconfigLib('librsvg-2.0') + default: + throw new Error('Unknown library: ' + query) + } +} + +process.stdout.write(main(query).toString()) diff --git a/util/has_lib.sh b/util/has_lib.sh deleted file mode 100755 index 75e216c8c..000000000 --- a/util/has_lib.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/sh - -has_ldconfig() { - hash ldconfig 2>/dev/null -} - -has_system_lib() { - regex="lib$1.+(so|dylib)" - - # Add /sbin to path as ldconfig is located there on some systems - e.g. Debian - # (and it still can be used by unprivileged users): - PATH="$PATH:/sbin" - export PATH - - # Try using ldconfig on Linux systems - if has_ldconfig; then - for _ in $(ldconfig -p 2>/dev/null | grep -E "$regex"); do - return 0 - done - fi - - # Try just checking common library locations - for dir in /lib /usr/lib /usr/local/lib /opt/local/lib /usr/lib/x86_64-linux-gnu /usr/lib/i386-linux-gnu; do - test -d "$dir" && echo "$dir"/* | grep -E "$regex" && return 0 - done - - return 1 -} - -has_freetype() { - pkg-config cairo --cflags-only-I | grep freetype2 -} - -has_pkgconfig_lib() { - pkg-config --exists "$1" -} - -case "$1" in - gif) - has_system_lib "gif" > /dev/null - result=$? - ;; - jpeg) - has_system_lib "jpeg" > /dev/null - result=$? - ;; - pango) - has_pkgconfig_lib "pango" > /dev/null - result=$? - ;; - freetype) - has_freetype > /dev/null - result=$? - ;; - *) - >&2 echo "Unknown library: $1" - exit 1 -esac - -if test $result -eq 0; then - echo "true" -else - echo "false" -fi diff --git a/util/win_jpeg_lookup.js b/util/win_jpeg_lookup.js new file mode 100644 index 000000000..79815f650 --- /dev/null +++ b/util/win_jpeg_lookup.js @@ -0,0 +1,21 @@ +const fs = require('fs') +const paths = ['C:/libjpeg-turbo'] + +if (process.arch === 'x64') { + paths.unshift('C:/libjpeg-turbo64') +} + +paths.forEach(function (path) { + if (exists(path)) { + process.stdout.write(path) + process.exit() + } +}) + +function exists (path) { + try { + return fs.lstatSync(path).isDirectory() + } catch (e) { + return false + } +}