diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 8828eac4d..10c0a04ab 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -14,4 +14,4 @@ var ctx = canvas.getContext('2d'); ## Your Environment * Version of node-canvas (output of `npm list canvas` or `yarn list canvas`): -* Environment (e.g. node 4.2.0 on Mac OS X 10.8): +* Environment (e.g. node 20.9.0 on macOS 14.1.1): diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e565aa439..963d21721 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,18 +7,18 @@ on: paths-ignore: - ".github/workflows/prebuild.yaml" -jobs: +jobs: Linux: name: Test on Linux runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 16] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Dependencies run: | sudo apt update @@ -30,21 +30,24 @@ jobs: Windows: name: Test on Windows - runs-on: windows-latest + runs-on: windows-2025 strategy: matrix: - node: [10, 12, 14, 16] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Dependencies run: | - Invoke-WebRequest "https://ftp-osl.osuosl.org/pub/gnome/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" + 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 @@ -52,35 +55,35 @@ jobs: macOS: name: Test on macOS - runs-on: macos-latest + runs-on: macos-15 strategy: matrix: - node: [10, 12, 14, 16] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Dependencies run: | brew update - brew install pkg-config cairo pango libpng jpeg giflib librsvg + 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 + run: npm test Lint: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: - node-version: 14 - - uses: actions/checkout@v2 + 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 dtslint + run: npm run tsd diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index b10cc1522..88b6288bf 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -1,236 +1,13 @@ -# Triggering prebuilds: -# 1. Create a draft release manually using the GitHub UI. -# 2. Set the `jobs.*.strategy.matrix.node` arrays to the set of Node.js versions -# to build for. -# 3. Set the `jobs.*.strategy.matrix.canvas_tag` arrays to the set of Canvas -# tags to build. (Usually this is a single tag, but can be an array when a -# new version of Node.js is released and older versions of Canvas need to be -# built.) -# 4. Commit and push this file to master. -# 5. In the Actions tab, navigate to the "Make Prebuilds" workflow and click -# "Run workflow". -# 6. Once the builds succeed, promote the draft release to a full release. +# 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 -# UPLOAD_TO can be specified to upload the release assets under a different tag -# name (e.g. for testing). If omitted, the assets are published under the same -# release tag as the canvas version being built. -# env: -# UPLOAD_TO: "v0.0.1" - jobs: Linux: - strategy: - matrix: - node: [8, 9, 10, 11, 12, 13, 14] - canvas_tag: [] # e.g. "v2.6.1" - name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Linux + name: Nothing runs-on: ubuntu-latest - container: - image: chearon/canvas-prebuilt:7 - env: - CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} - steps: - - uses: actions/checkout@v2 - with: - ref: ${{ matrix.canvas_tag }} - - - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - - name: Build - run: | - npm install -g node-gyp - npm install --ignore-scripts - . prebuild/Linux/preinstall.sh - cp prebuild/Linux/binding.gyp binding.gyp - node-gyp rebuild -j 2 - . prebuild/Linux/bundle.sh - - - name: Test binary - run: | - cd /root/harfbuzz-* && make uninstall - cd /root/cairo-* && make uninstall - cd /root/pango-* && make uninstall - cd /root/libpng-* && make uninstall - cd /root/libjpeg-* && make uninstall - cd /root/giflib-* && make uninstall - cd $GITHUB_WORKSPACE && npm test - - - name: Make bundle - id: make_bundle - run: . prebuild/tarball.sh - - - name: Upload - uses: actions/github-script@0.9.0 - with: - script: | - const fs = require("fs"); - const assetName = "${{ steps.make_bundle.outputs.asset_name }}"; - const tagName = process.env.UPLOAD_TO || process.env.CANVAS_VERSION_TO_BUILD; - const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/"); - - const releases = await github.repos.listReleases({owner, repo}); - const release = releases.data.find(r => r.tag_name === tagName); - if (!release) - throw new Error(`Tag ${tagName} not found. Did you make the GitHub release?`); - - const oldAsset = release.assets.find(a => a.name === assetName); - if (oldAsset) - await github.repos.deleteReleaseAsset({owner, repo, asset_id: oldAsset.id}); - - // (This is equivalent to actions/upload-release-asset. We're - // already in a script, so might as well do it here.) - const r = await github.repos.uploadReleaseAsset({ - url: release.upload_url, - headers: { - "content-type": "application/x-gzip", - "content-length": `${fs.statSync(assetName).size}` - }, - name: assetName, - data: fs.readFileSync(assetName) - }); - - macOS: - strategy: - matrix: - node: [8, 9, 10, 11, 12, 13, 14] - canvas_tag: [] # e.g. "v2.6.1" - name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, macOS - runs-on: macos-latest - env: - CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} steps: - - uses: actions/checkout@v2 - with: - ref: ${{ matrix.canvas_tag }} - - - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - - name: Build - run: | - npm install -g node-gyp - npm install --ignore-scripts - . prebuild/macOS/preinstall.sh - cp prebuild/macOS/binding.gyp binding.gyp - node-gyp rebuild -j 2 - . prebuild/macOS/bundle.sh - - - name: Test binary - run: | - brew uninstall --force cairo pango librsvg giflib harfbuzz - npm test - - - name: Make bundle - id: make_bundle - run: . prebuild/tarball.sh - - - name: Upload - uses: actions/github-script@0.9.0 - with: - script: | - const fs = require("fs"); - const assetName = "${{ steps.make_bundle.outputs.asset_name }}"; - const tagName = process.env.UPLOAD_TO || process.env.CANVAS_VERSION_TO_BUILD; - const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/"); - - const releases = await github.repos.listReleases({owner, repo}); - const release = releases.data.find(r => r.tag_name === tagName); - if (!release) - throw new Error(`Tag ${tagName} not found. Did you make the GitHub release?`); - - const oldAsset = release.assets.find(a => a.name === assetName); - if (oldAsset) - await github.repos.deleteReleaseAsset({owner, repo, asset_id: oldAsset.id}); - - // (This is equivalent to actions/upload-release-asset. We're - // already in a script, so might as well do it here.) - const r = await github.repos.uploadReleaseAsset({ - url: release.upload_url, - headers: { - "content-type": "application/x-gzip", - "content-length": `${fs.statSync(assetName).size}` - }, - name: assetName, - data: fs.readFileSync(assetName) - }); - - Win: - strategy: - matrix: - node: [8, 9, 10, 11, 12, 13, 14] - canvas_tag: [] # e.g. "v2.6.1" - name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Windows - runs-on: windows-latest - env: - CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} - steps: - # TODO drop when https://github.com/actions/virtual-environments/pull/632 lands - - uses: numworks/setup-msys2@v1 - with: - update: true - path-type: inherit - - - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - - uses: actions/checkout@v2 - with: - ref: ${{ matrix.canvas_tag }} - - - name: Build - run: | - npm install -g node-gyp - npm install --ignore-scripts - msys2do . prebuild/Windows/preinstall.sh - msys2do cp prebuild/Windows/binding.gyp binding.gyp - msys2do node-gyp configure - msys2do node-gyp rebuild -j 2 - - - name: Bundle - run: msys2do . prebuild/Windows/bundle.sh - - - name: Test binary - # By not running in msys2, this doesn't have access to the msys2 libs - run: npm test - - - name: Make asset - id: make_bundle - # I can't figure out why this isn't an env var already. It shows up with `env`. - run: msys2do UPLOAD_TO=${{ env.UPLOAD_TO }} CANVAS_VERSION_TO_BUILD=${{ env.CANVAS_VERSION_TO_BUILD}} . prebuild/tarball.sh - - - name: Upload - uses: actions/github-script@0.9.0 - with: - script: | - const fs = require("fs"); - const assetName = "${{ steps.make_bundle.outputs.asset_name }}"; - const tagName = process.env.UPLOAD_TO || process.env.CANVAS_VERSION_TO_BUILD; - const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/"); - - const releases = await github.repos.listReleases({owner, repo}); - const release = releases.data.find(r => r.tag_name === tagName); - if (!release) - throw new Error(`Tag ${tagName} not found. Did you make the GitHub release?`); - - const oldAsset = release.assets.find(a => a.name === assetName); - if (oldAsset) - await github.repos.deleteReleaseAsset({owner, repo, asset_id: oldAsset.id}); - - // (This is equivalent to actions/upload-release-asset. We're - // already in a script, so might as well do it here.) - const r = await github.repos.uploadReleaseAsset({ - url: release.upload_url, - headers: { - "content-type": "application/x-gzip", - "content-length": `${fs.statSync(assetName).size}` - }, - name: assetName, - data: fs.readFileSync(assetName) - }); + - name: Nothing + run: echo "Nothing to do here" diff --git a/.gitignore b/.gitignore index e5e14d5b6..4fd0b5eda 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build test/images/*.png examples/*.png examples/*.jpg +examples/*.pdf testing out.png out.pdf @@ -16,3 +17,5 @@ package-lock.json *.swp *.un~ npm-debug.log + +.idea diff --git a/CHANGELOG.md b/CHANGELOG.md index e87890de5..4a4deae39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,160 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed + +3.2.1 +================== +* 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) +* Fix compilation on GCC 15 by including (#2545) + +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 ================== diff --git a/Readme.md b/Readme.md index b27549962..d0429c520 100644 --- a/Readme.md +++ b/Readme.md @@ -11,9 +11,15 @@ node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation $ npm install canvas ``` -By default, binaries for macOS, Linux and Windows will be downloaded. If you want to build from source, use `npm install --build-from-source` and see the **Compiling** section below. +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 -The minimum version of Node.js required is **6.0.0**. +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**. ### Compiling @@ -23,7 +29,7 @@ For detailed installation information, see the [wiki](https://github.com/Automat OS | Command ----- | ----- -OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg` +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` @@ -78,6 +84,8 @@ This project is an implementation of the Web Canvas API and implements that API * [createImageData()](#createimagedata) * [loadImage()](#loadimage) * [registerFont()](#registerfont) +* [deregisterAllFonts()](#deregisterAllFonts) + ### Non-standard APIs @@ -91,7 +99,7 @@ This project is an implementation of the Web Canvas API and implements that API * [CanvasRenderingContext2D#patternQuality](#canvasrenderingcontext2dpatternquality) * [CanvasRenderingContext2D#quality](#canvasrenderingcontext2dquality) * [CanvasRenderingContext2D#textDrawingMode](#canvasrenderingcontext2dtextdrawingmode) -* [CanvasRenderingContext2D#globalCompositeOperator = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperator--saturate) +* [CanvasRenderingContext2D#globalCompositeOperation = 'saturate'](#canvasrenderingcontext2dglobalcompositeoperation--saturate) * [CanvasRenderingContext2D#antialias](#canvasrenderingcontext2dantialias) ### createCanvas() @@ -155,7 +163,7 @@ const myimg = await loadImage('http://server.com/image.png') > registerFont(path: string, { family: string, weight?: string, style?: string }) => void > ``` -To use a font file that is not installed as a system font, use `registerFont()` to register the font with Canvas. *This must be done before the Canvas is created.* +To use a font file that is not installed as a system font, use `registerFont()` to register the font with Canvas. ```js const { registerFont, createCanvas } = require('canvas') @@ -170,6 +178,35 @@ ctx.fillText('Everyone hates this font :(', 250, 10) 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' }) + + 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 > ```ts @@ -478,6 +515,26 @@ ctx.addPage(400, 800) ctx.fillText('Hello World 2', 50, 80) ``` +It is possible to add hyperlinks using `.beginTag()` and `.endTag()`: + +```js +ctx.beginTag('Link', "uri='https://google.com'") +ctx.font = '22px Helvetica' +ctx.fillText('Hello World', 50, 80) +ctx.endTag('Link') +``` + +Or with a defined rectangle: + +```js +ctx.beginTag('Link', "uri='https://google.com' rect=[50 80 100 20]") +ctx.endTag('Link') +``` + +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. + See also: * [Image#dataMode](#imagedatamode) for embedding JPEGs in PDFs diff --git a/benchmarks/run.js b/benchmarks/run.js index a6954da87..5a5c9d507 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -64,6 +64,34 @@ 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' diff --git a/binding.gyp b/binding.gyp index 57f14ab8c..bf647f7d1 100644 --- a/binding.gyp +++ b/binding.gyp @@ -57,7 +57,8 @@ }, { 'target_name': 'canvas', - 'include_dirs': [" import { Readable } from 'stream' @@ -7,12 +6,12 @@ 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_FITLER_SUB`, + * 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_FITLERS`. + * filters. Defaults to `canvas.PNG_ALL_FILTERS`. */ filters?: number /** @@ -64,23 +63,23 @@ export class Canvas { readonly stride: number; /** Constant used in PNG encoding methods. */ - readonly PNG_NO_FILTERS: number + static readonly PNG_NO_FILTERS: number /** Constant used in PNG encoding methods. */ - readonly PNG_ALL_FILTERS: number + static readonly PNG_ALL_FILTERS: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_NONE: number + static readonly PNG_FILTER_NONE: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_SUB: number + static readonly PNG_FILTER_SUB: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_UP: number + static readonly PNG_FILTER_UP: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_AVG: number + static readonly PNG_FILTER_AVG: number /** Constant used in PNG encoding methods. */ - readonly PNG_FILTER_PAETH: number + static readonly PNG_FILTER_PAETH: number constructor(width: number, height: number, type?: 'image'|'pdf'|'svg') - getContext(contextId: '2d', contextAttributes?: NodeCanvasRenderingContext2DSettings): NodeCanvasRenderingContext2D + getContext(contextId: '2d', contextAttributes?: NodeCanvasRenderingContext2DSettings): CanvasRenderingContext2D /** * For image canvases, encodes the canvas as a PNG. For PDF canvases, @@ -128,19 +127,132 @@ export class Canvas { toDataURL(mimeType: 'image/jpeg', quality: number, cb: (err: Error|null, result: string) => void): void } -declare class NodeCanvasRenderingContext2D extends CanvasRenderingContext2D { +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' - - /** - * _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to - * transformations affecting more than just patterns. - */ - quality: '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: * @@ -165,61 +277,30 @@ declare class NodeCanvasRenderingContext2D extends CanvasRenderingContext2D { * (aside from using the stroke and fill style, respectively). */ textDrawingMode: 'path' | 'glyph' - - /** _'saturate' is non-standard._ */ - globalCompositeOperation: 'saturate' | 'clear' | 'copy' | 'destination' | 'source-over' | 'destination-over' | - 'source-in' | 'destination-in' | 'source-out' | 'destination-out' | 'source-atop' | 'destination-atop' | - 'xor' | 'lighter' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' | 'color-burn' | - 'hard-light' | 'soft-light' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' - - /** _Non-standard_. Sets the antialiasing mode. */ - antialias: 'default' | 'gray' | 'none' | 'subpixel' - - // Standard, but not in the TS lib and needs node-canvas class return type. - /** Returns or sets a `DOMMatrix` for the current transformation matrix. */ - currentTransform: NodeCanvasDOMMatrix - - // Standard, but need node-canvas class versions: - getTransform(): NodeCanvasDOMMatrix - setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void - setTransform(transform?: NodeCanvasDOMMatrix): void - createImageData(sw: number, sh: number): NodeCanvasImageData - createImageData(imagedata: NodeCanvasImageData): NodeCanvasImageData - getImageData(sx: number, sy: number, sw: number, sh: number): NodeCanvasImageData - putImageData(imagedata: NodeCanvasImageData, dx: number, dy: number): void - putImageData(imagedata: NodeCanvasImageData, dx: number, dy: number, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number): void - 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 - /** - * **Do not use this overload. Use one of the other three overloads.** This - * is a catch-all definition required for compatibility with the base - * `CanvasRenderingContext2D` interface. - */ - drawImage(...args: any[]): void - createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): NodeCanvasCanvasPattern - /** - * **Do not use this overload. Use the other three overload.** This is a - * catch-all definition required for compatibility with the base - * `CanvasRenderingContext2D` interface. - */ - createPattern(...args: any[]): NodeCanvasCanvasPattern - createLinearGradient(x0: number, y0: number, x1: number, y1: number): NodeCanvasCanvasGradient; - createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): NodeCanvasCanvasGradient; - /** - * For PDF canvases, adds another page. If width and/or height are omitted, - * the canvas's initial size is used. + * _Non-standard_. Defaults to 'good'. Like `patternQuality`, but applies to + * transformations affecting more than just patterns. */ - addPage(width?: number, height?: number): void + 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 { NodeCanvasRenderingContext2D as CanvasRenderingContext2D } -declare class NodeCanvasCanvasGradient extends CanvasGradient {} -export { NodeCanvasCanvasGradient as CanvasGradient } +export class CanvasGradient { + addColorStop(offset: number, color: string): void; +} -declare class NodeCanvasCanvasPattern extends CanvasPattern {} -export { NodeCanvasCanvasPattern as CanvasPattern } +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. @@ -306,6 +387,11 @@ export function loadImage(src: string|Buffer, options?: any): Promise */ 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()`. */ @@ -313,17 +399,97 @@ export class JPEGStream extends Readable {} /** This class must not be constructed directly; use `canvas.createPDFStream()`. */ export class PDFStream extends Readable {} -declare class NodeCanvasDOMMatrix extends DOMMatrix {} -export { NodeCanvasDOMMatrix as DOMMatrix } +// TODO: this is wrong. See matrixTransform in lib/DOMMatrix.js +type DOMMatrixInit = DOMMatrix | string | number[]; -declare class NodeCanvasDOMPoint extends DOMPoint {} -export { NodeCanvasDOMPoint as DOMPoint } +interface DOMPointInit { + w?: number; + x?: number; + y?: number; + z?: number; +} -declare class NodeCanvasImageData extends ImageData {} -export { NodeCanvasImageData as ImageData } +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; +} -// This is marked private, but is exported... -// export function parseFont(description: string): object +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 diff --git a/index.js b/index.js index 4d11d9a81..adde4da12 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,6 @@ const Canvas = require('./lib/canvas') const Image = require('./lib/image') const CanvasRenderingContext2D = require('./lib/context2d') const CanvasPattern = require('./lib/pattern') -const parseFont = require('./lib/parse-font') const packageJson = require('./package.json') const bindings = require('./lib/bindings') const fs = require('fs') @@ -11,6 +10,8 @@ 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) } @@ -55,40 +56,39 @@ function deregisterAllFonts () { return Canvas._deregisterAllFonts() } -module.exports = { - Canvas, - Context2d: CanvasRenderingContext2D, // Legacy/compat export - CanvasRenderingContext2D, - CanvasGradient: bindings.CanvasGradient, - CanvasPattern, - Image, - ImageData: bindings.ImageData, - PNGStream, - PDFStream, - JPEGStream, - DOMMatrix, - DOMPoint, +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 - registerFont, - deregisterAllFonts, - parseFont, +exports.registerFont = registerFont +exports.deregisterAllFonts = deregisterAllFonts - createCanvas, - createImageData, - loadImage, +exports.createCanvas = createCanvas +exports.createImageData = createImageData +exports.loadImage = loadImage - backends: bindings.Backends, +exports.backends = bindings.Backends - /** Library version. */ - version: packageJson.version, - /** Cairo version. */ - cairoVersion: bindings.cairoVersion, - /** jpeglib version. */ - jpegVersion: bindings.jpegVersion, - /** gif_lib version. */ - gifVersion: bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined, - /** freetype version. */ - freetypeVersion: bindings.freetypeVersion, - /** rsvg version. */ - rsvgVersion: bindings.rsvgVersion -} +/** 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 index 479e7e6e5..97015adcf 100644 --- a/lib/DOMMatrix.js +++ b/lib/DOMMatrix.js @@ -17,6 +17,26 @@ class DOMPoint { 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) @@ -163,6 +183,13 @@ class DOMMatrix { 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 @@ -587,6 +614,37 @@ Object.defineProperties(DOMMatrix.prototype, { 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, + } + } } }) diff --git a/lib/bindings.js b/lib/bindings.js index c638a5878..40cef3c69 100644 --- a/lib/bindings.js +++ b/lib/bindings.js @@ -4,10 +4,40 @@ 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/context2d.js b/lib/context2d.js index 75f50635b..103ec6325 100644 --- a/lib/context2d.js +++ b/lib/context2d.js @@ -7,8 +7,5 @@ */ const bindings = require('./bindings') -const parseFont = require('./parse-font') -const { DOMMatrix } = require('./DOMMatrix') -bindings.CanvasRenderingContext2dInit(DOMMatrix, parseFont) module.exports = bindings.CanvasRenderingContext2d diff --git a/lib/image.js b/lib/image.js index c5b594f8a..a6c81ba83 100644 --- a/lib/image.js +++ b/lib/image.js @@ -14,9 +14,6 @@ const bindings = require('./bindings') const Image = module.exports = bindings.Image const util = require('util') -// Lazily loaded simple-get -let get - const { GetSource, SetSource } = bindings Object.defineProperty(Image.prototype, 'src', { @@ -47,22 +44,29 @@ Object.defineProperty(Image.prototype, 'src', { } } - if (!get) get = require('simple-get') - - get.concat(val, (err, res, data) => { - if (err) return onerror(err) - - if (res.statusCode < 200 || res.statusCode >= 300) { - return onerror(new Error(`Server responded with ${res.statusCode}`)) - } - - setSource(this, data) + 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 } }, diff --git a/lib/parse-font.js b/lib/parse-font.js deleted file mode 100644 index 713db5082..000000000 --- a/lib/parse-font.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict' - -/** - * Font RegExp helpers. - */ - -const weights = 'bold|bolder|lighter|[1-9]00' -const styles = 'italic|oblique' -const variants = 'small-caps' -const stretches = 'ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded' -const units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q' -const string = '\'([^\']+)\'|"([^"]+)"|[\\w\\s-]+' - -// [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? -// <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] -// https://drafts.csswg.org/css-fonts-3/#font-prop -const weightRe = new RegExp(`(${weights}) +`, 'i') -const styleRe = new RegExp(`(${styles}) +`, 'i') -const variantRe = new RegExp(`(${variants}) +`, 'i') -const stretchRe = new RegExp(`(${stretches}) +`, 'i') -const sizeFamilyRe = new RegExp( - `([\\d\\.]+)(${units}) *((?:${string})( *, *(?:${string}))*)`) - -/** - * Cache font parsing. - */ - -const cache = {} - -const defaultHeight = 16 // pt, common browser default - -/** - * Parse font `str`. - * - * @param {String} str - * @return {Object} Parsed font. `size` is in device units. `unit` is the unit - * appearing in the input string. - * @api private - */ - -module.exports = str => { - // Cached - if (cache[str]) return cache[str] - - // Try for required properties first. - const sizeFamily = sizeFamilyRe.exec(str) - if (!sizeFamily) return // invalid - - // Default values and required properties - const font = { - weight: 'normal', - style: 'normal', - stretch: 'normal', - variant: 'normal', - size: parseFloat(sizeFamily[1]), - unit: sizeFamily[2], - family: sizeFamily[3].replace(/["']/g, '').replace(/ *, */g, ',') - } - - // Optional, unordered properties. - let weight, style, variant, stretch - // Stop search at `sizeFamily.index` - const substr = str.substring(0, sizeFamily.index) - if ((weight = weightRe.exec(substr))) font.weight = weight[1] - if ((style = styleRe.exec(substr))) font.style = style[1] - if ((variant = variantRe.exec(substr))) font.variant = variant[1] - if ((stretch = stretchRe.exec(substr))) font.stretch = stretch[1] - - // Convert to device units. (`font.unit` is the original unit) - // TODO: ch, ex - switch (font.unit) { - case 'pt': - font.size /= 0.75 - break - case 'pc': - font.size *= 16 - break - case 'in': - font.size *= 96 - break - case 'cm': - font.size *= 96.0 / 2.54 - break - case 'mm': - font.size *= 96.0 / 25.4 - break - case '%': - // TODO disabled because existing unit tests assume 100 - // font.size *= defaultHeight / 100 / 0.75 - break - case 'em': - case 'rem': - font.size *= defaultHeight / 0.75 - break - case 'q': - font.size *= 96 / 25.4 / 4 - break - } - - return (cache[str] = font) -} diff --git a/lib/pattern.js b/lib/pattern.js index 1de497681..fe5bbc300 100644 --- a/lib/pattern.js +++ b/lib/pattern.js @@ -7,9 +7,7 @@ */ const bindings = require('./bindings') -const { DOMMatrix } = require('./DOMMatrix') -bindings.CanvasPatternInit(DOMMatrix) module.exports = bindings.CanvasPattern bindings.CanvasPattern.prototype.toString = function () { diff --git a/package.json b/package.json index b833e7168..7ba228af8 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.9.0", + "version": "3.2.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", + "types": "index.d.ts", "contributors": [ "Nathan Rajlich ", "Rod Vagg ", @@ -29,41 +30,42 @@ "test": "mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js", - "install": "node-pre-gyp install --fallback-to-build", - "dtslint": "dtslint types" - }, - "binary": { - "module_name": "canvas", - "module_path": "build/Release", - "host": "https://github.com/Automattic/node-canvas/releases/download/", - "remote_path": "v{version}", - "package_name": "{module_name}-v{version}-{node_abi}-{platform}-{libc}-{arch}.tar.gz" + "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/", - "types/index.d.ts" + "util/" ], - "types": "types/index.d.ts", "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.15.0", - "simple-get": "^3.0.3" + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" }, "devDependencies": { "@types/node": "^10.12.18", "assert-rejects": "^1.0.0", - "dtslint": "^4.0.7", "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": ">=6" + "node": "^18.12.0 || >= 20.9.0" + }, + "binary": { + "napi_versions": [ + 7 + ] }, "license": "MIT" } diff --git a/prebuild/Linux/Dockerfile b/prebuild/Linux/Dockerfile deleted file mode 100644 index be68e5b5e..000000000 --- a/prebuild/Linux/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM debian:stretch -RUN apt-get update && apt-get -y install curl git cmake make gcc g++ nasm wget gperf bzip2 meson uuid-dev perl libxml-parser-perl - -RUN bash -c 'cd; curl -LO https://pkg-config.freedesktop.org/releases/pkg-config-0.29.2.tar.gz; tar -xvf pkg-config-0.29.2.tar.gz; cd pkg-config-0.29.2; ./configure --with-internal-glib; make; make install' -RUN bash -c 'cd; curl -O https://zlib.net/fossils/zlib-1.2.11.tar.gz; tar -xvf zlib-1.2.11.tar.gz; cd zlib-1.2.11; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://github.com/libffi/libffi/releases/download/v3.3/libffi-3.3.tar.gz; tar -xvf libffi-3.3.tar.gz; cd libffi-3.3; ./configure; make; make install' -RUN bash -c 'cd; curl -O https://www.openssl.org/source/openssl-1.1.1i.tar.gz; tar -xvf openssl-1.1.1i.tar.gz; cd openssl-1.1.1i; ./config; make; make install' -RUN ldconfig -RUN bash -c 'cd; curl -O https://www.python.org/ftp/python/3.9.1/Python-3.9.1.tgz; tar -xvf Python-3.9.1.tgz; cd Python-3.9.1; ./configure --enable-shared --with-ensurepip=yes; make; make install' -RUN ldconfig -RUN pip3 install meson -RUN bash -c 'cd; curl -LO https://download.sourceforge.net/giflib/giflib-5.2.1.tar.gz; tar -xvf giflib-5.2.1.tar.gz; cd giflib-5.2.1; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://download.sourceforge.net/libpng/libpng-1.6.37.tar.gz; tar -xvf libpng-1.6.37.tar.gz; cd libpng-1.6.37; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://github.com/libjpeg-turbo/libjpeg-turbo/archive/2.0.6.tar.gz; tar -xvf 2.0.6.tar.gz; cd libjpeg-turbo-2.0.6; mkdir b; cd b; cmake -G"Unix Makefiles" -DCMAKE_INSTALL_PREFIX=/usr/local ..; make; make install' -RUN bash -c 'cd; curl -O https://ftp.pcre.org/pub/pcre/pcre-8.44.tar.bz2; tar -xvf pcre-8.44.tar.bz2; cd pcre-8.44; ./configure --enable-pcre16 --enable-pcre32 --enable-utf --enable-unicode-properties; make; make install ' -RUN ldconfig -RUN bash -c 'cd; curl -LO https://download.gnome.org/sources/glib/2.67/glib-2.67.1.tar.xz; tar -xvf glib-2.67.1.tar.xz; cd glib-2.67.1; meson _build; cd _build; ninja; ninja install' -RUN ldconfig -RUN bash -c 'cd; curl -LO https://download.sourceforge.net/freetype/freetype-2.10.4.tar.gz; tar -xvf freetype-2.10.4.tar.gz; cd freetype-2.10.4; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://github.com/harfbuzz/harfbuzz/releases/download/2.7.4/harfbuzz-2.7.4.tar.xz; tar -xvf harfbuzz-2.7.4.tar.xz; cd harfbuzz-2.7.4; ./configure; make; make install;' -RUN bash -c 'cd; curl -LO https://github.com/libexpat/libexpat/releases/download/R_2_2_10/expat-2.2.10.tar.gz; tar -xvf expat-2.2.10.tar.gz; cd expat-2.2.10; ./configure; make; make install' -RUN ldconfig -RUN ls -l /usr/include -RUN bash -c 'cd; curl -O https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.13.1.tar.bz2; tar -xvf fontconfig-2.13.1.tar.bz2; cd fontconfig-2.13.1; UUID_LIBS="-L/lib/x86_64-linux-gnu -luuid" UUID_CFLAGS="-I/include" ./configure --enable-static --sysconfdir=/etc --localstatedir=/var; make; make install' -RUN bash -c 'cd; curl -O https://www.cairographics.org/releases/pixman-0.40.0.tar.gz; tar -xvf pixman-0.40.0.tar.gz; cd pixman-0.40.0; ./configure; make; make install' -RUN bash -c 'cd; curl -O https://cairographics.org/releases/cairo-1.16.0.tar.xz; tar -xvf cairo-1.16.0.tar.xz; cd cairo-1.16.0; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://github.com/fribidi/fribidi/releases/download/v1.0.10/fribidi-1.0.10.tar.xz; tar -xvf fribidi-1.0.10.tar.xz; cd fribidi-1.0.10; ./configure; make; make install' -RUN bash -c 'cd; curl -LO https://download.gnome.org/sources/pango/1.48/pango-1.48.0.tar.xz; tar -xvf pango-1.48.0.tar.xz; cd pango-1.48.0; meson -Dharfbuzz:docs=disabled -Dgtk_doc=false _build; cd _build; ninja; ninja install' -RUN ldconfig - -# librsvg -RUN bash -c 'curl https://sh.rustup.rs -sSf | sh -s -- -y'; -RUN bash -c 'curl -O http://xmlsoft.org/sources/libxml2-2.9.10.tar.gz; tar -xvf libxml2-2.9.10.tar.gz; cd libxml2-2.9.10; ./configure --without-python; make; make install' -RUN bash -c 'curl -O https://ftp.gnu.org/pub/gnu/gettext/gettext-0.21.tar.gz; tar -xvf gettext-0.21.tar.gz; cd gettext-0.21; ./configure; make; make install' -RUN ldconfig -RUN bash -c 'curl -LO https://launchpad.net/intltool/trunk/0.51.0/+download/intltool-0.51.0.tar.gz; tar -xvf intltool-0.51.0.tar.gz; cd intltool-0.51.0; ./configure; make; make install' -# using an old version of shared-mime-info because 2.1 has a ridiculous number of dependencies for what is essentially just a database -RUN bash -c 'curl -O https://people.freedesktop.org/~hadess/shared-mime-info-1.8.tar.xz; tar -xvf shared-mime-info-1.8.tar.xz; cd shared-mime-info-1.8; ./configure; make; make install' -RUN bash -c 'curl -LO https://download.gnome.org/sources/gdk-pixbuf/2.42/gdk-pixbuf-2.42.2.tar.xz; tar -xvf gdk-pixbuf-2.42.2.tar.xz; cd gdk-pixbuf-2.42.2; meson _build; cd _build; ninja install'; -RUN ldconfig -RUN bash -c 'curl -LO https://download.gnome.org/sources/libcroco/0.6/libcroco-0.6.13.tar.xz; tar -xvf libcroco-0.6.13.tar.xz; cd libcroco-0.6.13; ./configure; make; make install' -RUN bash -c 'cd; . .cargo/env; curl -LO https://download.gnome.org/sources/librsvg/2.50/librsvg-2.50.2.tar.xz; tar -xvf librsvg-2.50.2.tar.xz; cd librsvg-2.50.2; ./configure --enable-introspection=no; make; make install' -RUN ldconfig diff --git a/prebuild/Linux/binding.gyp b/prebuild/Linux/binding.gyp deleted file mode 100644 index 1a967667d..000000000 --- a/prebuild/Linux/binding.gyp +++ /dev/null @@ -1,53 +0,0 @@ -{ - 'targets': [ - { - 'target_name': 'canvas', - 'sources': [ - 'src/backend/Backend.cc', - 'src/backend/ImageBackend.cc', - 'src/backend/PdfBackend.cc', - 'src/backend/SvgBackend.cc', - 'src/bmp/BMPParser.cc', - 'src/Backends.cc', - 'src/Canvas.cc', - 'src/CanvasGradient.cc', - 'src/CanvasPattern.cc', - 'src/CanvasRenderingContext2d.cc', - 'src/closure.cc', - 'src/color.cc', - 'src/Image.cc', - 'src/ImageData.cc', - 'src/init.cc', - 'src/register_font.cc' - ], - 'defines': [ - 'HAVE_GIF', - 'HAVE_JPEG', - 'HAVE_RSVG' - ], - 'libraries': [ - ' /dev/null 2>&1 || { - echo "could not find lib$lib.dll, have to skip "; - continue; - } - - dlltool -d lib$lib.def -l /mingw64/lib/lib$lib.lib > /dev/null 2>&1 || { - echo "could not create dll for lib$lib.dll"; - continue; - } - - echo "created lib$lib.lib from lib$lib.dll"; - - rm lib$lib.def -done diff --git a/prebuild/macOS/binding.gyp b/prebuild/macOS/binding.gyp deleted file mode 100644 index 00ae2ccbc..000000000 --- a/prebuild/macOS/binding.gyp +++ /dev/null @@ -1,51 +0,0 @@ -{ - 'targets': [ - { - 'target_name': 'canvas', - 'sources': [ - 'src/backend/Backend.cc', - 'src/backend/ImageBackend.cc', - 'src/backend/PdfBackend.cc', - 'src/backend/SvgBackend.cc', - 'src/bmp/BMPParser.cc', - 'src/Backends.cc', - 'src/Canvas.cc', - 'src/CanvasGradient.cc', - 'src/CanvasPattern.cc', - 'src/CanvasRenderingContext2d.cc', - 'src/closure.cc', - 'src/color.cc', - 'src/Image.cc', - 'src/ImageData.cc', - 'src/init.cc', - 'src/register_font.cc' - ], - 'defines': [ - 'HAVE_GIF', - 'HAVE_JPEG', - 'HAVE_RSVG' - ], - 'libraries': [ - ' target) { - Nan::HandleScope scope; +void +Backends::Initialize(Napi::Env env, Napi::Object exports) { + Napi::Object obj = Napi::Object::New(env); - Local obj = Nan::New(); ImageBackend::Initialize(obj); PdfBackend::Initialize(obj); SvgBackend::Initialize(obj); - Nan::Set(target, Nan::New("Backends").ToLocalChecked(), obj).Check(); + exports.Set("Backends", obj); } diff --git a/src/Backends.h b/src/Backends.h index dbea053ce..66a1c1db8 100644 --- a/src/Backends.h +++ b/src/Backends.h @@ -1,10 +1,9 @@ #pragma once #include "backend/Backend.h" -#include -#include +#include -class Backends : public Nan::ObjectWrap { +class Backends : public Napi::ObjectWrap { public: - static void Initialize(v8::Local target); + static void Initialize(Napi::Env env, Napi::Object exports); }; diff --git a/src/Canvas.cc b/src/Canvas.cc index bd39d4329..bc790add5 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -1,7 +1,7 @@ // Copyright (c) 2010 LearnBoost #include "Canvas.h" - +#include "InstanceData.h" #include // std::min #include #include @@ -21,6 +21,7 @@ #include "Util.h" #include #include "node_buffer.h" +#include "FontParser.h" #ifdef HAVE_JPEG #include "JPEGStream.h" @@ -35,145 +36,151 @@ "with at least a family (string) and optionally weight (string/number) " \ "and style (string)." -using namespace v8; using namespace std; -Nan::Persistent Canvas::constructor; +std::vector Canvas::font_face_list; -std::vector 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 - SetProtoAccessor(proto, Nan::New("type").ToLocalChecked(), GetType, NULL, ctor); - SetProtoAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride, NULL, ctor); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); - - 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::SetMethod(ctor, "_deregisterAllFonts", DeregisterAllFonts); + 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) + }); - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, - Nan::New("Canvas").ToLocalChecked(), - ctor->GetFunction(ctx).ToLocalChecked()); + 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; - if (info[0]->IsNumber()) { - int width = Nan::To(info[0]).FromMaybe(0), height = 0; - - if (info[1]->IsNumber()) height = Nan::To(info[1]).FromMaybe(0); - - if (info[2]->IsString()) { - if (0 == strcmp("pdf", *Nan::Utf8String(info[2]))) - backend = new PdfBackend(width, height); - else if (0 == strcmp("svg", *Nan::Utf8String(info[2]))) - backend = new SvgBackend(width, height); - else - backend = new ImageBackend(width, height); + 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 - backend = new ImageBackend(width, height); - } - else if (info[0]->IsObject()) { - if (Nan::New(ImageBackend::constructor)->HasInstance(info[0]) || - Nan::New(PdfBackend::constructor)->HasInstance(info[0]) || - Nan::New(SvgBackend::constructor)->HasInstance(info[0])) { - backend = Nan::ObjectWrap::Unwrap(Nan::To(info[0]).ToLocalChecked()); - }else{ - return Nan::ThrowTypeError("Invalid arguments"); + } 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()); } - else { - backend = new ImageBackend(0, 0); - } + + backend->setCanvas(this); if (!backend->isSurfaceValid()) { - delete backend; - return Nan::ThrowError(backend->getError()); + Napi::Error::New(env, backend->getError()).ThrowAsJavaScriptException(); + return; } - Canvas* canvas = new Canvas(backend); - canvas->Wrap(info.This()); - - backend->setCanvas(canvas); - - 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->backend()->getName()).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->getWidth())); +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->backend()->setWidth(Nan::To(value).FromMaybe(0)); - 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()); } } @@ -181,20 +188,20 @@ NAN_SETTER(Canvas::SetWidth) { * Get height. */ -NAN_GETTER(Canvas::GetHeight) { - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(canvas->getHeight())); +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->backend()->setHeight(Nan::To(value).FromMaybe(0)); - 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()); } } @@ -203,8 +210,8 @@ NAN_SETTER(Canvas::SetHeight) { */ void -Canvas::ToPngBufferAsync(uv_work_t *req) { - PngClosure* closure = static_cast(req->data); +Canvas::ToPngBufferAsync(Closure* base) { + PngClosure* closure = static_cast(base); closure->status = canvas_write_to_png_stream( closure->canvas->surface(), @@ -214,102 +221,84 @@ Canvas::ToPngBufferAsync(uv_work_t *req) { #ifdef HAVE_JPEG void -Canvas::ToJpegBufferAsync(uv_work_t *req) { - JpegClosure* closure = static_cast(req->data); +Canvas::ToJpegBufferAsync(Closure* base) { + JpegClosure* closure = static_cast(base); write_to_jpeg_buffer(closure->canvas->surface(), closure); } #endif -/* - * EIO after toBuffer callback. - */ - -void -Canvas::ToBufferAsyncAfter(uv_work_t *req) { - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:ToBufferAsyncAfter"); - Closure* closure = static_cast(req->data); - delete req; - - if (closure->status) { - Local argv[1] = { Canvas::Error(closure->status) }; - closure->cb.Call(1, argv, &async); - } else { - Local buf = Nan::CopyBuffer((char*)&closure->vec[0], closure->vec.size()).ToLocalChecked(); - Local argv[2] = { Nan::Null(), buf }; - closure->cb.Call(sizeof argv / sizeof *argv, argv, &async); - } - - closure->canvas->Unref(); - delete closure; -} - -static void parsePNGArgs(Local arg, PngClosure& pngargs) { - if (arg->IsObject()) { - Local obj = Nan::To(arg).ToLocalChecked(); +static void +parsePNGArgs(Napi::Value arg, PngClosure& pngargs) { + if (arg.IsObject()) { + Napi::Object obj = arg.As(); + Napi::Value cLevel; - Local cLevel = Nan::Get(obj, Nan::New("compressionLevel").ToLocalChecked()).ToLocalChecked(); - if (cLevel->IsUint32()) { - uint32_t val = Nan::To(cLevel).FromMaybe(0); + 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; } - Local rez = Nan::Get(obj, Nan::New("resolution").ToLocalChecked()).ToLocalChecked(); - if (rez->IsUint32()) { - uint32_t val = Nan::To(rez).FromMaybe(0); + Napi::Value rez; + if (obj.Get("resolution").UnwrapTo(&rez) && rez.IsNumber()) { + uint32_t val = rez.As().Uint32Value(); if (val > 0) pngargs.resolution = val; } - Local filters = Nan::Get(obj, Nan::New("filters").ToLocalChecked()).ToLocalChecked(); - if (filters->IsUint32()) pngargs.filters = Nan::To(filters).FromMaybe(0); + Napi::Value filters; + if (obj.Get("filters").UnwrapTo(&filters) && filters.IsNumber()) { + pngargs.filters = filters.As().Uint32Value(); + } - Local palette = Nan::Get(obj, Nan::New("palette").ToLocalChecked()).ToLocalChecked(); - if (palette->IsUint8ClampedArray()) { - Local palette_ta = palette.As(); - pngargs.nPaletteColors = palette_ta->Length(); - if (pngargs.nPaletteColors % 4 != 0) { - throw "Palette length must be a multiple of 4."; - } - pngargs.nPaletteColors /= 4; - Nan::TypedArrayContents _paletteColors(palette_ta); - pngargs.palette = *_paletteColors; - // Optional background color index: - Local backgroundIndexVal = Nan::Get(obj, Nan::New("backgroundIndex").ToLocalChecked()).ToLocalChecked(); - if (backgroundIndexVal->IsUint32()) { - pngargs.backgroundIndex = static_cast(Nan::To(backgroundIndexVal).FromMaybe(0)); + 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(); + } } } } } #ifdef HAVE_JPEG -static void parseJPEGArgs(Local arg, JpegClosure& jpegargs) { +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()) { - Local obj = Nan::To(arg).ToLocalChecked(); + if (arg.IsObject()) { + Napi::Object obj = arg.As(); - Local qual = Nan::Get(obj, Nan::New("quality").ToLocalChecked()).ToLocalChecked(); - if (qual->IsNumber()) { - double quality = Nan::To(qual).FromMaybe(0); + 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); } } - Local chroma = Nan::Get(obj, Nan::New("chromaSubsampling").ToLocalChecked()).ToLocalChecked(); - if (chroma->IsBoolean()) { - bool subsample = Nan::To(chroma).FromMaybe(0); - jpegargs.chromaSubsampling = subsample ? 2 : 1; - } else if (chroma->IsNumber()) { - jpegargs.chromaSubsampling = Nan::To(chroma).FromMaybe(0); + 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(); + } } - Local progressive = Nan::Get(obj, Nan::New("progressive").ToLocalChecked()).ToLocalChecked(); - if (!progressive->IsUndefined()) { - jpegargs.progressive = Nan::To(progressive).FromMaybe(0); + Napi::Value progressive; + if (obj.Get("progressive").UnwrapTo(&progressive) && progressive.IsBoolean()) { + jpegargs.progressive = progressive.As().Value(); } } } @@ -317,29 +306,27 @@ static void parseJPEGArgs(Local arg, JpegClosure& jpegargs) { #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) -static inline void setPdfMetaStr(cairo_surface_t* surf, Local opts, - cairo_pdf_metadata_t t, const char* pName) { - auto propName = Nan::New(pName).ToLocalChecked(); - auto propValue = Nan::Get(opts, propName).ToLocalChecked(); - if (propValue->IsString()) { +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, *Nan::Utf8String(propValue)); + cairo_pdf_surface_set_metadata(surf, t, propValue.As().Utf8Value().c_str()); } } -static inline void setPdfMetaDate(cairo_surface_t* surf, Local opts, - cairo_pdf_metadata_t t, const char* pName) { - auto propName = Nan::New(pName).ToLocalChecked(); - auto propValue = Nan::Get(opts, propName).ToLocalChecked(); - if (propValue->IsDate()) { - auto date = static_cast(propValue.As()->ValueOf() / 1000); // ms -> s +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); } } -static void setPdfMetadata(Canvas* canvas, Local opts) { +static void setPdfMetadata(Canvas* canvas, Napi::Object opts) { cairo_surface_t* surf = canvas->surface(); setPdfMetaStr(surf, opts, CAIRO_PDF_METADATA_TITLE, "title"); @@ -379,146 +366,145 @@ static void setPdfMetadata(Canvas* canvas, Local opts) { ((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; - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); // Vector canvases, sync only - const std::string name = canvas->backend()->getName(); + 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(canvas->backend())->closure(); + closure = static_cast(backend())->closure(); #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) - if (info[1]->IsObject()) { // toBuffer("application/pdf", config) - setPdfMetadata(canvas, Nan::To(info[1]).ToLocalChecked()); + if (info[1].IsObject()) { // toBuffer("application/pdf", config) + setPdfMetadata(this, info[1].As()); } #endif // CAIRO 16+ } else { - closure = static_cast(canvas->backend())->closure(); + closure = static_cast(backend())->closure(); } - cairo_surface_finish(canvas->surface()); - Local buf = Nan::CopyBuffer((char*)&closure->vec[0], closure->vec.size()).ToLocalChecked(); - info.GetReturnValue().Set(buf); - return; + cairo_surface_t *surf = surface(); + cairo_surface_finish(surf); + + 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()); } // Raw ARGB data -- just a memcpy() - if (info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { - cairo_surface_t *surface = canvas->surface(); + if (info[0].StrictEquals(Napi::String::New(env, "raw"))) { + cairo_surface_t *surface = this->surface(); cairo_surface_flush(surface); - if (canvas->nBytes() > node::Buffer::kMaxLength) { - Nan::ThrowError("Data exceeds maximum buffer length."); - return; + if (nBytes() > node::Buffer::kMaxLength) { + Napi::Error::New(env, "Data exceeds maximum buffer length.").ThrowAsJavaScriptException(); + return env.Undefined(); } - const unsigned char *data = cairo_image_surface_get_data(surface); - Isolate* iso = Nan::GetCurrentContext()->GetIsolate(); - Local buf = node::Buffer::Copy(iso, reinterpret_cast(data), canvas->nBytes()).ToLocalChecked(); - info.GetReturnValue().Set(buf); - return; + return Napi::Buffer::Copy(env, cairo_image_surface_get_data(surface), nBytes()); } // Sync PNG, default - if (info[0]->IsUndefined() || info[0]->StrictEquals(Nan::New("image/png").ToLocalChecked())) { + if (info[0].IsUndefined() || info[0].StrictEquals(Napi::String::New(env, "image/png"))) { try { - PngClosure closure(canvas); + PngClosure closure(this); parsePNGArgs(info[1], closure); if (closure.nPaletteColors == 0xFFFFFFFF) { - Nan::ThrowError("Palette length must be a multiple of 4."); - return; + Napi::Error::New(env, "Palette length must be a multiple of 4.").ThrowAsJavaScriptException(); + return env.Undefined(); } - Nan::TryCatch try_catch; - status = canvas_write_to_png_stream(canvas->surface(), PngClosure::writeVec, &closure); + status = canvas_write_to_png_stream(surface(), PngClosure::writeVec, &closure); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } else if (status) { - throw status; - } else { - // TODO it's possible to avoid this copy - Local buf = Nan::CopyBuffer((char *)&closure.vec[0], closure.vec.size()).ToLocalChecked(); - info.GetReturnValue().Set(buf); + 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) { - Nan::ThrowError(Canvas::Error(ex)); + CairoError(ex).ThrowAsJavaScriptException(); } catch (const char* ex) { - Nan::ThrowError(ex); + Napi::Error::New(env, ex).ThrowAsJavaScriptException(); } - return; + + return env.Undefined(); } // Async PNG - if (info[0]->IsFunction() && - (info[1]->IsUndefined() || info[1]->StrictEquals(Nan::New("image/png").ToLocalChecked()))) { + if (info[0].IsFunction() && + (info[1].IsUndefined() || info[1].StrictEquals(Napi::String::New(env, "image/png")))) { PngClosure* closure; try { - closure = new PngClosure(canvas); + closure = new PngClosure(this); parsePNGArgs(info[2], *closure); } catch (cairo_status_t ex) { - Nan::ThrowError(Canvas::Error(ex)); - return; + CairoError(ex).ThrowAsJavaScriptException(); + return env.Undefined(); } catch (const char* ex) { - Nan::ThrowError(ex); - return; + Napi::Error::New(env, ex).ThrowAsJavaScriptException(); + return env.Undefined(); } - canvas->Ref(); - closure->cb.Reset(info[0].As()); + Ref(); + closure->cb = Napi::Persistent(info[0].As()); - uv_work_t* req = new uv_work_t; - req->data = closure; // Make sure the surface exists since we won't have an isolate context in the async block: - canvas->surface(); - uv_queue_work(uv_default_loop(), req, ToPngBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); + surface(); + EncodingWorker* worker = new EncodingWorker(env); + worker->Init(&ToPngBufferAsync, closure); + worker->Queue(); - return; + return env.Undefined(); } #ifdef HAVE_JPEG // Sync JPEG - Local jpegStr = Nan::New("image/jpeg").ToLocalChecked(); - if (info[0]->StrictEquals(jpegStr)) { + Napi::Value jpegStr = Napi::String::New(env, "image/jpeg"); + if (info[0].StrictEquals(jpegStr)) { try { - JpegClosure closure(canvas); + JpegClosure closure(this); parseJPEGArgs(info[1], closure); - Nan::TryCatch try_catch; - write_to_jpeg_buffer(canvas->surface(), &closure); + write_to_jpeg_buffer(surface(), &closure); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } else { + if (!env.IsExceptionPending()) { // TODO it's possible to avoid this copy. - Local buf = Nan::CopyBuffer((char *)&closure.vec[0], closure.vec.size()).ToLocalChecked(); - info.GetReturnValue().Set(buf); + return Napi::Buffer::Copy(env, &closure.vec[0], closure.vec.size()); } } catch (cairo_status_t ex) { - Nan::ThrowError(Canvas::Error(ex)); + CairoError(ex).ThrowAsJavaScriptException(); + return env.Undefined(); } - return; + return env.Undefined(); } // Async JPEG - if (info[0]->IsFunction() && info[1]->StrictEquals(jpegStr)) { - JpegClosure* closure = new JpegClosure(canvas); + if (info[0].IsFunction() && info[1].StrictEquals(jpegStr)) { + JpegClosure* closure = new JpegClosure(this); parseJPEGArgs(info[2], *closure); - canvas->Ref(); - closure->cb.Reset(info[0].As()); + Ref(); + closure->cb = Napi::Persistent(info[0].As()); - uv_work_t* req = new uv_work_t; - req->data = closure; // Make sure the surface exists since we won't have an isolate context in the async block: - canvas->surface(); - uv_queue_work(uv_default_loop(), req, ToJpegBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter); - - return; + surface(); + EncodingWorker* worker = new EncodingWorker(env); + worker->Init(&ToJpegBufferAsync, closure); + worker->Queue(); + return env.Undefined(); } #endif + + return env.Undefined(); } /* @@ -527,15 +513,12 @@ NAN_METHOD(Canvas::ToBuffer) { static cairo_status_t streamPNG(void *c, const uint8_t *data, unsigned len) { - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:StreamPNG"); PngClosure* closure = (PngClosure*) c; - Local buf = Nan::CopyBuffer((char *)data, len).ToLocalChecked(); - Local argv[3] = { - Nan::Null() - , buf - , Nan::New(len) }; - closure->cb.Call(sizeof argv / sizeof *argv, argv, &async); + 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; } @@ -544,69 +527,51 @@ streamPNG(void *c, const uint8_t *data, unsigned len) { * StreamPngSync(this, options: {palette?: Uint8ClampedArray, backgroundIndex?: uint32, compressionLevel: uint32, filters: uint32}) */ -NAN_METHOD(Canvas::StreamPNGSync) { - if (!info[0]->IsFunction()) - return Nan::ThrowTypeError("callback function required"); - - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); +void +Canvas::StreamPNGSync(const Napi::CallbackInfo& info) { + if (!info[0].IsFunction()) { + Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException(); + return; + } - PngClosure closure(canvas); + PngClosure closure(this); parsePNGArgs(info[1], closure); - closure.cb.Reset(Local::Cast(info[0])); + closure.cb = Napi::Persistent(info[0].As()); - Nan::TryCatch try_catch; + cairo_status_t status = canvas_write_to_png_stream(surface(), streamPNG, &closure); - cairo_status_t status = canvas_write_to_png_stream(canvas->surface(), streamPNG, &closure); - - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - return; - } else if (status) { - Local argv[1] = { Canvas::Error(status) }; - Nan::Call(closure.cb, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, argv); - } else { - Local argv[3] = { - Nan::Null() - , Nan::Null() - , Nan::New(0) }; - Nan::Call(closure.cb, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, 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; } struct PdfStreamInfo { - Local fn; + Napi::Function fn; uint32_t len; uint8_t* data; }; - -/* - * Canvas::StreamPDF FreeCallback - */ - -void stream_pdf_free(char *, void *) {} - /* * Canvas::StreamPDF callback. */ static cairo_status_t streamPDF(void *c, const uint8_t *data, unsigned len) { - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:StreamPDF"); 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. - Local buf = Nan::NewBuffer(const_cast(reinterpret_cast(data)), len, stream_pdf_free, 0).ToLocalChecked(); - Local argv[3] = { - Nan::Null() - , buf - , Nan::New(len) }; - async.runInAsyncScope(Nan::GetCurrentContext()->Global(), streaminfo->fn, sizeof argv / sizeof *argv, argv); + 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; } @@ -630,45 +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->backend()->getName() != "pdf") - return Nan::ThrowTypeError("wrong canvas type"); + if (backend()->getName() != "pdf") { + Napi::TypeError::New(env, "wrong canvas type").ThrowAsJavaScriptException(); + return; + } #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) - if (info[1]->IsObject()) { - setPdfMetadata(canvas, Nan::To(info[1]).ToLocalChecked()); + if (info[1].IsObject()) { + setPdfMetadata(this, info[1].As()); } #endif - cairo_surface_finish(canvas->surface()); + cairo_surface_finish(surface()); - PdfSvgClosure* closure = static_cast(canvas->backend())->closure(); - Local fn = info[0].As(); + 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(); - Nan::TryCatch try_catch; + cairo_status_t status = canvas_write_to_pdf_stream(surface(), streamPDF, &streaminfo); - cairo_status_t status = canvas_write_to_pdf_stream(canvas->surface(), streamPDF, &streaminfo); - - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } else if (status) { - Local error = Canvas::Error(status); - Nan::Call(fn, Nan::GetCurrentContext()->Global(), 1, &error); - } else { - Local argv[3] = { - Nan::Null() - , Nan::Null() - , Nan::New(0) }; - Nan::Call(fn, Nan::GetCurrentContext()->Global(), sizeof argv / sizeof *argv, 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) }); + } } } @@ -683,60 +644,65 @@ static uint32_t getSafeBufSize(Canvas* canvas) { return (std::min)(canvas->getWidth() * canvas->getHeight() * 4, static_cast(PAGE_SIZE)); } -NAN_METHOD(Canvas::StreamJPEGSync) { - if (!info[1]->IsFunction()) - return Nan::ThrowTypeError("callback function required"); +void +Canvas::StreamJPEGSync(const Napi::CallbackInfo& info) { + if (!info[1].IsFunction()) { + Napi::TypeError::New(env, "callback function required").ThrowAsJavaScriptException(); + return; + } - Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); - JpegClosure closure(canvas); + JpegClosure closure(this); parseJPEGArgs(info[0], closure); - closure.cb.Reset(Local::Cast(info[1])); + closure.cb = Napi::Persistent(info[1].As()); - Nan::TryCatch try_catch; - uint32_t bufsize = getSafeBufSize(canvas); - write_to_jpeg_stream(canvas->surface(), bufsize, &closure); - - 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(*Nan::Utf8String(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; } - Nan::Utf8String 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 = Nan::To(info[1]).ToLocalChecked(); - 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(Nan::Get(js_user_desc, family_prop).ToLocalChecked(), NULL, false); - char *weight = str_value(Nan::Get(js_user_desc, weight_prop).ToLocalChecked(), "normal", true); - char *style = str_value(Nan::Get(js_user_desc, style_prop).ToLocalChecked(), "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)); @@ -750,56 +716,79 @@ NAN_METHOD(Canvas::RegisterFont) { 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)) { + } else if (register_font((unsigned char *) filePath.c_str())) { FontFace face; face.user_desc = user_desc; face.sys_desc = sys_desc; - strncpy((char *)face.file_path, (char *) *filePath, 1023); + 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++; } -NAN_METHOD(Canvas::DeregisterAllFonts) { +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(); - if (!success) Nan::ThrowError("Could not deregister one or more fonts"); + fontSerial++; + if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException(); } /* - * Initialize 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(Backend* backend) : ObjectWrap() { - _backend = backend; -} + if (info.Length() != 1) return env.Undefined(); -/* - * Destroy cairo surface. - */ + 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); + } -Canvas::~Canvas() { - if (_backend != NULL) { - delete _backend; - } + 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))); + + return obj; } /* @@ -879,18 +868,21 @@ Canvas::ResolveFontDescription(const PangoFontDescription *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 (renamed_families.size()) renamed_families += ','; - renamed_families += sys_desc_family_name; - } - if (first && (best.user_desc == nullptr || pango_font_description_better_match(desc, best.user_desc, ff.user_desc))) { - best = ff; + 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; } } @@ -910,21 +902,20 @@ Canvas::ResolveFontDescription(const PangoFontDescription *desc) { */ void -Canvas::resurface(Local canvas) { - Nan::HandleScope scope; - Local context; - - backend()->recreateSurface(); - - // Reset context - context = Nan::Get(canvas, Nan::New("context").ToLocalChecked()).ToLocalChecked(); - if (!context->IsUndefined()) { - Context2d *context2d = ObjectWrap::Unwrap(Nan::To(context).ToLocalChecked()); - cairo_t *prev = context2d->context(); - context2d->setContext(createCairoContext()); - context2d->resetState(); - cairo_destroy(prev); - } +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); + } } /** @@ -942,7 +933,7 @@ Canvas::createCairoContext() { * 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 f356af035..7d0bc9d6d 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -2,24 +2,17 @@ #pragma once +struct Closure; + #include "backend/Backend.h" +#include "closure.h" #include #include "dll_visibility.h" -#include +#include #include -#include #include #include -/* - * Maxmimum states per context. - * TODO: remove/resize - */ - -#ifndef CANVAS_MAX_STATES -#define CANVAS_MAX_STATES 64 -#endif - /* * FontFace describes a font file in terms of one PangoFontDescription that * will resolve to it and one that the user describes it as (like @font-face) @@ -31,52 +24,78 @@ class FontFace { 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: - 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 NAN_METHOD(DeregisterAllFonts); - static v8::Local Error(cairo_status_t status); - static void ToPngBufferAsync(uv_work_t *req); - static void ToJpegBufferAsync(uv_work_t *req); - static void ToBufferAsyncAfter(uv_work_t *req); + 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); DLL_PUBLIC inline Backend* backend() { return _backend; } - DLL_PUBLIC inline cairo_surface_t* surface(){ return backend()->getSurface(); } + 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(getHeight()) * stride(); + return static_cast(backend()->getHeight()) * stride(); } DLL_PUBLIC inline int getWidth() { return backend()->getWidth(); } DLL_PUBLIC inline int getHeight() { return backend()->getHeight(); } - Canvas(Backend* backend); - void resurface(v8::Local canvas); + void resurface(Napi::Object This); + + Napi::Env env; + static int fontSerial; private: - ~Canvas(); Backend* _backend; + Napi::ObjectReference _jsBackend; + Napi::FunctionReference ctor; + static std::vector font_face_list; }; diff --git a/src/CanvasError.h b/src/CanvasError.h index cb751e312..535d153fa 100644 --- a/src/CanvasError.h +++ b/src/CanvasError.h @@ -1,6 +1,7 @@ #pragma once #include +#include class CanvasError { public: @@ -20,4 +21,17 @@ class CanvasError { 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 280fc2e8c..ceb0e5054 100644 --- a/src/CanvasGradient.cc +++ b/src/CanvasGradient.cc @@ -1,123 +1,113 @@ // Copyright (c) 2010 LearnBoost #include "CanvasGradient.h" +#include "InstanceData.h" #include "Canvas.h" #include "color.h" -using namespace v8; - -Nan::Persistent Gradient::constructor; +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); - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, - Nan::New("CanvasGradient").ToLocalChecked(), - ctor->GetFunction(ctx).ToLocalChecked()); +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( - Nan::To(info[0]).FromMaybe(0) - , Nan::To(info[1]).FromMaybe(0) - , Nan::To(info[2]).FromMaybe(0) - , Nan::To(info[3]).FromMaybe(0)); - 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( - Nan::To(info[0]).FromMaybe(0) - , Nan::To(info[1]).FromMaybe(0) - , Nan::To(info[2]).FromMaybe(0) - , Nan::To(info[3]).FromMaybe(0) - , Nan::To(info[4]).FromMaybe(0) - , Nan::To(info[5]).FromMaybe(0)); - 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; - Nan::Utf8String 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() - , Nan::To(info[0]).FromMaybe(0) + _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 b6902c428..103e80748 100644 --- a/src/CanvasGradient.h +++ b/src/CanvasGradient.h @@ -2,21 +2,19 @@ #pragma once -#include -#include +#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; }; diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index fa3848b37..ec30b6f09 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -4,122 +4,115 @@ #include "Canvas.h" #include "Image.h" +#include "InstanceData.h" -using namespace v8; +using namespace Napi; const cairo_user_data_key_t *pattern_repeat_key; -Nan::Persistent Pattern::constructor; -Nan::Persistent Pattern::_DOMMatrix; - /* * 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()); - Nan::SetPrototypeMethod(ctor, "setTransform", SetTransform); + Napi::Function ctor = DefineClass(env, "CanvasPattern", { + InstanceMethod<&Pattern::setTransform>("setTransform", napi_default_method) + }); // Prototype - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, Nan::New("CanvasPattern").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); - Nan::Set(target, Nan::New("CanvasPatternInit").ToLocalChecked(), Nan::New(SaveExternalModules)); -} - -/* - * Save some external modules as private references. - */ - -NAN_METHOD(Pattern::SaveExternalModules) { - _DOMMatrix.Reset(Nan::To(info[0]).ToLocalChecked()); + 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 = Nan::To(info[0]).ToLocalChecked(); - // 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; } - repeat_type_t repeat = REPEAT; - if (0 == strcmp("no-repeat", *Nan::Utf8String(info[1]))) { - repeat = NO_REPEAT; - } else if (0 == strcmp("repeat-x", *Nan::Utf8String(info[1]))) { - repeat = REPEAT_X; - } else if (0 == strcmp("repeat-y", *Nan::Utf8String(info[1]))) { - repeat = REPEAT_Y; + _pattern = cairo_pattern_create_for_surface(surface); + + 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; + } } - Pattern *pattern = new Pattern(surface, repeat); - pattern->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + + cairo_pattern_set_user_data(_pattern, pattern_repeat_key, &_repeat, NULL); } /* * Set the pattern-space to user-space transform. */ -NAN_METHOD(Pattern::SetTransform) { - Pattern *pattern = Nan::ObjectWrap::Unwrap(info.This()); - Local ctx = Nan::GetCurrentContext(); - Local mat = Nan::To(info[0]).ToLocalChecked(); - -#if NODE_MAJOR_VERSION >= 8 - if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { - return Nan::ThrowTypeError("Expected DOMMatrix"); +void +Pattern::setTransform(const Napi::CallbackInfo& info) { + if (!info[0].IsObject()) { + Napi::TypeError::New(env, "Expected DOMMatrix").ThrowAsJavaScriptException(); + return; } -#endif + + 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, - Nan::To(Nan::Get(mat, Nan::New("a").ToLocalChecked()).ToLocalChecked()).FromMaybe(1), - Nan::To(Nan::Get(mat, Nan::New("b").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("c").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("d").ToLocalChecked()).ToLocalChecked()).FromMaybe(1), - Nan::To(Nan::Get(mat, Nan::New("e").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("f").ToLocalChecked()).ToLocalChecked()).FromMaybe(0) + 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->_pattern, &matrix); -} - - -/* - * Initialize pattern. - */ - -Pattern::Pattern(cairo_surface_t *surface, repeat_type_t repeat) { - _pattern = cairo_pattern_create_for_surface(surface); - _repeat = repeat; - cairo_pattern_set_user_data(_pattern, pattern_repeat_key, &_repeat, NULL); + cairo_pattern_set_matrix(_pattern, &matrix); } repeat_type_t Pattern::get_repeat_type_for_cairo_pattern(cairo_pattern_t *pattern) { @@ -132,5 +125,5 @@ repeat_type_t Pattern::get_repeat_type_for_cairo_pattern(cairo_pattern_t *patter */ Pattern::~Pattern() { - cairo_pattern_destroy(_pattern); + if (_pattern) cairo_pattern_destroy(_pattern); } diff --git a/src/CanvasPattern.h b/src/CanvasPattern.h index 29e2171b6..1f768e03b 100644 --- a/src/CanvasPattern.h +++ b/src/CanvasPattern.h @@ -3,8 +3,7 @@ #pragma once #include -#include -#include +#include /* * Canvas types. @@ -19,19 +18,16 @@ typedef enum { 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 Nan::Persistent _DOMMatrix; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_METHOD(SaveExternalModules); - static NAN_METHOD(SetTransform); + 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); - Pattern(cairo_surface_t *surface, repeat_type_t repeat); inline cairo_pattern_t *pattern(){ return _pattern; } - private: ~Pattern(); + Napi::Env env; + private: cairo_pattern_t *_pattern; - repeat_type_t _repeat; + repeat_type_t _repeat = REPEAT; }; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index f3415a7ed..3f52c1fdd 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -8,6 +8,8 @@ #include "Canvas.h" #include "CanvasGradient.h" #include "CanvasPattern.h" +#include "InstanceData.h" +#include "FontParser.h" #include #include #include "Image.h" @@ -19,10 +21,6 @@ #include "Util.h" #include -using namespace v8; - -Nan::Persistent Context2d::constructor; - /* * Rectangle arg assertions. */ @@ -36,18 +34,7 @@ Nan::Persistent Context2d::constructor; 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. @@ -56,14 +43,31 @@ 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 Nan::FunctionCallbackInfo &info, double *args, int argsNum, int offset = 0){ - int argsEnd = offset + argsNum; +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++) { - double val = Nan::To(info[i]).FromMaybe(0); + 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)) { @@ -81,103 +85,158 @@ inline static bool checkArgs(const Nan::FunctionCallbackInfo &info, doubl return areArgsValid; } -Nan::Persistent Context2d::_DOMMatrix; -Nan::Persistent Context2d::_parseFont; - /* * 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, "createImageData", CreateImageData); - 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, "getTransform", GetTransform); - Nan::SetPrototypeMethod(ctor, "resetTransform", ResetTransform); - Nan::SetPrototypeMethod(ctor, "setTransform", SetTransform); - 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, "ellipse", Ellipse); - Nan::SetPrototypeMethod(ctor, "setLineDash", SetLineDash); - Nan::SetPrototypeMethod(ctor, "getLineDash", GetLineDash); - Nan::SetPrototypeMethod(ctor, "createPattern", CreatePattern); - Nan::SetPrototypeMethod(ctor, "createLinearGradient", CreateLinearGradient); - Nan::SetPrototypeMethod(ctor, "createRadialGradient", CreateRadialGradient); - SetProtoAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat, NULL, ctor); - SetProtoAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality, ctor); - SetProtoAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled, ctor); - SetProtoAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation, ctor); - SetProtoAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha, ctor); - SetProtoAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor, ctor); - SetProtoAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit, ctor); - SetProtoAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth, ctor); - SetProtoAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap, ctor); - SetProtoAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin, ctor); - SetProtoAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset, ctor); - SetProtoAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX, ctor); - SetProtoAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY, ctor); - SetProtoAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur, ctor); - SetProtoAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias, ctor); - SetProtoAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode, ctor); - SetProtoAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality, ctor); - SetProtoAccessor(proto, Nan::New("currentTransform").ToLocalChecked(), GetCurrentTransform, SetCurrentTransform, ctor); - SetProtoAccessor(proto, Nan::New("fillStyle").ToLocalChecked(), GetFillStyle, SetFillStyle, ctor); - SetProtoAccessor(proto, Nan::New("strokeStyle").ToLocalChecked(), GetStrokeStyle, SetStrokeStyle, ctor); - SetProtoAccessor(proto, Nan::New("font").ToLocalChecked(), GetFont, SetFont, ctor); - SetProtoAccessor(proto, Nan::New("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline, ctor); - SetProtoAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign, ctor); - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, Nan::New("CanvasRenderingContext2d").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); - Nan::Set(target, Nan::New("CanvasRenderingContext2dInit").ToLocalChecked(), Nan::New(SaveExternalModules)); +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 = canvas->createCairoContext(); +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); - state = states[stateno = 0] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); - resetState(true); + // 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); } /* @@ -185,12 +244,8 @@ 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(); } @@ -198,41 +253,16 @@ Context2d::~Context2d() { * Reset canvas state. */ -void Context2d::resetState(bool init) { - if (!init) { - pango_font_description_free(state->fontDescription); - } - - state->shadowBlur = 0; - state->shadowOffsetX = state->shadowOffsetY = 0; - state->globalAlpha = 1; - state->textAlignment = -1; - state->fillPattern = nullptr; - state->strokePattern = nullptr; - state->fillGradient = nullptr; - state->strokeGradient = nullptr; - 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->imageSmoothingEnabled = true; - state->textDrawingMode = TEXT_DRAW_PATHS; - state->fontDescription = pango_font_description_from_string("sans"); - pango_font_description_set_absolute_size(state->fontDescription, 10 * PANGO_SCALE); +void Context2d::resetState() { + states.pop(); + states.emplace(); pango_layout_set_font_description(_layout, state->fontDescription); - _resetPersistentHandles(); } void Context2d::_resetPersistentHandles() { _fillStyle.Reset(); _strokeStyle.Reset(); - _font.Reset(); - _textBaseline.Reset(); - _textAlign.Reset(); } /* @@ -241,13 +271,9 @@ void Context2d::_resetPersistentHandles() { 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(); } /* @@ -256,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); } } @@ -315,7 +339,6 @@ create_transparent_gradient(cairo_pattern_t *source, float alpha) { cairo_pattern_get_radial_circles(source, &x0, &y0, &r0, &x1, &y1, &r1); newGradient = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); } else { - Nan::ThrowError("Unexpected gradient type"); return NULL; } for ( i = 0; i < count; i++ ) { @@ -337,7 +360,6 @@ create_transparent_pattern(cairo_pattern_t *source, float alpha) { height); cairo_t *mask_context = cairo_create(mask_surface); if (cairo_status(mask_context) != CAIRO_STATUS_SUCCESS) { - Nan::ThrowError("Failed to initialize context"); return NULL; } cairo_set_source(mask_context, source); @@ -353,11 +375,11 @@ create_transparent_pattern(cairo_pattern_t *source, float alpha) { */ void -Context2d::setFillRule(v8::Local value) { +Context2d::setFillRule(Napi::Value value) { cairo_fill_rule_t rule = CAIRO_FILL_RULE_WINDING; - if (value->IsString()) { - Nan::Utf8String 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; } } @@ -367,11 +389,13 @@ Context2d::setFillRule(v8::Local value) { void Context2d::fill(bool preserve) { cairo_pattern_t *new_pattern; + bool needsRestore = false; if (state->fillPattern) { if (state->globalAlpha < 1) { new_pattern = create_transparent_pattern(state->fillPattern, state->globalAlpha); if (new_pattern == NULL) { - // failed to allocate; Nan::ThrowError has already been called, so return from this fn. + Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException(); + // failed to allocate return; } cairo_set_source(_context, new_pattern); @@ -381,16 +405,43 @@ Context2d::fill(bool preserve) { cairo_set_source(_context, state->fillPattern); } repeat_type_t repeat = Pattern::get_repeat_type_for_cairo_pattern(state->fillPattern); - if (NO_REPEAT == repeat) { + 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) { if (state->globalAlpha < 1) { new_pattern = create_transparent_gradient(state->fillGradient, state->globalAlpha); if (new_pattern == NULL) { - // failed to recognize gradient; Nan::ThrowError has already been called, so return from this fn. + Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException(); + // failed to recognize gradient return; } cairo_pattern_set_filter(new_pattern, state->patternQuality); @@ -412,6 +463,9 @@ Context2d::fill(bool preserve) { ? shadow(cairo_fill) : cairo_fill(_context); } + if (needsRestore) { + cairo_restore(_context); + } } /* @@ -425,7 +479,8 @@ Context2d::stroke(bool preserve) { if (state->globalAlpha < 1) { new_pattern = create_transparent_pattern(state->strokePattern, state->globalAlpha); if (new_pattern == NULL) { - // failed to allocate; Nan::ThrowError has already been called, so return from this fn. + Napi::Error::New(env, "Failed to initialize context").ThrowAsJavaScriptException(); + // failed to allocate return; } cairo_set_source(_context, new_pattern); @@ -444,7 +499,8 @@ Context2d::stroke(bool preserve) { if (state->globalAlpha < 1) { new_pattern = create_transparent_gradient(state->strokeGradient, state->globalAlpha); if (new_pattern == NULL) { - // failed to recognize gradient; Nan::ThrowError has already been called, so return from this fn. + Napi::Error::New(env, "Unexpected gradient type").ThrowAsJavaScriptException(); + // failed to recognize gradient return; } cairo_pattern_set_filter(new_pattern, state->patternQuality); @@ -609,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)); @@ -629,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;yIsObject()) - return Nan::ThrowTypeError("Canvas expected"); - Local obj = Nan::To(info[0]).ToLocalChecked(); - if (!Nan::New(Canvas::constructor)->HasInstance(obj)) - return Nan::ThrowTypeError("Canvas expected"); - Canvas *canvas = Nan::ObjectWrap::Unwrap(obj); - - bool isImageBackend = canvas->backend()->getName() == "image"; - if (isImageBackend) { - cairo_format_t format = ImageBackend::DEFAULT_FORMAT; - if (info[1]->IsObject()) { - Local ctxAttributes = Nan::To(info[1]).ToLocalChecked(); - - Local pixelFormat = Nan::Get(ctxAttributes, Nan::New("pixelFormat").ToLocalChecked()).ToLocalChecked(); - if (pixelFormat->IsString()) { - Nan::Utf8String utf8PixelFormat(pixelFormat); - if (!strcmp(*utf8PixelFormat, "RGBA32")) format = CAIRO_FORMAT_ARGB32; - else if (!strcmp(*utf8PixelFormat, "RGB24")) format = CAIRO_FORMAT_RGB24; - else if (!strcmp(*utf8PixelFormat, "A8")) format = CAIRO_FORMAT_A8; - else if (!strcmp(*utf8PixelFormat, "RGB16_565")) format = CAIRO_FORMAT_RGB16_565; - else if (!strcmp(*utf8PixelFormat, "A1")) format = CAIRO_FORMAT_A1; -#ifdef CAIRO_FORMAT_RGB30 - else if (!strcmp(utf8PixelFormat, "RGB30")) format = CAIRO_FORMAT_RGB30; -#endif - } - - // alpha: false forces use of RGB24 - Local alpha = Nan::Get(ctxAttributes, Nan::New("alpha").ToLocalChecked()).ToLocalChecked(); - if (alpha->IsBoolean() && !Nan::To(alpha).FromMaybe(false)) { - format = CAIRO_FORMAT_RGB24; - } - } - static_cast(canvas->backend())->setFormat(format); - } - - Context2d *context = new Context2d(canvas); - - context->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); -} - -/* - * Save some external modules as private references. - */ - -NAN_METHOD(Context2d::SaveExternalModules) { - _DOMMatrix.Reset(Nan::To(info[0]).ToLocalChecked()); - _parseFont.Reset(Nan::To(info[1]).ToLocalChecked()); -} - /* * Get format (string). */ -NAN_GETTER(Context2d::GetFormat) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetFormat(const Napi::CallbackInfo& info) { std::string pixelFormatString; - switch (context->canvas()->backend()->getFormat()) { + switch (canvas()->backend()->getFormat()) { case CAIRO_FORMAT_ARGB32: pixelFormatString = "RGBA32"; break; case CAIRO_FORMAT_RGB24: pixelFormatString = "RGB24"; break; case CAIRO_FORMAT_A8: pixelFormatString = "A8"; break; @@ -738,27 +742,68 @@ NAN_GETTER(Context2d::GetFormat) { #ifdef CAIRO_FORMAT_RGB30 case CAIRO_FORMAT_RGB30: pixelFormatString = "RGB30"; break; #endif - default: return info.GetReturnValue().SetNull(); + default: return env.Null(); } - info.GetReturnValue().Set(Nan::New(pixelFormatString).ToLocalChecked()); + return Napi::String::New(env, pixelFormatString); } /* * Create a new page. */ -NAN_METHOD(Context2d::AddPage) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (context->canvas()->backend()->getName() != "pdf") { - return Nan::ThrowError("only PDF canvases support .addPage()"); +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()); - int width = Nan::To(info[0]).FromMaybe(0); - int height = Nan::To(info[1]).FromMaybe(0); - if (width < 1) width = context->canvas()->getWidth(); - if (height < 1) height = context->canvas()->getHeight(); - cairo_pdf_surface_set_size(context->canvas()->surface(), width, height); - 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; } /* @@ -769,29 +814,37 @@ NAN_METHOD(Context2d::AddPage) { * */ -NAN_METHOD(Context2d::PutImageData) { - if (!info[0]->IsObject()) - return Nan::ThrowTypeError("ImageData expected"); - Local obj = Nan::To(info[0]).ToLocalChecked(); - 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 dstStride = context->canvas()->stride(); - int Bpp = dstStride / context->canvas()->getWidth(); + int dstStride = canvas()->stride(); + int Bpp = dstStride / canvas()->getWidth(); int srcStride = Bpp * imageData->width(); int sx = 0 , sy = 0 , sw = 0 , sh = 0 - , dx = Nan::To(info[1]).FromMaybe(0) - , dy = Nan::To(info[2]).FromMaybe(0) + , dx = info[1].ToNumber().UnwrapOr(zero).Int32Value() + , dy = info[2].ToNumber().UnwrapOr(zero).Int32Value() , rows , cols; @@ -803,10 +856,10 @@ NAN_METHOD(Context2d::PutImageData) { break; // imageData, dx, dy, sx, sy, sw, sh case 7: - sx = Nan::To(info[3]).FromMaybe(0); - sy = Nan::To(info[4]).FromMaybe(0); - sw = Nan::To(info[5]).FromMaybe(0); - sh = Nan::To(info[6]).FromMaybe(0); + 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; @@ -821,7 +874,8 @@ NAN_METHOD(Context2d::PutImageData) { dy += sy; break; default: - return Nan::ThrowError("invalid arguments"); + Napi::Error::New(env, "invalid arguments").ThrowAsJavaScriptException(); + return; } // chop off outlying source data @@ -830,12 +884,12 @@ NAN_METHOD(Context2d::PutImageData) { // 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()->getWidth() - dx); - rows = (std::min)(sh, context->canvas()->getHeight() - dy); + cols = (std::min)(sw, canvas()->getWidth() - dx); + rows = (std::min)(sh, canvas()->getHeight() - dy); if (cols <= 0 || rows <= 0) return; - switch (context->canvas()->backend()->getFormat()) { + switch (canvas()->backend()->getFormat()) { case CAIRO_FORMAT_ARGB32: { src += sy * srcStride + sx * 4; dst += dstStride * dy + 4 * dx; @@ -916,7 +970,8 @@ NAN_METHOD(Context2d::PutImageData) { } case CAIRO_FORMAT_A1: { // TODO Should this be totally packed, or maintain a stride divisible by 4? - Nan::ThrowError("putImageData for CANVAS_FORMAT_A1 is not yet implemented"); + Napi::Error::New(env, "putImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException(); + break; } case CAIRO_FORMAT_RGB16_565: { @@ -932,18 +987,19 @@ NAN_METHOD(Context2d::PutImageData) { #ifdef CAIRO_FORMAT_RGB30 case CAIRO_FORMAT_RGB30: { // TODO - Nan::ThrowError("putImageData for CANVAS_FORMAT_RGB30 is not yet implemented"); + Napi::Error::New(env, "putImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException(); + break; } #endif default: { - Nan::ThrowError("Invalid pixel format"); - break; + 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 @@ -957,27 +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 = Nan::To(info[0]).FromMaybe(0); - int sy = Nan::To(info[1]).FromMaybe(0); - int sw = Nan::To(info[2]).FromMaybe(0); - int sh = Nan::To(info[3]).FromMaybe(0); + 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) - return Nan::ThrowError("IndexSizeError: The source width is 0."); - if (!sh) - return Nan::ThrowError("IndexSizeError: The source height is 0."); + 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 width = canvas->getWidth(); int height = canvas->getHeight(); - if (!width) - return Nan::ThrowTypeError("Canvas width is 0"); - if (!height) - return Nan::ThrowTypeError("Canvas 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: @@ -990,21 +1055,26 @@ NAN_METHOD(Context2d::GetImageData) { sh = -sh; } - if (sx + sw > width) sw = width - sx; - if (sy + sh > height) sh = 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; } @@ -1015,25 +1085,27 @@ NAN_METHOD(Context2d::GetImageData) { uint8_t *src = canvas->data(); - Local buffer = ArrayBuffer::New(Isolate::GetCurrent(), size); - Local dataArray; + Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, size); + Napi::TypedArray dataArray; if (canvas->backend()->getFormat() == CAIRO_FORMAT_RGB16_565) { - dataArray = Uint16Array::New(buffer, 0, size >> 1); + dataArray = Napi::Uint16Array::New(env, size >> 1, buffer, 0); } else { - dataArray = Uint8ClampedArray::New(buffer, 0, size); + dataArray = Napi::Uint8Array::New(env, size, buffer, 0, napi_uint8_clamped_array); } - Nan::TypedArrayContents typedArrayContents(dataArray); - 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 < sh; ++y) { + 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; @@ -1062,10 +1134,11 @@ NAN_METHOD(Context2d::GetImageData) { 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 < sh; ++y) { + 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 r = *pixel >> 16; @@ -1082,22 +1155,25 @@ NAN_METHOD(Context2d::GetImageData) { break; } case CAIRO_FORMAT_A8: { - for (int y = 0; y < sh; ++y) { + dst += oy * dstStride + ox; + for (int y = 0; y < ch; ++y) { uint8_t *row = (uint8_t *)(src + srcStride * (y + sy)); - memcpy(dst, row + sx, dstStride); + 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? - Nan::ThrowError("getImageData for CANVAS_FORMAT_A1 is not yet implemented"); + Napi::Error::New(env, "getImageData for CANVAS_FORMAT_A1 is not yet implemented").ThrowAsJavaScriptException(); + break; } case CAIRO_FORMAT_RGB16_565: { - for (int y = 0; y < sh; ++y) { + 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, dstStride); + memcpy(dst, row + sx, cw * 2); dst += dstStride; } break; @@ -1105,26 +1181,25 @@ NAN_METHOD(Context2d::GetImageData) { #ifdef CAIRO_FORMAT_RGB30 case CAIRO_FORMAT_RGB30: { // TODO - Nan::ThrowError("getImageData for CANVAS_FORMAT_RGB30 is not yet implemented"); + Napi::Error::New(env, "getImageData for CANVAS_FORMAT_RGB30 is not yet implemented").ThrowAsJavaScriptException(); + break; } #endif default: { // Unlikely - Nan::ThrowError("Invalid pixel format"); - break; + Napi::Error::New(env, "Invalid pixel format or not an image canvas").ThrowAsJavaScriptException(); + return env.Null(); } } - const int argc = 3; - Local swHandle = Nan::New(sw); - Local shHandle = Nan::New(sh); - Local argv[argc] = { dataArray, swHandle, shHandle }; +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 }); - Local ctor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); - - info.GetReturnValue().Set(instance); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); } /** @@ -1132,40 +1207,37 @@ NAN_METHOD(Context2d::GetImageData) { * `ImageData` instance for dimensions. */ -NAN_METHOD(Context2d::CreateImageData){ - Isolate *iso = Isolate::GetCurrent(); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Canvas *canvas = context->canvas(); +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()) { - Local obj = Nan::To(info[0]).ToLocalChecked(); - width = Nan::To(Nan::Get(obj, Nan::New("width").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); - height = Nan::To(Nan::Get(obj, Nan::New("height").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); + 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 = Nan::To(info[0]).FromMaybe(0); - height = Nan::To(info[1]).FromMaybe(0); + width = info[0].ToNumber().UnwrapOr(zero).Int32Value(); + height = info[1].ToNumber().UnwrapOr(zero).Int32Value(); } int stride = canvas->stride(); double Bpp = static_cast(stride) / canvas->getWidth(); int nBytes = static_cast(Bpp * width * height + .5); - Local ab = ArrayBuffer::New(iso, nBytes); - Local arr; + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, nBytes); + Napi::Value arr; if (canvas->backend()->getFormat() == CAIRO_FORMAT_RGB16_565) - arr = Uint16Array::New(ab, 0, nBytes / 2); + arr = Napi::Uint16Array::New(env, nBytes / 2, ab, 0); else - arr = Uint8ClampedArray::New(ab, 0, nBytes); - - const int argc = 3; - Local argv[argc] = { arr, Nan::New(width), Nan::New(height) }; + arr = Napi::Uint8Array::New(env, nBytes, ab, 0, napi_uint8_clamped_array); - Local ctor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); + Napi::Function ctor = env.GetInstanceData()->ImageDataCtor.Value(); + Napi::Maybe ret = ctor.New({ arr, Napi::Number::New(env, width), Napi::Number::New(env, height) }); - info.GetReturnValue().Set(instance); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); } /* @@ -1191,13 +1263,19 @@ void decompose_matrix(cairo_matrix_t matrix, double *destination) { * */ -NAN_METHOD(Context2d::DrawImage) { +void +Context2d::DrawImage(const Napi::CallbackInfo& info) { int infoLen = info.Length(); - if (infoLen != 3 && infoLen != 5 && infoLen != 9) - return Nan::ThrowTypeError("Invalid arguments"); - if (!info[0]->IsObject()) - return Nan::ThrowTypeError("The first argument must be an object"); + 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)) @@ -1216,32 +1294,35 @@ NAN_METHOD(Context2d::DrawImage) { cairo_surface_t *surface; - Local obj = Nan::To(info[0]).ToLocalChecked(); + 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; } 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); + } 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 (infoLen) { @@ -1280,7 +1361,7 @@ NAN_METHOD(Context2d::DrawImage) { cairo_matrix_t matrix; double transforms[6]; - cairo_get_matrix(context->context(), &matrix); + 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. @@ -1292,7 +1373,7 @@ NAN_METHOD(Context2d::DrawImage) { 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 == context->canvas()->surface(); + bool sameCanvas = surface == canvas()->surface(); bool needsExtraSurface = sameCanvas || needCut || needScale; cairo_surface_t *surfTemp = NULL; cairo_t *ctxTemp = NULL; @@ -1340,23 +1421,23 @@ NAN_METHOD(Context2d::DrawImage) { translate_y = sy; } cairo_set_source_surface(ctxTemp, surface, -translate_x, -translate_y); - cairo_pattern_set_filter(cairo_get_source(ctxTemp), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); + 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. @@ -1364,17 +1445,17 @@ 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 + context->state->shadowOffsetX - pad + 1.4, - dy + context->state->shadowOffsetY - 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 + (context->state->shadowOffsetX), - dy + (context->state->shadowOffsetY)); + dx + (state->shadowOffsetX), + dy + (state->shadowOffsetY)); } } @@ -1389,9 +1470,9 @@ NAN_METHOD(Context2d::DrawImage) { } // Paint cairo_set_source_surface(ctx, surface, scaled_dx + extra_dx, scaled_dy + extra_dy); - cairo_pattern_set_filter(cairo_get_source(ctx), context->state->imageSmoothingEnabled ? context->state->patternQuality : CAIRO_FILTER_NEAREST); + 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, context->state->globalAlpha); + cairo_paint_with_alpha(ctx, state->globalAlpha); cairo_restore(ctx); @@ -1405,20 +1486,21 @@ NAN_METHOD(Context2d::DrawImage) { * 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 = Nan::To(value).FromMaybe(0); - 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; } } @@ -1426,11 +1508,11 @@ 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)) { // composite modes: case CAIRO_OPERATOR_CLEAR: op = "clear"; break; @@ -1467,28 +1549,31 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { 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()); - Nan::Utf8String quality(Nan::To(value).ToLocalChecked()); - 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; + } } } @@ -1496,138 +1581,146 @@ NAN_SETTER(Context2d::SetPatternQuality) { * 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. */ -NAN_SETTER(Context2d::SetImageSmoothingEnabled) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(false); +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. */ -NAN_GETTER(Context2d::GetImageSmoothingEnabled) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(context->state->imageSmoothingEnabled)); +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(); - Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); // 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); - if (op != blendmodes.end()) cairo_set_operator(ctx, op->second); +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); + } } /* * 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 = Nan::To(value).FromMaybe(0); +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 = Nan::To(value).FromMaybe(0); +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 = Nan::To(value).FromMaybe(0); - 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; + } } } @@ -1635,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) { - Nan::Utf8String str(Nan::To(value).ToLocalChecked()); - 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) { - Nan::Utf8String str(Nan::To(value).ToLocalChecked()); - 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; + } } } @@ -1705,77 +1805,77 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { * Get filter. */ -NAN_GETTER(Context2d::GetQuality) { - 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::SetQuality) { - Nan::Utf8String str(Nan::To(value).ToLocalChecked()); - 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); } - cairo_pattern_set_filter(cairo_get_source(context->context()), filter); } /* * Helper for get current transform matrix */ -Local -get_current_transform(Context2d *context) { - Isolate *iso = Isolate::GetCurrent(); - - Local arr = Float64Array::New(ArrayBuffer::New(iso, 48), 0, 6); - Nan::TypedArrayContents dest(arr); +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->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; - - const int argc = 1; - Local argv[argc] = { arr }; - return Nan::NewInstance(context->_DOMMatrix.Get(iso), argc, argv).ToLocalChecked(); + 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, Local mat) { +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, - Nan::To(Nan::Get(mat, Nan::New("a").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("b").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("c").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("d").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("e").ToLocalChecked()).ToLocalChecked()).FromMaybe(0), - Nan::To(Nan::Get(mat, Nan::New("f").ToLocalChecked()).ToLocalChecked()).FromMaybe(0) + 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() ); } @@ -1784,74 +1884,70 @@ void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { * Get current transform. */ -NAN_GETTER(Context2d::GetCurrentTransform) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Local instance = get_current_transform(context); - - info.GetReturnValue().Set(instance); +Napi::Value +Context2d::GetCurrentTransform(const Napi::CallbackInfo& info) { + return get_current_transform(); } /* * Set current transform. */ -NAN_SETTER(Context2d::SetCurrentTransform) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Local ctx = Nan::GetCurrentContext(); - Local mat = Nan::To(value).ToLocalChecked(); +void +Context2d::SetCurrentTransform(const Napi::CallbackInfo& info, const Napi::Value& value) { + Napi::Object mat; -#if NODE_MAJOR_VERSION >= 8 - if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { - return Nan::ThrowTypeError("Expected DOMMatrix"); - } -#endif + 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_matrix_t matrix; + parse_matrix_from_object(matrix, mat); - cairo_transform(context->context(), &matrix); + cairo_transform(context(), &matrix); + } } /* * Get current fill style. */ -NAN_GETTER(Context2d::GetFillStyle) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local style; +Napi::Value +Context2d::GetFillStyle(const Napi::CallbackInfo& info) { + Napi::Value style; - if (context->_fillStyle.IsEmpty()) - style = context->_getFillColor(); + if (_fillStyle.IsEmpty()) + style = _getFillColor(); else - style = context->_fillStyle.Get(iso); + style = _fillStyle.Value(); - info.GetReturnValue().Set(style); + return style; } /* * Set current fill style. */ -NAN_SETTER(Context2d::SetFillStyle) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - - if (value->IsString()) { - MaybeLocal mstr = Nan::To(value); - if (mstr.IsEmpty()) return; - Local str = mstr.ToLocalChecked(); - context->_fillStyle.Reset(); - context->_setFillColor(str); - } else if (value->IsObject()) { - Local obj = Nan::To(value).ToLocalChecked(); - if (Nan::New(Gradient::constructor)->HasInstance(obj)) { - context->_fillStyle.Reset(value); - Gradient *grad = Nan::ObjectWrap::Unwrap(obj); - context->state->fillGradient = grad->pattern(); - } else if (Nan::New(Pattern::constructor)->HasInstance(obj)) { - context->_fillStyle.Reset(value); - Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); - context->state->fillPattern = pattern->pattern(); +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(); } } } @@ -1860,41 +1956,38 @@ NAN_SETTER(Context2d::SetFillStyle) { * Get current stroke style. */ -NAN_GETTER(Context2d::GetStrokeStyle) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Local style; +Napi::Value +Context2d::GetStrokeStyle(const Napi::CallbackInfo& info) { + Napi::Value style; - if (context->_strokeStyle.IsEmpty()) - style = context->_getStrokeColor(); + if (_strokeStyle.IsEmpty()) + style = _getStrokeColor(); else - style = context->_strokeStyle.Get(Isolate::GetCurrent()); + style = _strokeStyle.Value(); - info.GetReturnValue().Set(style); + return style; } /* * Set current stroke style. */ -NAN_SETTER(Context2d::SetStrokeStyle) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - - if (value->IsString()) { - MaybeLocal mstr = Nan::To(value); - if (mstr.IsEmpty()) return; - Local str = mstr.ToLocalChecked(); - context->_strokeStyle.Reset(); - context->_setStrokeColor(str); - } else if (value->IsObject()) { - Local obj = Nan::To(value).ToLocalChecked(); - if (Nan::New(Gradient::constructor)->HasInstance(obj)) { - context->_strokeStyle.Reset(value); - Gradient *grad = Nan::ObjectWrap::Unwrap(obj); - context->state->strokeGradient = grad->pattern(); - } else if (Nan::New(Pattern::constructor)->HasInstance(obj)) { - context->_strokeStyle.Reset(value); - Pattern *pattern = Nan::ObjectWrap::Unwrap(obj); - context->state->strokePattern = pattern->pattern(); +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(); } } } @@ -1903,20 +1996,21 @@ NAN_SETTER(Context2d::SetStrokeStyle) { * 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 = Nan::To(value).FromMaybe(0); - 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); } } @@ -1924,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 = Nan::To(value).FromMaybe(0); - 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); + } } } @@ -1945,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(); - Nan::Utf8String type(Nan::To(value).ToLocalChecked()); - 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); + } } } @@ -1977,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(); - Nan::Utf8String type(Nan::To(value).ToLocalChecked()); - 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); + } } } @@ -2009,30 +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 = Nan::To(info[0]).FromMaybe(0) - , y = Nan::To(info[1]).FromMaybe(0); - context->setFillRule(info[2]); - info.GetReturnValue().Set(Nan::New(cairo_in_fill(ctx, x, y) || cairo_in_stroke(ctx, x, y))); - return; +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)); } - info.GetReturnValue().Set(Nan::False()); + 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; - Nan::Utf8String str(Nan::To(value).ToLocalChecked()); - 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); } } @@ -2040,44 +2145,54 @@ 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= */ -void Context2d::_setFillColor(Local arg) { +void +Context2d::_setFillColor(Napi::Value arg) { + Napi::Maybe stringValue = arg.ToString(); short ok; - Nan::Utf8String str(arg); - uint32_t rgba = rgba_from_string(*str, &ok); - if (!ok) return; - state->fillPattern = state->fillGradient = NULL; - 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. */ -Local Context2d::_getFillColor() { +Napi::Value +Context2d::_getFillColor() { char buf[64]; rgba_to_string(state->fill, buf, sizeof(buf)); - return Nan::New(buf).ToLocalChecked(); + return Napi::String::New(env, buf); } /* * Set stroke color, used internally for strokeStyle= */ -void Context2d::_setStrokeColor(Local arg) { +void +Context2d::_setStrokeColor(Napi::Value arg) { short ok; - Nan::Utf8String str(arg); - 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; state->strokePattern = state->strokeGradient = NULL; state->stroke = rgba_create(rgba); @@ -2087,59 +2202,46 @@ void Context2d::_setStrokeColor(Local arg) { * Get stroke color. */ -Local Context2d::_getStrokeColor() { +Napi::Value +Context2d::_getStrokeColor() { char buf[64]; rgba_to_string(state->stroke, buf, sizeof(buf)); - return Nan::New(buf).ToLocalChecked(); + return Napi::String::New(env, buf); } -NAN_METHOD(Context2d::CreatePattern) { - Local image = info[0]; - Local repetition = info[1]; - - if (!Nan::To(repetition).FromMaybe(false)) - repetition = Nan::New("repeat").ToLocalChecked(); - - const int argc = 2; - Local argv[argc] = { image, repetition }; - - Local ctor = Nan::GetFunction(Nan::New(Pattern::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); - - info.GetReturnValue().Set(instance); +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(); } -NAN_METHOD(Context2d::CreateLinearGradient) { - const int argc = 4; - Local argv[argc] = { info[0], info[1], info[2], info[3] }; +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(); - Local ctor = Nan::GetFunction(Nan::New(Gradient::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); - - info.GetReturnValue().Set(instance); } -NAN_METHOD(Context2d::CreateRadialGradient) { - const int argc = 6; - Local argv[argc] = { info[0], info[1], info[2], info[3], info[4], info[5] }; - - Local ctor = Nan::GetFunction(Nan::New(Gradient::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); - - info.GetReturnValue().Set(instance); +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) { +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() + cairo_curve_to(context() , args[0] , args[1] , args[2] @@ -2152,13 +2254,13 @@ NAN_METHOD(Context2d::BezierCurveTo) { * Quadratic curve approximation from libsvg-cairo. */ -NAN_METHOD(Context2d::QuadraticCurveTo) { +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 = args[0] @@ -2184,56 +2286,57 @@ 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) { +void +Context2d::Rotate(const Napi::CallbackInfo& info) { double args[1]; if(!checkArgs(info, args, 1)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_rotate(context->context(), args[0]); + 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; @@ -2247,52 +2350,49 @@ NAN_METHOD(Context2d::Transform) { , args[4] , args[5]); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_transform(context->context(), &matrix); + cairo_transform(context(), &matrix); } /* * Get the CTM */ -NAN_METHOD(Context2d::GetTransform) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Local instance = get_current_transform(context); - - info.GetReturnValue().Set(instance); +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. */ -NAN_METHOD(Context2d::SetTransform) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - if (info.Length() == 1) { - Local mat = Nan::To(info[0]).ToLocalChecked(); +void +Context2d::SetTransform(const Napi::CallbackInfo& info) { + Napi::Object mat; - #if NODE_MAJOR_VERSION >= 8 - Local ctx = Nan::GetCurrentContext(); - if (!mat->InstanceOf(ctx, _DOMMatrix.Get(Isolate::GetCurrent())).ToChecked()) { - return Nan::ThrowTypeError("Expected DOMMatrix"); + 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(); } - #endif + return; + } cairo_matrix_t matrix; parse_matrix_from_object(matrix, mat); - cairo_set_matrix(context->context(), &matrix); + cairo_set_matrix(context(), &matrix); } else { - cairo_identity_matrix(context->context()); + cairo_identity_matrix(context()); Context2d::Transform(info); } } @@ -2301,36 +2401,36 @@ NAN_METHOD(Context2d::SetTransform) { * Translate transformation. */ -NAN_METHOD(Context2d::Translate) { +void +Context2d::Translate(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_translate(context->context(), args[0], args[1]); + cairo_translate(context(), args[0], args[1]); } /* * Scale transformation. */ -NAN_METHOD(Context2d::Scale) { +void +Context2d::Scale(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_scale(context->context(), args[0], args[1]); + 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); } @@ -2338,19 +2438,19 @@ 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); } /* @@ -2370,45 +2470,72 @@ get_text_scale(PangoLayout *layout, double maxWidth) { } } +/* + * Make sure the layout's font list is up-to-date + */ void -paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { +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()) + if (argsNum == 3 && info[3].IsUndefined()) argsNum = 2; double args[3]; if(!checkArgs(info, args, argsNum, 1)) return; - Nan::Utf8String str(Nan::To(info[0]).ToLocalChecked()); + Napi::String strValue; + + if (!info[0].ToString().UnwrapTo(&strValue)) return; + + std::string str = strValue.Utf8Value(); double x = args[0]; double y = args[1]; double scaled_by = 1; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - PangoLayout *layout = context->layout(); + 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); - pango_layout_set_text(layout, *str, -1); - pango_cairo_update_layout(context->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->context()); - cairo_scale(context->context(), scaled_by, 1); + cairo_save(context()); + cairo_scale(context(), scaled_by, 1); } - context->savePath(); - if (context->state->textDrawingMode == TEXT_DRAW_GLYPHS) { - if (stroke == true) { context->stroke(); } else { context->fill(); } - context->setTextPath(x / scaled_by, y); - } else if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { - context->setTextPath(x / scaled_by, y); - if (stroke == true) { context->stroke(); } else { context->fill(); } + 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(); } } - context->restorePath(); + restorePath(); if (argsNum == 3) { - cairo_restore(context->context()); + cairo_restore(context()); } } @@ -2416,7 +2543,8 @@ paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { * Fill text at (x, y). */ -NAN_METHOD(Context2d::FillText) { +void +Context2d::FillText(const Napi::CallbackInfo& info) { paintText(info, false); } @@ -2424,7 +2552,8 @@ NAN_METHOD(Context2d::FillText) { * Stroke text at (x ,y). */ -NAN_METHOD(Context2d::StrokeText) { +void +Context2d::StrokeText(const Napi::CallbackInfo& info) { paintText(info, true); } @@ -2451,6 +2580,20 @@ inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { } } +text_align_t +Context2d::resolveTextAlignment() { + text_align_t alignment = state->textAlignment; + + // 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; + } + + return alignment; +} + /* * Set text path for the string in the layout at (x, y). * This function is called by paintText and won't behave correctly @@ -2461,18 +2604,19 @@ inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { void Context2d::setTextPath(double x, double y) { PangoRectangle logical_rect; + text_align_t alignment = resolveTextAlignment(); - switch (state->textAlignment) { - // center - case 0: + switch (alignment) { + case TEXT_ALIGNMENT_CENTER: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width / 2; break; - // right - case 1: + case TEXT_ALIGNMENT_RIGHT: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; + default: // TEXT_ALIGNMENT_LEFT + break; } y -= getBaselineAdjustment(_layout, state->textBaseline); @@ -2489,43 +2633,35 @@ Context2d::setTextPath(double x, double y) { * Adds a point to the current subpath. */ -NAN_METHOD(Context2d::LineTo) { +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(), args[0], args[1]); + cairo_line_to(context(), args[0], args[1]); } /* * Creates a new subpath at the given point. */ -NAN_METHOD(Context2d::MoveTo) { +void +Context2d::MoveTo(const Napi::CallbackInfo& info) { double args[2]; if(!checkArgs(info, args, 2)) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_move_to(context->context(), args[0], args[1]); + cairo_move_to(context(), args[0], args[1]); } /* * Get font. */ -NAN_GETTER(Context2d::GetFont) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local font; - - if (context->_font.IsEmpty()) - font = Nan::New("10px sans-serif").ToLocalChecked(); - else - font = context->_font.Get(iso); - - info.GetReturnValue().Set(font); +Napi::Value +Context2d::GetFont(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->font); } /* @@ -2537,138 +2673,133 @@ NAN_GETTER(Context2d::GetFont) { * - family */ -NAN_SETTER(Context2d::SetFont) { - if (!value->IsString()) return; - - Isolate *iso = Isolate::GetCurrent(); - Local ctx = Nan::GetCurrentContext(); - - Local str = Nan::To(value).ToLocalChecked(); - if (!str->Length()) return; - - const int argc = 1; - Local argv[argc] = { value }; +void +Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; - Local mparsed = Nan::Call(_parseFont.Get(iso), ctx->Global(), argc, argv).ToLocalChecked(); - // parseFont returns undefined for invalid CSS font strings - if (mparsed->IsUndefined()) return; - Local font = Nan::To(mparsed).ToLocalChecked(); + std::string str = value.As().Utf8Value(); + if (!str.length()) return; - Nan::Utf8String weight(Nan::Get(font, Nan::New("weight").ToLocalChecked()).ToLocalChecked()); - Nan::Utf8String style(Nan::Get(font, Nan::New("style").ToLocalChecked()).ToLocalChecked()); - double size = Nan::To(Nan::Get(font, Nan::New("size").ToLocalChecked()).ToLocalChecked()).FromMaybe(0); - Nan::Utf8String unit(Nan::Get(font, Nan::New("unit").ToLocalChecked()).ToLocalChecked()); - Nan::Utf8String family(Nan::Get(font, Nan::New("family").ToLocalChecked()).ToLocalChecked()); + 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) { + 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 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); + 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; - pango_layout_set_font_description(context->_layout, sys_desc); + state->fontDescription = sys_desc; + pango_layout_set_font_description(_layout, sys_desc); - context->_font.Reset(value); + state->font = str; } /* * Get text baseline. */ -NAN_GETTER(Context2d::GetTextBaseline) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local font; - - if (context->_textBaseline.IsEmpty()) - font = Nan::New("alphabetic").ToLocalChecked(); - else - font = context->_textBaseline.Get(iso); - - info.GetReturnValue().Set(font); +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. */ -NAN_SETTER(Context2d::SetTextBaseline) { - if (!value->IsString()) return; - - Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); - const std::map modes = { - {"alphabetic", 0}, - {"top", 1}, - {"bottom", 2}, - {"middle", 3}, - {"ideographic", 4}, - {"hanging", 5} +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); + auto op = modes.find(opStr); if (op == modes.end()) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textBaseline = op->second; - context->_textBaseline.Reset(value); + state->textBaseline = op->second; } /* * Get text align. */ -NAN_GETTER(Context2d::GetTextAlign) { - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - Isolate *iso = Isolate::GetCurrent(); - Local font; - - if (context->_textAlign.IsEmpty()) - font = Nan::New("start").ToLocalChecked(); - else - font = context->_textAlign.Get(iso); - - info.GetReturnValue().Set(font); +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. */ -NAN_SETTER(Context2d::SetTextAlign) { - if (!value->IsString()) return; - - Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); - const std::map modes = { - {"center", 0}, - {"left", -1}, - {"start", -1}, - {"right", 1}, - {"end", 1} +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); + auto op = modes.find(opStr); if (op == modes.end()) return; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - context->state->textAlignment = op->second; - context->_textAlign.Reset(value); + state->textAlignment = op->second; } /* @@ -2678,19 +2809,25 @@ NAN_SETTER(Context2d::SetTextAlign) { * 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(); - Nan::Utf8String str(Nan::To(info[0]).ToLocalChecked()); - Local obj = Nan::New(); + Napi::Object obj = Napi::Object::New(env); 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); // Normally you could use pango_layout_get_pixel_extents and be done, or use @@ -2712,50 +2849,35 @@ NAN_METHOD(Context2d::MeasureText) { 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; } - cairo_matrix_t matrix; - cairo_get_matrix(ctx, &matrix); - double y_offset = getBaselineAdjustment(layout, context->state->textBaseline); - - Nan::Set(obj, - Nan::New("width").ToLocalChecked(), - Nan::New(logical_rect.width)).Check(); - Nan::Set(obj, - Nan::New("actualBoundingBoxLeft").ToLocalChecked(), - Nan::New(x_offset - PANGO_LBEARING(ink_rect))).Check(); - Nan::Set(obj, - Nan::New("actualBoundingBoxRight").ToLocalChecked(), - Nan::New(x_offset + PANGO_RBEARING(ink_rect))).Check(); - Nan::Set(obj, - Nan::New("actualBoundingBoxAscent").ToLocalChecked(), - Nan::New(y_offset + PANGO_ASCENT(ink_rect))).Check(); - Nan::Set(obj, - Nan::New("actualBoundingBoxDescent").ToLocalChecked(), - Nan::New(PANGO_DESCENT(ink_rect) - y_offset)).Check(); - Nan::Set(obj, - Nan::New("emHeightAscent").ToLocalChecked(), - Nan::New(-(PANGO_ASCENT(logical_rect) - y_offset))).Check(); - Nan::Set(obj, - Nan::New("emHeightDescent").ToLocalChecked(), - Nan::New(PANGO_DESCENT(logical_rect) - y_offset)).Check(); - Nan::Set(obj, - Nan::New("alphabeticBaseline").ToLocalChecked(), - Nan::New(-(pango_font_metrics_get_ascent(metrics) * inverse_pango_scale - y_offset))).Check(); + double y_offset = getBaselineAdjustment(layout, state->textBaseline); + + 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))); pango_font_metrics_unref(metrics); - info.GetReturnValue().Set(obj); + return obj; } /* @@ -2763,22 +2885,22 @@ NAN_METHOD(Context2d::MeasureText) { * 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 = Nan::Get(dash, i % dash->Length()).ToLocalChecked(); - if (!d->IsNumber()) return; - a[i] = Nan::To(d).FromMaybe(0); + 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); if (zero_dashes == dashes) { @@ -2793,31 +2915,33 @@ NAN_METHOD(Context2d::SetLineDash) { * 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); + Napi::Array dash = Napi::Array::New(env, dashes); for (int i=0; i(i), Nan::New(a[i])).Check(); + dash.Set(Napi::Number::New(env, i), Napi::Number::New(env, a[i])); } - info.GetReturnValue().Set(dash); + return dash; } /* * Set line dash offset * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ -NAN_SETTER(Context2d::SetLineDashOffset) { - double offset = Nan::To(value).FromMaybe(0); +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); @@ -2829,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); } @@ -2890,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); @@ -2905,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 = Nan::To(info[5]).FromMaybe(false); + 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]; + } + + bool clockwise = true; + if (width < 0) { + clockwise = false; + x += width; + width = -width; + std::swap(upperLeft, upperRight); + std::swap(lowerLeft, lowerRight); + } + + 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; + } + } - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + // 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 { + 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. + */ - if (anticlockwise && M_PI * 2 != Nan::To(info[4]).FromMaybe(0)) { - cairo_arc_negative(ctx - , Nan::To(info[0]).FromMaybe(0) - , Nan::To(info[1]).FromMaybe(0) - , Nan::To(info[2]).FromMaybe(0) - , Nan::To(info[3]).FromMaybe(0) - , Nan::To(info[4]).FromMaybe(0)); +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 - , Nan::To(info[0]).FromMaybe(0) - , Nan::To(info[1]).FromMaybe(0) - , Nan::To(info[2]).FromMaybe(0) - , Nan::To(info[3]).FromMaybe(0) - , Nan::To(info[4]).FromMaybe(0)); + cairo_arc(ctx, x, y, radius, startAngle, endAngle); } } @@ -2944,13 +3296,13 @@ NAN_METHOD(Context2d::Arc) { * Implementation influenced by WebKit. */ -NAN_METHOD(Context2d::ArcTo) { +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; @@ -3049,7 +3401,8 @@ NAN_METHOD(Context2d::ArcTo) { * going in the given direction by anticlockwise (defaulting to clockwise). */ -NAN_METHOD(Context2d::Ellipse) { +void +Context2d::Ellipse(const Napi::CallbackInfo& info) { double args[7]; if(!checkArgs(info, args, 7)) return; @@ -3064,10 +3417,12 @@ NAN_METHOD(Context2d::Ellipse) { double rotation = args[4]; double startAngle = args[5]; double endAngle = args[6]; - bool anticlockwise = Nan::To(info[7]).FromMaybe(false); + Napi::Boolean anticlockwiseValue; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + 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; @@ -3095,3 +3450,53 @@ NAN_METHOD(Context2d::Ellipse) { } 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 8afb433d1..1d9548895 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -5,13 +5,9 @@ #include "cairo.h" #include "Canvas.h" #include "color.h" -#include "nan.h" +#include "napi.h" #include - -typedef enum { - TEXT_DRAW_PATHS, - TEXT_DRAW_GLYPHS -} canvas_draw_mode_t; +#include /* * State struct. @@ -20,25 +16,59 @@ 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; - bool imageSmoothingEnabled; -} 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 = ""; + + canvas_state_t() { + fontDescription = pango_font_description_from_string("sans"); + pango_font_description_set_absolute_size(fontDescription, 10 * PANGO_SCALE); + } + + 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 @@ -54,110 +84,111 @@ typedef struct { float height; } float_rectangle; -void state_assign_fontFamily(canvas_state_t *state, const char *str); - -class Context2d: public Nan::ObjectWrap { +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 _DOMMatrix; - static Nan::Persistent _parseFont; - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_METHOD(SaveExternalModules); - 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(GetTransform); - static NAN_METHOD(ResetTransform); - static NAN_METHOD(SetTransform); - 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(SetStrokePattern); - 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(Ellipse); - static NAN_METHOD(GetImageData); - static NAN_METHOD(CreateImageData); - static NAN_METHOD(GetStrokeColor); - static NAN_METHOD(CreatePattern); - static NAN_METHOD(CreateLinearGradient); - static NAN_METHOD(CreateRadialGradient); - static NAN_GETTER(GetFormat); - static NAN_GETTER(GetPatternQuality); - static NAN_GETTER(GetImageSmoothingEnabled); - static NAN_GETTER(GetGlobalCompositeOperation); - static NAN_GETTER(GetGlobalAlpha); - static NAN_GETTER(GetShadowColor); - 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(GetQuality); - static NAN_GETTER(GetCurrentTransform); - static NAN_GETTER(GetFillStyle); - static NAN_GETTER(GetStrokeStyle); - static NAN_GETTER(GetFont); - static NAN_GETTER(GetTextBaseline); - static NAN_GETTER(GetTextAlign); - static NAN_SETTER(SetPatternQuality); - static NAN_SETTER(SetImageSmoothingEnabled); - 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(SetQuality); - static NAN_SETTER(SetCurrentTransform); - static NAN_SETTER(SetFillStyle); - static NAN_SETTER(SetStrokeStyle); - static NAN_SETTER(SetFont); - static NAN_SETTER(SetTextBaseline); - static NAN_SETTER(SetTextAlign); + 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; } @@ -173,31 +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(bool init = false); + void resetState(); inline PangoLayout *layout(){ return _layout; } + ~Context2d(); + Napi::Env env; private: - ~Context2d(); void _resetPersistentHandles(); - v8::Local _getFillColor(); - v8::Local _getStrokeColor(); - void _setFillColor(v8::Local arg); - void _setFillPattern(v8::Local arg); - void _setStrokeColor(v8::Local arg); - void _setStrokePattern(v8::Local arg); - Nan::Persistent _fillStyle; - Nan::Persistent _strokeStyle; - Nan::Persistent _font; - Nan::Persistent _textBaseline; - Nan::Persistent _textAlign; + 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; }; diff --git a/src/CharData.h b/src/CharData.h new file mode 100644 index 000000000..00fc4effc --- /dev/null +++ b/src/CharData.h @@ -0,0 +1,233 @@ +// 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 + +#include + +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 103b65ee7..973736505 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1,6 +1,7 @@ // Copyright (c) 2010 LearnBoost #include "Image.h" +#include "InstanceData.h" #include "bmp/BMPParser.h" #include "Canvas.h" @@ -8,7 +9,7 @@ #include #include #include -#include "Util.h" +#include /* Cairo limit: * https://lists.cairographics.org/archives/cairo/2010-December/021422.html @@ -37,90 +38,88 @@ struct canvas_jpeg_error_mgr: jpeg_error_mgr { */ typedef struct { + Napi::Env* env; unsigned len; uint8_t *buf; } read_closure_t; -using namespace v8; - -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(); - SetProtoAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete, NULL, ctor); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight, ctor); - SetProtoAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth, NULL, ctor); - SetProtoAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight, NULL, ctor); - SetProtoAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode, ctor); - - ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); - ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); - - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, Nan::New("Image").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); +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 - NAN_EXPORT(target, GetSource); - NAN_EXPORT(target, SetSource); + 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()); - Nan::Set(info.This(), Nan::New("onload").ToLocalChecked(), Nan::Null()).Check(); - Nan::Set(info.This(), Nan::New("onerror").ToLocalChecked(), Nan::Null()).Check(); - 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) { - info.GetReturnValue().Set(Nan::New(true)); +Napi::Value +Image::GetComplete(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(env, true); } /* * 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 = Nan::To(value).FromMaybe(0); - 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; } } @@ -128,28 +127,28 @@ NAN_SETTER(Image::SetDataMode) { * Get natural width */ -NAN_GETTER(Image::GetNaturalWidth) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->naturalWidth)); +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. */ -NAN_SETTER(Image::SetWidth) { - if (value->IsNumber()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->width = Nan::To(value).FromMaybe(0); +void +Image::SetWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + width = value.As().Uint32Value(); } } @@ -157,27 +156,27 @@ NAN_SETTER(Image::SetWidth) { * Get natural height */ -NAN_GETTER(Image::GetNaturalHeight) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(img->naturalHeight)); +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. */ -NAN_SETTER(Image::SetHeight) { - if (value->IsNumber()) { - Image *img = Nan::ObjectWrap::Unwrap(info.This()); - img->height = Nan::To(value).FromMaybe(0); +void +Image::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (value.IsNumber()) { + height = value.As().Uint32Value(); } } @@ -185,14 +184,11 @@ NAN_SETTER(Image::SetHeight) { * Get src path. */ -NAN_METHOD(Image::GetSource){ - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - // #1534 - Nan::ThrowTypeError("Method Image.GetSource called on incompatible receiver"); - return; - } - 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 : ""); } /* @@ -203,7 +199,7 @@ void Image::clearData() { if (_surface) { cairo_surface_destroy(_surface); - Nan::AdjustExternalMemory(-_data_len); + Napi::MemoryManagement::AdjustExternalMemory(env, -_data_len); _data_len = 0; _surface = NULL; } @@ -230,55 +226,49 @@ Image::clearData() { * Set src path. */ -NAN_METHOD(Image::SetSource){ - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - // #1534 - Nan::ThrowTypeError("Method Image.SetSource called on incompatible receiver"); - return; - } - 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; - Local value = info[0]; + Napi::Value value = info[0]; img->clearData(); // Clear errno in case some unrelated previous syscall failed errno = 0; // url string - if (value->IsString()) { - Nan::Utf8String 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 (node::Buffer::HasInstance(value)) { - uint8_t *buf = (uint8_t *) node::Buffer::Data(Nan::To(value).ToLocalChecked()); - unsigned len = node::Buffer::Length(Nan::To(value).ToLocalChecked()); + } else if (value.IsBuffer()) { + uint8_t *buf = value.As>().Data(); + unsigned len = value.As>().Length(); status = img->loadFromBuffer(buf, len); } if (status) { - Local onerrorFn = Nan::Get(info.This(), Nan::New("onerror").ToLocalChecked()).ToLocalChecked(); - if (onerrorFn->IsFunction()) { - Local argv[1]; - CanvasError errorInfo = img->errorInfo; - if (errorInfo.cerrno) { - argv[0] = Nan::ErrnoException(errorInfo.cerrno, errorInfo.syscall.c_str(), errorInfo.message.c_str(), errorInfo.path.c_str()); - } else if (!errorInfo.message.empty()) { - argv[0] = Nan::Error(Nan::New(errorInfo.message).ToLocalChecked()); + 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 { - argv[0] = Nan::Error(Nan::New(cairo_status_to_string(status)).ToLocalChecked()); + arg = img->errorInfo.toError(env); } - Local ctx = Nan::GetCurrentContext(); - Nan::Call(onerrorFn.As(), ctx->Global(), 1, argv); + onerrorFn.As().Call({ arg.Value() }); } } else { img->loaded(); - Local onloadFn = Nan::Get(info.This(), Nan::New("onload").ToLocalChecked()).ToLocalChecked(); - if (onloadFn->IsFunction()) { - Local ctx = Nan::GetCurrentContext(); - Nan::Call(onloadFn.As(), ctx->Global(), 0, NULL); + Napi::Value onloadFn; + if (This.Get("onload").UnwrapTo(&onloadFn) && onloadFn.IsFunction()) { + onloadFn.As().Call({}); } } } @@ -348,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; @@ -366,25 +357,6 @@ Image::readPNG(void *c, uint8_t *data, unsigned int len) { return CAIRO_STATUS_SUCCESS; } -/* - * Initialize a new Image. - */ - -Image::Image() { - 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 -} - /* * Destroy image and associated surface. */ @@ -412,13 +384,13 @@ Image::load() { void Image::loaded() { - Nan::HandleScope scope; + Napi::HandleScope scope(env); state = COMPLETE; 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); - Nan::AdjustExternalMemory(_data_len); + Napi::MemoryManagement::AdjustExternalMemory(env, _data_len); } /* @@ -435,7 +407,8 @@ cairo_surface_t *Image::surface() { cairo_status_t status = renderSVGToSurface(); if (status != CAIRO_STATUS_SUCCESS) { g_object_unref(_rsvg); - Nan::ThrowError(Canvas::Error(status)); + Napi::Error::New(env, cairo_status_to_string(status)).ThrowAsJavaScriptException(); + return NULL; } } @@ -793,6 +766,50 @@ 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) { @@ -811,10 +828,11 @@ void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src */ cairo_status_t -Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { +Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args, Orientation orientation) { + const int channels = 4; cairo_status_t status = CAIRO_STATUS_SUCCESS; - uint8_t *data = new uint8_t[naturalWidth * naturalHeight * 4]; + uint8_t *data = new uint8_t[naturalWidth * naturalHeight * channels]; if (!data) { jpeg_abort_decompress(args); jpeg_destroy_decompress(args); @@ -861,6 +879,8 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { break; } + updateDimensionsForOrientation(orientation); + if (!status) { _surface = cairo_image_surface_create_for_data( data @@ -874,6 +894,8 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { jpeg_destroy_decompress(args); status = cairo_surface_status(_surface); + rotatePixels(data, naturalWidth, naturalHeight, channels, orientation); + delete[] src; if (status) { @@ -949,6 +971,10 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { 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 @@ -967,6 +993,8 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { return status; } + rotatePixels(data, naturalWidth, naturalHeight, 1, orientation); + _data = data; return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); @@ -978,7 +1006,8 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { void clearMimeData(void *closure) { - Nan::AdjustExternalMemory( + Napi::MemoryManagement::AdjustExternalMemory( + *static_cast(closure)->env, -static_cast((static_cast(closure)->len))); free(static_cast(closure)->buf); free(closure); @@ -1007,10 +1036,11 @@ Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { 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 @@ -1026,6 +1056,9 @@ Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { 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; @@ -1053,7 +1086,7 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { width = naturalWidth = args.output_width; height = naturalHeight = args.output_height; - return decodeJPEGIntoSurface(&args); + return decodeJPEGIntoSurface(&args, orientation); } /* @@ -1069,6 +1102,13 @@ Image::loadJPEG(FILE *stream) { #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 canvas_jpeg_error_mgr err; @@ -1101,7 +1141,7 @@ Image::loadJPEG(FILE *stream) { width = naturalWidth = args.output_width; height = naturalHeight = args.output_height; - status = decodeJPEGIntoSurface(&args); + status = decodeJPEGIntoSurface(&args, orientation); fclose(stream); } else { // We'll need the actual source jpeg data, so read fully. uint8_t *buf; @@ -1141,6 +1181,283 @@ Image::loadJPEG(FILE *stream) { 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 @@ -1153,26 +1470,24 @@ cairo_status_t Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { _is_svg = true; - cairo_status_t status; - GError *gerr = NULL; - - if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, &gerr))) { + if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, nullptr))) { return CAIRO_STATUS_READ_ERROR; } - RsvgDimensionData *dims = new RsvgDimensionData(); - rsvg_handle_get_dimensions(_rsvg, dims); + double d_width; + double d_height; - width = naturalWidth = dims->width; - height = naturalHeight = dims->height; + rsvg_handle_get_intrinsic_size_in_pixels(_rsvg, &d_width, &d_height); - status = renderSVGToSurface(); - if (status != CAIRO_STATUS_SUCCESS) { - g_object_unref(_rsvg); - return status; + 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 CAIRO_STATUS_SUCCESS; + return renderSVGToSurface(); } /* @@ -1191,16 +1506,19 @@ Image::renderSVGToSurface() { } cairo_t *cr = cairo_create(_surface); - cairo_scale(cr, - (double)width / (double)naturalWidth, - (double)height / (double)naturalHeight); status = cairo_status(cr); if (status != CAIRO_STATUS_SUCCESS) { g_object_unref(_rsvg); return status; } - gboolean render_ok = rsvg_handle_render_cairo(_rsvg, cr); + 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); diff --git a/src/Image.h b/src/Image.h index 62bc3f13b..6ead16fdb 100644 --- a/src/Image.h +++ b/src/Image.h @@ -5,9 +5,8 @@ #include #include "CanvasError.h" #include -#include +#include #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 -#include #ifdef HAVE_JPEG #include @@ -34,25 +33,26 @@ using JPEGDecodeL = std::function; -class Image: public Nan::ObjectWrap { +class Image : public Napi::ObjectWrap { public: char *filename; int width, height; int naturalWidth, naturalHeight; - static Nan::Persistent constructor; - static void Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target); - static NAN_METHOD(New); - static NAN_GETTER(GetComplete); - static NAN_GETTER(GetWidth); - static NAN_GETTER(GetHeight); - static NAN_GETTER(GetNaturalWidth); - static NAN_GETTER(GetNaturalHeight); - static NAN_GETTER(GetDataMode); - static NAN_SETTER(SetDataMode); - static NAN_SETTER(SetWidth); - static NAN_SETTER(SetHeight); - static NAN_METHOD(GetSource); - static NAN_METHOD(SetSource); + 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); @@ -78,19 +78,39 @@ class Image: public Nan::ObjectWrap { 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); void jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode); - cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info); + 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 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 @@ -123,5 +143,4 @@ class Image: public Nan::ObjectWrap { int _svg_last_width; int _svg_last_height; #endif - ~Image(); }; diff --git a/src/ImageData.cc b/src/ImageData.cc index 668733d39..d334ca894 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -1,140 +1,132 @@ // Copyright (c) 2010 LearnBoost #include "ImageData.h" - -#include "Util.h" - -using namespace v8; - -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(); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, NULL, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, NULL, ctor); - Local ctx = Nan::GetCurrentContext(); - Nan::Set(target, Nan::New("ImageData").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); +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'"); - } - - Local dataArray; +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 = Nan::To(info[0]).FromMaybe(0); + 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 = Nan::To(info[1]).FromMaybe(0); + 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; // ImageData(w, h) constructor assumes 4 BPP; documented. - dataArray = Uint8ClampedArray::New(ArrayBuffer::New(Isolate::GetCurrent(), length), 0, length); - - } else if (info[0]->IsUint8ClampedArray() && info[1]->IsUint32()) { - dataArray = info[0].As(); + 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->Length(); + length = dataArray.ElementLength(); if (length == 0) { - Nan::ThrowRangeError("The input data has a zero byte length."); + Napi::RangeError::New(env, "The input data has a zero byte length.").ThrowAsJavaScriptException(); return; } // Don't assert that the ImageData length is a multiple of four because some // data formats are not 4 BPP. - width = Nan::To(info[1]).FromMaybe(0); + 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; } // Don't assert that the byte length is a multiple of 4 * width, ditto. - if (info[2]->IsUint32()) { // Explicit height given - height = Nan::To(info[2]).FromMaybe(0); + 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; + } - } else if (info[0]->IsUint16Array() && info[1]->IsUint32()) { // Intended for RGB16_565 format - dataArray = info[0].As(); - - length = dataArray->Length(); - if (length == 0) { - Nan::ThrowRangeError("The input data has a zero byte length."); - return; - } - - width = Nan::To(info[1]).FromMaybe(0); - if (width == 0) { - Nan::ThrowRangeError("The source width is zero."); - return; - } - - if (info[2]->IsUint32()) { // Explicit height given - height = Nan::To(info[2]).FromMaybe(0); - } else { // Calculate height assuming 2 BPP - int size = length / 2; - height = size / width; - } + 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]), (Uint16Array, width[, height]) or (width, height)"); + Napi::TypeError::New(env, "Expected (Uint8ClampedArray, width[, height]), (Uint16Array, width[, height]) or (width, height)").ThrowAsJavaScriptException(); return; } - Nan::TypedArrayContents dataPtr(dataArray); + _width = width; + _height = height; + _data = dataArray.As().Data(); - ImageData *imageData = new ImageData(reinterpret_cast(*dataPtr), width, height); - imageData->Wrap(info.This()); - Nan::Set(info.This(), Nan::New("data").ToLocalChecked(), dataArray).Check(); - 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 4832b37b2..32d6037d1 100644 --- a/src/ImageData.h +++ b/src/ImageData.h @@ -2,22 +2,21 @@ #pragma once -#include +#include #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 -#include -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; } - ImageData(uint8_t *data, int width, int height) : _width(width), _height(height), _data(data) {} + + Napi::Env env; private: int _width; 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 b8efeed21..43c74f139 100644 --- a/src/JPEGStream.h +++ b/src/JPEGStream.h @@ -23,18 +23,15 @@ init_closure_destination(j_compress_ptr cinfo){ boolean empty_closure_output_buffer(j_compress_ptr cinfo){ - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:empty_closure_output_buffer"); 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"); - v8::Local buf = Nan::NewBuffer((char *)dest->buffer, dest->bufsize).ToLocalChecked(); + Napi::Object buf = Napi::Buffer::New(env, (char *)dest->buffer, dest->bufsize); // emit "data" - v8::Local argv[2] = { - Nan::Null() - , buf - }; - dest->closure->cb.Call(sizeof argv / sizeof *argv, argv, &async); + dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async); dest->buffer = (JOCTET *)malloc(dest->bufsize); cinfo->dest->next_output_byte = dest->buffer; @@ -44,25 +41,18 @@ empty_closure_output_buffer(j_compress_ptr cinfo){ void term_closure_destination(j_compress_ptr cinfo){ - Nan::HandleScope scope; - Nan::AsyncResource async("canvas:term_closure_destination"); 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 */ - v8::Local buf = Nan::NewBuffer((char *)dest->buffer, dest->bufsize - dest->pub.free_in_buffer).ToLocalChecked(); + Napi::Object buf = Napi::Buffer::New(env, (char *)dest->buffer, dest->bufsize - dest->pub.free_in_buffer); - v8::Local data_argv[2] = { - Nan::Null() - , buf - }; - dest->closure->cb.Call(sizeof data_argv / sizeof *data_argv, data_argv, &async); + dest->closure->cb.MakeCallback(env.Global(), {env.Null(), buf}, async); // emit "end" - v8::Local end_argv[2] = { - Nan::Null() - , Nan::Null() - }; - dest->closure->cb.Call(sizeof end_argv / sizeof *end_argv, end_argv, &async); + dest->closure->cb.MakeCallback(env.Global(), {env.Null(), env.Null()}, async); } void diff --git a/src/Point.h b/src/Point.h index d3228acfa..a61f8b1ba 100644 --- a/src/Point.h +++ b/src/Point.h @@ -1,10 +1,11 @@ // Copyright (c) 2010 LearnBoost - #pragma once -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; }; diff --git a/src/Util.h b/src/Util.h index 8f935359d..0e6d1d89c 100644 --- a/src/Util.h +++ b/src/Util.h @@ -1,30 +1,6 @@ #pragma once -#include -#include - -// Wrapper around Nan::SetAccessor that makes it easier to change the last -// argument (signature). Getters/setters must be accessed only when there is -// actually an instance, i.e. MyClass.prototype.getter1 should not try to -// unwrap the non-existent 'this'. See #803, #847, #885, nodejs/node#15099, ... -inline void SetProtoAccessor( - v8::Local tpl, - v8::Local name, - Nan::GetterCallback getter, - Nan::SetterCallback setter, - v8::Local ctor - ) { - Nan::SetAccessor( - tpl, - name, - getter, - setter, - v8::Local(), - v8::DEFAULT, - v8::None, - v8::AccessorSignature::New(v8::Isolate::GetCurrent(), ctor) - ); -} +#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) { diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 528f61a08..4607fb646 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -1,27 +1,14 @@ #include "Backend.h" #include +#include -Backend::Backend(std::string name, int width, int height) - : name(name) - , width(width) - , height(height) -{} - -Backend::~Backend() -{ - this->destroySurface(); -} - -void Backend::init(const Nan::FunctionCallbackInfo &info) { +Backend::Backend(std::string name, Napi::CallbackInfo& info) : name(name), env(info.Env()) { int width = 0; int height = 0; - if (info[0]->IsNumber()) width = Nan::To(info[0]).FromMaybe(0); - if (info[1]->IsNumber()) height = Nan::To(info[1]).FromMaybe(0); - - Backend *backend = construct(width, height); - - backend->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); + 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) @@ -30,27 +17,6 @@ void Backend::setCanvas(Canvas* _canvas) } -cairo_surface_t* Backend::recreateSurface() -{ - this->destroySurface(); - - return this->createSurface(); -} - -DLL_PUBLIC cairo_surface_t* Backend::getSurface() { - if (!surface) createSurface(); - return surface; -} - -void Backend::destroySurface() -{ - if(this->surface) - { - cairo_surface_destroy(this->surface); - this->surface = NULL; - } -} - std::string Backend::getName() { @@ -63,8 +29,8 @@ int Backend::getWidth() } void Backend::setWidth(int width_) { + this->destroySurface(); this->width = width_; - this->recreateSurface(); } int Backend::getHeight() @@ -73,32 +39,27 @@ int Backend::getHeight() } void Backend::setHeight(int height_) { + this->destroySurface(); this->height = height_; - this->recreateSurface(); } -bool Backend::isSurfaceValid(){ - bool hadSurface = surface != NULL; +bool Backend::isSurfaceValid() { bool isValid = true; - cairo_status_t status = cairo_surface_status(getSurface()); + cairo_status_t status = cairo_surface_status(ensureSurface()); if (status != CAIRO_STATUS_SUCCESS) { error = cairo_status_to_string(status); isValid = false; } - if (!hadSurface) - destroySurface(); - return isValid; } BackendOperationNotAvailable::BackendOperationNotAvailable(Backend* backend, std::string operation_name) - : backend(backend) - , operation_name(operation_name) + : operation_name(operation_name) { msg = "operation " + operation_name + " not supported by backend " + backend->getName(); diff --git a/src/backend/Backend.h b/src/backend/Backend.h index df65194b9..d51eb7601 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -3,13 +3,12 @@ #include #include "../dll_visibility.h" #include -#include +#include #include -#include class Canvas; -class Backend : public Nan::ObjectWrap +class Backend { private: const std::string name; @@ -18,23 +17,17 @@ class Backend : public Nan::ObjectWrap protected: int width; int height; - cairo_surface_t* surface = nullptr; Canvas* canvas = nullptr; - Backend(std::string name, int width, int height); - static void init(const Nan::FunctionCallbackInfo &info); - static Backend *construct(int width, int height){ return nullptr; } + Backend(std::string name, Napi::CallbackInfo& info); public: - virtual ~Backend(); + Napi::Env env; void setCanvas(Canvas* canvas); - virtual cairo_surface_t* createSurface() = 0; - virtual cairo_surface_t* recreateSurface(); - - DLL_PUBLIC cairo_surface_t* getSurface(); - virtual void destroySurface(); + virtual cairo_surface_t* ensureSurface() = 0; + virtual void destroySurface() = 0; DLL_PUBLIC std::string getName(); @@ -57,7 +50,6 @@ class Backend : public Nan::ObjectWrap class BackendOperationNotAvailable: public std::exception { private: - Backend* backend; std::string operation_name; std::string msg; diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index d354d92cc..1fede0736 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -1,16 +1,16 @@ #include "ImageBackend.h" +#include "../InstanceData.h" +#include +#include -using namespace v8; +ImageBackend::ImageBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("image", info) {} -ImageBackend::ImageBackend(int width, int height) - : Backend("image", width, height) - {} - -Backend *ImageBackend::construct(int width, int height){ - return new ImageBackend(width, height); +ImageBackend::~ImageBackend() { + destroySurface(); } -// This returns an approximate value only, suitable for Nan::AdjustExternalMemory. +// 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) { @@ -31,11 +31,12 @@ int32_t ImageBackend::approxBytesPerPixel() { } } -cairo_surface_t* ImageBackend::createSurface() { - assert(!surface); - surface = cairo_image_surface_create(format, width, height); - assert(surface); - Nan::AdjustExternalMemory(approxBytesPerPixel() * width * height); +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; } @@ -43,7 +44,7 @@ void ImageBackend::destroySurface() { if (surface) { cairo_surface_destroy(surface); surface = nullptr; - Nan::AdjustExternalMemory(-approxBytesPerPixel() * width * height); + Napi::MemoryManagement::AdjustExternalMemory(env, -approxBytesPerPixel() * width * height); } } @@ -52,23 +53,15 @@ cairo_format_t ImageBackend::getFormat() { } void ImageBackend::setFormat(cairo_format_t _format) { - this->format = _format; + this->destroySurface(); + this->format = _format; } -Nan::Persistent ImageBackend::constructor; - -void ImageBackend::Initialize(Local target) { - Nan::HandleScope scope; - - Local ctor = Nan::New(ImageBackend::New); - ImageBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("ImageBackend").ToLocalChecked()); - Nan::Set(target, - Nan::New("ImageBackend").ToLocalChecked(), - Nan::GetFunction(ctor).ToLocalChecked()).Check(); -} +Napi::FunctionReference ImageBackend::constructor; -NAN_METHOD(ImageBackend::New) { - init(info); +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 index f68dacfdb..14946c7b9 100644 --- a/src/backend/ImageBackend.h +++ b/src/backend/ImageBackend.h @@ -1,26 +1,26 @@ #pragma once #include "Backend.h" -#include +#include -class ImageBackend : public Backend +class ImageBackend : public Napi::ObjectWrap, public Backend { private: - cairo_surface_t* createSurface(); + cairo_surface_t* ensureSurface(); void destroySurface(); cairo_format_t format = DEFAULT_FORMAT; + cairo_surface_t* surface = nullptr; public: - ImageBackend(int width, int height); - static Backend *construct(int width, int height); + ImageBackend(Napi::CallbackInfo& info); + ~ImageBackend(); cairo_format_t getFormat(); void setFormat(cairo_format_t format); int32_t approxBytesPerPixel(); - static Nan::Persistent constructor; - static void Initialize(v8::Local target); - static NAN_METHOD(New); + 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 index d8bd23422..4eb46168c 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -1,53 +1,40 @@ #include "PdfBackend.h" #include +#include +#include "../InstanceData.h" #include "../Canvas.h" #include "../closure.h" -using namespace v8; - -PdfBackend::PdfBackend(int width, int height) - : Backend("pdf", width, height) { - createSurface(); -} +PdfBackend::PdfBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("pdf", info) {} PdfBackend::~PdfBackend() { - cairo_surface_finish(surface); - if (_closure) delete _closure; destroySurface(); } -Backend *PdfBackend::construct(int width, int height){ - return new PdfBackend(width, height); -} - -cairo_surface_t* PdfBackend::createSurface() { - if (!_closure) _closure = new PdfSvgClosure(canvas); - surface = cairo_pdf_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); +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; } -cairo_surface_t* PdfBackend::recreateSurface() { - cairo_pdf_surface_set_size(surface, width, height); - - return surface; -} - - -Nan::Persistent PdfBackend::constructor; - -void PdfBackend::Initialize(Local target) { - Nan::HandleScope scope; - - Local ctor = Nan::New(PdfBackend::New); - PdfBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("PdfBackend").ToLocalChecked()); - Nan::Set(target, - Nan::New("PdfBackend").ToLocalChecked(), - Nan::GetFunction(ctor).ToLocalChecked()).Check(); +void PdfBackend::destroySurface() { + if (surface) { + cairo_surface_finish(surface); + cairo_surface_destroy(surface); + surface = nullptr; + assert(_closure); + delete _closure; + _closure = nullptr; + } } -NAN_METHOD(PdfBackend::New) { - init(info); +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 index 03656f500..6ae8415c8 100644 --- a/src/backend/PdfBackend.h +++ b/src/backend/PdfBackend.h @@ -2,23 +2,23 @@ #include "Backend.h" #include "../closure.h" -#include +#include -class PdfBackend : public Backend +class PdfBackend : public Napi::ObjectWrap, public Backend { private: - cairo_surface_t* createSurface(); - cairo_surface_t* recreateSurface(); + cairo_surface_t* ensureSurface(); + void destroySurface(); + cairo_surface_t* surface = nullptr; public: PdfSvgClosure* _closure = NULL; inline PdfSvgClosure* closure() { return _closure; } - PdfBackend(int width, int height); + PdfBackend(Napi::CallbackInfo& info); ~PdfBackend(); - static Backend *construct(int width, int height); - static Nan::Persistent constructor; - static void Initialize(v8::Local target); - static NAN_METHOD(New); + 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 index 10bf4caa7..475c07dea 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -1,61 +1,44 @@ #include "SvgBackend.h" #include +#include #include "../Canvas.h" #include "../closure.h" +#include "../InstanceData.h" #include -using namespace v8; +using namespace Napi; -SvgBackend::SvgBackend(int width, int height) - : Backend("svg", width, height) { - createSurface(); -} +SvgBackend::SvgBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("svg", info) {} SvgBackend::~SvgBackend() { - cairo_surface_finish(surface); - if (_closure) { - delete _closure; - _closure = nullptr; - } destroySurface(); } -Backend *SvgBackend::construct(int width, int height){ - return new SvgBackend(width, height); -} - -cairo_surface_t* SvgBackend::createSurface() { - assert(!_closure); - _closure = new PdfSvgClosure(canvas); - surface = cairo_svg_surface_create_for_stream(PdfSvgClosure::writeVec, _closure, width, height); +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; } -cairo_surface_t* SvgBackend::recreateSurface() { - cairo_surface_finish(surface); - delete _closure; - _closure = nullptr; - cairo_surface_destroy(surface); - - return createSurface(); +void SvgBackend::destroySurface() { + if (surface) { + cairo_surface_finish(surface); + cairo_surface_destroy(surface); + surface = nullptr; + assert(_closure); + delete _closure; + _closure = nullptr; + } } - -Nan::Persistent SvgBackend::constructor; - -void SvgBackend::Initialize(Local target) { - Nan::HandleScope scope; - - Local ctor = Nan::New(SvgBackend::New); - SvgBackend::constructor.Reset(ctor); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - ctor->SetClassName(Nan::New("SvgBackend").ToLocalChecked()); - Nan::Set(target, - Nan::New("SvgBackend").ToLocalChecked(), - Nan::GetFunction(ctor).ToLocalChecked()).Check(); -} - -NAN_METHOD(SvgBackend::New) { - init(info); +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 index 6377b438b..f44842690 100644 --- a/src/backend/SvgBackend.h +++ b/src/backend/SvgBackend.h @@ -2,23 +2,21 @@ #include "Backend.h" #include "../closure.h" -#include +#include -class SvgBackend : public Backend +class SvgBackend : public Napi::ObjectWrap, public Backend { private: - cairo_surface_t* createSurface(); - cairo_surface_t* recreateSurface(); + cairo_surface_t* ensureSurface(); + void destroySurface(); + cairo_surface_t* surface = nullptr; public: PdfSvgClosure* _closure = NULL; inline PdfSvgClosure* closure() { return _closure; } - SvgBackend(int width, int height); + SvgBackend(Napi::CallbackInfo& info); ~SvgBackend(); - static Backend *construct(int width, int height); - static Nan::Persistent constructor; - static void Initialize(v8::Local target); - static NAN_METHOD(New); + static void Initialize(Napi::Object target); }; diff --git a/src/bmp/BMPParser.cc b/src/bmp/BMPParser.cc index d2c2ddbb3..9058be8af 100644 --- a/src/bmp/BMPParser.cc +++ b/src/bmp/BMPParser.cc @@ -1,6 +1,7 @@ #include "BMPParser.h" #include +#include using namespace std; using namespace BMPParser; @@ -384,7 +385,8 @@ string Parser::getErrMsg() const{ template inline T Parser::get(){ if(check) CHECK_OVERRUN(ptr, sizeof(T), T); - T val = *(T*)ptr; + T val; + std::memcpy(&val, ptr, sizeof(T)); ptr += sizeof(T); return val; } diff --git a/src/closure.cc b/src/closure.cc index e821e7f22..3290db2e5 100644 --- a/src/closure.cc +++ b/src/closure.cc @@ -1,4 +1,5 @@ #include "closure.h" +#include "Canvas.h" #ifdef HAVE_JPEG void JpegClosure::init_destination(j_compress_ptr cinfo) { @@ -24,3 +25,28 @@ void JpegClosure::term_destination(j_compress_ptr cinfo) { } #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 3126114eb..ce5ec489c 100644 --- a/src/closure.h +++ b/src/closure.h @@ -8,7 +8,7 @@ #include #endif -#include +#include #include #include // node < 7 uses libstdc++ on macOS which lacks complete c++11 #include @@ -23,7 +23,7 @@ struct Closure { std::vector vec; - Nan::Callback cb; + Napi::FunctionReference cb; Canvas* canvas = nullptr; cairo_status_t status = CAIRO_STATUS_SUCCESS; @@ -79,3 +79,15 @@ struct JpegClosure : Closure { } }; #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; + + private: + void (*work_fn)(Closure*) = nullptr; + Closure* closure = nullptr; +}; diff --git a/src/color.cc b/src/color.cc index 8c41f1135..f82629460 100644 --- a/src/color.cc +++ b/src/color.cc @@ -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; } @@ -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; \ @@ -609,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; } @@ -630,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)); @@ -721,6 +740,7 @@ rgba_from_hex_string(const char *str, short *ok) { static int32_t rgba_from_name_string(const char *str, short *ok) { + WHITESPACE; std::string lowered(str); std::transform(lowered.begin(), lowered.end(), lowered.begin(), tolower); auto color = named_colors.find(lowered); @@ -747,6 +767,7 @@ rgba_from_name_string(const char *str, short *ok) { int32_t rgba_from_string(const char *str, short *ok) { + WHITESPACE; if ('#' == str[0]) return rgba_from_hex_string(++str, ok); if (str == strstr(str, "rgba")) diff --git a/src/init.cc b/src/init.cc index 816ba5837..ad9207846 100644 --- a/src/init.cc +++ b/src/init.cc @@ -21,27 +21,47 @@ #include "CanvasRenderingContext2d.h" #include "Image.h" #include "ImageData.h" +#include "InstanceData.h" #include #include FT_FREETYPE_H -using namespace v8; +/* + * 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) { - Backends::Initialize(target); - 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()); - Nan::Set(target, Nan::New("cairoVersion").ToLocalChecked(), Nan::New(cairo_version_string()).ToLocalChecked()).Check(); + 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); + + 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 @@ -67,26 +87,30 @@ NAN_MODULE_INIT(init) { } else { snprintf(jpeg_version, 10, "%d", JPEG_LIB_VERSION_MAJOR); } - Nan::Set(target, Nan::New("jpegVersion").ToLocalChecked(), Nan::New(jpeg_version).ToLocalChecked()).Check(); + 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); - Nan::Set(target, Nan::New("gifVersion").ToLocalChecked(), Nan::New(gif_version).ToLocalChecked()).Check(); + exports.Set("gifVersion", Napi::String::New(env, gif_version)); #else - Nan::Set(target, Nan::New("gifVersion").ToLocalChecked(), Nan::New(GIF_LIB_VERSION).ToLocalChecked()).Check(); + exports.Set("gifVersion", Napi::String::New(env, GIF_LIB_VERSION)); #endif #endif #ifdef HAVE_RSVG - Nan::Set(target, Nan::New("rsvgVersion").ToLocalChecked(), Nan::New(LIBRSVG_VERSION).ToLocalChecked()).Check(); + 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); - Nan::Set(target, Nan::New("freetypeVersion").ToLocalChecked(), Nan::New(freetype_version).ToLocalChecked()).Check(); + 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 1b8632dd2..ae2ece584 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -8,6 +8,7 @@ #include #elif defined(_WIN32) #include +#include #else #include #endif @@ -94,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); @@ -131,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 @@ -193,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 */ @@ -202,8 +214,47 @@ 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); @@ -216,7 +267,8 @@ get_pango_font_description(unsigned char* filepath) { return NULL; } - pango_font_description_set_family_static(desc, family); + 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)); @@ -227,7 +279,6 @@ get_pango_font_description(unsigned char* filepath) { return desc; } } - pango_font_description_free(desc); return NULL; @@ -245,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 @@ -273,7 +330,12 @@ deregister_font(unsigned char *filepath) { CFURLRef filepathUrl = CFURLCreateFromFileSystemRepresentation(NULL, filepath, strlen((char*)filepath), false); success = CTFontManagerUnregisterFontsForURL(filepathUrl, kCTFontManagerScopeProcess, NULL); #elif defined(_WIN32) - success = RemoveFontResourceExA((LPCSTR)filepath, FR_PRIVATE, 0) != 0; + 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; diff --git a/test/canvas.test.js b/test/canvas.test.js index c2a2eda80..4655c31c7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -14,98 +14,38 @@ const { createCanvas, createImageData, loadImage, - parseFont, registerFont, - Canvas + Canvas, + deregisterAllFonts } = require('../') +function assertApprox(actual, expected, tol) { + assert(Math.abs(expected - actual) <= tol, + "Expected " + actual + " to be " + expected + " +/- " + tol); +} + describe('Canvas', function () { // 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 }, /incompatible receiver/) + assert.throws(function () { Canvas.prototype.width }, /invalid argument/i) assert(!c.hasOwnProperty('width')) assert('width' in c) assert('width' in Canvas.prototype) }) - it('.parseFont()', function () { - const 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' }, // TODO I think this is a bad assertion - ZB 23-Jul-2017 - '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' }, - 'normal normal normal 16px Impact', - { size: 16, unit: 'px', weight: 'normal', family: 'Impact', style: 'normal', variant: 'normal' }, - 'italic small-caps bolder 16px cursive', - { size: 16, unit: 'px', style: 'italic', variant: 'small-caps', weight: 'bolder', family: 'cursive' }, - '20px "new century schoolbook", serif', - { size: 20, unit: 'px', family: 'new century schoolbook,serif' }, - '20px "Arial bold 300"', // synthetic case with weight keyword inside family - { size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' } - ] - - for (let i = 0, len = tests.length; i < len; ++i) { - const str = tests[i++] - const expected = tests[i] - const actual = parseFont(str) - - if (!expected.style) expected.style = 'normal' - if (!expected.weight) expected.weight = 'normal' - if (!expected.stretch) expected.stretch = 'normal' - if (!expected.variant) expected.variant = 'normal' - - assert.deepEqual(actual, expected, 'Failed to parse: ' + str) - } - - assert.strictEqual(parseFont('Helvetica, sans'), undefined) - }) - 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('color serialization', function () { const canvas = createCanvas(200, 200) @@ -152,6 +92,13 @@ describe('Canvas', function () { ctx.fillStyle = '#FGG' assert.equal('#ff0000', ctx.fillStyle) + ctx.fillStyle = ' #FCA' + assert.equal('#ffccaa', ctx.fillStyle) + + ctx.fillStyle = ' #ffccaa' + assert.equal('#ffccaa', ctx.fillStyle) + + ctx.fillStyle = '#fff' ctx.fillStyle = 'afasdfasdf' assert.equal('#ffffff', ctx.fillStyle) @@ -196,6 +143,95 @@ describe('Canvas', function () { ctx.fillStyle = 'rgba(0, 0, 0, 42.42)' assert.equal('#000000', ctx.fillStyle) + ctx.fillStyle = 'rgba(255, 250, 255)'; + assert.equal('#fffaff', ctx.fillStyle); + + ctx.fillStyle = 'rgba(124, 58, 26, 0)'; + assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); + + ctx.fillStyle = 'rgba( 255, 200, 90, 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90, 50 %)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90, 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90, 10 %)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90 / 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90 / 0.5)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255, 200, 90 / 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255 200 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgba( 255 200 90 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb(0, 0, 0, 42.42)' + assert.equal('#000000', ctx.fillStyle) + + ctx.fillStyle = 'rgb(255, 250, 255)'; + assert.equal('#fffaff', ctx.fillStyle); + + ctx.fillStyle = 'rgb(124, 58, 26, 0)'; + assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); + + ctx.fillStyle = 'rgb( 255, 200, 90, 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90, 50 %)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90, 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90, 10 %)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90 / 40%)' + assert.equal('rgba(255, 200, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90 / 0.5)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 200, 90 / 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255 200 90 / 10%)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255 200 90 0.1)' + assert.equal('rgba(255, 200, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = ' rgb( 255 100 90 0.1)' + assert.equal('rgba(255, 100, 90, 0.10)', ctx.fillStyle) + + ctx.fillStyle = 'rgb(124.00, 58, 26, 0)'; + assert.equal('rgba(124, 58, 26, 0.00)', ctx.fillStyle); + + ctx.fillStyle = 'rgb( 255, 200.09, 90, 40%)' + assert.equal('rgba(255, 201, 90, 0.40)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255.00, 199.03, 90, 50 %)' + assert.equal('rgba(255, 200, 90, 0.50)', ctx.fillStyle) + + ctx.fillStyle = 'rgb( 255, 300.09, 90, 40%)' + assert.equal('rgba(255, 255, 90, 0.40)', ctx.fillStyle) // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' @@ -219,6 +255,9 @@ describe('Canvas', function () { ctx.fillStyle = 'hsl(237, 76%, 25%)' assert.equal('#0f1470', ctx.fillStyle) + ctx.fillStyle = ' hsl(0, 150%, 150%)' + assert.equal('#ffffff', ctx.fillStyle) + ctx.fillStyle = 'hsl(240, 73%, 25%)' assert.equal('#11116e', ctx.fillStyle) @@ -580,6 +619,11 @@ describe('Canvas', function () { 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) @@ -709,6 +753,11 @@ describe('Canvas', function () { 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 () { @@ -937,20 +986,79 @@ describe('Canvas', function () { let metrics = ctx.measureText('Alphabet') // Actual value depends on font library version. Have observed values // between 0 and 0.769. - assert.ok(metrics.alphabeticBaseline >= 0 && metrics.alphabeticBaseline <= 1) + assertApprox(metrics.alphabeticBaseline, 0.5, 0.5) // Positive = going up from the baseline assert.ok(metrics.actualBoundingBoxAscent > 0) // Positive = going down from the baseline - assert.ok(metrics.actualBoundingBoxDescent > 0) // ~4-5 + assertApprox(metrics.actualBoundingBoxDescent, 5, 2) ctx.textBaseline = 'bottom' metrics = ctx.measureText('Alphabet') assert.strictEqual(ctx.textBaseline, 'bottom') - assert.ok(metrics.alphabeticBaseline > 0) // ~4-5 + 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 () { @@ -1174,6 +1282,430 @@ describe('Canvas', function () { 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 @@ -1189,6 +1721,68 @@ describe('Canvas', 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 () { @@ -1357,6 +1951,14 @@ describe('Canvas', function () { assert.strictEqual(pattern.toString(), '[object CanvasPattern]') }) + it('CanvasPattern has class string of `CanvasPattern`', async function () { + const img = await loadImage(path.join(__dirname, '/fixtures/checkers.png')); + const canvas = createCanvas(20, 20) + const ctx = canvas.getContext('2d') + const pattern = ctx.createPattern(img) + assert.strictEqual(Object.prototype.toString.call(pattern), '[object CanvasPattern]') + }) + it('Context2d#createLinearGradient()', function () { const canvas = createCanvas(20, 1) const ctx = canvas.getContext('2d') @@ -1386,6 +1988,11 @@ describe('Canvas', function () { assert.equal(0, imageData.data[i + 2]) assert.equal(255, imageData.data[i + 3]) }) + it('Canvas has class string of `HTMLCanvasElement`', function () { + const canvas = createCanvas(20, 1) + + assert.strictEqual(Object.prototype.toString.call(canvas), '[object HTMLCanvasElement]') + }) it('CanvasGradient stringifies as [object CanvasGradient]', function () { const canvas = createCanvas(20, 1) @@ -1394,6 +2001,13 @@ describe('Canvas', function () { assert.strictEqual(gradient.toString(), '[object CanvasGradient]') }) + it('CanvasGradient has class string of `CanvasGradient`', function () { + const canvas = createCanvas(20, 1) + const ctx = canvas.getContext('2d') + const gradient = ctx.createLinearGradient(1, 1, 19, 1) + assert.strictEqual(Object.prototype.toString.call(gradient), '[object CanvasGradient]') + }) + describe('Context2d#putImageData()', function () { it('throws for invalid arguments', function () { const canvas = createCanvas(2, 1) @@ -1402,6 +2016,18 @@ describe('Canvas', function () { assert.throws(function () { ctx.putImageData(undefined, 0, 0) }, TypeError) }) + it('throws if canvas is a PDF canvas (#1853)', function () { + const canvas = createCanvas(3, 6, 'pdf') + 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) + assert.throws(() => { + ctx.putImageData(srcImageData, -1, -1) + }) + }) + it('works for negative source values', function () { const canvas = createCanvas(2, 2) const ctx = canvas.getContext('2d') @@ -1837,4 +2463,104 @@ describe('Canvas', function () { 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 index 2c29e73eb..71be6d59e 100644 --- a/test/dommatrix.test.js +++ b/test/dommatrix.test.js @@ -586,4 +586,68 @@ describe('DOMMatrix', function () { '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/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/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 8d54dd90f..edae78beb 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -24,12 +24,17 @@ const bmpDir = path.join(__dirname, '/fixtures/bmp') describe('Image', function () { 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 }, /incompatible receiver/) + 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) @@ -177,7 +182,7 @@ describe('Image', function () { it('returns a nice, coded error for fopen failures', function (done) { const img = new Image() img.onerror = err => { - assert.equal(err.code, 'ENOENT') + 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) @@ -511,6 +516,24 @@ describe('Image', function () { 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) diff --git a/test/imageData.test.js b/test/imageData.test.js index d3c84c29a..774bcf14e 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -9,7 +9,7 @@ const assert = require('assert') describe('ImageData', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { - assert.throws(function () { ImageData.prototype.width }, /incompatible receiver/) + assert.throws(function () { ImageData.prototype.width }, /invalid argument/i) }) it('stringifies as [object ImageData]', function () { @@ -17,6 +17,11 @@ describe('ImageData', function () { 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(() => { createImageData(0, 0) }, /width is zero/) assert.throws(() => { createImageData(1, 0) }, /height is zero/) diff --git a/test/public/tests.js b/test/public/tests.js index bbf3c6050..582c0ce28 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -3,6 +3,8 @@ let Image let imageSrc const tests = {} +/* global btoa */ + if (typeof module !== 'undefined' && module.exports) { module.exports = tests Image = require('../../').Image @@ -95,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() @@ -146,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) @@ -471,6 +544,24 @@ tests['createPattern() with globalAlpha'] = function (ctx, 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 () { @@ -2294,6 +2385,28 @@ tests['putImageData() 10'] = function (ctx) { 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) @@ -2574,7 +2687,8 @@ tests['measureText()'] = function (ctx) { const metrics = ctx.measureText(text) ctx.strokeStyle = 'blue' ctx.strokeRect( - x - metrics.actualBoundingBoxLeft + 0.5, + // 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 @@ -2593,8 +2707,24 @@ tests['measureText()'] = function (ctx) { 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) { @@ -2651,3 +2781,59 @@ tests['transformed drawimage'] = function (ctx) { 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/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/types/Readme.md b/types/Readme.md deleted file mode 100644 index 4beb7f528..000000000 --- a/types/Readme.md +++ /dev/null @@ -1,3 +0,0 @@ -Notes: - -* `"unified-signatures": false` because of https://github.com/Microsoft/dtslint/issues/183 diff --git a/types/test.ts b/types/test.ts deleted file mode 100644 index 8cff19419..000000000 --- a/types/test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as Canvas from 'canvas' - -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) -const h: number = id.height - -ctx.currentTransform = ctx.getTransform() - -ctx.quality = 'best' -ctx.textDrawingMode = 'glyph' -ctx.globalCompositeOperation = 'saturate' - -const grad: Canvas.CanvasGradient = ctx.createLinearGradient(0, 1, 2, 3) -grad.addColorStop(0.1, 'red') - -const dm = new Canvas.DOMMatrix([1, 2, 3, 4, 5, 6]) -const a: number = dm.a - -const b1: Buffer = canv.toBuffer() -canv.toBuffer("application/pdf") -canv.toBuffer((err, data) => {}, "image/png") -canv.createJPEGStream({quality: 0.5}) -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.ImageData = Canvas.createImageData(new Uint16Array(4), 1) - -ctx.drawImage(canv, 0, 0) diff --git a/types/tsconfig.json b/types/tsconfig.json deleted file mode 100644 index 226482c23..000000000 --- a/types/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "lib": ["es6"], - "noImplicitAny": true, - "noImplicitThis": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "noEmit": true, - "baseUrl": ".", - "paths": { "canvas": ["."] } - } -} diff --git a/types/tslint.json b/types/tslint.json deleted file mode 100644 index 64e2a316f..000000000 --- a/types/tslint.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "dtslint/dtslint.json", - "rules": { - "semicolon": false, - "unified-signatures": false - } -}