From 83a8df1592ebf442dcce3af659f3eff6a57f5087 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 30 Dec 2021 18:08:22 -0800 Subject: [PATCH 001/128] Add missing cctype include for toupper Fixes #1848 --- CHANGELOG.md | 1 + src/Util.h | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e87890de5..467e95ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed * Stringify CanvasGradient, CanvasPattern and ImageData like browsers do. (#1639, #1646) +* Add missing include for `toupper`. 2.9.0 ================== diff --git a/src/Util.h b/src/Util.h index 8f935359d..dba6883a2 100644 --- a/src/Util.h +++ b/src/Util.h @@ -2,6 +2,7 @@ #include #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 From 0bccc05b384cc1f3e969cd9ed7859a95376ec88c Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 30 Dec 2021 18:40:50 -0800 Subject: [PATCH 002/128] bug: fix process crash in getImageData for PDF/SVG canvases Fixes #1853 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 8 ++++---- test/canvas.test.js | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 467e95ba2..84f155f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### 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) 2.9.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index f3415a7ed..b98fe4520 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -937,8 +937,8 @@ NAN_METHOD(Context2d::PutImageData) { } #endif default: { - Nan::ThrowError("Invalid pixel format"); - break; + Nan::ThrowError("Invalid pixel format or not an image canvas"); + return; } } @@ -1111,8 +1111,8 @@ NAN_METHOD(Context2d::GetImageData) { #endif default: { // Unlikely - Nan::ThrowError("Invalid pixel format"); - break; + Nan::ThrowError("Invalid pixel format or not an image canvas"); + return; } } diff --git a/test/canvas.test.js b/test/canvas.test.js index c2a2eda80..b4c8eb7e1 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1189,6 +1189,14 @@ 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) + }) + }) }) it('Context2d#createPattern(Canvas)', function () { @@ -1402,6 +1410,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') From 6fd0fa55343aa244e7097f6ee50b271a8636ee63 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 24 Feb 2022 14:12:43 -0500 Subject: [PATCH 003/128] make types compatible with typescript 4.6 (#1986) --- CHANGELOG.md | 1 + types/index.d.ts | 6 ------ types/test.ts | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f155f0d..1e33918a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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 2.9.0 ================== diff --git a/types/index.d.ts b/types/index.d.ts index c667dfbf1..f613281e2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -166,12 +166,6 @@ declare class NodeCanvasRenderingContext2D extends CanvasRenderingContext2D { */ 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' diff --git a/types/test.ts b/types/test.ts index 8cff19419..bfbe26429 100644 --- a/types/test.ts +++ b/types/test.ts @@ -16,7 +16,6 @@ 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') From 009d5942c6d0a1f1c1ec421e701bd62e3334409a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 9 Feb 2022 23:16:28 +0000 Subject: [PATCH 004/128] replace some remaining glib calls --- src/Canvas.cc | 10 ++++----- src/register_font.cc | 48 +++++++++++--------------------------------- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index bd39d4329..9270031f2 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -706,9 +706,9 @@ NAN_METHOD(Canvas::StreamJPEGSync) { 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)); + return strdup(*Nan::Utf8String(val)); } else if (fallback) { - return g_strdup(fallback); + return strdup(fallback); } else { return NULL; } @@ -765,9 +765,9 @@ NAN_METHOD(Canvas::RegisterFont) { Nan::ThrowError(GENERIC_FACE_ERROR); } - g_free(family); - g_free(weight); - g_free(style); + free(family); + free(weight); + free(style); } NAN_METHOD(Canvas::DeregisterAllFonts) { diff --git a/src/register_font.cc b/src/register_font.cc index 1b8632dd2..927a66b4b 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -94,35 +94,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 +108,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 From ddce10f478a7fe15a312e17dd41d1225efcead6d Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 7 Aug 2021 15:53:55 +0000 Subject: [PATCH 005/128] select fonts via postscript name on Linux greatly improves font matching accuracy Fixes #1572 --- CHANGELOG.md | 1 + src/register_font.cc | 69 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e33918a2..3f2a55d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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) 2.9.0 ================== diff --git a/src/register_font.cc b/src/register_font.cc index 927a66b4b..18ba0252d 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -1,5 +1,6 @@ #include "register_font.h" +#include #include #include #include @@ -10,6 +11,7 @@ #include #else #include +#include #endif #include @@ -32,11 +34,29 @@ #define PREFERRED_ENCODING_ID TT_MS_ID_UNICODE_CS #endif +// With PangoFcFontMaps (the pango font module on Linux) we're able to add a +// hook that lets us get perfect matching. Tie the conditions for enabling that +// feature to one variable +#if !defined(__APPLE__) && !defined(_WIN32) && PANGO_VERSION_CHECK(1, 47, 0) +#define PERFECT_MATCHES_ENABLED +#endif + #define IS_PREFERRED_ENC(X) \ X.platform_id == PREFERRED_PLATFORM_ID && X.encoding_id == PREFERRED_ENCODING_ID +#ifdef PERFECT_MATCHES_ENABLED +// On Linux-like OSes using FontConfig, the PostScript name ranks higher than +// preferred family and family name since we'll use it to get perfect font +// matching (see fc_font_map_substitute_hook) +#define GET_NAME_RANK(X) \ + ((IS_PREFERRED_ENC(X) ? 1 : 0) << 2) | \ + ((X.name_id == TT_NAME_ID_PS_NAME ? 1 : 0) << 1) | \ + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) +#else #define GET_NAME_RANK(X) \ - (IS_PREFERRED_ENC(X) ? 1 : 0) + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) + ((IS_PREFERRED_ENC(X) ? 1 : 0) << 1) | \ + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) +#endif /* * Return a UTF-8 encoded string given a TrueType name buf+len @@ -104,15 +124,31 @@ get_family_name(FT_Face face) { for (unsigned i = 0; i < FT_Get_Sfnt_Name_Count(face); ++i) { FT_Get_Sfnt_Name(face, i, &name); - if (name.name_id == TT_NAME_ID_FONT_FAMILY || name.name_id == TT_NAME_ID_PREFERRED_FAMILY) { - char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); + if ( + name.name_id == TT_NAME_ID_FONT_FAMILY || +#ifdef PERFECT_MATCHES_ENABLED + name.name_id == TT_NAME_ID_PS_NAME || +#endif + name.name_id == TT_NAME_ID_PREFERRED_FAMILY + ) { + int rank = GET_NAME_RANK(name); - if (buf) { - int rank = GET_NAME_RANK(name); - if (rank > best_rank) { + if (rank > best_rank) { + char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); + if (buf) { best_rank = rank; if (best_buf) free(best_buf); best_buf = buf; + +#ifdef PERFECT_MATCHES_ENABLED + // Prepend an '@' to the postscript name + if (name.name_id == TT_NAME_ID_PS_NAME) { + std::string best_buf_modified = "@"; + best_buf_modified += best_buf; + free(best_buf); + best_buf = strdup(best_buf_modified.c_str()); + } +#endif } else { free(buf); } @@ -209,6 +245,21 @@ get_pango_font_description(unsigned char* filepath) { return NULL; } +#ifdef PERFECT_MATCHES_ENABLED +static void +fc_font_map_substitute_hook(FcPattern *pat, gpointer data) { + FcChar8 *family; + + for (int i = 0; FcPatternGetString(pat, FC_FAMILY, i, &family) == FcResultMatch; i++) { + if (family[0] == '@') { + FcPatternAddString(pat, FC_POSTSCRIPT_NAME, (FcChar8 *)family + 1); + FcPatternRemove(pat, FC_FAMILY, i); + i -= 1; + } + } +} +#endif + /* * Register font with the OS */ @@ -233,6 +284,12 @@ register_font(unsigned char *filepath) { // font families. pango_cairo_font_map_set_default(NULL); +#ifdef PERFECT_MATCHES_ENABLED + PangoFontMap* map = pango_cairo_font_map_get_default(); + PangoFcFontMap* fc_map = PANGO_FC_FONT_MAP(map); + pango_fc_font_map_set_default_substitute(fc_map, fc_font_map_substitute_hook, NULL, NULL); +#endif + return true; } From d7c4673c4539235a7f583495f58cbf0b282eeb1e Mon Sep 17 00:00:00 2001 From: tignear Date: Mon, 9 Aug 2021 01:13:39 +0900 Subject: [PATCH 006/128] Add support for multi-byte font path on Windows --- CHANGELOG.md | 1 + .../pfennigMultiByte\360\237\232\200.ttf" | Bin 0 -> 308868 bytes src/register_font.cc | 93 +++++++++++++++++- test/canvas.test.js | 10 +- 4 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 "examples/pfennigFont/pfennigMultiByte\360\237\232\200.ttf" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2a55d0d..e6d13bf0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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. 2.9.0 ================== diff --git "a/examples/pfennigFont/pfennigMultiByte\360\237\232\200.ttf" "b/examples/pfennigFont/pfennigMultiByte\360\237\232\200.ttf" new file mode 100644 index 0000000000000000000000000000000000000000..bbbe4636d3ae6e45b9964c2dd87cadd2a646a78b GIT binary patch literal 308868 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Ì>PVhnKZMoah4}SDSp8jyUq6J^ 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 #elif defined(_WIN32) #include +#include #else #include #include @@ -205,6 +206,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 */ @@ -214,8 +250,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, + NULL, + 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); @@ -239,7 +314,6 @@ get_pango_font_description(unsigned char* filepath) { return desc; } } - pango_font_description_free(desc); return NULL; @@ -272,7 +346,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 @@ -306,7 +386,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 b4c8eb7e1..e8998fa1f 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -16,7 +16,8 @@ const { loadImage, parseFont, registerFont, - Canvas + Canvas, + deregisterAllFonts } = require('../') describe('Canvas', function () { @@ -105,7 +106,12 @@ describe('Canvas', 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) From 427377178eb7f660fc924bc8a7f71a2ab3109511 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 19 Mar 2022 11:01:24 -0700 Subject: [PATCH 007/128] Fix CI Use latest setup-node and pin to Windows 2019. Windows 2022 needs dealing with later. --- .github/workflows/ci.yaml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e565aa439..173955036 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: matrix: node: [10, 12, 14, 16] steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - uses: actions/checkout@v2 @@ -30,12 +30,12 @@ jobs: Windows: name: Test on Windows - runs-on: windows-latest + runs-on: windows-2019 strategy: matrix: node: [10, 12, 14, 16] steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - uses: actions/checkout@v2 @@ -45,6 +45,8 @@ jobs: 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 + npm install -g node-gyp@latest + 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 @@ -57,7 +59,7 @@ jobs: matrix: node: [10, 12, 14, 16] steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - uses: actions/checkout@v2 @@ -68,13 +70,13 @@ jobs: - 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@v3 with: node-version: 14 - uses: actions/checkout@v2 From 9d8da5bf1a272ee3e14637feeef545b622822a03 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 19 Mar 2022 11:44:31 -0700 Subject: [PATCH 008/128] v2.9.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d13bf0e..b32b16f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed + +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) diff --git a/package.json b/package.json index b833e7168..5f382675e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.9.0", + "version": "2.9.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 3f3af3af3d590869b799a7ae630854623147c7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=2E=20Rom=C3=A1n?= Date: Thu, 16 Jun 2022 10:18:03 +0200 Subject: [PATCH 009/128] fix: resolved inconsistent exports in ESM (#2047) * feat: add ESM support * docs: updated CHANGELOG * refactor: destructure once Co-authored-by: Mohammed Keyvanzadeh * fix: use `exports.[name] = value` instead Co-authored-by: Mohammed Keyvanzadeh --- CHANGELOG.md | 1 + index.js | 64 +++++++++++++++++++++++++--------------------------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b32b16f52..f104c3d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Fixed ESM exports. 2.9.1 ================== diff --git a/index.js b/index.js index 4d11d9a81..0cb14a1ed 100644 --- a/index.js +++ b/index.js @@ -55,40 +55,38 @@ 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 +exports.parseFont = parseFont - 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 From 1f2b156a2c1da29118d55c2a1747a04a87e02d7a Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Tue, 21 Jun 2022 01:15:11 +0200 Subject: [PATCH 010/128] Replace binary for rebuild cases (#1982) --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f104c3d9b..6f2fc10d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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/package.json b/package.json index 5f382675e..55d95d2ed 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test": "mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js", - "install": "node-pre-gyp install --fallback-to-build", + "install": "node-pre-gyp install --fallback-to-build --update-binary", "dtslint": "dtslint types" }, "binary": { From d4dc2a87c3843b44dfdb8e26c738c5f38e4cadf8 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 23 Jun 2022 15:08:19 -0700 Subject: [PATCH 011/128] v2.9.2 --- CHANGELOG.md | 7 ++++++- package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f2fc10d8..611b485d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,12 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed -* Fixed ESM exports. + +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 ================== diff --git a/package.json b/package.json index 55d95d2ed..e0cb3777b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.9.1", + "version": "2.9.2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 6fa9f38e00b9fd30332cc1765fb13d473eb0184b Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Fri, 24 Jun 2022 15:07:37 +0000 Subject: [PATCH 012/128] improve multi-family output in font desc resolver the problem was exposed by #1987, but was always there fixes #2041 --- CHANGELOG.md | 1 + src/Canvas.cc | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 611b485d6..f55b6e50f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Wrong fonts used when calling `registerFont` multiple times with the same family name (#2041) 2.9.2 ================== diff --git a/src/Canvas.cc b/src/Canvas.cc index 9270031f2..3e339f033 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -879,18 +879,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; } } From 7a8a60661ff13c744010996e9b75ff4bcaffb496 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 24 Jun 2022 12:22:28 -0700 Subject: [PATCH 013/128] v2.9.3 --- CHANGELOG.md | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f55b6e50f..583abd886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,11 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed -* Wrong fonts used when calling `registerFont` multiple times with the same family name (#2041) + +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 ================== diff --git a/package.json b/package.json index e0cb3777b..e8a80dab7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.9.2", + "version": "2.9.3", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 64fdf185dc898837973b2613887442021d3b3c46 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 27 Jun 2022 10:07:40 -0400 Subject: [PATCH 014/128] export pangoVersion to help debugging --- CHANGELOG.md | 1 + index.js | 2 ++ src/init.cc | 2 ++ 3 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 583abd886..6672a88ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* Export `pangoVersion` ### Added ### Fixed diff --git a/index.js b/index.js index 0cb14a1ed..f605077f3 100644 --- a/index.js +++ b/index.js @@ -90,3 +90,5 @@ exports.gifVersion = bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g exports.freetypeVersion = bindings.freetypeVersion /** rsvg version. */ exports.rsvgVersion = bindings.rsvgVersion +/** pango version. */ +exports.pangoVersion = bindings.pangoVersion diff --git a/src/init.cc b/src/init.cc index 816ba5837..fd143973e 100644 --- a/src/init.cc +++ b/src/init.cc @@ -84,6 +84,8 @@ NAN_MODULE_INIT(init) { Nan::Set(target, Nan::New("rsvgVersion").ToLocalChecked(), Nan::New(LIBRSVG_VERSION).ToLocalChecked()).Check(); #endif + Nan::Set(target, Nan::New("pangoVersion").ToLocalChecked(), Nan::New(PANGO_VERSION_STRING).ToLocalChecked()).Check(); + 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(); From b0d4f44b5acf148b9b0a28f2354635a2eabc5b68 Mon Sep 17 00:00:00 2001 From: Marco Antonio Dominguez Date: Wed, 6 Jul 2022 21:12:45 -0400 Subject: [PATCH 015/128] Update instructions for OSX local build While testing the solution for a local build, I got the next output: ```sh Package pixman-1 was not found in the pkg-config search path. Perhaps you should add the directory containing `pixman-1.pc' to the PKG_CONFIG_PATH environment variable No package 'pixman-1' found gyp: Call to 'pkg-config pixman-1 --libs' returned exit status 1 while in binding.gyp. while trying to load binding.gyp ``` After updating the command to the following: `brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman`, I was able to generate a succesful build. ```sh brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman; cd /node_modules/canva && npx node-gyp rebuild; ``` --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index b27549962..992904ce3 100644 --- a/Readme.md +++ b/Readme.md @@ -23,7 +23,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` +OS X | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman` 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` From 52551952c3d78ff12110880a2101ab980dd7bf6c Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 6 Jul 2022 21:49:46 -0700 Subject: [PATCH 016/128] Parse rgba(r,g,b) correctly `rgb()` and `rgba()` are supposed to have identical grammar and behavior: https://www.w3.org/TR/css-color-4/#rgb-functions. Fixes #2029 --- CHANGELOG.md | 1 + src/color.cc | 5 +++-- test/canvas.test.js | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6672a88ae..85f8c6e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Export `pangoVersion` ### Added ### Fixed +* `rgba(r,g,b)` with no alpha should parse as opaque, not transparent. ([#2029](https://github.com/Automattic/node-canvas/issues/2029)) 2.9.3 ================== diff --git a/src/color.cc b/src/color.cc index 8c41f1135..1ea96e195 100644 --- a/src/color.cc +++ b/src/color.cc @@ -225,12 +225,13 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { #define LIGHTNESS(NAME) SATURATION(NAME) #define ALPHA(NAME) \ - if (*str >= '1' && *str <= '9') { \ + if (*str >= '1' && *str <= '9') { \ NAME = 1; \ } else { \ if ('0' == *str) ++str; \ if ('.' == *str) { \ ++str; \ + NAME = 0; \ float n = .1f; \ while (*str >= '0' && *str <= '9') { \ NAME += (*str++ - '0') * n; \ @@ -630,7 +631,7 @@ 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); diff --git a/test/canvas.test.js b/test/canvas.test.js index e8998fa1f..a81de892e 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -202,6 +202,9 @@ 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); + // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' From f8d4949cfbea3d764bbcced947ac248c0d6014a7 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 7 Jul 2022 09:22:01 -0700 Subject: [PATCH 017/128] Fix FITLER/FILTER typo in index.d.ts Fixes #2072 --- CHANGELOG.md | 1 + types/index.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85f8c6e42..8437833d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### 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)) 2.9.3 ================== diff --git a/types/index.d.ts b/types/index.d.ts index f613281e2..04691c4ed 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,12 +7,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 /** From c6a154673831a37d1aebe8fcc094ebe3adf87f3e Mon Sep 17 00:00:00 2001 From: Calvin Storoschuk Date: Mon, 27 Jun 2022 21:16:58 -0400 Subject: [PATCH 018/128] fix repeat-x/y support in createPattern() --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 32 +++++++++++++++++++++++++++++++- test/public/tests.js | 18 ++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8437833d5..07322e45d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### 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)) 2.9.3 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index b98fe4520..2bd0533f5 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -367,6 +367,7 @@ 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); @@ -381,10 +382,36 @@ 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) { @@ -412,6 +439,9 @@ Context2d::fill(bool preserve) { ? shadow(cairo_fill) : cairo_fill(_context); } + if (needsRestore) { + cairo_restore(_context); + } } /* diff --git a/test/public/tests.js b/test/public/tests.js index bbf3c6050..e079ad827 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -471,6 +471,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 () { From bdc497a2b34bc22b99a8c75d1d9989f3451b7464 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 5 Aug 2022 02:52:27 -0700 Subject: [PATCH 019/128] Use node-gyp 8.x for Win CI v9 dropped Node.js v10 support. --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 173955036..3c92ea1c8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,7 +45,7 @@ jobs: 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 - npm install -g node-gyp@latest + 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 From 288f4bfa1dbd5cdb750ffaa315f07d434fe0dcf6 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 24 Jul 2022 00:24:46 -0700 Subject: [PATCH 020/128] add WPT tests --- package.json | 3 + test/wpt/drawing-text-to-the-canvas.yaml | 1061 ++++ test/wpt/fill-and-stroke-styles.yaml | 2244 +++++++++ test/wpt/generate.js | 260 + .../generated/drawing-text-to-the-canvas.js | 1122 +++++ test/wpt/generated/line-styles.js | 1136 +++++ test/wpt/generated/meta.js | 92 + test/wpt/generated/path-objects.js | 4352 +++++++++++++++++ test/wpt/generated/pixel-manipulation.js | 1448 ++++++ test/wpt/generated/shadows.js | 1203 +++++ test/wpt/generated/text-styles.js | 614 +++ test/wpt/generated/the-canvas-element.js | 273 ++ test/wpt/generated/the-canvas-state.js | 206 + test/wpt/generated/transformations.js | 675 +++ test/wpt/line-styles.yaml | 1017 ++++ test/wpt/meta.yaml | 555 +++ test/wpt/path-objects.yaml | 3646 ++++++++++++++ test/wpt/pixel-manipulation.yaml | 1145 +++++ test/wpt/shadows.yaml | 1150 +++++ test/wpt/text-styles.yaml | 525 ++ test/wpt/the-canvas-element.yaml | 169 + test/wpt/the-canvas-state.yaml | 107 + test/wpt/transformations.yaml | 402 ++ 23 files changed, 23405 insertions(+) create mode 100644 test/wpt/drawing-text-to-the-canvas.yaml create mode 100644 test/wpt/fill-and-stroke-styles.yaml create mode 100644 test/wpt/generate.js create mode 100644 test/wpt/generated/drawing-text-to-the-canvas.js create mode 100644 test/wpt/generated/line-styles.js create mode 100644 test/wpt/generated/meta.js create mode 100644 test/wpt/generated/path-objects.js create mode 100644 test/wpt/generated/pixel-manipulation.js create mode 100644 test/wpt/generated/shadows.js create mode 100644 test/wpt/generated/text-styles.js create mode 100644 test/wpt/generated/the-canvas-element.js create mode 100644 test/wpt/generated/the-canvas-state.js create mode 100644 test/wpt/generated/transformations.js create mode 100644 test/wpt/line-styles.yaml create mode 100644 test/wpt/meta.yaml create mode 100644 test/wpt/path-objects.yaml create mode 100644 test/wpt/pixel-manipulation.yaml create mode 100644 test/wpt/shadows.yaml create mode 100644 test/wpt/text-styles.yaml create mode 100644 test/wpt/the-canvas-element.yaml create mode 100644 test/wpt/the-canvas-state.yaml create mode 100644 test/wpt/transformations.yaml diff --git a/package.json b/package.json index e8a80dab7..5d4185d5e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "test": "mocha test/*.test.js", "pretest-server": "node-gyp build", "test-server": "node test/server.js", + "generate-wpt": "node ./test/wpt/generate.js", + "test-wpt": "mocha test/wpt/generated/*.js", "install": "node-pre-gyp install --fallback-to-build --update-binary", "dtslint": "dtslint types" }, @@ -57,6 +59,7 @@ "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", 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..4921d2277 --- /dev/null +++ b/test/wpt/generate.js @@ -0,0 +1,260 @@ +// 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.path.arc.nonfinite", // https://github.com/Automattic/node-canvas/issues/2055 + "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..d01c89072 --- /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.skip("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..8b1a6817e --- /dev/null +++ b/test/wpt/generated/the-canvas-element.js @@ -0,0 +1,273 @@ +// 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.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 From a484cf2d1807c67c580622370023f48f2cc00fb8 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 27 Jul 2022 13:59:27 -0700 Subject: [PATCH 021/128] fix crashes and hangs in arc() The WPT tests for this now pass. See issue for test content; I think it makes more sense to land the WPT tests than to copy individual ones into the node-canvas tests. Fixes #2055 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 39 ++++++++++++++++----------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07322e45d..1f19144f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * `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)) 2.9.3 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 2bd0533f5..2264ea6fd 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2936,35 +2936,34 @@ NAN_METHOD(Context2d::Rect) { } /* - * Adds an arc at x, y with the given radis and start/end angles. + * Adds an arc at x, y with the given radii and start/end angles. */ NAN_METHOD(Context2d::Arc) { - if (!info[0]->IsNumber() - || !info[1]->IsNumber() - || !info[2]->IsNumber() - || !info[3]->IsNumber() - || !info[4]->IsNumber()) return; + double args[5]; + if(!checkArgs(info, args, 5)) + return; - bool anticlockwise = Nan::To(info[5]).FromMaybe(false); + auto x = args[0]; + auto y = args[1]; + auto radius = args[2]; + auto startAngle = args[3]; + auto endAngle = args[4]; + + if (radius < 0) { + Nan::ThrowRangeError("The radius provided is negative."); + return; + } + + bool counterclockwise = Nan::To(info[5]).FromMaybe(false); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - 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)); + if (counterclockwise && M_PI * 2 != endAngle) { + 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); } } From 73d7893ccb44158f53d007f1918a6d9228d8137e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 27 Jul 2022 14:32:35 -0700 Subject: [PATCH 022/128] fix arc geometry calculations Borrowed from Chromium instead of reinventing the wheel. Firefox's is similar: https://searchfox.org/mozilla-central/source/gfx/2d/PathHelpers.h#127 Fixes #1736 Fixes #1808 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 53 ++++++++++++++++++++++++++++++++- test/public/tests.js | 29 ++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f19144f6..6c354c6bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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))) 2.9.3 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 2264ea6fd..831f47652 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -36,6 +36,8 @@ Nan::Persistent Context2d::constructor; double width = args[2]; \ double height = args[3]; +constexpr double twoPi = M_PI * 2.; + /* * Text baselines. */ @@ -2935,6 +2937,52 @@ NAN_METHOD(Context2d::Rect) { } } +// 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. */ @@ -2960,7 +3008,10 @@ NAN_METHOD(Context2d::Arc) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); - if (counterclockwise && M_PI * 2 != endAngle) { + canonicalizeAngle(startAngle, endAngle); + endAngle = adjustEndAngle(startAngle, endAngle, counterclockwise); + + if (counterclockwise) { cairo_arc_negative(ctx, x, y, radius, startAngle, endAngle); } else { cairo_arc(ctx, x, y, radius, startAngle, endAngle); diff --git a/test/public/tests.js b/test/public/tests.js index e079ad827..c852fcaed 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -146,6 +146,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) From 10b208e3594ba461b1e9f29798b8c2e38a5953ad Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 5 Aug 2022 03:38:26 -0700 Subject: [PATCH 023/128] un-skip 2d.path.arc.nonfinite; now fixed --- test/wpt/generate.js | 1 - test/wpt/generated/path-objects.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/wpt/generate.js b/test/wpt/generate.js index 4921d2277..74fbcb623 100644 --- a/test/wpt/generate.js +++ b/test/wpt/generate.js @@ -9,7 +9,6 @@ const yamlFiles = fs.readdirSync(__dirname).filter(f => f.endsWith(".yaml")); 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.path.arc.nonfinite", // https://github.com/Automattic/node-canvas/issues/2055 "2d.imageData.create2.negative", "2d.imageData.create2.zero", "2d.imageData.create2.nonfinite", diff --git a/test/wpt/generated/path-objects.js b/test/wpt/generated/path-objects.js index d01c89072..397ec76f6 100644 --- a/test/wpt/generated/path-objects.js +++ b/test/wpt/generated/path-objects.js @@ -1614,7 +1614,7 @@ describe("WPT: path-objects", function () { _assertPixel(canvas, 98,48, 0,255,0,255); }); - it.skip("2d.path.arc.nonfinite", function () { + it("2d.path.arc.nonfinite", function () { // arc() with Infinity/NaN is ignored const canvas = createCanvas(100, 50); const ctx = canvas.getContext("2d"); From eba1e4a7452cebddf9b3c4f5d6ff1b423c0562b5 Mon Sep 17 00:00:00 2001 From: Steve Rubin Date: Fri, 19 Aug 2022 15:26:09 -0500 Subject: [PATCH 024/128] Adds deregisterAllFonts to the typescript declaration file (#2096) * Adds deregisterAllFonts to the typescript declaration file * updates changelog with deregisterAllFonts type fix --- CHANGELOG.md | 1 + types/index.d.ts | 5 +++++ types/test.ts | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c354c6bb..bd53dde00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * `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)) 2.9.3 ================== diff --git a/types/index.d.ts b/types/index.d.ts index 04691c4ed..7b53f4851 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -300,6 +300,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()`. */ diff --git a/types/test.ts b/types/test.ts index bfbe26429..b48c78011 100644 --- a/types/test.ts +++ b/types/test.ts @@ -1,4 +1,7 @@ import * as Canvas from 'canvas' +import * as path from "path"; + +Canvas.registerFont(path.join(__dirname, '../pfennigFont/Pfennig.ttf'), {family: 'pfennigFont'}) Canvas.createCanvas(5, 10) Canvas.createCanvas(200, 200, 'pdf') @@ -39,3 +42,5 @@ img.onload = null; const id2: Canvas.ImageData = Canvas.createImageData(new Uint16Array(4), 1) ctx.drawImage(canv, 0, 0) + +Canvas.deregisterAllFonts(); From dce0fd166c387e562113a1c57b959dc4337e6682 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Fri, 5 Aug 2022 13:03:06 -0700 Subject: [PATCH 025/128] Add roundRect() support https://developer.chrome.com/blog/canvas2d/#round-rect WPT tests: 326 passing (1s) 9 pending 129 failing (down from 179) --- CHANGELOG.md | 1 + binding.gyp | 3 +- src/CanvasRenderingContext2d.cc | 174 ++++++++++++++++++++++++++++++++ src/CanvasRenderingContext2d.h | 1 + src/Point.h | 5 +- test/public/tests.js | 37 +++++++ 6 files changed, 218 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd53dde00..b6662cc62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed * Export `pangoVersion` ### Added +* [`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)) diff --git a/binding.gyp b/binding.gyp index 57f14ab8c..19a33e816 100644 --- a/binding.gyp +++ b/binding.gyp @@ -96,7 +96,8 @@ '<(GTK_Root)/lib/glib-2.0/include' ], 'defines': [ - '_USE_MATH_DEFINES' # for M_PI + '_USE_MATH_DEFINES', # for M_PI + 'NOMINMAX' # allow std::min/max to work ], 'configurations': { 'Debug': { diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 831f47652..10629cee7 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -126,6 +126,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "strokeRect", StrokeRect); Nan::SetPrototypeMethod(ctor, "clearRect", ClearRect); Nan::SetPrototypeMethod(ctor, "rect", Rect); + Nan::SetPrototypeMethod(ctor, "roundRect", RoundRect); Nan::SetPrototypeMethod(ctor, "measureText", MeasureText); Nan::SetPrototypeMethod(ctor, "moveTo", MoveTo); Nan::SetPrototypeMethod(ctor, "lineTo", LineTo); @@ -2937,6 +2938,179 @@ NAN_METHOD(Context2d::Rect) { } } +// 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 Local& v) { + if (v->IsObject()) { // 5.1 DOMPointInit + auto rx = Nan::Get(v.As(), Nan::New("x").ToLocalChecked()).ToLocalChecked(); + auto ry = Nan::Get(v.As(), Nan::New("y").ToLocalChecked()).ToLocalChecked(); + if (rx->IsNumber() && ry->IsNumber()) { + auto rxv = Nan::To(rx).FromJust(); + auto ryv = Nan::To(ry).FromJust(); + if (!std::isfinite(rxv) || !std::isfinite(ryv)) + return true; + if (rxv < 0 || ryv < 0) { + Nan::ThrowRangeError("radii must be positive."); + return true; + } + p.x = rxv; + p.y = ryv; + return false; + } + } else if (v->IsNumber()) { // 5.2 unrestricted double + auto rv = Nan::To(v).FromJust(); + if (!std::isfinite(rv)) + return true; + if (rv < 0) { + Nan::ThrowRangeError("radii must be positive."); + 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]] + */ +NAN_METHOD(Context2d::RoundRect) { + RECT_ARGS; + Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + cairo_t *ctx = context->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)) { + Nan::ThrowRangeError("radii must be a list of one, two, three or four radii."); + return; + } + // 5. For each radius of radii + for (size_t i = 0; i < nRadii; i++) { + auto r = Nan::Get(radiiList, i).ToLocalChecked(); + if (getRadius(normalizedRadii[i], r)) + 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; + } + } + + 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.x *= scale; + lowerLeft.y *= scale; + lowerLeft.y *= scale; + lowerRight.y *= scale; + lowerRight.y *= scale; + } + } + + // 12. Draw + cairo_move_to(ctx, x + upperLeft.x, y); + if (clockwise) { + cairo_line_to(ctx, x + width - upperRight.x, y); + elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0.); + cairo_line_to(ctx, x + width, y + height - lowerRight.y); + elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2.); + cairo_line_to(ctx, x + lowerLeft.x, y + height); + elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI); + cairo_line_to(ctx, x, y + upperLeft.y); + elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2.); + } else { + 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 diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 8afb433d1..89c86df67 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -104,6 +104,7 @@ class Context2d: public Nan::ObjectWrap { static NAN_METHOD(StrokeRect); static NAN_METHOD(ClearRect); static NAN_METHOD(Rect); + static NAN_METHOD(RoundRect); static NAN_METHOD(Arc); static NAN_METHOD(ArcTo); static NAN_METHOD(Ellipse); diff --git a/src/Point.h b/src/Point.h index d3228acfa..50c7b711c 100644 --- a/src/Point.h +++ b/src/Point.h @@ -2,9 +2,10 @@ #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; }; diff --git a/test/public/tests.js b/test/public/tests.js index c852fcaed..66bb14ddb 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -95,6 +95,43 @@ 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() +} + tests['lineTo()'] = function (ctx) { // Filled triangle ctx.beginPath() From 3fb4ed9d7c460666daa26decc4784661b58c833c Mon Sep 17 00:00:00 2001 From: Antoine Apollis Date: Mon, 29 Aug 2022 16:54:49 +0200 Subject: [PATCH 026/128] fix: add user agent to remote images request --- CHANGELOG.md | 1 + lib/image.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6662cc62..886602912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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 ================== diff --git a/lib/image.js b/lib/image.js index c5b594f8a..4a37849ee 100644 --- a/lib/image.js +++ b/lib/image.js @@ -49,7 +49,10 @@ Object.defineProperty(Image.prototype, 'src', { if (!get) get = require('simple-get') - get.concat(val, (err, res, data) => { + get.concat({ + url: val, + headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36' } + }, (err, res, data) => { if (err) return onerror(err) if (res.statusCode < 200 || res.statusCode >= 300) { From 561d933fe251c9c9ea28f715dccf496f08667c46 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 3 Sep 2022 19:52:26 -0700 Subject: [PATCH 027/128] v2.10.0 --- CHANGELOG.md | 7 ++++++- package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 886602912..85b51d474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,13 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed -* Export `pangoVersion` ### Added +### Fixed + +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)) diff --git a/package.json b/package.json index 5d4185d5e..72849d64d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.9.3", + "version": "2.10.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 6862532c593af0e86327ddb4c52341ee5bd0df54 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Tue, 6 Sep 2022 05:32:28 -0700 Subject: [PATCH 028/128] Fix actualBoundingBoxLeft/Right with center/right alignment (#2109) This bug goes back 10 years to the original implementation. Fixes #1909 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 6 +++--- test/canvas.test.js | 37 ++++++++++++++++++++++++++++++--- test/public/tests.js | 14 ++++++++++++- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b51d474..085dd412f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Fix `actualBoundingBoxLeft` and `actualBoundingBoxRight` when `textAlign='center'` or `'right'` ([#1909](https://github.com/Automattic/node-canvas/issues/1909)) 2.10.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 10629cee7..60a86cb3b 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2748,7 +2748,7 @@ NAN_METHOD(Context2d::MeasureText) { double x_offset; switch (context->state->textAlignment) { case 0: // center - x_offset = logical_rect.width / 2; + x_offset = logical_rect.width / 2.; break; case 1: // right x_offset = logical_rect.width; @@ -2766,10 +2766,10 @@ NAN_METHOD(Context2d::MeasureText) { Nan::New(logical_rect.width)).Check(); Nan::Set(obj, Nan::New("actualBoundingBoxLeft").ToLocalChecked(), - Nan::New(x_offset - PANGO_LBEARING(ink_rect))).Check(); + Nan::New(PANGO_LBEARING(ink_rect) + x_offset)).Check(); Nan::Set(obj, Nan::New("actualBoundingBoxRight").ToLocalChecked(), - Nan::New(x_offset + PANGO_RBEARING(ink_rect))).Check(); + Nan::New(PANGO_RBEARING(ink_rect) - x_offset)).Check(); Nan::Set(obj, Nan::New("actualBoundingBoxAscent").ToLocalChecked(), Nan::New(y_offset + PANGO_ASCENT(ink_rect))).Check(); diff --git a/test/canvas.test.js b/test/canvas.test.js index a81de892e..5abd45cb8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -20,6 +20,11 @@ const { 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); @@ -946,20 +951,46 @@ 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('Context2d#fillText()', function () { diff --git a/test/public/tests.js b/test/public/tests.js index 66bb14ddb..651105e36 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2658,7 +2658,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 @@ -2677,8 +2678,19 @@ 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['image sampling (#1084)'] = function (ctx, done) { From 93749430f49f506d4917129ed6cc3d7939b946f1 Mon Sep 17 00:00:00 2001 From: Sahel LUCAS--SAOUDI Date: Wed, 7 Sep 2022 18:24:55 +0200 Subject: [PATCH 029/128] Parse rgba(r,g,b,0) correctly --- .gitignore | 2 ++ src/color.cc | 5 ++++- test/canvas.test.js | 5 ++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e5e14d5b6..ff66b1103 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ package-lock.json *.swp *.un~ npm-debug.log + +.idea diff --git a/src/color.cc b/src/color.cc index 1ea96e195..230fb8dbe 100644 --- a/src/color.cc +++ b/src/color.cc @@ -228,7 +228,10 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { if (*str >= '1' && *str <= '9') { \ NAME = 1; \ } else { \ - if ('0' == *str) ++str; \ + if ('0' == *str) { \ + NAME = 0; \ + ++str; \ + } \ if ('.' == *str) { \ ++str; \ NAME = 0; \ diff --git a/test/canvas.test.js b/test/canvas.test.js index 5abd45cb8..670127783 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -210,6 +210,9 @@ describe('Canvas', function () { 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); + // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' @@ -985,7 +988,7 @@ describe('Canvas', function () { 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) From bc75c6af9edc0f328271e7b84fa21b59b4f4df74 Mon Sep 17 00:00:00 2001 From: Sahel LUCAS--SAOUDI Date: Wed, 7 Sep 2022 18:28:40 +0200 Subject: [PATCH 030/128] add line in CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 085dd412f..ece076034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### 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. 2.10.0 ================== From b3e7df319c045c1dc74e390f4b3af161304c9c55 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 7 Sep 2022 09:41:50 -0700 Subject: [PATCH 031/128] v2.10.1 --- CHANGELOG.md | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ece076034..513643483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,12 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed + +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. +* 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 ================== diff --git a/package.json b/package.json index 72849d64d..034783003 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.10.0", + "version": "2.10.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 0e6504a1f6ad28eba5f40835fc233275a4170d46 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 10 Sep 2022 12:45:10 -0700 Subject: [PATCH 032/128] remove save() limit, improve save/restore perf, fix some props 1. One WPT test fails if there are not at least 512 save/restore slots. This removes that limit entirely. 2. Gets rid of clunky C code and uses `std::stack` with a proper C++ class. End result is >1.6x faster with MSVC. 3. Reorders fields and types some enums so the state struct shrinks from 192 bytes to 168 bytes (-24 bytes; i.e. 24 bytes saved per state). 4. Fixes several properties that were not saved/restored: `textBaseline`, `textAlign`. `quality` is not saved/restored, but it's not wired up to anything and needs to be removed. Fixes #1936 --- CHANGELOG.md | 3 + Readme.md | 2 +- benchmarks/run.js | 12 +++ src/Canvas.h | 32 +++++-- src/CanvasRenderingContext2d.cc | 155 ++++++++++++-------------------- src/CanvasRenderingContext2d.h | 84 ++++++++++------- test/canvas.test.js | 53 +++++++++++ 7 files changed, 200 insertions(+), 141 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 513643483..09678e346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* Improve performance and memory usage of `save()`/`restore()`. +* `save()`/`restore()` no longer have a maximum depth (previously 64 states). ### Added ### Fixed +* `textBaseline` and `textAlign` were not saved/restored by `save()`/`restore()`. ([#1936](https://github.com/Automattic/node-canvas/issues/2029)) 2.10.1 ================== diff --git a/Readme.md b/Readme.md index 992904ce3..cc945aa9a 100644 --- a/Readme.md +++ b/Readme.md @@ -91,7 +91,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() diff --git a/benchmarks/run.js b/benchmarks/run.js index a6954da87..14f4db379 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -64,6 +64,18 @@ function done (benchmark, times, start, isAsync) { // node-canvas +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/src/Canvas.h b/src/Canvas.h index f356af035..60d3b4216 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -11,15 +11,6 @@ #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,6 +22,29 @@ 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, + // Currently same as LEFT and RIGHT without RTL support: + TEXT_ALIGNMENT_START = -2, + TEXT_ALIGNMENT_END = 2 +}; + +enum canvas_draw_mode_t : uint8_t { + TEXT_DRAW_PATHS, + TEXT_DRAW_GLYPHS +}; + /* * Canvas. */ diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 60a86cb3b..5e37ea257 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -38,19 +38,6 @@ Nan::Persistent Context2d::constructor; constexpr double twoPi = M_PI * 2.; -/* - * Text baselines. - */ - -enum { - TEXT_BASELINE_ALPHABETIC - , TEXT_BASELINE_TOP - , TEXT_BASELINE_BOTTOM - , TEXT_BASELINE_MIDDLE - , TEXT_BASELINE_IDEOGRAPHIC - , TEXT_BASELINE_HANGING -}; - /* * Simple helper macro for a rather verbose function call. */ @@ -178,9 +165,9 @@ Context2d::Context2d(Canvas *canvas) { _canvas = canvas; _context = canvas->createCairoContext(); _layout = pango_cairo_create_layout(_context); - state = states[stateno = 0] = (canvas_state_t *) malloc(sizeof(canvas_state_t)); - - resetState(true); + states.emplace(); + state = &states.top(); + pango_layout_set_font_description(_layout, state->fontDescription); } /* @@ -188,10 +175,6 @@ 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); _resetPersistentHandles(); @@ -201,32 +184,10 @@ 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(); } @@ -234,8 +195,6 @@ void Context2d::_resetPersistentHandles() { _fillStyle.Reset(); _strokeStyle.Reset(); _font.Reset(); - _textBaseline.Reset(); - _textAlign.Reset(); } /* @@ -244,13 +203,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(); } /* @@ -259,12 +214,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); } } @@ -2496,13 +2449,12 @@ Context2d::setTextPath(double x, double y) { PangoRectangle logical_rect; switch (state->textAlignment) { - // center - case 0: + 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_END: + case TEXT_ALIGNMENT_RIGHT: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; @@ -2629,15 +2581,17 @@ NAN_SETTER(Context2d::SetFont) { 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); + const char* baseline; + switch (context->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; + } + info.GetReturnValue().Set(Nan::New(baseline).ToLocalChecked()); } /* @@ -2648,20 +2602,19 @@ 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} + const std::map modes = { + {"alphabetic", TEXT_BASELINE_ALPHABETIC}, + {"top", TEXT_BASELINE_TOP}, + {"bottom", TEXT_BASELINE_BOTTOM}, + {"middle", TEXT_BASELINE_MIDDLE}, + {"ideographic", TEXT_BASELINE_IDEOGRAPHIC}, + {"hanging", TEXT_BASELINE_HANGING} }; auto op = modes.find(*opStr); if (op == modes.end()) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->textBaseline = op->second; - context->_textBaseline.Reset(value); } /* @@ -2670,15 +2623,17 @@ NAN_SETTER(Context2d::SetTextBaseline) { 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); + const char* align; + switch (context->state->textAlignment) { + default: + // TODO the default is supposed to be "start" + 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; + } + info.GetReturnValue().Set(Nan::New(align).ToLocalChecked()); } /* @@ -2689,19 +2644,18 @@ 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} + const std::map modes = { + {"center", TEXT_ALIGNMENT_CENTER}, + {"left", TEXT_ALIGNMENT_LEFT}, + {"start", TEXT_ALIGNMENT_START}, + {"right", TEXT_ALIGNMENT_RIGHT}, + {"end", TEXT_ALIGNMENT_END} }; auto op = modes.find(*opStr); if (op == modes.end()) return; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->textAlignment = op->second; - context->_textAlign.Reset(value); } /* @@ -2747,13 +2701,16 @@ NAN_METHOD(Context2d::MeasureText) { double x_offset; switch (context->state->textAlignment) { - case 0: // center + case TEXT_ALIGNMENT_CENTER: x_offset = logical_rect.width / 2.; break; - case 1: // right + case TEXT_ALIGNMENT_END: + case TEXT_ALIGNMENT_RIGHT: x_offset = logical_rect.width; break; - default: // left + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_LEFT: + default: x_offset = 0.0; } diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 89c86df67..568ebc8cc 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -7,11 +7,7 @@ #include "color.h" #include "nan.h" #include - -typedef enum { - TEXT_DRAW_PATHS, - TEXT_DRAW_GLYPHS -} canvas_draw_mode_t; +#include /* * State struct. @@ -20,25 +16,54 @@ 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; + cairo_filter_t patternQuality = CAIRO_FILTER_GOOD; + float globalAlpha = 1.f; + int shadowBlur = 0; + text_align_t textAlignment = TEXT_ALIGNMENT_LEFT; // TODO default is supposed to be START + text_baseline_t textBaseline = TEXT_BASELINE_ALPHABETIC; + canvas_draw_mode_t textDrawingMode = TEXT_DRAW_PATHS; + bool imageSmoothingEnabled = true; + + 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); + imageSmoothingEnabled = other.imageSmoothingEnabled; + } + + ~canvas_state_t() { + pango_font_description_free(fontDescription); + } +}; /* * Equivalent to a PangoRectangle but holds floats instead of ints @@ -54,12 +79,9 @@ 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 Nan::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; @@ -180,7 +202,7 @@ class Context2d: public Nan::ObjectWrap { void save(); void restore(); void setFontFromState(); - void resetState(bool init = false); + void resetState(); inline PangoLayout *layout(){ return _layout; } private: @@ -195,8 +217,6 @@ class Context2d: public Nan::ObjectWrap { Nan::Persistent _fillStyle; Nan::Persistent _strokeStyle; Nan::Persistent _font; - Nan::Persistent _textBaseline; - Nan::Persistent _textAlign; Canvas *_canvas; cairo_t *_context; cairo_path_t *_path; diff --git a/test/canvas.test.js b/test/canvas.test.js index 670127783..af33befaa 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -547,6 +547,8 @@ describe('Canvas', function () { const canvas = createCanvas(200, 200) const ctx = canvas.getContext('2d') + assert.equal('left', ctx.textAlign) // default TODO wrong default + ctx.textAlign = 'start' assert.equal('start', ctx.textAlign) ctx.textAlign = 'center' assert.equal('center', ctx.textAlign) @@ -1900,4 +1902,55 @@ 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'], // TODO #1946 + ['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'] + ] + + 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() + }) + } + }) }) From 2876b6e380029a9396d67cf0c8c5f9d2535d3484 Mon Sep 17 00:00:00 2001 From: TheDadi Date: Sun, 30 Oct 2022 02:23:06 +0100 Subject: [PATCH 033/128] Bugfix/Node.js 18 -> Assertion failed: (object->InternalFieldCount() > 0) (#2133) * add node 16 & 18 to build * bump version * fix Assertion failed: (object->InternalFieldCount() > 0), function Unwrap, file nan_object_wrap.h, line 32. * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * reimplement accessors on prototype * Update CHANGELOG.md * add review comments * remove deprecated constructor overload --- .github/workflows/ci.yaml | 6 +- .github/workflows/prebuild.yaml | 6 +- CHANGELOG.md | 8 ++ package.json | 2 +- src/Canvas.cc | 36 ++++- src/CanvasRenderingContext2d.cc | 226 ++++++++++++++++++++++++++++---- src/Image.cc | 46 ++++++- src/ImageData.cc | 14 +- src/Point.h | 8 +- src/Util.h | 25 ---- 10 files changed, 304 insertions(+), 73 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3c92ea1c8..31b7497bc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18] steps: - uses: actions/setup-node@v3 with: @@ -33,7 +33,7 @@ jobs: runs-on: windows-2019 strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18] steps: - uses: actions/setup-node@v3 with: @@ -57,7 +57,7 @@ jobs: runs-on: macos-latest strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18] steps: - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index b10cc1522..a112eef87 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -24,7 +24,7 @@ jobs: Linux: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Linux runs-on: ubuntu-latest @@ -97,7 +97,7 @@ jobs: macOS: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, macOS runs-on: macos-latest @@ -163,7 +163,7 @@ jobs: Win: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Windows runs-on: windows-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 09678e346..a666d5a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * `textBaseline` and `textAlign` were not saved/restored by `save()`/`restore()`. ([#1936](https://github.com/Automattic/node-canvas/issues/2029)) +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)) +### Changed +* Update nan to v2.17.0 to ensure Node.js v18+ support. +* Implement valid `this` checks in all `SetAccessor` methods. + 2.10.1 ================== ### Fixed diff --git a/package.json b/package.json index 034783003..cd6754ea4 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "types": "types/index.d.ts", "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.15.0", + "nan": "^2.17.0", "simple-get": "^3.0.3" }, "devDependencies": { diff --git a/src/Canvas.cc b/src/Canvas.cc index 3e339f033..a7318ca82 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -64,10 +64,10 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { #ifdef HAVE_JPEG Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); #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::SetAccessor(proto, Nan::New("type").ToLocalChecked(), GetType); + Nan::SetAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); Nan::SetTemplate(proto, "PNG_NO_FILTERS", Nan::New(PNG_NO_FILTERS)); Nan::SetTemplate(proto, "PNG_FILTER_NONE", Nan::New(PNG_FILTER_NONE)); @@ -144,6 +144,10 @@ NAN_METHOD(Canvas::New) { */ NAN_GETTER(Canvas::GetType) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.GetType called on incompatible receiver"); + return; + } Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->backend()->getName()).ToLocalChecked()); } @@ -152,6 +156,10 @@ NAN_GETTER(Canvas::GetType) { * Get stride. */ NAN_GETTER(Canvas::GetStride) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.GetStride called on incompatible receiver"); + return; + } Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->stride())); } @@ -161,6 +169,10 @@ NAN_GETTER(Canvas::GetStride) { */ NAN_GETTER(Canvas::GetWidth) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.GetWidth called on incompatible receiver"); + return; + } Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getWidth())); } @@ -170,6 +182,10 @@ NAN_GETTER(Canvas::GetWidth) { */ NAN_SETTER(Canvas::SetWidth) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.SetWidth called on incompatible receiver"); + return; + } if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setWidth(Nan::To(value).FromMaybe(0)); @@ -182,6 +198,10 @@ NAN_SETTER(Canvas::SetWidth) { */ NAN_GETTER(Canvas::GetHeight) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.GetHeight called on incompatible receiver"); + return; + } Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getHeight())); } @@ -191,6 +211,10 @@ NAN_GETTER(Canvas::GetHeight) { */ NAN_SETTER(Canvas::SetHeight) { + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Canvas.SetHeight called on incompatible receiver"); + return; + } if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setHeight(Nan::To(value).FromMaybe(0)); @@ -773,13 +797,13 @@ NAN_METHOD(Canvas::RegisterFont) { NAN_METHOD(Canvas::DeregisterAllFonts) { // 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"); } diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 5e37ea257..667e1cf93 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -129,29 +129,29 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { 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); + Nan::SetAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat); + Nan::SetAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality); + Nan::SetAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled); + Nan::SetAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation); + Nan::SetAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha); + Nan::SetAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor); + Nan::SetAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit); + Nan::SetAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth); + Nan::SetAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap); + Nan::SetAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin); + Nan::SetAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset); + Nan::SetAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX); + Nan::SetAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY); + Nan::SetAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur); + Nan::SetAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias); + Nan::SetAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode); + Nan::SetAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality); + Nan::SetAccessor(proto, Nan::New("currentTransform").ToLocalChecked(), GetCurrentTransform, SetCurrentTransform); + Nan::SetAccessor(proto, Nan::New("fillStyle").ToLocalChecked(), GetFillStyle, SetFillStyle); + Nan::SetAccessor(proto, Nan::New("strokeStyle").ToLocalChecked(), GetStrokeStyle, SetStrokeStyle); + Nan::SetAccessor(proto, Nan::New("font").ToLocalChecked(), GetFont, SetFont); + Nan::SetAccessor(proto, Nan::New("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline); + Nan::SetAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign); 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)); @@ -713,6 +713,10 @@ NAN_METHOD(Context2d::SaveExternalModules) { */ NAN_GETTER(Context2d::GetFormat) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetFormat called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); std::string pixelFormatString; switch (context->canvas()->backend()->getFormat()) { @@ -1392,6 +1396,10 @@ NAN_METHOD(Context2d::DrawImage) { */ NAN_GETTER(Context2d::GetGlobalAlpha) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetGlobalAlpha called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->globalAlpha)); } @@ -1401,6 +1409,10 @@ NAN_GETTER(Context2d::GetGlobalAlpha) { */ NAN_SETTER(Context2d::SetGlobalAlpha) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetGlobalAlpha called on incompatible receiver"); + return; + } double n = Nan::To(value).FromMaybe(0); if (n >= 0 && n <= 1) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1413,6 +1425,10 @@ NAN_SETTER(Context2d::SetGlobalAlpha) { */ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetGlobalCompositeOperation called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -1463,6 +1479,10 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { */ NAN_SETTER(Context2d::SetPatternQuality) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetPatternQuality called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Nan::Utf8String quality(Nan::To(value).ToLocalChecked()); if (0 == strcmp("fast", *quality)) { @@ -1483,6 +1503,10 @@ NAN_SETTER(Context2d::SetPatternQuality) { */ NAN_GETTER(Context2d::GetPatternQuality) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetPatternQuality called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *quality; switch (context->state->patternQuality) { @@ -1500,6 +1524,10 @@ NAN_GETTER(Context2d::GetPatternQuality) { */ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetImageSmoothingEnabled called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(false); } @@ -1509,6 +1537,10 @@ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { */ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetImageSmoothingEnabled called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->imageSmoothingEnabled)); } @@ -1518,6 +1550,10 @@ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { */ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetGlobalCompositeOperation called on incompatible receiver"); + return; + } 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 @@ -1565,6 +1601,10 @@ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { */ NAN_GETTER(Context2d::GetShadowOffsetX) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetShadowOffsetX called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetX)); } @@ -1574,6 +1614,10 @@ NAN_GETTER(Context2d::GetShadowOffsetX) { */ NAN_SETTER(Context2d::SetShadowOffsetX) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetShadowOffsetX called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetX = Nan::To(value).FromMaybe(0); } @@ -1583,6 +1627,10 @@ NAN_SETTER(Context2d::SetShadowOffsetX) { */ NAN_GETTER(Context2d::GetShadowOffsetY) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetShadowOffsetY called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetY)); } @@ -1592,6 +1640,10 @@ NAN_GETTER(Context2d::GetShadowOffsetY) { */ NAN_SETTER(Context2d::SetShadowOffsetY) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetShadowOffsetY called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetY = Nan::To(value).FromMaybe(0); } @@ -1601,6 +1653,10 @@ NAN_SETTER(Context2d::SetShadowOffsetY) { */ NAN_GETTER(Context2d::GetShadowBlur) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetShadowBlur called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowBlur)); } @@ -1610,6 +1666,10 @@ NAN_GETTER(Context2d::GetShadowBlur) { */ NAN_SETTER(Context2d::SetShadowBlur) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetShadowBlur called on incompatible receiver"); + return; + } int n = Nan::To(value).FromMaybe(0); if (n >= 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1622,6 +1682,10 @@ NAN_SETTER(Context2d::SetShadowBlur) { */ NAN_GETTER(Context2d::GetAntiAlias) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetAntiAlias called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *aa; switch (cairo_get_antialias(context->context())) { @@ -1638,6 +1702,10 @@ NAN_GETTER(Context2d::GetAntiAlias) { */ NAN_SETTER(Context2d::SetAntiAlias) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetAntiAlias called on incompatible receiver"); + return; + } Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -1661,6 +1729,10 @@ NAN_SETTER(Context2d::SetAntiAlias) { */ NAN_GETTER(Context2d::GetTextDrawingMode) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetTextDrawingMode called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *mode; if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { @@ -1678,6 +1750,10 @@ NAN_GETTER(Context2d::GetTextDrawingMode) { */ NAN_SETTER(Context2d::SetTextDrawingMode) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetTextDrawingMode called on incompatible receiver"); + return; + } Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (0 == strcmp("path", *str)) { @@ -1692,6 +1768,10 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { */ NAN_GETTER(Context2d::GetQuality) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetQuality called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *filter; switch (cairo_pattern_get_filter(cairo_get_source(context->context()))) { @@ -1709,6 +1789,10 @@ NAN_GETTER(Context2d::GetQuality) { */ NAN_SETTER(Context2d::SetQuality) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetQuality called on incompatible receiver"); + return; + } Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_filter_t filter; @@ -1771,6 +1855,10 @@ void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { */ NAN_GETTER(Context2d::GetCurrentTransform) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetCurrentTransform called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local instance = get_current_transform(context); @@ -1782,6 +1870,10 @@ NAN_GETTER(Context2d::GetCurrentTransform) { */ NAN_SETTER(Context2d::SetCurrentTransform) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetCurrentTransform called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local ctx = Nan::GetCurrentContext(); Local mat = Nan::To(value).ToLocalChecked(); @@ -1803,6 +1895,10 @@ NAN_SETTER(Context2d::SetCurrentTransform) { */ NAN_GETTER(Context2d::GetFillStyle) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetFillStyle called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Isolate *iso = Isolate::GetCurrent(); Local style; @@ -1820,6 +1916,10 @@ NAN_GETTER(Context2d::GetFillStyle) { */ NAN_SETTER(Context2d::SetFillStyle) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetFillStyle called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1847,6 +1947,10 @@ NAN_SETTER(Context2d::SetFillStyle) { */ NAN_GETTER(Context2d::GetStrokeStyle) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetStrokeStyle called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local style; @@ -1863,6 +1967,10 @@ NAN_GETTER(Context2d::GetStrokeStyle) { */ NAN_SETTER(Context2d::SetStrokeStyle) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetStrokeStyle called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1890,6 +1998,10 @@ NAN_SETTER(Context2d::SetStrokeStyle) { */ NAN_GETTER(Context2d::GetMiterLimit) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetMiterLimit called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_miter_limit(context->context()))); } @@ -1899,6 +2011,10 @@ NAN_GETTER(Context2d::GetMiterLimit) { */ NAN_SETTER(Context2d::SetMiterLimit) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetMiterLimit called on incompatible receiver"); + return; + } double n = Nan::To(value).FromMaybe(0); if (n > 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1911,6 +2027,10 @@ NAN_SETTER(Context2d::SetMiterLimit) { */ NAN_GETTER(Context2d::GetLineWidth) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetLineWidth called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_line_width(context->context()))); } @@ -1920,6 +2040,10 @@ NAN_GETTER(Context2d::GetLineWidth) { */ NAN_SETTER(Context2d::SetLineWidth) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetLineWidth called on incompatible receiver"); + return; + } double n = Nan::To(value).FromMaybe(0); if (n > 0 && n != std::numeric_limits::infinity()) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1932,6 +2056,10 @@ NAN_SETTER(Context2d::SetLineWidth) { */ NAN_GETTER(Context2d::GetLineJoin) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetLineJoin called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *join; switch (cairo_get_line_join(context->context())) { @@ -1947,6 +2075,10 @@ NAN_GETTER(Context2d::GetLineJoin) { */ NAN_SETTER(Context2d::SetLineJoin) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetLineJoin called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -1964,6 +2096,10 @@ NAN_SETTER(Context2d::SetLineJoin) { */ NAN_GETTER(Context2d::GetLineCap) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetLineCap called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *cap; switch (cairo_get_line_cap(context->context())) { @@ -1979,6 +2115,10 @@ NAN_GETTER(Context2d::GetLineCap) { */ NAN_SETTER(Context2d::SetLineCap) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetLineCap called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -2013,6 +2153,10 @@ NAN_METHOD(Context2d::IsPointInPath) { */ NAN_SETTER(Context2d::SetShadowColor) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetShadowColor called on incompatible receiver"); + return; + } short ok; Nan::Utf8String str(Nan::To(value).ToLocalChecked()); uint32_t rgba = rgba_from_string(*str, &ok); @@ -2027,6 +2171,10 @@ NAN_SETTER(Context2d::SetShadowColor) { */ NAN_GETTER(Context2d::GetShadowColor) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetShadowColor called on incompatible receiver"); + return; + } char buf[64]; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); rgba_to_string(context->state->shadow, buf, sizeof(buf)); @@ -2501,6 +2649,10 @@ NAN_METHOD(Context2d::MoveTo) { */ NAN_GETTER(Context2d::GetFont) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetFont called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Isolate *iso = Isolate::GetCurrent(); Local font; @@ -2523,6 +2675,10 @@ NAN_GETTER(Context2d::GetFont) { */ NAN_SETTER(Context2d::SetFont) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetFont called on incompatible receiver"); + return; + } if (!value->IsString()) return; Isolate *iso = Isolate::GetCurrent(); @@ -2580,6 +2736,10 @@ NAN_SETTER(Context2d::SetFont) { */ NAN_GETTER(Context2d::GetTextBaseline) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetTextBaseline called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char* baseline; switch (context->state->textBaseline) { @@ -2599,6 +2759,10 @@ NAN_GETTER(Context2d::GetTextBaseline) { */ NAN_SETTER(Context2d::SetTextBaseline) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetTextBaseline called on incompatible receiver"); + return; + } if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); @@ -2622,6 +2786,10 @@ NAN_SETTER(Context2d::SetTextBaseline) { */ NAN_GETTER(Context2d::GetTextAlign) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetTextAlign called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char* align; switch (context->state->textAlignment) { @@ -2641,6 +2809,10 @@ NAN_GETTER(Context2d::GetTextAlign) { */ NAN_SETTER(Context2d::SetTextAlign) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetTextAlign called on incompatible receiver"); + return; + } if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); @@ -2803,6 +2975,10 @@ NAN_METHOD(Context2d::GetLineDash) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_SETTER(Context2d::SetLineDashOffset) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.SetLineDashOffset called on incompatible receiver"); + return; + } double offset = Nan::To(value).FromMaybe(0); if (!std::isfinite(offset)) return; @@ -2820,6 +2996,10 @@ NAN_SETTER(Context2d::SetLineDashOffset) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_GETTER(Context2d::GetLineDashOffset) { + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Context2d.GetLineDashOffset called on incompatible receiver"); + return; + } Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); double offset; diff --git a/src/Image.cc b/src/Image.cc index 103b65ee7..35ee7947a 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -8,7 +8,6 @@ #include #include #include -#include "Util.h" /* Cairo limit: * https://lists.cairographics.org/archives/cairo/2010-December/021422.html @@ -60,12 +59,12 @@ Image::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { // 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); + Nan::SetAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); + Nan::SetAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth); + Nan::SetAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight); + Nan::SetAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode); ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); @@ -108,6 +107,10 @@ NAN_GETTER(Image::GetComplete) { */ NAN_GETTER(Image::GetDataMode) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetDataMode called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->data_mode)); } @@ -117,6 +120,10 @@ NAN_GETTER(Image::GetDataMode) { */ NAN_SETTER(Image::SetDataMode) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.SetDataMode called on incompatible receiver"); + return; + } if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); int mode = Nan::To(value).FromMaybe(0); @@ -129,6 +136,10 @@ NAN_SETTER(Image::SetDataMode) { */ NAN_GETTER(Image::GetNaturalWidth) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetNaturalWidth called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->naturalWidth)); } @@ -138,6 +149,10 @@ NAN_GETTER(Image::GetNaturalWidth) { */ NAN_GETTER(Image::GetWidth) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetWidth called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->width)); } @@ -147,6 +162,10 @@ NAN_GETTER(Image::GetWidth) { */ NAN_SETTER(Image::SetWidth) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.SetWidth called on incompatible receiver"); + return; + } if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); img->width = Nan::To(value).FromMaybe(0); @@ -158,6 +177,10 @@ NAN_SETTER(Image::SetWidth) { */ NAN_GETTER(Image::GetNaturalHeight) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetNaturalHeight called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->naturalHeight)); } @@ -167,6 +190,10 @@ NAN_GETTER(Image::GetNaturalHeight) { */ NAN_GETTER(Image::GetHeight) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method Image.GetHeight called on incompatible receiver"); + return; + } Image *img = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(img->height)); } @@ -175,6 +202,11 @@ NAN_GETTER(Image::GetHeight) { */ NAN_SETTER(Image::SetHeight) { + if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + // #1534 + Nan::ThrowTypeError("Method Image.SetHeight called on incompatible receiver"); + return; + } if (value->IsNumber()) { Image *img = Nan::ObjectWrap::Unwrap(info.This()); img->height = Nan::To(value).FromMaybe(0); diff --git a/src/ImageData.cc b/src/ImageData.cc index 668733d39..03da2e270 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -2,8 +2,6 @@ #include "ImageData.h" -#include "Util.h" - using namespace v8; Nan::Persistent ImageData::constructor; @@ -24,8 +22,8 @@ ImageData::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { // Prototype Local proto = ctor->PrototypeTemplate(); - SetProtoAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, NULL, ctor); - SetProtoAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, NULL, ctor); + Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth); + Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight); Local ctx = Nan::GetCurrentContext(); Nan::Set(target, Nan::New("ImageData").ToLocalChecked(), ctor->GetFunction(ctx).ToLocalChecked()); } @@ -126,6 +124,10 @@ NAN_METHOD(ImageData::New) { */ NAN_GETTER(ImageData::GetWidth) { + if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method ImageData.GetWidth called on incompatible receiver"); + return; + } ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(imageData->width())); } @@ -135,6 +137,10 @@ NAN_GETTER(ImageData::GetWidth) { */ NAN_GETTER(ImageData::GetHeight) { + if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { + Nan::ThrowTypeError("Method ImageData.GetHeight called on incompatible receiver"); + return; + } ImageData *imageData = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(imageData->height())); } diff --git a/src/Point.h b/src/Point.h index 50c7b711c..a797dc46c 100644 --- a/src/Point.h +++ b/src/Point.h @@ -1,11 +1,17 @@ // Copyright (c) 2010 LearnBoost - #pragma once +#include + template class Point { public: T x, y; Point(T x=0, T y=0): x(x), y(y) {} Point(const Point&) = default; + Point& operator=(Point other) { + std::swap(x, other.x); + std::swap(y, other.y); + return *this; + } }; diff --git a/src/Util.h b/src/Util.h index dba6883a2..0e6d1d89c 100644 --- a/src/Util.h +++ b/src/Util.h @@ -1,32 +1,7 @@ #pragma once -#include -#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) - ); -} - inline bool streq_casein(std::string& str1, std::string& str2) { return str1.size() == str2.size() && std::equal(str1.begin(), str1.end(), str2.begin(), [](char& c1, char& c2) { return c1 == c2 || std::toupper(c1) == std::toupper(c2); From ad18c6ce0ab2452d64df36a199c740a926ceb939 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 29 Oct 2022 18:24:19 -0700 Subject: [PATCH 034/128] src: shorten copy assignment operator decl for Point --- src/Point.h | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Point.h b/src/Point.h index a797dc46c..a61f8b1ba 100644 --- a/src/Point.h +++ b/src/Point.h @@ -1,17 +1,11 @@ // Copyright (c) 2010 LearnBoost #pragma once -#include - template class Point { public: T x, y; Point(T x=0, T y=0): x(x), y(y) {} Point(const Point&) = default; - Point& operator=(Point other) { - std::swap(x, other.x); - std::swap(y, other.y); - return *this; - } + Point& operator=(const Point&) = default; }; From cc32159bf5db44edbded532249f56f7844b36aeb Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 29 Oct 2022 18:32:13 -0700 Subject: [PATCH 035/128] src: shorten receiver checks --- src/Canvas.cc | 38 ++---- src/CanvasRenderingContext2d.cc | 233 ++++++++------------------------ 2 files changed, 67 insertions(+), 204 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index a7318ca82..860d5bb46 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -35,6 +35,12 @@ "with at least a family (string) and optionally weight (string/number) " \ "and style (string)." +#define CHECK_RECEIVER(prop) \ + if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ + Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ + return; \ + } + using namespace v8; using namespace std; @@ -144,10 +150,7 @@ NAN_METHOD(Canvas::New) { */ NAN_GETTER(Canvas::GetType) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.GetType called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.GetType); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->backend()->getName()).ToLocalChecked()); } @@ -156,10 +159,7 @@ NAN_GETTER(Canvas::GetType) { * Get stride. */ NAN_GETTER(Canvas::GetStride) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.GetStride called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.GetStride); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->stride())); } @@ -169,10 +169,7 @@ NAN_GETTER(Canvas::GetStride) { */ NAN_GETTER(Canvas::GetWidth) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.GetWidth called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.GetWidth); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getWidth())); } @@ -182,10 +179,7 @@ NAN_GETTER(Canvas::GetWidth) { */ NAN_SETTER(Canvas::SetWidth) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.SetWidth called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.SetWidth); if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setWidth(Nan::To(value).FromMaybe(0)); @@ -198,10 +192,7 @@ NAN_SETTER(Canvas::SetWidth) { */ NAN_GETTER(Canvas::GetHeight) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.GetHeight called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.GetHeight); Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(canvas->getHeight())); } @@ -211,10 +202,7 @@ NAN_GETTER(Canvas::GetHeight) { */ NAN_SETTER(Canvas::SetHeight) { - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Canvas.SetHeight called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Canvas.SetHeight); if (value->IsNumber()) { Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); canvas->backend()->setHeight(Nan::To(value).FromMaybe(0)); @@ -973,3 +961,5 @@ Local Canvas::Error(cairo_status_t status) { return Exception::Error(Nan::New(cairo_status_to_string(status)).ToLocalChecked()); } + +#undef CHECK_RECEIVER diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 667e1cf93..699ec88fc 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -36,6 +36,12 @@ Nan::Persistent Context2d::constructor; double width = args[2]; \ double height = args[3]; +#define CHECK_RECEIVER(prop) \ + if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ + Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ + return; \ + } + constexpr double twoPi = M_PI * 2.; /* @@ -713,10 +719,7 @@ NAN_METHOD(Context2d::SaveExternalModules) { */ NAN_GETTER(Context2d::GetFormat) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetFormat called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetFormat); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); std::string pixelFormatString; switch (context->canvas()->backend()->getFormat()) { @@ -1396,10 +1399,7 @@ NAN_METHOD(Context2d::DrawImage) { */ NAN_GETTER(Context2d::GetGlobalAlpha) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetGlobalAlpha called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetGlobalAlpha); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->globalAlpha)); } @@ -1409,10 +1409,7 @@ NAN_GETTER(Context2d::GetGlobalAlpha) { */ NAN_SETTER(Context2d::SetGlobalAlpha) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetGlobalAlpha called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetGlobalAlpha); double n = Nan::To(value).FromMaybe(0); if (n >= 0 && n <= 1) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1425,10 +1422,7 @@ NAN_SETTER(Context2d::SetGlobalAlpha) { */ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetGlobalCompositeOperation called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetGlobalCompositeOperation); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -1479,10 +1473,7 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { */ NAN_SETTER(Context2d::SetPatternQuality) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetPatternQuality called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetPatternQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Nan::Utf8String quality(Nan::To(value).ToLocalChecked()); if (0 == strcmp("fast", *quality)) { @@ -1503,10 +1494,7 @@ NAN_SETTER(Context2d::SetPatternQuality) { */ NAN_GETTER(Context2d::GetPatternQuality) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetPatternQuality called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetPatternQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *quality; switch (context->state->patternQuality) { @@ -1524,10 +1512,7 @@ NAN_GETTER(Context2d::GetPatternQuality) { */ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetImageSmoothingEnabled called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetImageSmoothingEnabled); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->imageSmoothingEnabled = Nan::To(value).FromMaybe(false); } @@ -1537,10 +1522,7 @@ NAN_SETTER(Context2d::SetImageSmoothingEnabled) { */ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetImageSmoothingEnabled called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetImageSmoothingEnabled); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->imageSmoothingEnabled)); } @@ -1550,10 +1532,7 @@ NAN_GETTER(Context2d::GetImageSmoothingEnabled) { */ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetGlobalCompositeOperation called on incompatible receiver"); - return; - } + CHECK_RECEIVER(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 @@ -1601,10 +1580,7 @@ NAN_SETTER(Context2d::SetGlobalCompositeOperation) { */ NAN_GETTER(Context2d::GetShadowOffsetX) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetShadowOffsetX called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetShadowOffsetX); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetX)); } @@ -1614,10 +1590,7 @@ NAN_GETTER(Context2d::GetShadowOffsetX) { */ NAN_SETTER(Context2d::SetShadowOffsetX) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetShadowOffsetX called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetShadowOffsetX); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetX = Nan::To(value).FromMaybe(0); } @@ -1627,10 +1600,7 @@ NAN_SETTER(Context2d::SetShadowOffsetX) { */ NAN_GETTER(Context2d::GetShadowOffsetY) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetShadowOffsetY called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetShadowOffsetY); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowOffsetY)); } @@ -1640,10 +1610,7 @@ NAN_GETTER(Context2d::GetShadowOffsetY) { */ NAN_SETTER(Context2d::SetShadowOffsetY) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetShadowOffsetY called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetShadowOffsetY); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); context->state->shadowOffsetY = Nan::To(value).FromMaybe(0); } @@ -1653,10 +1620,7 @@ NAN_SETTER(Context2d::SetShadowOffsetY) { */ NAN_GETTER(Context2d::GetShadowBlur) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetShadowBlur called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetShadowBlur); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(context->state->shadowBlur)); } @@ -1666,10 +1630,7 @@ NAN_GETTER(Context2d::GetShadowBlur) { */ NAN_SETTER(Context2d::SetShadowBlur) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetShadowBlur called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetShadowBlur); int n = Nan::To(value).FromMaybe(0); if (n >= 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -1682,10 +1643,7 @@ NAN_SETTER(Context2d::SetShadowBlur) { */ NAN_GETTER(Context2d::GetAntiAlias) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetAntiAlias called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetAntiAlias); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *aa; switch (cairo_get_antialias(context->context())) { @@ -1702,10 +1660,7 @@ NAN_GETTER(Context2d::GetAntiAlias) { */ NAN_SETTER(Context2d::SetAntiAlias) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetAntiAlias called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetAntiAlias); Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); @@ -1729,10 +1684,7 @@ NAN_SETTER(Context2d::SetAntiAlias) { */ NAN_GETTER(Context2d::GetTextDrawingMode) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetTextDrawingMode called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetTextDrawingMode); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *mode; if (context->state->textDrawingMode == TEXT_DRAW_PATHS) { @@ -1750,10 +1702,7 @@ NAN_GETTER(Context2d::GetTextDrawingMode) { */ NAN_SETTER(Context2d::SetTextDrawingMode) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetTextDrawingMode called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetTextDrawingMode); Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (0 == strcmp("path", *str)) { @@ -1768,10 +1717,7 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { */ NAN_GETTER(Context2d::GetQuality) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetQuality called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetQuality); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *filter; switch (cairo_pattern_get_filter(cairo_get_source(context->context()))) { @@ -1789,10 +1735,7 @@ NAN_GETTER(Context2d::GetQuality) { */ NAN_SETTER(Context2d::SetQuality) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetQuality called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetQuality); Nan::Utf8String str(Nan::To(value).ToLocalChecked()); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_filter_t filter; @@ -1855,10 +1798,7 @@ void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { */ NAN_GETTER(Context2d::GetCurrentTransform) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetCurrentTransform called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetCurrentTransform); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local instance = get_current_transform(context); @@ -1870,10 +1810,7 @@ NAN_GETTER(Context2d::GetCurrentTransform) { */ NAN_SETTER(Context2d::SetCurrentTransform) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetCurrentTransform called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetCurrentTransform); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local ctx = Nan::GetCurrentContext(); Local mat = Nan::To(value).ToLocalChecked(); @@ -1895,10 +1832,7 @@ NAN_SETTER(Context2d::SetCurrentTransform) { */ NAN_GETTER(Context2d::GetFillStyle) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetFillStyle called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetFillStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Isolate *iso = Isolate::GetCurrent(); Local style; @@ -1916,10 +1850,7 @@ NAN_GETTER(Context2d::GetFillStyle) { */ NAN_SETTER(Context2d::SetFillStyle) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetFillStyle called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetFillStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1947,10 +1878,7 @@ NAN_SETTER(Context2d::SetFillStyle) { */ NAN_GETTER(Context2d::GetStrokeStyle) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetStrokeStyle called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetStrokeStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Local style; @@ -1967,10 +1895,7 @@ NAN_GETTER(Context2d::GetStrokeStyle) { */ NAN_SETTER(Context2d::SetStrokeStyle) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetStrokeStyle called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetStrokeStyle); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); if (value->IsString()) { @@ -1998,10 +1923,7 @@ NAN_SETTER(Context2d::SetStrokeStyle) { */ NAN_GETTER(Context2d::GetMiterLimit) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetMiterLimit called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetMiterLimit); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_miter_limit(context->context()))); } @@ -2011,10 +1933,7 @@ NAN_GETTER(Context2d::GetMiterLimit) { */ NAN_SETTER(Context2d::SetMiterLimit) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetMiterLimit called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetMiterLimit); double n = Nan::To(value).FromMaybe(0); if (n > 0) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2027,10 +1946,7 @@ NAN_SETTER(Context2d::SetMiterLimit) { */ NAN_GETTER(Context2d::GetLineWidth) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetLineWidth called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetLineWidth); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(Nan::New(cairo_get_line_width(context->context()))); } @@ -2040,10 +1956,7 @@ NAN_GETTER(Context2d::GetLineWidth) { */ NAN_SETTER(Context2d::SetLineWidth) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetLineWidth called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetLineWidth); double n = Nan::To(value).FromMaybe(0); if (n > 0 && n != std::numeric_limits::infinity()) { Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); @@ -2056,10 +1969,7 @@ NAN_SETTER(Context2d::SetLineWidth) { */ NAN_GETTER(Context2d::GetLineJoin) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetLineJoin called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetLineJoin); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *join; switch (cairo_get_line_join(context->context())) { @@ -2075,10 +1985,7 @@ NAN_GETTER(Context2d::GetLineJoin) { */ NAN_SETTER(Context2d::SetLineJoin) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetLineJoin called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetLineJoin); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -2096,10 +2003,7 @@ NAN_SETTER(Context2d::SetLineJoin) { */ NAN_GETTER(Context2d::GetLineCap) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetLineCap called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetLineCap); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char *cap; switch (cairo_get_line_cap(context->context())) { @@ -2115,10 +2019,7 @@ NAN_GETTER(Context2d::GetLineCap) { */ NAN_SETTER(Context2d::SetLineCap) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetLineCap called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetLineCap); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); Nan::Utf8String type(Nan::To(value).ToLocalChecked()); @@ -2153,10 +2054,7 @@ NAN_METHOD(Context2d::IsPointInPath) { */ NAN_SETTER(Context2d::SetShadowColor) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetShadowColor called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetShadowColor); short ok; Nan::Utf8String str(Nan::To(value).ToLocalChecked()); uint32_t rgba = rgba_from_string(*str, &ok); @@ -2171,10 +2069,7 @@ NAN_SETTER(Context2d::SetShadowColor) { */ NAN_GETTER(Context2d::GetShadowColor) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetShadowColor called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetShadowColor); char buf[64]; Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); rgba_to_string(context->state->shadow, buf, sizeof(buf)); @@ -2649,10 +2544,7 @@ NAN_METHOD(Context2d::MoveTo) { */ NAN_GETTER(Context2d::GetFont) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetFont called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetFont); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); Isolate *iso = Isolate::GetCurrent(); Local font; @@ -2675,10 +2567,7 @@ NAN_GETTER(Context2d::GetFont) { */ NAN_SETTER(Context2d::SetFont) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetFont called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetFont); if (!value->IsString()) return; Isolate *iso = Isolate::GetCurrent(); @@ -2736,10 +2625,7 @@ NAN_SETTER(Context2d::SetFont) { */ NAN_GETTER(Context2d::GetTextBaseline) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetTextBaseline called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetTextBaseline); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char* baseline; switch (context->state->textBaseline) { @@ -2759,10 +2645,7 @@ NAN_GETTER(Context2d::GetTextBaseline) { */ NAN_SETTER(Context2d::SetTextBaseline) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetTextBaseline called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetTextBaseline); if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); @@ -2786,10 +2669,7 @@ NAN_SETTER(Context2d::SetTextBaseline) { */ NAN_GETTER(Context2d::GetTextAlign) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetTextAlign called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetTextAlign); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); const char* align; switch (context->state->textAlignment) { @@ -2809,10 +2689,7 @@ NAN_GETTER(Context2d::GetTextAlign) { */ NAN_SETTER(Context2d::SetTextAlign) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetTextAlign called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetTextAlign); if (!value->IsString()) return; Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); @@ -2975,10 +2852,7 @@ NAN_METHOD(Context2d::GetLineDash) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_SETTER(Context2d::SetLineDashOffset) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.SetLineDashOffset called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.SetLineDashOffset); double offset = Nan::To(value).FromMaybe(0); if (!std::isfinite(offset)) return; @@ -2996,10 +2870,7 @@ NAN_SETTER(Context2d::SetLineDashOffset) { * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ NAN_GETTER(Context2d::GetLineDashOffset) { - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Context2d.GetLineDashOffset called on incompatible receiver"); - return; - } + CHECK_RECEIVER(Context2d.GetLineDashOffset); Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); cairo_t *ctx = context->context(); double offset; @@ -3486,3 +3357,5 @@ NAN_METHOD(Context2d::Ellipse) { } cairo_set_matrix(ctx, &save_matrix); } + +#undef CHECK_RECEIVER From 672104c1a4bd202e56d8837ef83ebf7aee2dfce2 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 29 Oct 2022 18:35:33 -0700 Subject: [PATCH 036/128] v2.10.2 --- CHANGELOG.md | 9 ++++----- package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a666d5a23..f17f6c077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,19 +8,18 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed -* Improve performance and memory usage of `save()`/`restore()`. -* `save()`/`restore()` no longer have a maximum depth (previously 64 states). ### Added ### Fixed -* `textBaseline` and `textAlign` were not saved/restored by `save()`/`restore()`. ([#1936](https://github.com/Automattic/node-canvas/issues/2029)) 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)) -### Changed +* `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. -* Implement valid `this` checks in all `SetAccessor` methods. +### Changed +* Improve performance and memory usage of `save()`/`restore()`. +* `save()`/`restore()` no longer have a maximum depth (previously 64 states). 2.10.1 ================== diff --git a/package.json b/package.json index cd6754ea4..bfc152224 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.10.1", + "version": "2.10.2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 40b43822a478ceb251b5f86b29f2763208bd2f03 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 13 Dec 2022 23:15:07 -0500 Subject: [PATCH 037/128] use tailored types instead of extending DOM Fixes #1656 --- CHANGELOG.md | 1 + types/index.d.ts | 263 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 204 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f17f6c077..f1a61ef90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### 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 ================== diff --git a/types/index.d.ts b/types/index.d.ts index 7b53f4851..3537fcf75 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,5 +1,4 @@ // TypeScript Version: 3.0 -/// import { Readable } from 'stream' @@ -80,7 +79,7 @@ export class Canvas { 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,126 @@ 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 actualBoundingBoxAscent: number; + readonly actualBoundingBoxDescent: number; + readonly actualBoundingBoxLeft: number; + readonly actualBoundingBoxRight: 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; + 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; /** * _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,55 +271,27 @@ declare class NodeCanvasRenderingContext2D extends CanvasRenderingContext2D { * (aside from using the stroke and fill style, respectively). */ textDrawingMode: 'path' | 'glyph' - - /** _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; } -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. @@ -312,14 +390,79 @@ 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 } +export class DOMPoint { + w: number; + x: number; + y: number; + z: number; +} -declare class NodeCanvasDOMPoint extends DOMPoint {} -export { NodeCanvasDOMPoint as 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; + 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; + 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; +} -declare class NodeCanvasImageData extends ImageData {} -export { NodeCanvasImageData as ImageData } +export class ImageData { + constructor(sw: number, sh: number); + constructor(data: Uint8ClampedArray, sw: number, sh?: number); + readonly data: Uint8ClampedArray; + readonly height: number; + readonly width: number; +} // This is marked private, but is exported... // export function parseFont(description: string): object From fc160f5d3a4bc1171fa012391dda923561fb497e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 22 Dec 2022 09:07:02 -0800 Subject: [PATCH 038/128] v2.11.0 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1a61ef90..75e335c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed + +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 diff --git a/package.json b/package.json index bfc152224..c45c4ea4d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.10.2", + "version": "2.11.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 9f313dffb285218937b6ca12bb2637599439bb37 Mon Sep 17 00:00:00 2001 From: Suyooo Date: Sat, 24 Dec 2022 11:47:23 +0100 Subject: [PATCH 039/128] Add canvas property to CanvasRenderingContext2D type --- CHANGELOG.md | 1 + types/index.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e335c0a..484dca949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Add missing property `canvas` to the `CanvasRenderingContext2D` type 2.11.0 ================== diff --git a/types/index.d.ts b/types/index.d.ts index 3537fcf75..8bcfd105e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -283,6 +283,7 @@ export class CanvasRenderingContext2D { font: string; textBaseline: CanvasTextBaseline; textAlign: CanvasTextAlign; + canvas: Canvas; } export class CanvasGradient { From 55d39cb532a3a8e828516603d7bbc9fafe5c190b Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 2 Jan 2023 18:40:33 -0500 Subject: [PATCH 040/128] fix macos CI --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 31b7497bc..89916e479 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -66,6 +66,7 @@ jobs: - name: Install Dependencies run: | brew update + brew install python3 || : # python doesn't need to be linked brew install pkg-config cairo pango libpng jpeg giflib librsvg - name: Install run: npm install --build-from-source From 4c276e07f570ee6a727f88dcc67251823895c671 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 2 Jan 2023 17:50:32 -0500 Subject: [PATCH 041/128] fix incorrect text width with newer (1.43?) Pango Text renders wider or narrower, as if letter-spacing is set, than it should with newer versions of Pango. My best understanding of this problem is that around version 1.43 Pango dropped support for font hinting because it switched to Harbuzz for glyph postions instead of Freetype. For some reason it still rounds the glyph positions by default. There's no need for node-canvas to support font hinting. The maintainers of the Linux font stack (Behdad and Matthias) have stated that they wont, and font hinting is subjective, and browsers have moved to subpixel positioning too. Reading (warning: lots of drama to wade through): - https://gitlab.gnome.org/GNOME/pango/-/issues/404 - https://gitlab.gnome.org/GNOME/pango/-/issues/463 - https://github.com/harfbuzz/harfbuzz/issues/1892 - https://github.com/harfbuzz/harfbuzz/issues/2394 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 7 +++++++ test/public/tests.js | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 484dca949..4222c6ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed * Add missing property `canvas` to the `CanvasRenderingContext2D` type +* Fixed glyph positions getting rounded, resulting text having a slight `letter-spacing` effect 2.11.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 699ec88fc..c40b00fc2 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -171,6 +171,13 @@ Context2d::Context2d(Canvas *canvas) { _canvas = canvas; _context = canvas->createCairoContext(); _layout = pango_cairo_create_layout(_context); + + // 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 + states.emplace(); state = &states.top(); pango_layout_set_font_description(_layout, state->fontDescription); diff --git a/test/public/tests.js b/test/public/tests.js index 651105e36..d24202602 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2693,6 +2693,11 @@ tests['measureText()'] = function (ctx) { drawWithBBox('right', 195, 195) } +tests['glyph advances (#2184)'] = function (ctx) { + ctx.font = '8px Arial' + ctx.fillText('A float is a box that is shifted to the left or right on the current line.', 0, 8) +} + tests['image sampling (#1084)'] = function (ctx, done) { let loaded1, loaded2 const img1 = new Image() From fdf709a7b08abae33a93c510b96f71df6c13c7b0 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 7 Jan 2023 14:39:53 -0500 Subject: [PATCH 042/128] move ctx.font string to the state struct fixes #1946 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 12 ++---------- src/CanvasRenderingContext2d.h | 3 ++- test/canvas.test.js | 2 +- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4222c6ffd..11023e97c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### 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 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index c40b00fc2..b99c01602 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -207,7 +207,6 @@ void Context2d::resetState() { void Context2d::_resetPersistentHandles() { _fillStyle.Reset(); _strokeStyle.Reset(); - _font.Reset(); } /* @@ -2553,15 +2552,8 @@ NAN_METHOD(Context2d::MoveTo) { NAN_GETTER(Context2d::GetFont) { CHECK_RECEIVER(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); + info.GetReturnValue().Set(Nan::New(context->state->font).ToLocalChecked()); } /* @@ -2624,7 +2616,7 @@ NAN_SETTER(Context2d::SetFont) { context->state->fontDescription = sys_desc; pango_layout_set_font_description(context->_layout, sys_desc); - context->_font.Reset(value); + context->state->font = *Nan::Utf8String(value); } /* diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 568ebc8cc..8ea4d60b8 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -27,6 +27,7 @@ struct canvas_state_t { 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; @@ -57,6 +58,7 @@ struct canvas_state_t { shadowOffsetY = other.shadowOffsetY; textDrawingMode = other.textDrawingMode; fontDescription = pango_font_description_copy(other.fontDescription); + font = other.font; imageSmoothingEnabled = other.imageSmoothingEnabled; } @@ -216,7 +218,6 @@ class Context2d : public Nan::ObjectWrap { void _setStrokePattern(v8::Local arg); Nan::Persistent _fillStyle; Nan::Persistent _strokeStyle; - Nan::Persistent _font; Canvas *_canvas; cairo_t *_context; cairo_path_t *_path; diff --git a/test/canvas.test.js b/test/canvas.test.js index af33befaa..083459ab6 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1918,7 +1918,7 @@ describe('Canvas', function () { ['shadowBlur', 5], ['shadowColor', '#ff0000'], ['globalCompositeOperation', 'copy'], - // ['font', '25px serif'], // TODO #1946 + ['font', '25px serif'], ['textAlign', 'center'], ['textBaseline', 'bottom'], // Added vs. WPT From 9ecfb70518889735ad61354824c4590403f5edaa Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 20 Mar 2023 21:51:35 -0400 Subject: [PATCH 043/128] v2.11.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11023e97c..396cd7ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed + +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) diff --git a/package.json b/package.json index c45c4ea4d..29025e2d8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.11.0", + "version": "2.11.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 9910243d84941a8c6ced459f46abd1971ebf287b Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 25 Mar 2023 13:39:04 -0400 Subject: [PATCH 044/128] fix not compiling on certain windows versions --- src/register_font.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/register_font.cc b/src/register_font.cc index ae44c9aba..e43dd8125 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -264,7 +264,7 @@ get_pango_font_description(unsigned char* filepath) { FILE_SHARE_READ, NULL, OPEN_EXISTING, - NULL, + FILE_ATTRIBUTE_NORMAL, NULL ); if(!hFile){ From 38e0a3285a6e005e02a6505f3fc2809d0484e43b Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 27 Mar 2023 20:16:47 -0400 Subject: [PATCH 045/128] v2.11.2 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 396cd7ff0..cbbdb8254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed +2.11.2 +================== +### Fixed +* Building on Windows in CI (and maybe other Windows configurations?) (#2216) + 2.11.1 ================== ### Fixed diff --git a/package.json b/package.json index 29025e2d8..77e72328c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.11.1", + "version": "2.11.2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From f3184ba9da2737dbe25275631678a7ef5924fe6b Mon Sep 17 00:00:00 2001 From: Mohammed Keyvanzadeh Date: Thu, 13 Apr 2023 20:09:08 +0330 Subject: [PATCH 046/128] src: refactor and apply fixes - Defer the initialization of the `op` variable to the `default` switch case to avoid a compiler warning. - Use a `default` switch case with a null statement if some enum values aren't suppsed to be handled, this avoids a compiler warning. - 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. - Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. - Remove unused private field `backend` in the `Backend` class. - Fix a case of use-after-free. - Fix usage of garbage value by filling the allocated memory entirely with zeros if it's not modified. - Fix a potential memory leak. --- CHANGELOG.md | 8 ++++++++ src/Canvas.cc | 3 ++- src/CanvasRenderingContext2d.cc | 15 ++++++++++++--- src/Image.cc | 18 +++++++++++++----- src/backend/Backend.cc | 5 ++--- src/backend/Backend.h | 1 - src/backend/PdfBackend.cc | 2 +- src/backend/SvgBackend.cc | 2 +- src/register_font.cc | 1 + 9 files changed, 40 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbdb8254..be424e687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,16 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* 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) ### Added ### 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) 2.11.2 ================== diff --git a/src/Canvas.cc b/src/Canvas.cc index 860d5bb46..0cfe750d6 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -133,8 +133,9 @@ NAN_METHOD(Canvas::New) { } if (!backend->isSurfaceValid()) { + const char *error = backend->getError(); delete backend; - return Nan::ThrowError(backend->getError()); + return Nan::ThrowError(error); } Canvas* canvas = new Canvas(backend); diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index b99c01602..5bfe08d6a 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -607,8 +607,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)); @@ -627,6 +627,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;y(info.This()); cairo_t *ctx = context->context(); - const char *op = "source-over"; + const char *op{}; switch (cairo_get_operator(ctx)) { // composite modes: case CAIRO_OPERATOR_CLEAR: op = "clear"; break; @@ -1469,6 +1476,7 @@ 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()); @@ -2507,6 +2515,7 @@ Context2d::setTextPath(double x, double y) { pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; + default: ; } y -= getBaselineAdjustment(_layout, state->textBaseline); diff --git a/src/Image.cc b/src/Image.cc index 35ee7947a..301257769 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1192,11 +1192,13 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { 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); + + width = naturalWidth = d_width; + height = naturalHeight = d_height; status = renderSVGToSurface(); if (status != CAIRO_STATUS_SUCCESS) { @@ -1232,7 +1234,13 @@ Image::renderSVGToSurface() { return status; } - gboolean render_ok = rsvg_handle_render_cairo(_rsvg, cr); + RsvgRectangle viewport = { + .x = 0, + .y = 0, + .width = static_cast(width), + .height = 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/backend/Backend.cc b/src/backend/Backend.cc index 528f61a08..9f2b39dd3 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -9,7 +9,7 @@ Backend::Backend(std::string name, int width, int height) Backend::~Backend() { - this->destroySurface(); + Backend::destroySurface(); } void Backend::init(const Nan::FunctionCallbackInfo &info) { @@ -97,8 +97,7 @@ bool Backend::isSurfaceValid(){ 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..f8448c41a 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -57,7 +57,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/PdfBackend.cc b/src/backend/PdfBackend.cc index d8bd23422..fe831a68d 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -8,7 +8,7 @@ using namespace v8; PdfBackend::PdfBackend(int width, int height) : Backend("pdf", width, height) { - createSurface(); + PdfBackend::createSurface(); } PdfBackend::~PdfBackend() { diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index 10bf4caa7..7d4181fc2 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -9,7 +9,7 @@ using namespace v8; SvgBackend::SvgBackend(int width, int height) : Backend("svg", width, height) { - createSurface(); + SvgBackend::createSurface(); } SvgBackend::~SvgBackend() { diff --git a/src/register_font.cc b/src/register_font.cc index e43dd8125..37182c0ac 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -304,6 +304,7 @@ get_pango_font_description(unsigned char* filepath) { } pango_font_description_set_family_static(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)); From 59022195f7efc02205c7da50224c392e057b597e Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 18 Apr 2023 02:54:52 +0800 Subject: [PATCH 047/128] add string tags for browser polyglot classes (#2214) * add string tags * set toStringTag property to configurable: true --- CHANGELOG.md | 1 + lib/bindings.js | 30 ++++++++++++++++++++++++ test/canvas.test.js | 22 ++++++++++++++++- test/image.test.js | 5 ++++ test/imageData.test.js | 5 ++++ test/wpt/generated/the-canvas-element.js | 6 +++++ 6 files changed, 68 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be424e687..b3ba9a13b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * Avoid calling virtual methods in constructors/destructors to avoid bypassing virtual dispatch. (#2229) * Remove unused private field `backend` in the `Backend` class. (#2229) ### Added +* Added string tags to support class detection ### 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) 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/test/canvas.test.js b/test/canvas.test.js index 083459ab6..9573688f5 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1410,6 +1410,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') @@ -1439,6 +1447,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) @@ -1447,6 +1460,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) @@ -1943,7 +1963,7 @@ describe('Canvas', function () { ctx[k] = v ctx.restore() assert.strictEqual(ctx[k], old) - + // save() doesn't modify the value: ctx[k] = v old = ctx[k] diff --git a/test/image.test.js b/test/image.test.js index 8d54dd90f..ec1631a10 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -30,6 +30,11 @@ describe('Image', function () { 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) diff --git a/test/imageData.test.js b/test/imageData.test.js index d3c84c29a..04b117b45 100644 --- a/test/imageData.test.js +++ b/test/imageData.test.js @@ -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/wpt/generated/the-canvas-element.js b/test/wpt/generated/the-canvas-element.js index 8b1a6817e..cea4fd9b4 100644 --- a/test/wpt/generated/the-canvas-element.js +++ b/test/wpt/generated/the-canvas-element.js @@ -171,6 +171,12 @@ describe("WPT: the-canvas-element", function () { 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); From 5bd5b243c4353fb9bdf03287e993104964cc0c39 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 18 Apr 2023 04:44:15 +0200 Subject: [PATCH 048/128] Upgrade GitHub Actions to use checkout@v3 (#2213) * Upgrade GitHub Actions * Fix typo * Update ci.yaml --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 89916e479..613c91740 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Dependencies run: | sudo apt update @@ -38,7 +38,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - 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" @@ -62,7 +62,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Dependencies run: | brew update @@ -80,7 +80,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 14 - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install run: npm install --ignore-scripts - name: Lint From adf73ee39e2676b5c67b02bef5742b941033495c Mon Sep 17 00:00:00 2001 From: Hubert Argasinski Date: Thu, 27 Apr 2023 12:41:56 -0400 Subject: [PATCH 049/128] Add node 20 to CI * prebuild binaries for node 20 * update changelog * update changelog --- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/prebuild.yaml | 6 +++--- CHANGELOG.md | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 613c91740..c21812da6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,13 +7,13 @@ 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, 18] + node: [10, 12, 14, 16, 18, 20] steps: - uses: actions/setup-node@v3 with: @@ -33,7 +33,7 @@ jobs: runs-on: windows-2019 strategy: matrix: - node: [10, 12, 14, 16, 18] + node: [10, 12, 14, 16, 18, 20] steps: - uses: actions/setup-node@v3 with: @@ -57,7 +57,7 @@ jobs: runs-on: macos-latest strategy: matrix: - node: [10, 12, 14, 16, 18] + node: [10, 12, 14, 16, 18, 20] steps: - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index a112eef87..784069e06 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -24,7 +24,7 @@ jobs: Linux: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Linux runs-on: ubuntu-latest @@ -97,7 +97,7 @@ jobs: macOS: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, macOS runs-on: macos-latest @@ -163,7 +163,7 @@ jobs: Win: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18] + node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Windows runs-on: windows-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ba9a13b..e0768f8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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) ### Added * Added string tags to support class detection ### Fixed From ce29f697ced288b8d948e92b93b91d32ca3353d5 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 15 Apr 2023 11:49:49 -0400 Subject: [PATCH 050/128] port to node-addon-api (remove NAN, v8, libuv) --- CHANGELOG.md | 1 + Readme.md | 2 +- binding.gyp | 5 +- index.js | 3 + lib/context2d.js | 3 - lib/pattern.js | 2 - package.json | 4 +- src/Backends.cc | 10 +- src/Backends.h | 7 +- src/Canvas.cc | 756 ++++++------- src/Canvas.h | 55 +- src/CanvasError.h | 14 + src/CanvasGradient.cc | 128 +-- src/CanvasGradient.h | 18 +- src/CanvasPattern.cc | 135 ++- src/CanvasPattern.h | 20 +- src/CanvasRenderingContext2d.cc | 1830 +++++++++++++++---------------- src/CanvasRenderingContext2d.h | 222 ++-- src/Image.cc | 271 ++--- src/Image.h | 37 +- src/ImageData.cc | 148 ++- src/ImageData.h | 17 +- src/InstanceData.h | 15 + src/JPEGStream.h | 32 +- src/backend/Backend.cc | 26 +- src/backend/Backend.h | 11 +- src/backend/ImageBackend.cc | 41 +- src/backend/ImageBackend.h | 12 +- src/backend/PdfBackend.cc | 33 +- src/backend/PdfBackend.h | 13 +- src/backend/SvgBackend.cc | 33 +- src/backend/SvgBackend.h | 11 +- src/closure.cc | 26 + src/closure.h | 16 +- src/init.cc | 56 +- test/canvas.test.js | 2 +- test/image.test.js | 4 +- test/imageData.test.js | 2 +- 38 files changed, 1920 insertions(+), 2101 deletions(-) create mode 100644 src/InstanceData.h diff --git a/CHANGELOG.md b/CHANGELOG.md index e0768f8c3..97b2c2661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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) +* Migrated to N-API (by way of node-addon-api) and removed libuv and v8 dependencies ### Added * Added string tags to support class detection ### Fixed diff --git a/Readme.md b/Readme.md index cc945aa9a..c029e27cb 100644 --- a/Readme.md +++ b/Readme.md @@ -13,7 +13,7 @@ $ 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. -The minimum version of Node.js required is **6.0.0**. +The minimum version of Node.js required is **10.20.0**. ### Compiling diff --git a/binding.gyp b/binding.gyp index 19a33e816..166842641 100644 --- a/binding.gyp +++ b/binding.gyp @@ -57,7 +57,8 @@ }, { 'target_name': 'canvas', - 'include_dirs': ["=6" + "node": ">=10.20.0" }, "license": "MIT" } diff --git a/src/Backends.cc b/src/Backends.cc index 2256c32b6..3a557669c 100644 --- a/src/Backends.cc +++ b/src/Backends.cc @@ -4,15 +4,15 @@ #include "backend/PdfBackend.h" #include "backend/SvgBackend.h" -using namespace v8; +using namespace Napi; -void Backends::Initialize(Local 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 0cfe750d6..2555605f9 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 @@ -35,17 +35,8 @@ "with at least a family (string) and optionally weight (string/number) " \ "and style (string)." -#define CHECK_RECEIVER(prop) \ - if (!Canvas::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ - Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ - return; \ - } - -using namespace v8; using namespace std; -Nan::Persistent Canvas::constructor; - std::vector font_face_list; /* @@ -53,138 +44,138 @@ std::vector font_face_list; */ 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"), + InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync"), + InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync"), #ifdef HAVE_JPEG - Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); + InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync"), #endif - Nan::SetAccessor(proto, Nan::New("type").ToLocalChecked(), GetType); - Nan::SetAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); - - Nan::SetTemplate(proto, "PNG_NO_FILTERS", Nan::New(PNG_NO_FILTERS)); - Nan::SetTemplate(proto, "PNG_FILTER_NONE", Nan::New(PNG_FILTER_NONE)); - Nan::SetTemplate(proto, "PNG_FILTER_SUB", Nan::New(PNG_FILTER_SUB)); - Nan::SetTemplate(proto, "PNG_FILTER_UP", Nan::New(PNG_FILTER_UP)); - Nan::SetTemplate(proto, "PNG_FILTER_AVG", Nan::New(PNG_FILTER_AVG)); - Nan::SetTemplate(proto, "PNG_FILTER_PAETH", Nan::New(PNG_FILTER_PAETH)); - Nan::SetTemplate(proto, "PNG_ALL_FILTERS", Nan::New(PNG_ALL_FILTERS)); - - // Class methods - Nan::SetMethod(ctor, "_registerFont", RegisterFont); - Nan::SetMethod(ctor, "_deregisterAllFonts", DeregisterAllFonts); + InstanceAccessor<&Canvas::GetType>("type"), + InstanceAccessor<&Canvas::GetStride>("stride"), + InstanceAccessor<&Canvas::GetWidth, &Canvas::SetWidth>("width"), + InstanceAccessor<&Canvas::GetHeight, &Canvas::SetHeight>("height"), + InstanceValue("PNG_NO_FILTERS", Napi::Number::New(env, PNG_NO_FILTERS)), + InstanceValue("PNG_FILTER_NONE", Napi::Number::New(env, PNG_FILTER_NONE)), + InstanceValue("PNG_FILTER_SUB", Napi::Number::New(env, PNG_FILTER_SUB)), + InstanceValue("PNG_FILTER_UP", Napi::Number::New(env, PNG_FILTER_UP)), + InstanceValue("PNG_FILTER_AVG", Napi::Number::New(env, PNG_FILTER_AVG)), + InstanceValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH)), + InstanceValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS)), + StaticMethod<&Canvas::RegisterFont>("_registerFont"), + StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts") + }); - 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 { - backend = new ImageBackend(0, 0); + } 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()); } if (!backend->isSurfaceValid()) { - const char *error = backend->getError(); - delete backend; - return Nan::ThrowError(error); + Napi::Error::New(env, backend->getError()).ThrowAsJavaScriptException(); + return; } - Canvas* canvas = new Canvas(backend); - canvas->Wrap(info.This()); + backend->setCanvas(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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()); } } @@ -192,22 +183,20 @@ NAN_SETTER(Canvas::SetWidth) { * Get height. */ -NAN_GETTER(Canvas::GetHeight) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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()); } } @@ -216,8 +205,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(), @@ -227,102 +216,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. - */ +static void +parsePNGArgs(Napi::Value arg, PngClosure& pngargs) { + if (arg.IsObject()) { + Napi::Object obj = arg.As(); + Napi::Value cLevel; -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(); - - 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(); } } } @@ -330,29 +301,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"); @@ -392,146 +361,137 @@ 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) { + EncodingWorker *worker = new EncodingWorker(info.Env()); 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_finish(surface()); + 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(); + 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(); + worker->Init(&ToJpegBufferAsync, closure); + worker->Queue(); + return env.Undefined(); } #endif + + return env.Undefined(); } /* @@ -540,15 +500,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; } @@ -557,69 +514,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])); - - Nan::TryCatch try_catch; + closure.cb = Napi::Persistent(info[0].As()); - cairo_status_t status = canvas_write_to_png_stream(canvas->surface(), streamPNG, &closure); + cairo_status_t status = canvas_write_to_png_stream(surface(), streamPNG, &closure); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - return; - } else if (status) { - Local argv[1] = { Canvas::Error(status) }; - Nan::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; } @@ -643,45 +582,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(canvas->surface(), streamPDF, &streaminfo); + cairo_status_t status = canvas_write_to_pdf_stream(surface(), streamPDF, &streaminfo); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } else if (status) { - Local error = Canvas::Error(status); - Nan::Call(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) }); + } } } @@ -696,60 +631,64 @@ 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])); - - Nan::TryCatch try_catch; - uint32_t bufsize = getSafeBufSize(canvas); - write_to_jpeg_stream(canvas->surface(), bufsize, &closure); + closure.cb = Napi::Persistent(info[1].As()); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - } - return; + uint32_t bufsize = getSafeBufSize(this); + write_to_jpeg_stream(surface(), bufsize, &closure); } #endif char * -str_value(Local val, const char *fallback, bool can_be_number) { - if (val->IsString() || (can_be_number && val->IsNumber())) { - return strdup(*Nan::Utf8String(val)); - } else if (fallback) { - return 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); + 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)); @@ -763,19 +702,22 @@ 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(); + } } free(family); @@ -783,7 +725,9 @@ NAN_METHOD(Canvas::RegisterFont) { free(style); } -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; @@ -794,25 +738,7 @@ NAN_METHOD(Canvas::DeregisterAllFonts) { }); font_face_list.clear(); - if (!success) Nan::ThrowError("Could not deregister one or more fonts"); -} - -/* - * Initialize cairo surface. - */ - -Canvas::Canvas(Backend* backend) : ObjectWrap() { - _backend = backend; -} - -/* - * Destroy cairo surface. - */ - -Canvas::~Canvas() { - if (_backend != NULL) { - delete _backend; - } + if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException(); } /* @@ -926,21 +852,19 @@ 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()->recreateSurface(); + // Reset context + Context2d *context2d = Context2d::Unwrap(context.As()); + cairo_t *prev = context2d->context(); + context2d->setContext(createCairoContext()); + context2d->resetState(); + cairo_destroy(prev); + } } /** @@ -958,9 +882,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)); } - -#undef CHECK_RECEIVER diff --git a/src/Canvas.h b/src/Canvas.h index 60d3b4216..5f35b356b 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -2,12 +2,14 @@ #pragma once +struct Closure; + #include "backend/Backend.h" +#include "closure.h" #include #include "dll_visibility.h" -#include +#include #include -#include #include #include @@ -49,27 +51,26 @@ enum canvas_draw_mode_t : uint8_t { * 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); + 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); @@ -81,16 +82,18 @@ class Canvas: public Nan::ObjectWrap { 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; private: - ~Canvas(); Backend* _backend; + Napi::ObjectReference _jsBackend; + Napi::FunctionReference ctor; }; 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..9c2d42360 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") + }); + + 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..55b8bb7fb 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") + }); // 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 5bfe08d6a..9457122d0 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -8,6 +8,7 @@ #include "Canvas.h" #include "CanvasGradient.h" #include "CanvasPattern.h" +#include "InstanceData.h" #include #include #include "Image.h" @@ -19,10 +20,6 @@ #include "Util.h" #include -using namespace v8; - -Nan::Persistent Context2d::constructor; - /* * Rectangle arg assertions. */ @@ -36,12 +33,6 @@ Nan::Persistent Context2d::constructor; double width = args[2]; \ double height = args[3]; -#define CHECK_RECEIVER(prop) \ - if (!Context2d::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { \ - Nan::ThrowTypeError("Method " #prop " called on incompatible receiver"); \ - return; \ - } - constexpr double twoPi = M_PI * 2.; /* @@ -53,12 +44,13 @@ constexpr double twoPi = M_PI * 2.; pango_layout_get_font_description(LAYOUT), \ pango_context_get_language(pango_layout_get_context(LAYOUT))) -inline static bool checkArgs(const Nan::FunctionCallbackInfo &info, double *args, int argsNum, int offset = 0){ +inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){ + Napi::Number zero = Napi::Number::New(info.Env(), 0); int argsEnd = offset + argsNum; bool areArgsValid = true; for (int i = offset; i < argsEnd; i++) { - double val = Nan::To(info[i]).FromMaybe(0); + double val = info[i].ToNumber().UnwrapOr(zero).DoubleValue(); if (areArgsValid) { if (!std::isfinite(val)) { @@ -76,100 +68,139 @@ 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, "roundRect", RoundRect); - 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); - Nan::SetAccessor(proto, Nan::New("pixelFormat").ToLocalChecked(), GetFormat); - Nan::SetAccessor(proto, Nan::New("patternQuality").ToLocalChecked(), GetPatternQuality, SetPatternQuality); - Nan::SetAccessor(proto, Nan::New("imageSmoothingEnabled").ToLocalChecked(), GetImageSmoothingEnabled, SetImageSmoothingEnabled); - Nan::SetAccessor(proto, Nan::New("globalCompositeOperation").ToLocalChecked(), GetGlobalCompositeOperation, SetGlobalCompositeOperation); - Nan::SetAccessor(proto, Nan::New("globalAlpha").ToLocalChecked(), GetGlobalAlpha, SetGlobalAlpha); - Nan::SetAccessor(proto, Nan::New("shadowColor").ToLocalChecked(), GetShadowColor, SetShadowColor); - Nan::SetAccessor(proto, Nan::New("miterLimit").ToLocalChecked(), GetMiterLimit, SetMiterLimit); - Nan::SetAccessor(proto, Nan::New("lineWidth").ToLocalChecked(), GetLineWidth, SetLineWidth); - Nan::SetAccessor(proto, Nan::New("lineCap").ToLocalChecked(), GetLineCap, SetLineCap); - Nan::SetAccessor(proto, Nan::New("lineJoin").ToLocalChecked(), GetLineJoin, SetLineJoin); - Nan::SetAccessor(proto, Nan::New("lineDashOffset").ToLocalChecked(), GetLineDashOffset, SetLineDashOffset); - Nan::SetAccessor(proto, Nan::New("shadowOffsetX").ToLocalChecked(), GetShadowOffsetX, SetShadowOffsetX); - Nan::SetAccessor(proto, Nan::New("shadowOffsetY").ToLocalChecked(), GetShadowOffsetY, SetShadowOffsetY); - Nan::SetAccessor(proto, Nan::New("shadowBlur").ToLocalChecked(), GetShadowBlur, SetShadowBlur); - Nan::SetAccessor(proto, Nan::New("antialias").ToLocalChecked(), GetAntiAlias, SetAntiAlias); - Nan::SetAccessor(proto, Nan::New("textDrawingMode").ToLocalChecked(), GetTextDrawingMode, SetTextDrawingMode); - Nan::SetAccessor(proto, Nan::New("quality").ToLocalChecked(), GetQuality, SetQuality); - Nan::SetAccessor(proto, Nan::New("currentTransform").ToLocalChecked(), GetCurrentTransform, SetCurrentTransform); - Nan::SetAccessor(proto, Nan::New("fillStyle").ToLocalChecked(), GetFillStyle, SetFillStyle); - Nan::SetAccessor(proto, Nan::New("strokeStyle").ToLocalChecked(), GetStrokeStyle, SetStrokeStyle); - Nan::SetAccessor(proto, Nan::New("font").ToLocalChecked(), GetFont, SetFont); - Nan::SetAccessor(proto, Nan::New("textBaseline").ToLocalChecked(), GetTextBaseline, SetTextBaseline); - Nan::SetAccessor(proto, Nan::New("textAlign").ToLocalChecked(), GetTextAlign, SetTextAlign); - 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"), + InstanceMethod<&Context2d::PutImageData>("putImageData"), + InstanceMethod<&Context2d::GetImageData>("getImageData"), + InstanceMethod<&Context2d::CreateImageData>("createImageData"), + InstanceMethod<&Context2d::AddPage>("addPage"), + InstanceMethod<&Context2d::Save>("save"), + InstanceMethod<&Context2d::Restore>("restore"), + InstanceMethod<&Context2d::Rotate>("rotate"), + InstanceMethod<&Context2d::Translate>("translate"), + InstanceMethod<&Context2d::Transform>("transform"), + InstanceMethod<&Context2d::GetTransform>("getTransform"), + InstanceMethod<&Context2d::ResetTransform>("resetTransform"), + InstanceMethod<&Context2d::SetTransform>("setTransform"), + InstanceMethod<&Context2d::IsPointInPath>("isPointInPath"), + InstanceMethod<&Context2d::Scale>("scale"), + InstanceMethod<&Context2d::Clip>("clip"), + InstanceMethod<&Context2d::Fill>("fill"), + InstanceMethod<&Context2d::Stroke>("stroke"), + InstanceMethod<&Context2d::FillText>("fillText"), + InstanceMethod<&Context2d::StrokeText>("strokeText"), + InstanceMethod<&Context2d::FillRect>("fillRect"), + InstanceMethod<&Context2d::StrokeRect>("strokeRect"), + InstanceMethod<&Context2d::ClearRect>("clearRect"), + InstanceMethod<&Context2d::Rect>("rect"), + InstanceMethod<&Context2d::RoundRect>("roundRect"), + InstanceMethod<&Context2d::MeasureText>("measureText"), + InstanceMethod<&Context2d::MoveTo>("moveTo"), + InstanceMethod<&Context2d::LineTo>("lineTo"), + InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo"), + InstanceMethod<&Context2d::QuadraticCurveTo>("quadraticCurveTo"), + InstanceMethod<&Context2d::BeginPath>("beginPath"), + InstanceMethod<&Context2d::ClosePath>("closePath"), + InstanceMethod<&Context2d::Arc>("arc"), + InstanceMethod<&Context2d::ArcTo>("arcTo"), + InstanceMethod<&Context2d::Ellipse>("ellipse"), + InstanceMethod<&Context2d::SetLineDash>("setLineDash"), + InstanceMethod<&Context2d::GetLineDash>("getLineDash"), + InstanceMethod<&Context2d::CreatePattern>("createPattern"), + InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient"), + InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient"), + InstanceAccessor<&Context2d::GetFormat>("pixelFormat"), + InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality"), + InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled"), + InstanceAccessor<&Context2d::GetGlobalCompositeOperation, &Context2d::SetGlobalCompositeOperation>("globalCompositeOperation"), + InstanceAccessor<&Context2d::GetGlobalAlpha, &Context2d::SetGlobalAlpha>("globalAlpha"), + InstanceAccessor<&Context2d::GetShadowColor, &Context2d::SetShadowColor>("shadowColor"), + InstanceAccessor<&Context2d::GetMiterLimit, &Context2d::SetMiterLimit>("miterLimit"), + InstanceAccessor<&Context2d::GetLineWidth, &Context2d::SetLineWidth>("lineWidth"), + InstanceAccessor<&Context2d::GetLineCap, &Context2d::SetLineCap>("lineCap"), + InstanceAccessor<&Context2d::GetLineJoin, &Context2d::SetLineJoin>("lineJoin"), + InstanceAccessor<&Context2d::GetLineDashOffset, &Context2d::SetLineDashOffset>("lineDashOffset"), + InstanceAccessor<&Context2d::GetShadowOffsetX, &Context2d::SetShadowOffsetX>("shadowOffsetX"), + InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY"), + InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur"), + InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias"), + InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode"), + InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality"), + InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform"), + InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle"), + InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle"), + InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font"), + InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline"), + InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign") + }); + + 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); // As of January 2023, Pango rounds glyph positions which renders text wider @@ -188,8 +219,8 @@ Context2d::Context2d(Canvas *canvas) { */ Context2d::~Context2d() { - g_object_unref(_layout); - cairo_destroy(_context); + if (_layout) g_object_unref(_layout); + if (_context) cairo_destroy(_context); _resetPersistentHandles(); } @@ -283,7 +314,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++ ) { @@ -305,7 +335,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); @@ -321,11 +350,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; } } @@ -340,7 +369,8 @@ Context2d::fill(bool preserve) { 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); @@ -385,7 +415,8 @@ Context2d::fill(bool preserve) { 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); @@ -423,7 +454,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); @@ -442,7 +474,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); @@ -668,74 +701,14 @@ Context2d::blur(cairo_surface_t *surface, int radius) { free(precalc); } -/* - * Initialize a new Context2d with the given canvas. - */ - -NAN_METHOD(Context2d::New) { - if (!info.IsConstructCall()) { - return Nan::ThrowTypeError("Class constructors cannot be invoked without 'new'"); - } - - if (!info[0]->IsObject()) - 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) { - CHECK_RECEIVER(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; @@ -744,27 +717,28 @@ 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); } /* @@ -775,29 +749,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; @@ -809,10 +791,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; @@ -827,7 +809,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 @@ -836,12 +819,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; @@ -922,7 +905,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: { @@ -938,18 +922,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 or not an image canvas"); + 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 @@ -963,27 +948,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: @@ -1021,17 +1015,16 @@ 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(); switch (canvas->backend()->getFormat()) { case CAIRO_FORMAT_ARGB32: { @@ -1097,7 +1090,8 @@ NAN_METHOD(Context2d::GetImageData) { } 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: { @@ -1111,26 +1105,24 @@ 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 or not an image canvas"); - return; + 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 }; - - Local ctor = Nan::GetFunction(Nan::New(ImageData::constructor)).ToLocalChecked(); - Local instance = Nan::NewInstance(ctor, argc, argv).ToLocalChecked(); + 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 }); - info.GetReturnValue().Set(instance); + return ret.IsJust() ? ret.Unwrap() : env.Undefined(); } /** @@ -1138,40 +1130,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); + arr = Napi::Uint8Array::New(env, nBytes, ab, 0, napi_uint8_clamped_array); - const int argc = 3; - Local argv[argc] = { arr, Nan::New(width), Nan::New(height) }; + Napi::Function ctor = env.GetInstanceData()->ImageDataCtor.Value(); + Napi::Maybe ret = ctor.New({ arr, Napi::Number::New(env, width), Napi::Number::New(env, height) }); - 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(); } /* @@ -1197,13 +1186,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)) @@ -1222,32 +1217,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) { @@ -1286,7 +1284,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. @@ -1298,7 +1296,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; @@ -1346,23 +1344,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. @@ -1370,17 +1368,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)); } } @@ -1395,9 +1393,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); @@ -1411,22 +1409,21 @@ NAN_METHOD(Context2d::DrawImage) { * Get global alpha. */ -NAN_GETTER(Context2d::GetGlobalAlpha) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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; } } @@ -1434,10 +1431,9 @@ NAN_SETTER(Context2d::SetGlobalAlpha) { * Get global composite operation. */ -NAN_GETTER(Context2d::GetGlobalCompositeOperation) { - CHECK_RECEIVER(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{}; switch (cairo_get_operator(ctx)) { @@ -1479,27 +1475,28 @@ NAN_GETTER(Context2d::GetGlobalCompositeOperation) { default: op = "source-over"; } - info.GetReturnValue().Set(Nan::New(op).ToLocalChecked()); + return Napi::String::New(env, op); } /* * Set pattern quality. */ -NAN_SETTER(Context2d::SetPatternQuality) { - CHECK_RECEIVER(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; + } } } @@ -1507,148 +1504,146 @@ NAN_SETTER(Context2d::SetPatternQuality) { * Get pattern quality. */ -NAN_GETTER(Context2d::GetPatternQuality) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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; + } } } @@ -1656,73 +1651,76 @@ NAN_SETTER(Context2d::SetShadowBlur) { * Get current antialiasing setting. */ -NAN_GETTER(Context2d::GetAntiAlias) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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; + } } } @@ -1730,79 +1728,77 @@ NAN_SETTER(Context2d::SetTextDrawingMode) { * Get filter. */ -NAN_GETTER(Context2d::GetQuality) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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() ); } @@ -1811,78 +1807,70 @@ void parse_matrix_from_object(cairo_matrix_t &matrix, Local mat) { * Get current transform. */ -NAN_GETTER(Context2d::GetCurrentTransform) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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(); } } } @@ -1891,43 +1879,38 @@ NAN_SETTER(Context2d::SetFillStyle) { * Get current stroke style. */ -NAN_GETTER(Context2d::GetStrokeStyle) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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(); } } } @@ -1936,22 +1919,21 @@ NAN_SETTER(Context2d::SetStrokeStyle) { * Get miter limit. */ -NAN_GETTER(Context2d::GetMiterLimit) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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); } } @@ -1959,22 +1941,23 @@ NAN_SETTER(Context2d::SetMiterLimit) { * Get line width. */ -NAN_GETTER(Context2d::GetLineWidth) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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); + } } } @@ -1982,33 +1965,35 @@ NAN_SETTER(Context2d::SetLineWidth) { * Get line join. */ -NAN_GETTER(Context2d::GetLineJoin) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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); + } } } @@ -2016,33 +2001,35 @@ NAN_SETTER(Context2d::SetLineJoin) { * Get line cap. */ -NAN_GETTER(Context2d::GetLineCap) { - CHECK_RECEIVER(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) { - CHECK_RECEIVER(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); + } } } @@ -2050,31 +2037,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) { - CHECK_RECEIVER(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); } } @@ -2082,45 +2068,51 @@ NAN_SETTER(Context2d::SetShadowColor) { * Get shadow color. */ -NAN_GETTER(Context2d::GetShadowColor) { - CHECK_RECEIVER(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()) { + std::string str = stringValue.Unwrap().Utf8Value(); + uint32_t rgba = rgba_from_string(str.c_str(), &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); @@ -2130,59 +2122,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] @@ -2195,13 +2174,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] @@ -2227,56 +2206,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; @@ -2290,52 +2270,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); } } @@ -2344,36 +2321,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); } @@ -2381,19 +2358,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); } /* @@ -2414,44 +2391,47 @@ get_text_scale(PangoLayout *layout, double maxWidth) { } void -paintText(const Nan::FunctionCallbackInfo &info, bool stroke) { +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(); - pango_layout_set_text(layout, *str, -1); - pango_cairo_update_layout(context->context(), layout); + pango_layout_set_text(layout, str.c_str(), -1); + pango_cairo_update_layout(context(), layout); if (argsNum == 3) { 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()); } } @@ -2459,7 +2439,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); } @@ -2467,7 +2448,8 @@ NAN_METHOD(Context2d::FillText) { * Stroke text at (x ,y). */ -NAN_METHOD(Context2d::StrokeText) { +void +Context2d::StrokeText(const Napi::CallbackInfo& info) { paintText(info, true); } @@ -2532,37 +2514,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) { - CHECK_RECEIVER(Context2d.GetFont); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - - info.GetReturnValue().Set(Nan::New(context->state->font).ToLocalChecked()); +Napi::Value +Context2d::GetFont(const Napi::CallbackInfo& info) { + return Napi::String::New(env, state->font); } /* @@ -2574,46 +2554,44 @@ NAN_GETTER(Context2d::GetFont) { * - family */ -NAN_SETTER(Context2d::SetFont) { - CHECK_RECEIVER(Context2d.SetFont); - if (!value->IsString()) return; +void +Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { + InstanceData* data = env.GetInstanceData(); - Isolate *iso = Isolate::GetCurrent(); - Local ctx = Nan::GetCurrentContext(); + if (!value.IsString()) return; - Local str = Nan::To(value).ToLocalChecked(); - if (!str->Length()) return; + if (!value.As().Utf8Value().length()) return; - const int argc = 1; - Local argv[argc] = { value }; + Napi::Value mparsed; - 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(); + if (!data->parseFont.Call({ value }).UnwrapTo(&mparsed) || mparsed.IsUndefined()) 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()); + Napi::Object font = mparsed.As(); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); + Napi::String empty = Napi::String::New(env, ""); + Napi::Number zero = Napi::Number::New(env, 0); - PangoFontDescription *desc = pango_font_description_copy(context->state->fontDescription); - pango_font_description_free(context->state->fontDescription); + std::string weight = font.Get("weight").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); + std::string style = font.Get("style").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); + double size = font.Get("size").UnwrapOr(zero).ToNumber().UnwrapOr(zero).DoubleValue(); + std::string unit = font.Get("unit").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); + std::string family = font.Get("family").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); - pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(*style)); - pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(*weight)); + PangoFontDescription *desc = pango_font_description_copy(state->fontDescription); + pango_font_description_free(state->fontDescription); - if (strlen(*family) > 0) { + pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(style.c_str())); + pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(weight.c_str())); + + 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()); } } @@ -2622,21 +2600,20 @@ NAN_SETTER(Context2d::SetFont) { if (size > 0) pango_font_description_set_absolute_size(sys_desc, size * 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->state->font = *Nan::Utf8String(value); + state->font = value.As().Utf8Value().c_str(); } /* * Get text baseline. */ -NAN_GETTER(Context2d::GetTextBaseline) { - CHECK_RECEIVER(Context2d.GetTextBaseline); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetTextBaseline(const Napi::CallbackInfo& info) { const char* baseline; - switch (context->state->textBaseline) { + switch (state->textBaseline) { default: case TEXT_BASELINE_ALPHABETIC: baseline = "alphabetic"; break; case TEXT_BASELINE_TOP: baseline = "top"; break; @@ -2645,18 +2622,18 @@ NAN_GETTER(Context2d::GetTextBaseline) { case TEXT_BASELINE_IDEOGRAPHIC: baseline = "ideographic"; break; case TEXT_BASELINE_HANGING: baseline = "hanging"; break; } - info.GetReturnValue().Set(Nan::New(baseline).ToLocalChecked()); + return Napi::String::New(env, baseline); } /* * Set text baseline. */ -NAN_SETTER(Context2d::SetTextBaseline) { - CHECK_RECEIVER(Context2d.SetTextBaseline); - if (!value->IsString()) return; +void +Context2d::SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; - Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); + std::string opStr = value.As(); const std::map modes = { {"alphabetic", TEXT_BASELINE_ALPHABETIC}, {"top", TEXT_BASELINE_TOP}, @@ -2665,22 +2642,20 @@ NAN_SETTER(Context2d::SetTextBaseline) { {"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; + state->textBaseline = op->second; } /* * Get text align. */ -NAN_GETTER(Context2d::GetTextAlign) { - CHECK_RECEIVER(Context2d.GetTextAlign); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); +Napi::Value +Context2d::GetTextAlign(const Napi::CallbackInfo& info) { const char* align; - switch (context->state->textAlignment) { + switch (state->textAlignment) { default: // TODO the default is supposed to be "start" case TEXT_ALIGNMENT_LEFT: align = "left"; break; @@ -2689,18 +2664,18 @@ NAN_GETTER(Context2d::GetTextAlign) { case TEXT_ALIGNMENT_RIGHT: align = "right"; break; case TEXT_ALIGNMENT_END: align = "end"; break; } - info.GetReturnValue().Set(Nan::New(align).ToLocalChecked()); + return Napi::String::New(env, align); } /* * Set text align. */ -NAN_SETTER(Context2d::SetTextAlign) { - CHECK_RECEIVER(Context2d.SetTextAlign); - if (!value->IsString()) return; +void +Context2d::SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value) { + if (!value.IsString()) return; - Nan::Utf8String opStr(Nan::To(value).ToLocalChecked()); + std::string opStr = value.As(); const std::map modes = { {"center", TEXT_ALIGNMENT_CENTER}, {"left", TEXT_ALIGNMENT_LEFT}, @@ -2708,11 +2683,10 @@ NAN_SETTER(Context2d::SetTextAlign) { {"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; + state->textAlignment = op->second; } /* @@ -2722,19 +2696,21 @@ 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); + pango_layout_set_text(layout, str.Utf8Value().c_str(), -1); pango_cairo_update_layout(ctx, layout); // Normally you could use pango_layout_get_pixel_extents and be done, or use @@ -2757,7 +2733,7 @@ NAN_METHOD(Context2d::MeasureText) { metrics = PANGO_LAYOUT_GET_METRICS(layout); double x_offset; - switch (context->state->textAlignment) { + switch (state->textAlignment) { case TEXT_ALIGNMENT_CENTER: x_offset = logical_rect.width / 2.; break; @@ -2773,36 +2749,20 @@ NAN_METHOD(Context2d::MeasureText) { 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(PANGO_LBEARING(ink_rect) + x_offset)).Check(); - Nan::Set(obj, - Nan::New("actualBoundingBoxRight").ToLocalChecked(), - Nan::New(PANGO_RBEARING(ink_rect) - x_offset)).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; } /* @@ -2810,22 +2770,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) { @@ -2840,32 +2800,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) { - CHECK_RECEIVER(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); @@ -2877,61 +2838,60 @@ NAN_SETTER(Context2d::SetLineDashOffset) { * Get line dash offset * ref: http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash */ -NAN_GETTER(Context2d::GetLineDashOffset) { - CHECK_RECEIVER(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); } @@ -2939,10 +2899,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); @@ -2972,29 +2932,34 @@ void elli_arc(cairo_t* ctx, double xc, double yc, double rx, double ry, double a } inline static -bool getRadius(Point& p, const Local& v) { - if (v->IsObject()) { // 5.1 DOMPointInit - auto rx = Nan::Get(v.As(), Nan::New("x").ToLocalChecked()).ToLocalChecked(); - auto ry = Nan::Get(v.As(), Nan::New("y").ToLocalChecked()).ToLocalChecked(); - if (rx->IsNumber() && ry->IsNumber()) { - auto rxv = Nan::To(rx).FromJust(); - auto ryv = Nan::To(ry).FromJust(); +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) { - Nan::ThrowRangeError("radii must be positive."); + 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 = Nan::To(v).FromJust(); + } else if (v.IsNumber()) { // 5.2 unrestricted double + auto rv = v.As().DoubleValue(); if (!std::isfinite(rv)) return true; if (rv < 0) { - Nan::ThrowRangeError("radii must be positive."); + Napi::RangeError::New(env, "radii must be positive.").ThrowAsJavaScriptException(); + return true; } p.x = p.y = rv; @@ -3007,30 +2972,30 @@ bool getRadius(Point& p, const Local& v) { * https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect * x, y, w, h, [radius|[radii]] */ -NAN_METHOD(Context2d::RoundRect) { +void +Context2d::RoundRect(const Napi::CallbackInfo& info) { RECT_ARGS; - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = this->context(); // 4. Let normalizedRadii be an empty list Point normalizedRadii[4]; size_t nRadii = 4; - if (info[4]->IsUndefined()) { + 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(); + } else if (info[4].IsArray()) { + auto radiiList = info[4].As(); + nRadii = radiiList.Length(); if (!(nRadii >= 1 && nRadii <= 4)) { - Nan::ThrowRangeError("radii must be a list of one, two, three or four radii."); + 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++) { - auto r = Nan::Get(radiiList, i).ToLocalChecked(); - if (getRadius(normalizedRadii[i], r)) + Napi::Value r; + if (!radiiList.Get(i).UnwrapTo(&r) || getRadius(normalizedRadii[i], r)) return; } @@ -3177,7 +3142,8 @@ static double adjustEndAngle(double startAngle, double endAngle, bool counterclo * Adds an arc at x, y with the given radii and start/end angles. */ -NAN_METHOD(Context2d::Arc) { +void +Context2d::Arc(const Napi::CallbackInfo& info) { double args[5]; if(!checkArgs(info, args, 5)) return; @@ -3189,14 +3155,15 @@ NAN_METHOD(Context2d::Arc) { auto endAngle = args[4]; if (radius < 0) { - Nan::ThrowRangeError("The radius provided is negative."); + Napi::RangeError::New(env, "The radius provided is negative.").ThrowAsJavaScriptException(); return; } - bool counterclockwise = Nan::To(info[5]).FromMaybe(false); + Napi::Boolean counterclockwiseValue; + if (!info[5].ToBoolean().UnwrapTo(&counterclockwiseValue)) return; + bool counterclockwise = counterclockwiseValue.Value(); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); canonicalizeAngle(startAngle, endAngle); endAngle = adjustEndAngle(startAngle, endAngle, counterclockwise); @@ -3214,13 +3181,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; @@ -3319,7 +3286,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; @@ -3334,10 +3302,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; + + if (!info[7].ToBoolean().UnwrapTo(&anticlockwiseValue)) return; + bool anticlockwise = anticlockwiseValue.Value(); - Context2d *context = Nan::ObjectWrap::Unwrap(info.This()); - cairo_t *ctx = context->context(); + cairo_t *ctx = context(); // See https://www.cairographics.org/cookbook/ellipses/ double xRatio = radiusX / radiusY; @@ -3365,5 +3335,3 @@ NAN_METHOD(Context2d::Ellipse) { } cairo_set_matrix(ctx, &save_matrix); } - -#undef CHECK_RECEIVER diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 8ea4d60b8..745106e2d 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -5,7 +5,7 @@ #include "cairo.h" #include "Canvas.h" #include "color.h" -#include "nan.h" +#include "napi.h" #include #include @@ -81,108 +81,103 @@ typedef struct { float height; } float_rectangle; -class Context2d : public Nan::ObjectWrap { +class Context2d : public Napi::ObjectWrap { public: 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(RoundRect); - 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); + 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); inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } @@ -198,7 +193,7 @@ 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(); @@ -206,20 +201,23 @@ class Context2d : public Nan::ObjectWrap { void setFontFromState(); 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; + 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 paintText(const Napi::CallbackInfo&, bool); + Napi::Reference _fillStyle; + Napi::Reference _strokeStyle; Canvas *_canvas; - cairo_t *_context; + cairo_t *_context = nullptr; cairo_path_t *_path; - PangoLayout *_layout; + PangoLayout *_layout = nullptr; }; diff --git a/src/Image.cc b/src/Image.cc index 301257769..a1f376136 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,6 +9,7 @@ #include #include #include +#include /* Cairo limit: * https://lists.cairographics.org/archives/cairo/2010-December/021422.html @@ -36,98 +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(); - Nan::SetAccessor(proto, Nan::New("complete").ToLocalChecked(), GetComplete); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); - Nan::SetAccessor(proto, Nan::New("naturalWidth").ToLocalChecked(), GetNaturalWidth); - Nan::SetAccessor(proto, Nan::New("naturalHeight").ToLocalChecked(), GetNaturalHeight); - Nan::SetAccessor(proto, Nan::New("dataMode").ToLocalChecked(), GetDataMode, SetDataMode); - - ctor->Set(Nan::New("MODE_IMAGE").ToLocalChecked(), Nan::New(DATA_IMAGE)); - ctor->Set(Nan::New("MODE_MIME").ToLocalChecked(), Nan::New(DATA_MIME)); - - 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"), + InstanceAccessor<&Image::GetWidth, &Image::SetWidth>("width"), + InstanceAccessor<&Image::GetHeight, &Image::SetHeight>("height"), + InstanceAccessor<&Image::GetNaturalWidth>("naturalWidth"), + InstanceAccessor<&Image::GetNaturalHeight>("naturalHeight"), + InstanceAccessor<&Image::GetDataMode, &Image::SetDataMode>("dataMode"), + StaticValue("MODE_IMAGE", Napi::Number::New(env, DATA_IMAGE)), + StaticValue("MODE_MIME", Napi::Number::New(env, DATA_MIME)) + }); // 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) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetDataMode called on incompatible receiver"); - return; - } - 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 (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.SetDataMode called on incompatible receiver"); - return; - } - 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; } } @@ -135,40 +127,28 @@ NAN_SETTER(Image::SetDataMode) { * Get natural width */ -NAN_GETTER(Image::GetNaturalWidth) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetNaturalWidth called on incompatible receiver"); - return; - } - 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) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetWidth called on incompatible receiver"); - return; - } - 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 (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.SetWidth called on incompatible receiver"); - return; - } - 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(); } } @@ -176,40 +156,27 @@ NAN_SETTER(Image::SetWidth) { * Get natural height */ -NAN_GETTER(Image::GetNaturalHeight) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetNaturalHeight called on incompatible receiver"); - return; - } - 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) { - if (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method Image.GetHeight called on incompatible receiver"); - return; - } - 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 (!Image::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - // #1534 - Nan::ThrowTypeError("Method Image.SetHeight called on incompatible receiver"); - return; - } - 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(); } } @@ -217,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 : ""); } /* @@ -235,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; } @@ -262,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({}); } } } @@ -380,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; @@ -398,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. */ @@ -444,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); } /* @@ -467,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; } } @@ -1010,7 +951,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); @@ -1039,10 +981,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 diff --git a/src/Image.h b/src/Image.h index 62bc3f13b..6b9b9593b 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); @@ -90,7 +90,7 @@ class Image: public Nan::ObjectWrap { CanvasError errorInfo; void loaded(); cairo_status_t load(); - Image(); + ~Image(); enum { DEFAULT @@ -123,5 +123,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 03da2e270..b9f556bb3 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -1,146 +1,132 @@ // Copyright (c) 2010 LearnBoost #include "ImageData.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(); - Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth); - Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight); - 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"), + InstanceAccessor<&ImageData::GetHeight>("height") + }); + + 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); + 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(); - } else if (info[0]->IsUint8ClampedArray() && info[1]->IsUint32()) { - 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) { - if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method ImageData.GetWidth called on incompatible receiver"); - return; - } - 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) { - if (!ImageData::constructor.Get(info.GetIsolate())->HasInstance(info.This())) { - Nan::ThrowTypeError("Method ImageData.GetHeight called on incompatible receiver"); - return; - } - 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/backend/Backend.cc b/src/backend/Backend.cc index 9f2b39dd3..14d67e7b5 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -1,29 +1,21 @@ #include "Backend.h" #include +#include -Backend::Backend(std::string name, int width, int height) - : name(name) - , width(width) - , height(height) -{} +Backend::Backend(std::string name, Napi::CallbackInfo& info) : name(name), env(info.Env()) { + int width = 0; + int height = 0; + if (info[0].IsNumber()) width = info[0].As().Int32Value(); + if (info[1].IsNumber()) height = info[1].As().Int32Value(); + this->width = width; + this->height = height; +} Backend::~Backend() { Backend::destroySurface(); } -void Backend::init(const Nan::FunctionCallbackInfo &info) { - 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()); -} - void Backend::setCanvas(Canvas* _canvas) { this->canvas = _canvas; diff --git a/src/backend/Backend.h b/src/backend/Backend.h index f8448c41a..d23573b6e 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; @@ -21,11 +20,11 @@ class Backend : public Nan::ObjectWrap 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: + Napi::Env env; + virtual ~Backend(); void setCanvas(Canvas* canvas); diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index d354d92cc..682c56b18 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -1,16 +1,14 @@ #include "ImageBackend.h" +#include "../InstanceData.h" +#include +#include -using namespace v8; - -ImageBackend::ImageBackend(int width, int height) - : Backend("image", width, height) - {} - -Backend *ImageBackend::construct(int width, int height){ - return new ImageBackend(width, height); +ImageBackend::ImageBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("image", info) +{ } -// 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) { @@ -35,7 +33,7 @@ cairo_surface_t* ImageBackend::createSurface() { assert(!surface); surface = cairo_image_surface_create(format, width, height); assert(surface); - Nan::AdjustExternalMemory(approxBytesPerPixel() * width * height); + Napi::MemoryManagement::AdjustExternalMemory(env, approxBytesPerPixel() * width * height); return surface; } @@ -43,7 +41,7 @@ void ImageBackend::destroySurface() { if (surface) { cairo_surface_destroy(surface); surface = nullptr; - Nan::AdjustExternalMemory(-approxBytesPerPixel() * width * height); + Napi::MemoryManagement::AdjustExternalMemory(env, -approxBytesPerPixel() * width * height); } } @@ -55,20 +53,11 @@ void ImageBackend::setFormat(cairo_format_t _format) { 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..032907f0f 100644 --- a/src/backend/ImageBackend.h +++ b/src/backend/ImageBackend.h @@ -1,9 +1,9 @@ #pragma once #include "Backend.h" -#include +#include -class ImageBackend : public Backend +class ImageBackend : public Napi::ObjectWrap, public Backend { private: cairo_surface_t* createSurface(); @@ -11,16 +11,14 @@ class ImageBackend : public Backend cairo_format_t format = DEFAULT_FORMAT; public: - ImageBackend(int width, int height); - static Backend *construct(int width, int height); + ImageBackend(Napi::CallbackInfo& info); 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 fe831a68d..ce214a044 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -1,13 +1,11 @@ #include "PdfBackend.h" #include +#include "../InstanceData.h" #include "../Canvas.h" #include "../closure.h" -using namespace v8; - -PdfBackend::PdfBackend(int width, int height) - : Backend("pdf", width, height) { +PdfBackend::PdfBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("pdf", info) { PdfBackend::createSurface(); } @@ -17,10 +15,6 @@ PdfBackend::~PdfBackend() { 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); @@ -33,21 +27,10 @@ cairo_surface_t* PdfBackend::recreateSurface() { 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(); -} - -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..59aa0fedd 100644 --- a/src/backend/PdfBackend.h +++ b/src/backend/PdfBackend.h @@ -2,9 +2,9 @@ #include "Backend.h" #include "../closure.h" -#include +#include -class PdfBackend : public Backend +class PdfBackend : public Napi::ObjectWrap, public Backend { private: cairo_surface_t* createSurface(); @@ -14,11 +14,10 @@ class PdfBackend : public Backend 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 7d4181fc2..530d0b571 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -1,14 +1,15 @@ #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) { +SvgBackend::SvgBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("svg", info) { SvgBackend::createSurface(); } @@ -21,10 +22,6 @@ SvgBackend::~SvgBackend() { destroySurface(); } -Backend *SvgBackend::construct(int width, int height){ - return new SvgBackend(width, height); -} - cairo_surface_t* SvgBackend::createSurface() { assert(!_closure); _closure = new PdfSvgClosure(canvas); @@ -42,20 +39,10 @@ cairo_surface_t* SvgBackend::recreateSurface() { } -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..301ec831c 100644 --- a/src/backend/SvgBackend.h +++ b/src/backend/SvgBackend.h @@ -2,9 +2,9 @@ #include "Backend.h" #include "../closure.h" -#include +#include -class SvgBackend : public Backend +class SvgBackend : public Napi::ObjectWrap, public Backend { private: cairo_surface_t* createSurface(); @@ -14,11 +14,8 @@ class SvgBackend : public Backend 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/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/init.cc b/src/init.cc index fd143973e..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,28 +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 - Nan::Set(target, Nan::New("pangoVersion").ToLocalChecked(), Nan::New(PANGO_VERSION_STRING).ToLocalChecked()).Check(); + 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/test/canvas.test.js b/test/canvas.test.js index 9573688f5..c3b83b271 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -31,7 +31,7 @@ describe('Canvas', function () { 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) diff --git a/test/image.test.js b/test/image.test.js index ec1631a10..a5d6f415c 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -24,7 +24,7 @@ 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')) @@ -182,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) diff --git a/test/imageData.test.js b/test/imageData.test.js index 04b117b45..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 () { From c9969aa49e4290eefcbe6110e573103277865452 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 17 Apr 2023 11:03:23 -0400 Subject: [PATCH 051/128] optimize checkArgs this makes the lineTo benchmark (lineTo executes a very small number of operations, so it mostly measures the js<->C++ barrier) run about 50% faster --- src/CanvasRenderingContext2d.cc | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 9457122d0..e0edd7476 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -45,12 +45,28 @@ constexpr double twoPi = M_PI * 2.; pango_context_get_language(pango_layout_get_context(LAYOUT))) inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){ - Napi::Number zero = Napi::Number::New(info.Env(), 0); - int argsEnd = offset + argsNum; + 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 = info[i].ToNumber().UnwrapOr(zero).DoubleValue(); + 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)) { From 16c28ab73d70ffff49288a8a037c7b47a61437b7 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 22 Apr 2023 14:58:11 -0400 Subject: [PATCH 052/128] optimize fillStyle to be 10% faster this is possibly a hot path, and avoiding the C++ string helps a little bit --- src/CanvasRenderingContext2d.cc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index e0edd7476..56e68d899 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2101,8 +2101,11 @@ Context2d::_setFillColor(Napi::Value arg) { short ok; if (stringValue.IsJust()) { - std::string str = stringValue.Unwrap().Utf8Value(); - uint32_t rgba = rgba_from_string(str.c_str(), &ok); + 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); From 9b9be4f5deb9934db4781bb41eba4a9bfbaba880 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 24 Sep 2023 21:44:12 -0400 Subject: [PATCH 053/128] fix broken font matching due to a use-after-free f3184ba9da2737dbe25275631678a7ef5924fe6b introduced a use-after- free bug. Pango does not copy the string when you use the _static version of pango_font_description_set_family. Font selection was not working for me at all. --- src/register_font.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/register_font.cc b/src/register_font.cc index 37182c0ac..cc0af52d7 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -303,7 +303,7 @@ 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)); From a5b379bbc241d2731c2a4f8d4410f71f123dd1ee Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 4 Oct 2023 10:55:18 -0700 Subject: [PATCH 054/128] v3.0.0 --- CHANGELOG.md | 11 ++++++++++- package.json | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b2c2661..420079d7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,22 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +### Added +### Fixed + +3.0.0 +================== + +This release notably changes to using N-API. 🎉 + +### Changed +* Migrated to N-API (by way of node-addon-api) and removed libuv and v8 dependencies * 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) -* Migrated to N-API (by way of node-addon-api) and removed libuv and v8 dependencies ### Added * Added string tags to support class detection ### Fixed diff --git a/package.json b/package.json index a240125ff..828d377b5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "2.11.2", + "version": "3.0.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 713208733b8a72821daa0189d7cfdd0a1aa0d6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 28 Nov 2023 14:52:59 +0100 Subject: [PATCH 055/128] Replace dtslint with tsd --- .github/workflows/ci.yaml | 2 +- CHANGELOG.md | 1 + types/index.d.ts => index.d.ts | 0 types/test.ts => index.test-d.ts | 33 +++++++++++++++++++------------- package.json | 12 +++++++----- types/Readme.md | 3 --- types/tsconfig.json | 13 ------------- types/tslint.json | 7 ------- 8 files changed, 29 insertions(+), 42 deletions(-) rename types/index.d.ts => index.d.ts (100%) rename types/test.ts => index.test-d.ts (50%) delete mode 100644 types/Readme.md delete mode 100644 types/tsconfig.json delete mode 100644 types/tslint.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c21812da6..2d985cfc6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -86,4 +86,4 @@ jobs: - name: Lint run: npm run lint - name: Lint Types - run: npm run dtslint + run: npm run tsd diff --git a/CHANGELOG.md b/CHANGELOG.md index 420079d7b..03d9d2732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ This release notably changes to using N-API. 🎉 * 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) ### Added * Added string tags to support class detection ### Fixed diff --git a/types/index.d.ts b/index.d.ts similarity index 100% rename from types/index.d.ts rename to index.d.ts diff --git a/types/test.ts b/index.test-d.ts similarity index 50% rename from types/test.ts rename to index.test-d.ts index b48c78011..86e8dfc28 100644 --- a/types/test.ts +++ b/index.test-d.ts @@ -1,5 +1,8 @@ -import * as Canvas from 'canvas' -import * as path from "path"; +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'}) @@ -13,34 +16,38 @@ canv.getContext('2d', {alpha: false}) // LHS is ImageData, not Canvas.ImageData const id = ctx.getImageData(0, 0, 10, 10) -const h: number = id.height +expectType(id.height) +expectType(id.width) ctx.currentTransform = ctx.getTransform() ctx.quality = 'best' ctx.textDrawingMode = 'glyph' -const grad: Canvas.CanvasGradient = ctx.createLinearGradient(0, 1, 2, 3) +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]) -const a: number = dm.a +expectType(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"}) +expectType(canv.toBuffer()) +expectType(canv.toBuffer('application/pdf')) +canv.toBuffer((err, data) => {}, 'image/png') +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; +img.onload = null -const id2: Canvas.ImageData = Canvas.createImageData(new Uint16Array(4), 1) +const id2 = Canvas.createImageData(new Uint16Array(4), 1) +expectType(id2) +ctx.putImageData(id2, 0, 0) ctx.drawImage(canv, 0, 0) -Canvas.deregisterAllFonts(); +Canvas.deregisterAllFonts() diff --git a/package.json b/package.json index 828d377b5..12b9be365 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", + "types": "index.d.ts", "contributors": [ "Nathan Rajlich ", "Rod Vagg ", @@ -32,7 +33,7 @@ "generate-wpt": "node ./test/wpt/generate.js", "test-wpt": "mocha test/wpt/generated/*.js", "install": "node-pre-gyp install --fallback-to-build --update-binary", - "dtslint": "dtslint types" + "tsd": "tsd" }, "binary": { "module_name": "canvas", @@ -43,12 +44,13 @@ }, "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", "node-addon-api": "^7.0.0", @@ -57,12 +59,12 @@ "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": { 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/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 - } -} From ad793dab1fd2fd64bd8e9c55381679423b840236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 29 Nov 2023 10:53:03 +0100 Subject: [PATCH 056/128] Drop support for older versions of Node.js (#2310) * Drop support for older versions of Node.js * Install python setuptools on macOS * Temporarily disable tests on Windows + Node.js 20 --- .github/ISSUE_TEMPLATE.md | 2 +- .github/workflows/ci.yaml | 13 +++++++++---- .github/workflows/prebuild.yaml | 6 +++--- CHANGELOG.md | 2 ++ package.json | 2 +- 5 files changed, 16 insertions(+), 9 deletions(-) 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 2d985cfc6..f0606175d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] steps: - uses: actions/setup-node@v3 with: @@ -33,7 +33,11 @@ jobs: runs-on: windows-2019 strategy: matrix: - node: [10, 12, 14, 16, 18, 20] + # FIXME: Node.js 20.9.0 is currently broken on Windows, in the `registerFont` test: + # ENOENT: no such file or directory, lstat 'D:\a\node-canvas\node-canvas\examples\pfennigFont\pfennigMultiByte🚀.ttf' + # ref: https://github.com/nodejs/node/issues/48673 + # ref: https://github.com/nodejs/node/pull/50650 + node: [18.12.0] steps: - uses: actions/setup-node@v3 with: @@ -57,7 +61,7 @@ jobs: runs-on: macos-latest strategy: matrix: - node: [10, 12, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] steps: - uses: actions/setup-node@v3 with: @@ -68,6 +72,7 @@ jobs: brew update brew install python3 || : # python doesn't need to be linked brew install pkg-config cairo pango libpng jpeg giflib librsvg + pip install setuptools - name: Install run: npm install --build-from-source - name: Test @@ -79,7 +84,7 @@ jobs: steps: - uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 20.9.0 - uses: actions/checkout@v3 - name: Install run: npm install --ignore-scripts diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index 784069e06..d1d30960f 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -24,7 +24,7 @@ jobs: Linux: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Linux runs-on: ubuntu-latest @@ -97,7 +97,7 @@ jobs: macOS: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, macOS runs-on: macos-latest @@ -163,7 +163,7 @@ jobs: Win: strategy: matrix: - node: [8, 9, 10, 11, 12, 13, 14, 16, 18, 20] + node: [18.12.0, 20.9.0] canvas_tag: [] # e.g. "v2.6.1" name: ${{ matrix.canvas_tag}}, Node.js ${{ matrix.node }}, Windows runs-on: windows-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d9d2732..52f66779b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ project adheres to [Semantic Versioning](http://semver.org/). 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 * Defer the initialization of the `op` variable to the `default` switch case to avoid a compiler warning. (#2229) diff --git a/package.json b/package.json index 12b9be365..d9c6526d2 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "typescript": "^4.2.2" }, "engines": { - "node": ">=10.20.0" + "node": "^18.12.0 || >= 20.9.0" }, "license": "MIT" } From 2c4d2a7dc61252913825cf9204730381051f0eba Mon Sep 17 00:00:00 2001 From: huan_kong <49610758+huankong233@users.noreply.github.com> Date: Fri, 8 Dec 2023 04:21:18 +0800 Subject: [PATCH 057/128] fix the wrong type of setTransform (#2322) * fix the wrong type of setTransform * Update CHANGELOG.md --- CHANGELOG.md | 1 + index.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52f66779b..4ccc8e7f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* fix the wrong type of setTransform 3.0.0 ================== diff --git a/index.d.ts b/index.d.ts index 8bcfd105e..73ad4cde9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -201,6 +201,7 @@ export class CanvasRenderingContext2D { 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; From 569e8fbbc0b39370b2db53812bc4a75e111c68e7 Mon Sep 17 00:00:00 2001 From: Dirk Stolle Date: Thu, 28 Dec 2023 02:22:24 +0100 Subject: [PATCH 058/128] Update actions in GitHub Actions CI The following updates are performed: * update actions/checkout to v4 * update actions/setup-node to v4 --- .github/workflows/ci.yaml | 16 ++++++++-------- .github/workflows/prebuild.yaml | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f0606175d..615ad0699 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,10 +15,10 @@ jobs: matrix: node: [18.12.0, 20.9.0] steps: - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Dependencies run: | sudo apt update @@ -39,10 +39,10 @@ jobs: # ref: https://github.com/nodejs/node/pull/50650 node: [18.12.0] steps: - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v3 + - 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" @@ -63,10 +63,10 @@ jobs: matrix: node: [18.12.0, 20.9.0] steps: - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Dependencies run: | brew update @@ -82,10 +82,10 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20.9.0 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install run: npm install --ignore-scripts - name: Lint diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index d1d30960f..036e115e3 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -33,11 +33,11 @@ jobs: env: CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: ref: ${{ matrix.canvas_tag }} - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -104,11 +104,11 @@ jobs: env: CANVAS_VERSION_TO_BUILD: ${{ matrix.canvas_tag }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: ref: ${{ matrix.canvas_tag }} - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -176,11 +176,11 @@ jobs: update: true path-type: inherit - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: ref: ${{ matrix.canvas_tag }} From 41428ee2dfa2622b82f65e4fccd3adfefd1e0a72 Mon Sep 17 00:00:00 2001 From: Stepan Mikhailiuk Date: Thu, 28 Dec 2023 15:01:14 -0800 Subject: [PATCH 059/128] added deregisterAllFonts to readme (#2328) * added deregisterAllFonts to readme * Update Readme.md --- Readme.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Readme.md b/Readme.md index c029e27cb..b84df72db 100644 --- a/Readme.md +++ b/Readme.md @@ -78,6 +78,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 @@ -170,6 +172,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 From ff0f2abd0e3385f94da5057e9fd4bc1fb1c74b94 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 28 Dec 2023 14:59:49 -0800 Subject: [PATCH 060/128] Move a changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ccc8e7f7..de18c3bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed -* fix the wrong type of setTransform 3.0.0 ================== @@ -34,6 +33,7 @@ This release notably changes to using N-API. 🎉 * 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 2.11.2 ================== From 25fbac52c8f2d63468992d7c9f110aff6ef58dfc Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 28 Dec 2023 17:10:26 -0800 Subject: [PATCH 061/128] switch to prebuild-install --- CHANGELOG.md | 1 + package.json | 11 ++--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de18c3bef..73ad0b00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This release notably changes to using N-API. 🎉 * 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) diff --git a/package.json b/package.json index d9c6526d2..c4e340972 100644 --- a/package.json +++ b/package.json @@ -32,16 +32,9 @@ "test-server": "node test/server.js", "generate-wpt": "node ./test/wpt/generate.js", "test-wpt": "mocha test/wpt/generated/*.js", - "install": "node-pre-gyp install --fallback-to-build --update-binary", + "install": "prebuild-install -r napi || node-gyp rebuild", "tsd": "tsd" }, - "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" - }, "files": [ "binding.gyp", "browser.js", @@ -52,8 +45,8 @@ "util/" ], "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", "simple-get": "^3.0.3" }, "devDependencies": { From 4018095a9ec4d9277a927c6a1af4577f45f72fda Mon Sep 17 00:00:00 2001 From: Harlen Bains Date: Sat, 13 Apr 2024 22:13:07 -0700 Subject: [PATCH 062/128] change OS X to macOS in Readme changed OS X to macOS --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index b84df72db..cdf5c73ed 100644 --- a/Readme.md +++ b/Readme.md @@ -23,7 +23,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 pixman` +macOS | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman` 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` From f934f226fc01b2ccb4554d0056718a24279ac087 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 13 Apr 2024 22:31:47 -0700 Subject: [PATCH 063/128] remove use of designated initializers Added in #2229, this is a C++20 feature. For now we're on C++17 (by way of Node.js v18). --- src/Image.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index a1f376136..7a4831ae0 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1178,10 +1178,10 @@ Image::renderSVGToSurface() { } RsvgRectangle viewport = { - .x = 0, - .y = 0, - .width = static_cast(width), - .height = static_cast(height), + 0, // x + 0, // y + static_cast(width), + static_cast(height) }; gboolean render_ok = rsvg_handle_render_document(_rsvg, cr, &viewport, nullptr); if (!render_ok) { From 3f3b2e62a999b8ecfb57034be016f8079988e96a Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 19 Jun 2024 14:14:23 -0700 Subject: [PATCH 064/128] rm prebuild files from master branch Prebuilds are built using the prebuilds branch. These files are unused and confusing. --- .github/workflows/prebuild.yaml | 233 +------------------------------- prebuild/Linux/Dockerfile | 43 ------ prebuild/Linux/binding.gyp | 53 -------- prebuild/Linux/bundle.sh | 5 - prebuild/Linux/preinstall.sh | 8 -- prebuild/Windows/binding.gyp | 79 ----------- prebuild/Windows/bundle.sh | 23 ---- prebuild/Windows/preinstall.sh | 38 ------ prebuild/macOS/binding.gyp | 51 ------- prebuild/macOS/bundle.sh | 4 - prebuild/macOS/preinstall.sh | 4 - prebuild/tarball.sh | 18 --- 12 files changed, 5 insertions(+), 554 deletions(-) delete mode 100644 prebuild/Linux/Dockerfile delete mode 100644 prebuild/Linux/binding.gyp delete mode 100644 prebuild/Linux/bundle.sh delete mode 100644 prebuild/Linux/preinstall.sh delete mode 100644 prebuild/Windows/binding.gyp delete mode 100644 prebuild/Windows/bundle.sh delete mode 100644 prebuild/Windows/preinstall.sh delete mode 100644 prebuild/macOS/binding.gyp delete mode 100644 prebuild/macOS/bundle.sh delete mode 100644 prebuild/macOS/preinstall.sh delete mode 100644 prebuild/tarball.sh diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index 036e115e3..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: [18.12.0, 20.9.0] - 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@v4 - with: - ref: ${{ matrix.canvas_tag }} - - - uses: actions/setup-node@v4 - 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: [18.12.0, 20.9.0] - 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@v4 - with: - ref: ${{ matrix.canvas_tag }} - - - uses: actions/setup-node@v4 - 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: [18.12.0, 20.9.0] - 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@v4 - with: - node-version: ${{ matrix.node }} - - - uses: actions/checkout@v4 - 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/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': [ - ' Date: Wed, 19 Jun 2024 16:08:07 -0700 Subject: [PATCH 065/128] update installation info in readme --- Readme.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index cdf5c73ed..1bd9c634e 100644 --- a/Readme.md +++ b/Readme.md @@ -11,9 +11,14 @@ 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 (*not* Apple silicon) +- Linux x86/64 (glibc only) +- Windows x86/64 -The minimum version of Node.js required is **10.20.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 From 130785fa1db9464e558755ff2a3bf60606ec7b8a Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 19 Jun 2024 16:42:51 -0700 Subject: [PATCH 066/128] publish v3.0.0-rc2 -rc1 never really existed, but I created the tag and don't want to delete it in case someone is installing from github. --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c4e340972..371b0767b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.0", + "version": "3.0.0-rc2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", @@ -63,5 +63,8 @@ "engines": { "node": "^18.12.0 || >= 20.9.0" }, + "binary": { + "napi_versions": [7] + }, "license": "MIT" } From 3b04bde706f6c034c9e460ec28b8c2f5ca1cdf1f Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 19 Jun 2024 17:29:55 -0700 Subject: [PATCH 067/128] add note about v3.0.0-rd2 --- Readme.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Readme.md b/Readme.md index 1bd9c634e..1a38bdaea 100644 --- a/Readme.md +++ b/Readme.md @@ -5,6 +5,13 @@ node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation for [Node.js](http://nodejs.org). +> [!TIP] +> **v3.0.0-rc2 is now available for testing on Linux and Windows!** It's the first version +> to use N-API and prebuild-install. Please give it a try and let us know if you run into any issues. +> ```sh +> npm install canvas@next +> ``` + ## Installation ```bash From 2de0f8b36dbb271c9dc1bdb211812c5dabca5129 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Wed, 19 Jun 2024 17:39:15 -0700 Subject: [PATCH 068/128] Add python-setuptools to MacOS compilation info --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 1a38bdaea..4a38e037b 100644 --- a/Readme.md +++ b/Readme.md @@ -35,7 +35,7 @@ For detailed installation information, see the [wiki](https://github.com/Automat OS | Command ----- | ----- -macOS | Using [Homebrew](https://brew.sh/):
`brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman` +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` From 20c98827223e242f6db4ee5eb99ce42569a11eec Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 22 Jun 2024 21:51:06 -0700 Subject: [PATCH 069/128] update prebuild status in readme --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 4a38e037b..7e7b88ba1 100644 --- a/Readme.md +++ b/Readme.md @@ -6,7 +6,7 @@ node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation for [Node.js](http://nodejs.org). > [!TIP] -> **v3.0.0-rc2 is now available for testing on Linux and Windows!** It's the first version +> **v3.0.0-rc2 is now available for testing on Linux (x64 glibc), macOS (x64) and Windows (x64)!** It's the first version > to use N-API and prebuild-install. Please give it a try and let us know if you run into any issues. > ```sh > npm install canvas@next From 7726ea5e2f60aeae041498fdd52aadfe1c78530c Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 22 Jun 2024 21:52:29 -0700 Subject: [PATCH 070/128] run CI on macos-12 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 615ad0699..cb6010cda 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,7 +58,7 @@ jobs: macOS: name: Test on macOS - runs-on: macos-latest + runs-on: macos-12 strategy: matrix: node: [18.12.0, 20.9.0] From e6d55d88c0b72f5742af2fe6f101e6149f0e672c Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sat, 18 May 2024 13:08:48 +0530 Subject: [PATCH 071/128] Add fix for alpha '%' parsing --- src/color.cc | 11 ++++++++++- test/canvas.test.js | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/color.cc b/src/color.cc index 230fb8dbe..8368f7b92 100644 --- a/src/color.cc +++ b/src/color.cc @@ -226,7 +226,16 @@ parse_clipped_percentage(const char** pStr, float *pFraction) { #define ALPHA(NAME) \ if (*str >= '1' && *str <= '9') { \ - NAME = 1; \ + 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) { \ NAME = 0; \ diff --git a/test/canvas.test.js b/test/canvas.test.js index c3b83b271..8a8fa05b7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -213,6 +213,17 @@ describe('Canvas', function () { 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) // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' From b3ecff1b6bc2bd7dc483756980ffd120256114a0 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sat, 18 May 2024 14:02:05 +0530 Subject: [PATCH 072/128] Add support for '/' in rgba --- src/color.cc | 5 ++++- test/canvas.test.js | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/color.cc b/src/color.cc index 8368f7b92..33b75e8c2 100644 --- a/src/color.cc +++ b/src/color.cc @@ -210,6 +210,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; \ @@ -649,7 +652,7 @@ rgba_from_rgba_string(const char *str, short *ok) { 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)); diff --git a/test/canvas.test.js b/test/canvas.test.js index 8a8fa05b7..25d6b54f6 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -224,6 +224,25 @@ describe('Canvas', function () { 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) + // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' From 3e0b75cebf62655328bdd6c65fd8be13bf834eda Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sat, 18 May 2024 19:59:13 +0530 Subject: [PATCH 073/128] Fix parsing of alpha in RGBA --- src/color.cc | 6 ++++-- test/canvas.test.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/color.cc b/src/color.cc index 33b75e8c2..86cfb73c8 100644 --- a/src/color.cc +++ b/src/color.cc @@ -625,13 +625,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; } diff --git a/test/canvas.test.js b/test/canvas.test.js index 25d6b54f6..a4a8a5b77 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -243,6 +243,46 @@ describe('Canvas', function () { 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) + + // hsl / hsla tests ctx.fillStyle = 'hsl(0, 0%, 0%)' From f138b3a667c50935d3e1ffa69ed0fb56f0f2877c Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sat, 18 May 2024 20:09:08 +0530 Subject: [PATCH 074/128] update changelog for 3.0.0 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ad0b00b..0e8ba2150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ This release notably changes to using N-API. 🎉 * 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) 2.11.2 ================== From 83a51260b0eb10f05311f049ce1531914cfcabe2 Mon Sep 17 00:00:00 2001 From: Pranav Sharma <43780292+pranav1344@users.noreply.github.com> Date: Mon, 15 Jul 2024 09:08:43 +0530 Subject: [PATCH 075/128] Fixes related to parsing of colors in RGB functions (#2398) * Fix leading whitespace in color string issue * Add parsing support for floating point numbers in rbg function * Update CHANGELOG.md --- CHANGELOG.md | 3 +++ src/color.cc | 7 +++++-- test/canvas.test.js | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8ba2150..77cc5db8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed + 3.0.0 ================== @@ -36,6 +37,8 @@ This release notably changes to using N-API. 🎉 * 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) 2.11.2 ================== diff --git a/src/color.cc b/src/color.cc index 86cfb73c8..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; } @@ -739,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); @@ -765,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/test/canvas.test.js b/test/canvas.test.js index a4a8a5b77..1de5134a7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -163,6 +163,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) @@ -282,7 +289,20 @@ describe('Canvas', function () { 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%)' @@ -306,6 +326,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) From ae1aacb72a292f20cacdbe324d04524da8e01a6c Mon Sep 17 00:00:00 2001 From: Danko Aleksejevs Date: Fri, 24 May 2024 05:26:53 +0300 Subject: [PATCH 076/128] Allow quotes within font-family names Add fix to the changelog Use regexp syntax for "string" Co-authored-by: Caleb Hearon --- CHANGELOG.md | 1 + lib/parse-font.js | 13 +++++++++++-- test/canvas.test.js | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77cc5db8e..0505b04e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ This release notably changes to using N-API. 🎉 * 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 2.11.2 ================== diff --git a/lib/parse-font.js b/lib/parse-font.js index 713db5082..a18f05e51 100644 --- a/lib/parse-font.js +++ b/lib/parse-font.js @@ -9,7 +9,7 @@ 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-]+' +const string = /'((\\'|[^'])+)'|"((\\"|[^"])+)"|[\w\s-]+/.source // [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? // <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] @@ -18,6 +18,9 @@ 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 familyRe = new RegExp(string, 'g') +const unquoteRe = /^['"](.*)['"]$/ +const unescapeRe = /\\(['"])/g const sizeFamilyRe = new RegExp( `([\\d\\.]+)(${units}) *((?:${string})( *, *(?:${string}))*)`) @@ -46,6 +49,12 @@ module.exports = str => { const sizeFamily = sizeFamilyRe.exec(str) if (!sizeFamily) return // invalid + const names = sizeFamily[3] + .match(familyRe) + // remove actual bounding quotes, if any, unescape any remaining quotes inside + .map(s => s.trim().replace(unquoteRe, '$1').replace(unescapeRe, '$1')) + .filter(s => !!s) + // Default values and required properties const font = { weight: 'normal', @@ -54,7 +63,7 @@ module.exports = str => { variant: 'normal', size: parseFloat(sizeFamily[1]), unit: sizeFamily[2], - family: sizeFamily[3].replace(/["']/g, '').replace(/ *, */g, ',') + family: names.join(',') } // Optional, unordered properties. diff --git a/test/canvas.test.js b/test/canvas.test.js index 1de5134a7..d0feff0d1 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -88,7 +88,9 @@ describe('Canvas', function () { '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' } + { size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' }, + `50px "Helvetica 'Neue'", "foo \\"bar\\" baz" , "Someone's weird \\'edge\\' case", sans-serif`, + { size: 50, unit: 'px', family: `Helvetica 'Neue',foo "bar" baz,Someone's weird 'edge' case,sans-serif` } ] for (let i = 0, len = tests.length; i < len; ++i) { From 7dfeb0443a16f1566365b27d60635c5e3920a817 Mon Sep 17 00:00:00 2001 From: musou1500 Date: Wed, 10 Jul 2024 00:13:31 +0900 Subject: [PATCH 077/128] add TextMetrics properties update CHANGELOG fix changelog --- CHANGELOG.md | 1 + index.d.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0505b04e4..349e1c6f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ This release notably changes to using N-API. 🎉 * 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 2.11.2 ================== diff --git a/index.d.ts b/index.d.ts index 73ad4cde9..49636f396 100644 --- a/index.d.ts +++ b/index.d.ts @@ -128,10 +128,13 @@ export class Canvas { } 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; From ba896869eca6efbdd92b1d05d9d2af8f06a7cb4a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 11 Aug 2024 16:12:18 -0400 Subject: [PATCH 078/128] remove unnecessary call to cairo_get_matrix --- src/CanvasRenderingContext2d.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 56e68d899..9c4f6af9c 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2766,8 +2766,6 @@ Context2d::MeasureText(const Napi::CallbackInfo& info) { x_offset = 0.0; } - cairo_matrix_t matrix; - cairo_get_matrix(ctx, &matrix); double y_offset = getBaselineAdjustment(layout, state->textBaseline); obj.Set("width", Napi::Number::New(env, logical_rect.width)); From 2db6f49b98aad47a8614aa33535596d1124c8d35 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 22 Sep 2024 15:15:11 -0400 Subject: [PATCH 079/128] add exif browser tests incorrect now, correct after we merge #2296 --- test/fixtures/exif-orientation-f1.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f2.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f3.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f4.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f5.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f6.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f7.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-f8.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-fi.jpg | Bin 0 -> 3076 bytes test/fixtures/exif-orientation-fm.jpg | Bin 0 -> 3088 bytes test/fixtures/exif-orientation-fn.jpg | Bin 0 -> 3040 bytes test/public/tests.js | 39 ++++++++++++++++++++++++++ 12 files changed, 39 insertions(+) create mode 100644 test/fixtures/exif-orientation-f1.jpg create mode 100644 test/fixtures/exif-orientation-f2.jpg create mode 100644 test/fixtures/exif-orientation-f3.jpg create mode 100644 test/fixtures/exif-orientation-f4.jpg create mode 100644 test/fixtures/exif-orientation-f5.jpg create mode 100644 test/fixtures/exif-orientation-f6.jpg create mode 100644 test/fixtures/exif-orientation-f7.jpg create mode 100644 test/fixtures/exif-orientation-f8.jpg create mode 100644 test/fixtures/exif-orientation-fi.jpg create mode 100644 test/fixtures/exif-orientation-fm.jpg create mode 100644 test/fixtures/exif-orientation-fn.jpg diff --git a/test/fixtures/exif-orientation-f1.jpg b/test/fixtures/exif-orientation-f1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64847b1d2ddf5750128985a3d0f8cc937fc6c269 GIT binary patch literal 3076 zcmc&$2~ZPR8h%X@ZiE0H2-pg^;58@#Q9RW zfQteuDk_5%kwaM|AUF&l$`L?tAS50G2tzU;w5HQ z|M!2d;ZrCBHtvETJ^AL-$$?CQDmKS5Myn1r(V8EsVj?Vlr4PCKIh5L+gOaW|_`fv|8J2^A|cc2hE*g z&s^5ET~kmwc}tVRj{ z78RG=y;u66s`}ygH9yqWHMdAw+uEOYbjswFUioUmP^Eaf;P7UvuViL8YE8XNzL; z?yx5PDR$1X*-8zyab_P!Ebc#I_JP>nc|8MbQDJ}IfC%LGA)vQm7hnPd*a-|77!nhM z4Y+`&9l#~67=7@M6Bjn3=ZxnNJZl-PP&^r!s&b(G)wyK>GVAafp%v+V+5L9EaI)g% z;Hj+d`Wx$`LifxRl}Tbn39H)mRo5*K-?TDYh1Z2r9zUtO+0n_Wc8yALMGVZ^Q@&cPQsZg5HlQIY1L$iYr3I)OD{>HwX&;x($LB>p?&ixC3(xHcxcgK@y+n5%EU$M?ye;+RLScF~(%VKH6Y51RgmAO7+Hg2x@x4Z$)x_A?QgG zE7tgsbHoE#5U>`i)617r;p)u%<<^R-icDLHW0{Y9j*Hqx>HIpVFgI85A~@V>z>o2q z8yCO_senE&mfaBZL*FgQkL8h(%aI%#i~z2X@b0*(t$Zu;sb!fcTdxvOBKJFXT%b?Q z>+z+np8#Tb?W!4xJ<=qpvCww=E*`Z)DUFYg49Iz$S~8Ytq+EGI9_3|u({?zZxa@9| zWB!%m%ynYHw~zX4J%?|l)NYzor_ojG1SHLB!oy7mPRa!>c7vz<*kMO6&U2_8s%f{d zjKN1=OW#tZ5LBEHlP>{&;64Od_4$#9=h3?>uemTPc0*Ur^KzELV zv`+wn_q(9CFGg#VmP!L!&ss*X1*)E!2dAwBaXzOYn7#n_S0+~F>k88RqIUQPY}^sN zPx2;X`skayK}ky7l#4Z{$+p{=34l%$%g&1T67LqD?trLPhV4~iny=A#!bWup1ihWy zeGpI_-XsX#Ubw`dEtZWUsb>eOk;WAb(aTVI55%mnufz>$t7y)h`z^F3#Z`D#y*BcY zee%WtX9~8iQC!PVOE$mV{A2gCKsRBQQET-utV2JdY)hS#}gSFy0&Hzi|ot zql7<^*L)nxgl_Xu-!k6H^($SX+2m%GJH=6YRzCSNt=&l5Q45uM$$j)(1usdS9%8vu zv_3l{BtM`ey_^4yw|N*yzWHN(V*;RBAV_3DP-#i@&5#1FVrxF}%PZ7OUm~k3q0bB| z&?3ui41!Mr86%dX2i0j1pmKGX$@0XNLmfFX-f?;owWzSog=eyZ+|8S{eA z>_Xnga3A~9HcHe=wM64M6GM%^*6#;gxg$=_Pg76QP_9NsT#R)0|BN*Ld5AU|R+kZh zIO*_&GHdZrXG0Ja;`yL6r}~-nYuvO9U(p^19;YAh)6kSt#+&ol5)x+WF{8r{ylM8^L~)D{u4@u{ew zMB;qk!{CDDQ-iq$335H#`L{MCX9wzhLsZ!d>mV2j!S6soTjVDaR6`>&Uc(I4JP4W! zg-_7##V@ypU|924;1oHKkG8KS6&C_oIvEMUqb}sowNjB-ZRt6nY59a;nG^yJ`mgw( zL1X>2vBEX3d`9nUnG}%R2i-b~cRPI!r%#QixL3(wPp{lmqwug&{@42g_LNLDWcVzM Xf0PpA#0hZ5B#k;yV~4*b#g4xL5)i2L literal 0 HcmV?d00001 diff --git a/test/fixtures/exif-orientation-f2.jpg b/test/fixtures/exif-orientation-f2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..75064ea1cdff738cde2ae13094ad7ece9ca2b231 GIT binary patch literal 3076 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 X|0pHKi4)+ANg8#a#twf=iXDFgHNU9$ literal 0 HcmV?d00001 diff --git a/test/fixtures/exif-orientation-f5.jpg b/test/fixtures/exif-orientation-f5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ebdcf4db7e12d3564f543278981942af4dbe79ad GIT binary patch literal 3076 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 X|0pHKnH%7ONgH&b)((G5itT>`O^K-a literal 0 HcmV?d00001 diff --git a/test/fixtures/exif-orientation-f7.jpg b/test/fixtures/exif-orientation-f7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2d91716b7e31165679b3f0904f7679bbbd2aba16 GIT binary patch literal 3076 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 AfdBvi literal 0 HcmV?d00001 diff --git a/test/public/tests.js b/test/public/tests.js index d24202602..c904cde7e 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2752,3 +2752,42 @@ 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`) +} From efcde935516e31fb0c707bddff67f4accfe4fb54 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Fri, 27 Sep 2024 09:22:49 +0300 Subject: [PATCH 080/128] Fix class properties should have defaults as standard js classes would have Changed PNG consts to static properties --- CHANGELOG.md | 2 + index.d.ts | 14 ++-- index.test-d.ts | 2 +- src/Canvas.cc | 34 ++++----- src/CanvasGradient.cc | 2 +- src/CanvasPattern.cc | 2 +- src/CanvasRenderingContext2d.cc | 126 ++++++++++++++++---------------- src/Image.cc | 16 ++-- src/ImageData.cc | 4 +- test/canvas.test.js | 5 ++ 10 files changed, 107 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 349e1c6f7..0ca6def96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ This release notably changes to using N-API. 🎉 * 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 ### Added * Added string tags to support class detection ### Fixed @@ -41,6 +42,7 @@ This release notably changes to using N-API. 🎉 * 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) 2.11.2 ================== diff --git a/index.d.ts b/index.d.ts index 49636f396..97fe03962 100644 --- a/index.d.ts +++ b/index.d.ts @@ -63,19 +63,19 @@ 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') diff --git a/index.test-d.ts b/index.test-d.ts index 86e8dfc28..f898f2d58 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -33,7 +33,7 @@ expectType(dm.a) expectType(canv.toBuffer()) expectType(canv.toBuffer('application/pdf')) -canv.toBuffer((err, data) => {}, 'image/png') +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() diff --git a/src/Canvas.cc b/src/Canvas.cc index 2555605f9..ee79915be 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -50,25 +50,25 @@ Canvas::Initialize(Napi::Env& env, Napi::Object& exports) { // Constructor Napi::Function ctor = DefineClass(env, "Canvas", { - InstanceMethod<&Canvas::ToBuffer>("toBuffer"), - InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync"), - InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync"), + InstanceMethod<&Canvas::ToBuffer>("toBuffer", napi_default_method), + InstanceMethod<&Canvas::StreamPNGSync>("streamPNGSync", napi_default_method), + InstanceMethod<&Canvas::StreamPDFSync>("streamPDFSync", napi_default_method), #ifdef HAVE_JPEG - InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync"), + InstanceMethod<&Canvas::StreamJPEGSync>("streamJPEGSync", napi_default_method), #endif - InstanceAccessor<&Canvas::GetType>("type"), - InstanceAccessor<&Canvas::GetStride>("stride"), - InstanceAccessor<&Canvas::GetWidth, &Canvas::SetWidth>("width"), - InstanceAccessor<&Canvas::GetHeight, &Canvas::SetHeight>("height"), - InstanceValue("PNG_NO_FILTERS", Napi::Number::New(env, PNG_NO_FILTERS)), - InstanceValue("PNG_FILTER_NONE", Napi::Number::New(env, PNG_FILTER_NONE)), - InstanceValue("PNG_FILTER_SUB", Napi::Number::New(env, PNG_FILTER_SUB)), - InstanceValue("PNG_FILTER_UP", Napi::Number::New(env, PNG_FILTER_UP)), - InstanceValue("PNG_FILTER_AVG", Napi::Number::New(env, PNG_FILTER_AVG)), - InstanceValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH)), - InstanceValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS)), - StaticMethod<&Canvas::RegisterFont>("_registerFont"), - StaticMethod<&Canvas::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) }); data->CanvasCtor = Napi::Persistent(ctor); diff --git a/src/CanvasGradient.cc b/src/CanvasGradient.cc index 9c2d42360..ceb0e5054 100644 --- a/src/CanvasGradient.cc +++ b/src/CanvasGradient.cc @@ -18,7 +18,7 @@ Gradient::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceData* data = env.GetInstanceData(); Napi::Function ctor = DefineClass(env, "CanvasGradient", { - InstanceMethod<&Gradient::AddColorStop>("addColorStop") + InstanceMethod<&Gradient::AddColorStop>("addColorStop", napi_default_method) }); exports.Set("CanvasGradient", ctor); diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index 55b8bb7fb..ec30b6f09 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -21,7 +21,7 @@ Pattern::Initialize(Napi::Env& env, Napi::Object& exports) { // Constructor Napi::Function ctor = DefineClass(env, "CanvasPattern", { - InstanceMethod<&Pattern::setTransform>("setTransform") + InstanceMethod<&Pattern::setTransform>("setTransform", napi_default_method) }); // Prototype diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 9c4f6af9c..d0966e299 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -94,69 +94,69 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceData* data = env.GetInstanceData(); Napi::Function ctor = DefineClass(env, "CanvasRenderingContext2D", { - InstanceMethod<&Context2d::DrawImage>("drawImage"), - InstanceMethod<&Context2d::PutImageData>("putImageData"), - InstanceMethod<&Context2d::GetImageData>("getImageData"), - InstanceMethod<&Context2d::CreateImageData>("createImageData"), - InstanceMethod<&Context2d::AddPage>("addPage"), - InstanceMethod<&Context2d::Save>("save"), - InstanceMethod<&Context2d::Restore>("restore"), - InstanceMethod<&Context2d::Rotate>("rotate"), - InstanceMethod<&Context2d::Translate>("translate"), - InstanceMethod<&Context2d::Transform>("transform"), - InstanceMethod<&Context2d::GetTransform>("getTransform"), - InstanceMethod<&Context2d::ResetTransform>("resetTransform"), - InstanceMethod<&Context2d::SetTransform>("setTransform"), - InstanceMethod<&Context2d::IsPointInPath>("isPointInPath"), - InstanceMethod<&Context2d::Scale>("scale"), - InstanceMethod<&Context2d::Clip>("clip"), - InstanceMethod<&Context2d::Fill>("fill"), - InstanceMethod<&Context2d::Stroke>("stroke"), - InstanceMethod<&Context2d::FillText>("fillText"), - InstanceMethod<&Context2d::StrokeText>("strokeText"), - InstanceMethod<&Context2d::FillRect>("fillRect"), - InstanceMethod<&Context2d::StrokeRect>("strokeRect"), - InstanceMethod<&Context2d::ClearRect>("clearRect"), - InstanceMethod<&Context2d::Rect>("rect"), - InstanceMethod<&Context2d::RoundRect>("roundRect"), - InstanceMethod<&Context2d::MeasureText>("measureText"), - InstanceMethod<&Context2d::MoveTo>("moveTo"), - InstanceMethod<&Context2d::LineTo>("lineTo"), - InstanceMethod<&Context2d::BezierCurveTo>("bezierCurveTo"), - InstanceMethod<&Context2d::QuadraticCurveTo>("quadraticCurveTo"), - InstanceMethod<&Context2d::BeginPath>("beginPath"), - InstanceMethod<&Context2d::ClosePath>("closePath"), - InstanceMethod<&Context2d::Arc>("arc"), - InstanceMethod<&Context2d::ArcTo>("arcTo"), - InstanceMethod<&Context2d::Ellipse>("ellipse"), - InstanceMethod<&Context2d::SetLineDash>("setLineDash"), - InstanceMethod<&Context2d::GetLineDash>("getLineDash"), - InstanceMethod<&Context2d::CreatePattern>("createPattern"), - InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient"), - InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient"), - InstanceAccessor<&Context2d::GetFormat>("pixelFormat"), - InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality"), - InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled"), - InstanceAccessor<&Context2d::GetGlobalCompositeOperation, &Context2d::SetGlobalCompositeOperation>("globalCompositeOperation"), - InstanceAccessor<&Context2d::GetGlobalAlpha, &Context2d::SetGlobalAlpha>("globalAlpha"), - InstanceAccessor<&Context2d::GetShadowColor, &Context2d::SetShadowColor>("shadowColor"), - InstanceAccessor<&Context2d::GetMiterLimit, &Context2d::SetMiterLimit>("miterLimit"), - InstanceAccessor<&Context2d::GetLineWidth, &Context2d::SetLineWidth>("lineWidth"), - InstanceAccessor<&Context2d::GetLineCap, &Context2d::SetLineCap>("lineCap"), - InstanceAccessor<&Context2d::GetLineJoin, &Context2d::SetLineJoin>("lineJoin"), - InstanceAccessor<&Context2d::GetLineDashOffset, &Context2d::SetLineDashOffset>("lineDashOffset"), - InstanceAccessor<&Context2d::GetShadowOffsetX, &Context2d::SetShadowOffsetX>("shadowOffsetX"), - InstanceAccessor<&Context2d::GetShadowOffsetY, &Context2d::SetShadowOffsetY>("shadowOffsetY"), - InstanceAccessor<&Context2d::GetShadowBlur, &Context2d::SetShadowBlur>("shadowBlur"), - InstanceAccessor<&Context2d::GetAntiAlias, &Context2d::SetAntiAlias>("antialias"), - InstanceAccessor<&Context2d::GetTextDrawingMode, &Context2d::SetTextDrawingMode>("textDrawingMode"), - InstanceAccessor<&Context2d::GetQuality, &Context2d::SetQuality>("quality"), - InstanceAccessor<&Context2d::GetCurrentTransform, &Context2d::SetCurrentTransform>("currentTransform"), - InstanceAccessor<&Context2d::GetFillStyle, &Context2d::SetFillStyle>("fillStyle"), - InstanceAccessor<&Context2d::GetStrokeStyle, &Context2d::SetStrokeStyle>("strokeStyle"), - InstanceAccessor<&Context2d::GetFont, &Context2d::SetFont>("font"), - InstanceAccessor<&Context2d::GetTextBaseline, &Context2d::SetTextBaseline>("textBaseline"), - InstanceAccessor<&Context2d::GetTextAlign, &Context2d::SetTextAlign>("textAlign") + 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), + 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) }); exports.Set("CanvasRenderingContext2d", ctor); diff --git a/src/Image.cc b/src/Image.cc index 7a4831ae0..fcf4b3add 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -53,14 +53,14 @@ Image::Initialize(Napi::Env& env, Napi::Object& exports) { Napi::HandleScope scope(env); Napi::Function ctor = DefineClass(env, "Image", { - InstanceAccessor<&Image::GetComplete>("complete"), - InstanceAccessor<&Image::GetWidth, &Image::SetWidth>("width"), - InstanceAccessor<&Image::GetHeight, &Image::SetHeight>("height"), - InstanceAccessor<&Image::GetNaturalWidth>("naturalWidth"), - InstanceAccessor<&Image::GetNaturalHeight>("naturalHeight"), - InstanceAccessor<&Image::GetDataMode, &Image::SetDataMode>("dataMode"), - StaticValue("MODE_IMAGE", Napi::Number::New(env, DATA_IMAGE)), - StaticValue("MODE_MIME", Napi::Number::New(env, DATA_MIME)) + 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 diff --git a/src/ImageData.cc b/src/ImageData.cc index b9f556bb3..d334ca894 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -14,8 +14,8 @@ ImageData::Initialize(Napi::Env& env, Napi::Object& exports) { InstanceData *data = env.GetInstanceData(); Napi::Function ctor = DefineClass(env, "ImageData", { - InstanceAccessor<&ImageData::GetWidth>("width"), - InstanceAccessor<&ImageData::GetHeight>("height") + InstanceAccessor<&ImageData::GetWidth>("width", napi_default_jsproperty), + InstanceAccessor<&ImageData::GetHeight>("height", napi_default_jsproperty) }); exports.Set("ImageData", ctor); diff --git a/test/canvas.test.js b/test/canvas.test.js index d0feff0d1..1a75ac031 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -694,6 +694,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) From a2e10e61413a0d158174a7a869c16aa13e5d3575 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Thu, 18 Jul 2024 12:47:28 +0100 Subject: [PATCH 081/128] Throw surface errors in toBuffer --- CHANGELOG.md | 1 + src/Canvas.cc | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca6def96..e253d1e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ This release notably changes to using N-API. 🎉 * Changed PNG consts to static properties of Canvas class ### 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) diff --git a/src/Canvas.cc b/src/Canvas.cc index ee79915be..6ba312008 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -382,7 +382,15 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { closure = static_cast(backend())->closure(); } - cairo_surface_finish(surface()); + 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()); } From 77f0b99d10dd11a911607aa8abca9e4022adb8c6 Mon Sep 17 00:00:00 2001 From: prewett-toptal <47159039+prewett-toptal@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:07:33 -0500 Subject: [PATCH 082/128] Handle Exif orientations for JPEG images (fixes #1670) (#2296) * Handle Exif orientation for JPEG images * Updated CHANGELOG.md * Changes for PR --------- Co-authored-by: Geoffrey Prewett --- CHANGELOG.md | 1 + src/Image.cc | 348 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/Image.h | 22 +++- 3 files changed, 366 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e253d1e15..240fa2605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ This release notably changes to using N-API. 🎉 * 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) 2.11.2 ================== diff --git a/src/Image.cc b/src/Image.cc index fcf4b3add..970cd2e28 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -766,6 +766,52 @@ 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 { + if (_idx < _len) { + 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) { + fseeko(_stream, 0, SEEK_END); + _len = ftello(_stream); + fseeko(_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; + fseeko(_stream, _idx, SEEK_SET); + } + +private: + FILE* _stream; + off_t _len; + off_t _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) { @@ -784,10 +830,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); @@ -834,6 +881,8 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { break; } + updateDimensionsForOrientation(orientation); + if (!status) { _surface = cairo_image_surface_create_for_data( data @@ -847,6 +896,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) { @@ -922,6 +973,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 @@ -940,6 +995,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); @@ -1001,6 +1058,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; @@ -1028,7 +1088,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); } /* @@ -1044,6 +1104,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; @@ -1076,7 +1143,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; @@ -1116,6 +1183,279 @@ 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 = (isLE ? readUint32Little : readUint32Big); + auto readUint16 = (isLE ? readUint16Little : readUint16Big); + // 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 diff --git a/src/Image.h b/src/Image.h index 6b9b9593b..6ead16fdb 100644 --- a/src/Image.h +++ b/src/Image.h @@ -78,12 +78,32 @@ class Image : public Napi::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); From 8983c4ac2a4cf59c4091c3492db9d251e28ea93a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 5 Dec 2024 08:38:40 -0500 Subject: [PATCH 083/128] fix windows build (#2458) broke in #2296 because we don't run tests on PRs for some reason - getNext didn't return in all paths. Check wasn't necessary. - Windows doesn't have fseeko or ftello. I highly doubt we'll ever need 64 bits to address images. - Lambda functions get their own anonymous types and C++ standards don't require them to be interchangeable. --- src/Image.cc | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 970cd2e28..559f8a36c 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -773,9 +773,7 @@ class BufferReader : public Image::Reader { bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } uint8_t getNext() override { - if (_idx < _len) { - return _buf[_idx++]; - } + return _buf[_idx++]; } void skipBytes(unsigned n) override { _idx += n; } @@ -789,9 +787,9 @@ class BufferReader : public Image::Reader { class StreamReader : public Image::Reader { public: StreamReader(FILE *stream) : _stream(stream), _len(0), _idx(0) { - fseeko(_stream, 0, SEEK_END); - _len = ftello(_stream); - fseeko(_stream, 0, SEEK_SET); + fseek(_stream, 0, SEEK_END); + _len = ftell(_stream); + fseek(_stream, 0, SEEK_SET); } bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } @@ -803,13 +801,13 @@ class StreamReader : public Image::Reader { void skipBytes(unsigned n) override { _idx += n; - fseeko(_stream, _idx, SEEK_SET); + fseek(_stream, _idx, SEEK_SET); } private: FILE* _stream; - off_t _len; - off_t _idx; + unsigned _len; + unsigned _idx; }; void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) { @@ -1289,8 +1287,12 @@ Image::getExifOrientation(Reader& jpeg) { }; // The first two bytes of TIFF header are "II" if little-endian ("Intel") // and "MM" if big-endian ("Motorola") - auto readUint32 = (isLE ? readUint32Little : readUint32Big); - auto readUint16 = (isLE ? readUint16Little : readUint16Big); + 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); From d1ea3f82003773fd06a27644faf1b7b86113e505 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 5 Dec 2024 19:47:07 -0500 Subject: [PATCH 084/128] fix windows ci config (url went dead) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cb6010cda..5607331c0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,7 +45,7 @@ jobs: - 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 From 22820e1dacc420524af462bde14f0bf88be8c7c4 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 5 Dec 2024 19:50:41 -0500 Subject: [PATCH 085/128] fix macos ci config (macos-12 was deleted) --- .github/workflows/ci.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5607331c0..333288a3c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,7 +58,7 @@ jobs: macOS: name: Test on macOS - runs-on: macos-12 + runs-on: macos-15 strategy: matrix: node: [18.12.0, 20.9.0] @@ -70,9 +70,7 @@ jobs: - name: Install Dependencies run: | brew update - brew install python3 || : # python doesn't need to be linked - brew install pkg-config cairo pango libpng jpeg giflib librsvg - pip install setuptools + brew install python-setuptools pkg-config cairo pango libpng jpeg giflib librsvg - name: Install run: npm install --build-from-source - name: Test From 19a33287c4c571264f1d062e92d311bafb1685aa Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 7 Dec 2024 10:53:40 -0500 Subject: [PATCH 086/128] v3.0.0-rc3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 371b0767b..ae5df9f42 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.0-rc2", + "version": "3.0.0-rc3", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From d0278832f70e0b0a06e1e3441596d402b75a7fe8 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 10 Dec 2024 20:47:04 -0500 Subject: [PATCH 087/128] update README wrt macOS/aarch64 --- Readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 7e7b88ba1..d7a7c65cc 100644 --- a/Readme.md +++ b/Readme.md @@ -19,7 +19,8 @@ $ npm install canvas ``` By default, pre-built binaries will be downloaded if you're on one of the following platforms: -- macOS x86/64 (*not* Apple silicon) +- macOS x86/64 +- macOS aarch64 (aka Apple silicon) - Linux x86/64 (glibc only) - Windows x86/64 From b6b2dc760bfe983ebf0ac8e19d09b90ab03c3ed2 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 5 Dec 2024 20:41:46 -0500 Subject: [PATCH 088/128] modernize node version matrix These are the currently supported versions. Luckily the Windows file path issue was backported to node 20. --- .github/workflows/ci.yaml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 333288a3c..83ddb105b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [18.12.0, 20.9.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] steps: - uses: actions/setup-node@v4 with: @@ -33,11 +33,7 @@ jobs: runs-on: windows-2019 strategy: matrix: - # FIXME: Node.js 20.9.0 is currently broken on Windows, in the `registerFont` test: - # ENOENT: no such file or directory, lstat 'D:\a\node-canvas\node-canvas\examples\pfennigFont\pfennigMultiByte🚀.ttf' - # ref: https://github.com/nodejs/node/issues/48673 - # ref: https://github.com/nodejs/node/pull/50650 - node: [18.12.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] steps: - uses: actions/setup-node@v4 with: @@ -61,7 +57,7 @@ jobs: runs-on: macos-15 strategy: matrix: - node: [18.12.0, 20.9.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] steps: - uses: actions/setup-node@v4 with: From f5894dbe8e2da7cb7d0eb763a7ca55bb0e0274fc Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 31 Aug 2024 09:56:12 +0300 Subject: [PATCH 089/128] fix(DOMMatrix/DOMPoint): spec compatibility --- CHANGELOG.md | 1 + index.d.ts | 10 ++++++- lib/DOMMatrix.js | 56 ++++++++++++++++++++++++++++++++++++ test/dommatrix.test.js | 64 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 240fa2605..68e0b2614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ This release notably changes to using N-API. 🎉 * 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 ================== diff --git a/index.d.ts b/index.d.ts index 97fe03962..52db720d2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -400,10 +400,13 @@ export class DOMPoint { x: number; y: number; z: number; + matrixTransform(matrix?: DOMMatrixInit): DOMPoint; + toJSON(): any; + static fromPoint(other?: DOMPointInit): DOMPoint; } export class DOMMatrix { - constructor(init: string | number[]); + constructor(init?: string | number[]); toString(): string; multiply(other?: DOMMatrix): DOMMatrix; multiplySelf(other?: DOMMatrix): DOMMatrix; @@ -414,6 +417,10 @@ export class 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; @@ -430,6 +437,7 @@ export class DOMMatrix { invertSelf(): DOMMatrix; setMatrixValue(transformList: string): DOMMatrix; transformPoint(point?: DOMPoint): DOMPoint; + toJSON(): any; toFloat32Array(): Float32Array; toFloat64Array(): Float64Array; readonly is2D: boolean; diff --git a/lib/DOMMatrix.js b/lib/DOMMatrix.js index 479e7e6e5..20a41c0c8 100644 --- a/lib/DOMMatrix.js +++ b/lib/DOMMatrix.js @@ -17,6 +17,24 @@ class DOMPoint { this.z = typeof z === 'number' ? z : 0 this.w = typeof w === 'number' ? w : 1 } + + matrixTransform(init) { + 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 +181,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 +612,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/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, + }) + }) + }) }) From a8c035f6280e3de0682ff3fb3d43ce617db6e0fb Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 23 Dec 2024 11:33:53 -0500 Subject: [PATCH 090/128] Revert "select fonts via postscript name on Linux" This reverts commit ddce10f478a7fe15a312e17dd41d1225efcead6d. --- CHANGELOG.md | 2 ++ src/register_font.cc | 69 ++++---------------------------------------- 2 files changed, 8 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e0b2614..c835d3739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ This release notably changes to using N-API. 🎉 * 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() diff --git a/src/register_font.cc b/src/register_font.cc index cc0af52d7..ae2ece584 100644 --- a/src/register_font.cc +++ b/src/register_font.cc @@ -1,6 +1,5 @@ #include "register_font.h" -#include #include #include #include @@ -12,7 +11,6 @@ #include #else #include -#include #endif #include @@ -35,29 +33,11 @@ #define PREFERRED_ENCODING_ID TT_MS_ID_UNICODE_CS #endif -// With PangoFcFontMaps (the pango font module on Linux) we're able to add a -// hook that lets us get perfect matching. Tie the conditions for enabling that -// feature to one variable -#if !defined(__APPLE__) && !defined(_WIN32) && PANGO_VERSION_CHECK(1, 47, 0) -#define PERFECT_MATCHES_ENABLED -#endif - #define IS_PREFERRED_ENC(X) \ X.platform_id == PREFERRED_PLATFORM_ID && X.encoding_id == PREFERRED_ENCODING_ID -#ifdef PERFECT_MATCHES_ENABLED -// On Linux-like OSes using FontConfig, the PostScript name ranks higher than -// preferred family and family name since we'll use it to get perfect font -// matching (see fc_font_map_substitute_hook) -#define GET_NAME_RANK(X) \ - ((IS_PREFERRED_ENC(X) ? 1 : 0) << 2) | \ - ((X.name_id == TT_NAME_ID_PS_NAME ? 1 : 0) << 1) | \ - (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) -#else #define GET_NAME_RANK(X) \ - ((IS_PREFERRED_ENC(X) ? 1 : 0) << 1) | \ - (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) -#endif + (IS_PREFERRED_ENC(X) ? 1 : 0) + (X.name_id == TT_NAME_ID_PREFERRED_FAMILY ? 1 : 0) /* * Return a UTF-8 encoded string given a TrueType name buf+len @@ -125,31 +105,15 @@ get_family_name(FT_Face face) { for (unsigned i = 0; i < FT_Get_Sfnt_Name_Count(face); ++i) { FT_Get_Sfnt_Name(face, i, &name); - if ( - name.name_id == TT_NAME_ID_FONT_FAMILY || -#ifdef PERFECT_MATCHES_ENABLED - name.name_id == TT_NAME_ID_PS_NAME || -#endif - name.name_id == TT_NAME_ID_PREFERRED_FAMILY - ) { - int rank = GET_NAME_RANK(name); + if (name.name_id == TT_NAME_ID_FONT_FAMILY || name.name_id == TT_NAME_ID_PREFERRED_FAMILY) { + char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); - if (rank > best_rank) { - char *buf = to_utf8(name.string, name.string_len, name.platform_id, name.encoding_id); - if (buf) { + if (buf) { + int rank = GET_NAME_RANK(name); + if (rank > best_rank) { best_rank = rank; if (best_buf) free(best_buf); best_buf = buf; - -#ifdef PERFECT_MATCHES_ENABLED - // Prepend an '@' to the postscript name - if (name.name_id == TT_NAME_ID_PS_NAME) { - std::string best_buf_modified = "@"; - best_buf_modified += best_buf; - free(best_buf); - best_buf = strdup(best_buf_modified.c_str()); - } -#endif } else { free(buf); } @@ -320,21 +284,6 @@ get_pango_font_description(unsigned char* filepath) { return NULL; } -#ifdef PERFECT_MATCHES_ENABLED -static void -fc_font_map_substitute_hook(FcPattern *pat, gpointer data) { - FcChar8 *family; - - for (int i = 0; FcPatternGetString(pat, FC_FAMILY, i, &family) == FcResultMatch; i++) { - if (family[0] == '@') { - FcPatternAddString(pat, FC_POSTSCRIPT_NAME, (FcChar8 *)family + 1); - FcPatternRemove(pat, FC_FAMILY, i); - i -= 1; - } - } -} -#endif - /* * Register font with the OS */ @@ -365,12 +314,6 @@ register_font(unsigned char *filepath) { // font families. pango_cairo_font_map_set_default(NULL); -#ifdef PERFECT_MATCHES_ENABLED - PangoFontMap* map = pango_cairo_font_map_get_default(); - PangoFcFontMap* fc_map = PANGO_FC_FONT_MAP(map); - pango_fc_font_map_set_default_substitute(fc_map, fc_font_map_substitute_hook, NULL, NULL); -#endif - return true; } From 834651230003e8ea63d5945f4bd1ef4371ec3c63 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 23 Dec 2024 12:22:36 -0500 Subject: [PATCH 091/128] v3.0.0 --- Readme.md | 7 ------- package.json | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Readme.md b/Readme.md index d7a7c65cc..73c65d369 100644 --- a/Readme.md +++ b/Readme.md @@ -5,13 +5,6 @@ node-canvas is a [Cairo](http://cairographics.org/)-backed Canvas implementation for [Node.js](http://nodejs.org). -> [!TIP] -> **v3.0.0-rc2 is now available for testing on Linux (x64 glibc), macOS (x64) and Windows (x64)!** It's the first version -> to use N-API and prebuild-install. Please give it a try and let us know if you run into any issues. -> ```sh -> npm install canvas@next -> ``` - ## Installation ```bash diff --git a/package.json b/package.json index ae5df9f42..2f305fbd6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.0-rc3", + "version": "3.0.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 181710970861fd65cec7b10f2aa1036b8682ad3e Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 31 Dec 2024 13:44:42 -0500 Subject: [PATCH 092/128] add missing DOMMatrixInit and DOMPointInit types this was accidentally relying on people importing ambient DOM declarations --- CHANGELOG.md | 1 + index.d.ts | 10 ++++++++++ lib/DOMMatrix.js | 2 ++ 3 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c835d3739..32d861b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Fixed accidental depenency on ambient DOM types 3.0.0 diff --git a/index.d.ts b/index.d.ts index 52db720d2..ce21cef26 100644 --- a/index.d.ts +++ b/index.d.ts @@ -395,6 +395,16 @@ export class JPEGStream extends Readable {} /** This class must not be constructed directly; use `canvas.createPDFStream()`. */ export class PDFStream extends Readable {} +// TODO: this is wrong. See matrixTransform in lib/DOMMatrix.js +type DOMMatrixInit = DOMMatrix | string | number[]; + +interface DOMPointInit { + w?: number; + x?: number; + y?: number; + z?: number; +} + export class DOMPoint { w: number; x: number; diff --git a/lib/DOMMatrix.js b/lib/DOMMatrix.js index 20a41c0c8..97015adcf 100644 --- a/lib/DOMMatrix.js +++ b/lib/DOMMatrix.js @@ -19,6 +19,8 @@ class DOMPoint { } 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) } From 80e94ea7644b8f0c879b6e4ba899e50e6289e09a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 31 Dec 2024 16:17:30 -0500 Subject: [PATCH 093/128] v3.0.1 --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32d861b0c..67cf68da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,11 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed -* Fixed accidental depenency on ambient DOM types +3.0.1 +================== +### Fixed +* Fixed accidental depenency on ambient DOM types 3.0.0 ================== diff --git a/package.json b/package.json index 2f305fbd6..476f0ab24 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.0", + "version": "3.0.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 1d956b7246dd516cff9810db19a2915bc5598420 Mon Sep 17 00:00:00 2001 From: yumiura Date: Mon, 27 Nov 2023 17:49:17 +0900 Subject: [PATCH 094/128] use fetch api --- CHANGELOG.md | 1 + lib/image.js | 27 ++++++++++++--------------- package.json | 3 +-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67cf68da0..d4a0cea10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +* Replaced `simple-get ` with ` Node.js builtin` `fetch` (#2309) ### Added ### Fixed diff --git a/lib/image.js b/lib/image.js index 4a37849ee..9ffa3c794 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,20 +44,20 @@ Object.defineProperty(Image.prototype, 'src', { } } - if (!get) get = require('simple-get') - - get.concat({ - url: val, + 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' } - }, (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) }) + .then(res => { + if (!res.ok) { + throw new Error(`Server responded with ${res.statusCode}`) + } + return res.arrayBuffer() + }) + .then(data => { + setSource(this, Buffer.from(data)) + }) + .catch(onerror) } else { // local file path assumed setSource(this, val) } diff --git a/package.json b/package.json index 476f0ab24..8d4133042 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,7 @@ ], "dependencies": { "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "simple-get": "^3.0.3" + "prebuild-install": "^7.1.1" }, "devDependencies": { "@types/node": "^10.12.18", From 7ed0a96b91735d3c6f1df0ceb827a9646b998c9a Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 15 Sep 2024 16:58:18 -0400 Subject: [PATCH 095/128] add font setter benchmarks --- benchmarks/run.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/benchmarks/run.js b/benchmarks/run.js index 14f4db379..5a5c9d507 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -64,6 +64,22 @@ 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 From 728e76cc80da2748961ef973e9bb646f83f2c69e Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 26 Dec 2024 15:26:35 -0500 Subject: [PATCH 096/128] add C++ parser for the font shorthand This is the first part needed for the new font stack, which will look like the FontFace-based API the browsers have. FontFace uses a parser for font-family, font-size, etc., so I will need deeper control over the parser. FontFace will be implemented in C++ and I didn't want to carry over the awkward (and slow) switching between JS and C++. So here it is. I used Claude to generate initial classes and busy work, but it's been heavily examined and heavily modified. Caching aside, this is 3x faster in the benchmarks, which use random names to bypass the cache, and still a full 2x as fast when the JS version has a cached value. Those results were a bit inconsistent, so I'm not sure how much I trust them, but I expect this parser to have a stable performance profile nonetheless, so I'm not going to add any caching. It's also far more correct than what we had! --- CHANGELOG.md | 2 + binding.gyp | 3 +- index.js | 3 - lib/parse-font.js | 110 ------ src/Canvas.cc | 39 +- src/Canvas.h | 1 + src/CanvasRenderingContext2d.cc | 40 +-- src/CharData.h | 231 ++++++++++++ src/FontParser.cc | 605 ++++++++++++++++++++++++++++++++ src/FontParser.h | 115 ++++++ test/canvas.test.js | 73 ---- test/fontParser.test.js | 118 +++++++ 12 files changed, 1130 insertions(+), 210 deletions(-) delete mode 100644 lib/parse-font.js create mode 100644 src/CharData.h create mode 100644 src/FontParser.cc create mode 100644 src/FontParser.h create mode 100644 test/fontParser.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a0cea10..0e9e0ca08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### 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. + ### Added ### Fixed diff --git a/binding.gyp b/binding.gyp index 166842641..bf647f7d1 100644 --- a/binding.gyp +++ b/binding.gyp @@ -75,7 +75,8 @@ 'src/Image.cc', 'src/ImageData.cc', 'src/init.cc', - 'src/register_font.cc' + 'src/register_font.cc', + 'src/FontParser.cc' ], 'conditions': [ ['OS=="win"', { diff --git a/index.js b/index.js index 89f2daabc..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') @@ -12,7 +11,6 @@ const JPEGStream = require('./lib/jpegstream') const { DOMPoint, DOMMatrix } = require('./lib/DOMMatrix') bindings.setDOMMatrix(DOMMatrix) -bindings.setParseFont(parseFont) function createCanvas (width, height, type) { return new Canvas(width, height, type) @@ -73,7 +71,6 @@ exports.DOMPoint = DOMPoint exports.registerFont = registerFont exports.deregisterAllFonts = deregisterAllFonts -exports.parseFont = parseFont exports.createCanvas = createCanvas exports.createImageData = createImageData diff --git a/lib/parse-font.js b/lib/parse-font.js deleted file mode 100644 index a18f05e51..000000000 --- a/lib/parse-font.js +++ /dev/null @@ -1,110 +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-]+/.source - -// [ [ <‘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 familyRe = new RegExp(string, 'g') -const unquoteRe = /^['"](.*)['"]$/ -const unescapeRe = /\\(['"])/g -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 - - const names = sizeFamily[3] - .match(familyRe) - // remove actual bounding quotes, if any, unescape any remaining quotes inside - .map(s => s.trim().replace(unquoteRe, '$1').replace(unescapeRe, '$1')) - .filter(s => !!s) - - // Default values and required properties - const font = { - weight: 'normal', - style: 'normal', - stretch: 'normal', - variant: 'normal', - size: parseFloat(sizeFamily[1]), - unit: sizeFamily[2], - family: names.join(',') - } - - // 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/src/Canvas.cc b/src/Canvas.cc index 6ba312008..7b208bec2 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -21,6 +21,7 @@ #include "Util.h" #include #include "node_buffer.h" +#include "FontParser.h" #ifdef HAVE_JPEG #include "JPEGStream.h" @@ -68,7 +69,8 @@ Canvas::Initialize(Napi::Env& env, Napi::Object& exports) { 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::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method), + StaticMethod<&Canvas::ParseFont>("parseFont", napi_default_method) }); data->CanvasCtor = Napi::Persistent(ctor); @@ -694,6 +696,7 @@ Canvas::RegisterFont(const Napi::CallbackInfo& info) { // now check the attrs, there are many ways to be wrong Napi::Object js_user_desc = info[1].As(); + // 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); @@ -749,6 +752,40 @@ Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) { if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException(); } +/* + * Do not use! This is only exported for testing + */ +Napi::Value +Canvas::ParseFont(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + + if (info.Length() != 1) return env.Undefined(); + + Napi::String str; + if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined(); + + bool ok; + auto props = FontParser::parse(str, &ok); + if (!ok) return env.Undefined(); + + Napi::Object obj = Napi::Object::New(env); + obj.Set("size", Napi::Number::New(env, props.fontSize)); + Napi::Array families = Napi::Array::New(env); + obj.Set("families", families); + + unsigned int index = 0; + + for (auto& family : props.fontFamily) { + families[index++] = Napi::String::New(env, family); + } + + 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; +} + /* * Get a PangoStyle from a CSS string (like "italic") */ diff --git a/src/Canvas.h b/src/Canvas.h index 5f35b356b..5b039539a 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -68,6 +68,7 @@ class Canvas : public Napi::ObjectWrap { 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); diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index d0966e299..1597d089a 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -9,6 +9,7 @@ #include "CanvasGradient.h" #include "CanvasPattern.h" #include "InstanceData.h" +#include "FontParser.h" #include #include #include "Image.h" @@ -2575,34 +2576,29 @@ Context2d::GetFont(const Napi::CallbackInfo& info) { void Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { - InstanceData* data = env.GetInstanceData(); - if (!value.IsString()) return; - if (!value.As().Utf8Value().length()) return; - - Napi::Value mparsed; + std::string str = value.As().Utf8Value(); + if (!str.length()) return; - // parseFont returns undefined for invalid CSS font strings - if (!data->parseFont.Call({ value }).UnwrapTo(&mparsed) || mparsed.IsUndefined()) return; - - Napi::Object font = mparsed.As(); - - Napi::String empty = Napi::String::New(env, ""); - Napi::Number zero = Napi::Number::New(env, 0); - - std::string weight = font.Get("weight").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); - std::string style = font.Get("style").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); - double size = font.Get("size").UnwrapOr(zero).ToNumber().UnwrapOr(zero).DoubleValue(); - std::string unit = font.Get("unit").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); - std::string family = font.Get("family").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value(); + bool success; + auto props = FontParser::parse(str, &success); + if (!success) return; PangoFontDescription *desc = pango_font_description_copy(state->fontDescription); pango_font_description_free(state->fontDescription); - pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(style.c_str())); - pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(weight.c_str())); + 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_weight(desc, static_cast(props.fontWeight)); + + 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); @@ -2617,12 +2613,12 @@ Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) { 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); state->fontDescription = sys_desc; pango_layout_set_font_description(_layout, sys_desc); - state->font = value.As().Utf8Value().c_str(); + state->font = str; } /* diff --git a/src/CharData.h b/src/CharData.h new file mode 100644 index 000000000..ebc2dd5e1 --- /dev/null +++ b/src/CharData.h @@ -0,0 +1,231 @@ +// This is used for classifying characters according to the definition of tokens +// in the CSS standards, but could be extended for any other future uses + +#pragma once + +namespace CharData { + static constexpr uint8_t Whitespace = 0x1; + static constexpr uint8_t Newline = 0x2; + static constexpr uint8_t Hex = 0x4; + static constexpr uint8_t Nmstart = 0x8; + static constexpr uint8_t Nmchar = 0x10; + static constexpr uint8_t Sign = 0x20; + static constexpr uint8_t Digit = 0x40; + static constexpr uint8_t NumStart = 0x80; +}; + +using namespace CharData; + +constexpr const uint8_t charData[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-8 + Whitespace, // 9 (HT) + Whitespace | Newline, // 10 (LF) + 0, // 11 (VT) + Whitespace | Newline, // 12 (FF) + Whitespace | Newline, // 13 (CR) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14-31 + Whitespace, // 32 (Space) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 33-42 + Sign | NumStart, // 43 (+) + 0, // 44 + Nmchar | Sign | NumStart, // 45 (-) + 0, 0, // 46-47 + Nmchar | Digit | NumStart | Hex, // 48 (0) + Nmchar | Digit | NumStart | Hex, // 49 (1) + Nmchar | Digit | NumStart | Hex, // 50 (2) + Nmchar | Digit | NumStart | Hex, // 51 (3) + Nmchar | Digit | NumStart | Hex, // 52 (4) + Nmchar | Digit | NumStart | Hex, // 53 (5) + Nmchar | Digit | NumStart | Hex, // 54 (6) + Nmchar | Digit | NumStart | Hex, // 55 (7) + Nmchar | Digit | NumStart | Hex, // 56 (8) + Nmchar | Digit | NumStart | Hex, // 57 (9) + 0, 0, 0, 0, 0, 0, 0, // 58-64 + Nmstart | Nmchar | Hex, // 65 (A) + Nmstart | Nmchar | Hex, // 66 (B) + Nmstart | Nmchar | Hex, // 67 (C) + Nmstart | Nmchar | Hex, // 68 (D) + Nmstart | Nmchar | Hex, // 69 (E) + Nmstart | Nmchar | Hex, // 70 (F) + Nmstart | Nmchar, // 71 (G) + Nmstart | Nmchar, // 72 (H) + Nmstart | Nmchar, // 73 (I) + Nmstart | Nmchar, // 74 (J) + Nmstart | Nmchar, // 75 (K) + Nmstart | Nmchar, // 76 (L) + Nmstart | Nmchar, // 77 (M) + Nmstart | Nmchar, // 78 (N) + Nmstart | Nmchar, // 79 (O) + Nmstart | Nmchar, // 80 (P) + Nmstart | Nmchar, // 81 (Q) + Nmstart | Nmchar, // 82 (R) + Nmstart | Nmchar, // 83 (S) + Nmstart | Nmchar, // 84 (T) + Nmstart | Nmchar, // 85 (U) + Nmstart | Nmchar, // 86 (V) + Nmstart | Nmchar, // 87 (W) + Nmstart | Nmchar, // 88 (X) + Nmstart | Nmchar, // 89 (Y) + Nmstart | Nmchar, // 90 (Z) + 0, // 91 + Nmstart, // 92 (\) + 0, 0, // 93-94 + Nmstart | Nmchar, // 95 (_) + 0, // 96 + Nmstart | Nmchar | Hex, // 97 (a) + Nmstart | Nmchar | Hex, // 98 (b) + Nmstart | Nmchar | Hex, // 99 (c) + Nmstart | Nmchar | Hex, // 100 (d) + Nmstart | Nmchar | Hex, // 101 (e) + Nmstart | Nmchar | Hex, // 102 (f) + Nmstart | Nmchar, // 103 (g) + Nmstart | Nmchar, // 104 (h) + Nmstart | Nmchar, // 105 (i) + Nmstart | Nmchar, // 106 (j) + Nmstart | Nmchar, // 107 (k) + Nmstart | Nmchar, // 108 (l) + Nmstart | Nmchar, // 109 (m) + Nmstart | Nmchar, // 110 (n) + Nmstart | Nmchar, // 111 (o) + Nmstart | Nmchar, // 112 (p) + Nmstart | Nmchar, // 113 (q) + Nmstart | Nmchar, // 114 (r) + Nmstart | Nmchar, // 115 (s) + Nmstart | Nmchar, // 116 (t) + Nmstart | Nmchar, // 117 (u) + Nmstart | Nmchar, // 118 (v) + Nmstart | Nmchar, // 119 (w) + Nmstart | Nmchar, // 120 (x) + Nmstart | Nmchar, // 121 (y) + Nmstart | Nmchar, // 122 (z) + 0, 0, 0, 0, 0, // 123-127 + // Non-ASCII + Nmstart | Nmchar, // 128 + Nmstart | Nmchar, // 129 + Nmstart | Nmchar, // 130 + Nmstart | Nmchar, // 131 + Nmstart | Nmchar, // 132 + Nmstart | Nmchar, // 133 + Nmstart | Nmchar, // 134 + Nmstart | Nmchar, // 135 + Nmstart | Nmchar, // 136 + Nmstart | Nmchar, // 137 + Nmstart | Nmchar, // 138 + Nmstart | Nmchar, // 139 + Nmstart | Nmchar, // 140 + Nmstart | Nmchar, // 141 + Nmstart | Nmchar, // 142 + Nmstart | Nmchar, // 143 + Nmstart | Nmchar, // 144 + Nmstart | Nmchar, // 145 + Nmstart | Nmchar, // 146 + Nmstart | Nmchar, // 147 + Nmstart | Nmchar, // 148 + Nmstart | Nmchar, // 149 + Nmstart | Nmchar, // 150 + Nmstart | Nmchar, // 151 + Nmstart | Nmchar, // 152 + Nmstart | Nmchar, // 153 + Nmstart | Nmchar, // 154 + Nmstart | Nmchar, // 155 + Nmstart | Nmchar, // 156 + Nmstart | Nmchar, // 157 + Nmstart | Nmchar, // 158 + Nmstart | Nmchar, // 159 + Nmstart | Nmchar, // 160 + Nmstart | Nmchar, // 161 + Nmstart | Nmchar, // 162 + Nmstart | Nmchar, // 163 + Nmstart | Nmchar, // 164 + Nmstart | Nmchar, // 165 + Nmstart | Nmchar, // 166 + Nmstart | Nmchar, // 167 + Nmstart | Nmchar, // 168 + Nmstart | Nmchar, // 169 + Nmstart | Nmchar, // 170 + Nmstart | Nmchar, // 171 + Nmstart | Nmchar, // 172 + Nmstart | Nmchar, // 173 + Nmstart | Nmchar, // 174 + Nmstart | Nmchar, // 175 + Nmstart | Nmchar, // 176 + Nmstart | Nmchar, // 177 + Nmstart | Nmchar, // 178 + Nmstart | Nmchar, // 179 + Nmstart | Nmchar, // 180 + Nmstart | Nmchar, // 181 + Nmstart | Nmchar, // 182 + Nmstart | Nmchar, // 183 + Nmstart | Nmchar, // 184 + Nmstart | Nmchar, // 185 + Nmstart | Nmchar, // 186 + Nmstart | Nmchar, // 187 + Nmstart | Nmchar, // 188 + Nmstart | Nmchar, // 189 + Nmstart | Nmchar, // 190 + Nmstart | Nmchar, // 191 + Nmstart | Nmchar, // 192 + Nmstart | Nmchar, // 193 + Nmstart | Nmchar, // 194 + Nmstart | Nmchar, // 195 + Nmstart | Nmchar, // 196 + Nmstart | Nmchar, // 197 + Nmstart | Nmchar, // 198 + Nmstart | Nmchar, // 199 + Nmstart | Nmchar, // 200 + Nmstart | Nmchar, // 201 + Nmstart | Nmchar, // 202 + Nmstart | Nmchar, // 203 + Nmstart | Nmchar, // 204 + Nmstart | Nmchar, // 205 + Nmstart | Nmchar, // 206 + Nmstart | Nmchar, // 207 + Nmstart | Nmchar, // 208 + Nmstart | Nmchar, // 209 + Nmstart | Nmchar, // 210 + Nmstart | Nmchar, // 211 + Nmstart | Nmchar, // 212 + Nmstart | Nmchar, // 213 + Nmstart | Nmchar, // 214 + Nmstart | Nmchar, // 215 + Nmstart | Nmchar, // 216 + Nmstart | Nmchar, // 217 + Nmstart | Nmchar, // 218 + Nmstart | Nmchar, // 219 + Nmstart | Nmchar, // 220 + Nmstart | Nmchar, // 221 + Nmstart | Nmchar, // 222 + Nmstart | Nmchar, // 223 + Nmstart | Nmchar, // 224 + Nmstart | Nmchar, // 225 + Nmstart | Nmchar, // 226 + Nmstart | Nmchar, // 227 + Nmstart | Nmchar, // 228 + Nmstart | Nmchar, // 229 + Nmstart | Nmchar, // 230 + Nmstart | Nmchar, // 231 + Nmstart | Nmchar, // 232 + Nmstart | Nmchar, // 233 + Nmstart | Nmchar, // 234 + Nmstart | Nmchar, // 235 + Nmstart | Nmchar, // 236 + Nmstart | Nmchar, // 237 + Nmstart | Nmchar, // 238 + Nmstart | Nmchar, // 239 + Nmstart | Nmchar, // 240 + Nmstart | Nmchar, // 241 + Nmstart | Nmchar, // 242 + Nmstart | Nmchar, // 243 + Nmstart | Nmchar, // 244 + Nmstart | Nmchar, // 245 + Nmstart | Nmchar, // 246 + Nmstart | Nmchar, // 247 + Nmstart | Nmchar, // 248 + Nmstart | Nmchar, // 249 + Nmstart | Nmchar, // 250 + Nmstart | Nmchar, // 251 + Nmstart | Nmchar, // 252 + Nmstart | Nmchar, // 253 + Nmstart | Nmchar, // 254 + Nmstart | Nmchar // 255 +}; diff --git a/src/FontParser.cc b/src/FontParser.cc new file mode 100644 index 000000000..773502cb3 --- /dev/null +++ b/src/FontParser.cc @@ -0,0 +1,605 @@ +// This is written to exactly parse the `font` shorthand in CSS2: +// https://www.w3.org/TR/CSS22/fonts.html#font-shorthand +// https://www.w3.org/TR/CSS22/syndata.html#tokenization +// +// We may want to update it for CSS 3 (e.g. font-stretch, or updated +// tokenization) but I've only ever seen one or two issues filed in node-canvas +// due to parsing in my 8 years on the project + +#include "FontParser.h" +#include "CharData.h" +#include +#include + +Token::Token(Type type, std::string value) : type_(type), value_(std::move(value)) {} + +Token::Token(Type type, double value) : type_(type), value_(value) {} + +Token::Token(Type type) : type_(type), value_(std::string{}) {} + +const std::string& +Token::getString() const { + static const std::string empty; + auto* str = std::get_if(&value_); + return str ? *str : empty; +} + +double +Token::getNumber() const { + auto* num = std::get_if(&value_); + return num ? *num : 0.0f; +} + +Tokenizer::Tokenizer(std::string_view input) : input_(input) {} + +std::string +Tokenizer::utf8Encode(uint32_t codepoint) { + std::string result; + + if (codepoint < 0x80) { + result += static_cast(codepoint); + } else if (codepoint < 0x800) { + result += static_cast((codepoint >> 6) | 0xc0); + result += static_cast((codepoint & 0x3f) | 0x80); + } else if (codepoint < 0x10000) { + result += static_cast((codepoint >> 12) | 0xe0); + result += static_cast(((codepoint >> 6) & 0x3f) | 0x80); + result += static_cast((codepoint & 0x3f) | 0x80); + } else { + result += static_cast((codepoint >> 18) | 0xf0); + result += static_cast(((codepoint >> 12) & 0x3f) | 0x80); + result += static_cast(((codepoint >> 6) & 0x3f) | 0x80); + result += static_cast((codepoint & 0x3f) | 0x80); + } + + return result; +} + +char +Tokenizer::peek() const { + return position_ < input_.length() ? input_[position_] : '\0'; +} + +char +Tokenizer::advance() { + return position_ < input_.length() ? input_[position_++] : '\0'; +} + +Token +Tokenizer::parseNumber() { + enum class State { + Start, + AfterSign, + Digits, + AfterDecimal, + AfterE, + AfterESign, + ExponentDigits + }; + + size_t start = position_; + size_t ePosition = 0; + State state = State::Start; + bool valid = false; + + while (position_ < input_.length()) { + char c = peek(); + uint8_t flags = charData[static_cast(c)]; + + switch (state) { + case State::Start: + if (flags & CharData::Sign) { + position_++; + state = State::AfterSign; + } else if (flags & CharData::Digit) { + position_++; + state = State::Digits; + valid = true; + } else if (c == '.') { + position_++; + state = State::AfterDecimal; + } else { + goto done; + } + break; + + case State::AfterSign: + if (flags & CharData::Digit) { + position_++; + state = State::Digits; + valid = true; + } else if (c == '.') { + position_++; + state = State::AfterDecimal; + } else { + goto done; + } + break; + + case State::Digits: + if (flags & CharData::Digit) { + position_++; + } else if (c == '.') { + position_++; + state = State::AfterDecimal; + } else if (c == 'e' || c == 'E') { + ePosition = position_; + position_++; + state = State::AfterE; + valid = false; + } else { + goto done; + } + break; + + case State::AfterDecimal: + if (flags & CharData::Digit) { + position_++; + valid = true; + state = State::Digits; + } else { + goto done; + } + break; + + case State::AfterE: + if (flags & CharData::Sign) { + position_++; + state = State::AfterESign; + } else if (flags & CharData::Digit) { + position_++; + valid = true; + state = State::ExponentDigits; + } else { + position_ = ePosition; + valid = true; + goto done; + } + break; + + case State::AfterESign: + if (flags & CharData::Digit) { + position_++; + valid = true; + state = State::ExponentDigits; + } else { + position_ = ePosition; + valid = true; + goto done; + } + break; + + case State::ExponentDigits: + if (flags & CharData::Digit) { + position_++; + } else { + goto done; + } + break; + } + } + +done: + if (!valid) { + position_ = start; + return Token(Token::Type::Invalid); + } + + std::string number_str(input_.substr(start, position_ - start)); + double value = std::stod(number_str); + return Token(Token::Type::Number, value); +} + +// Note that identifiers are always lower-case. This helps us make easier/more +// efficient comparisons, but means that font-families specified as identifiers +// will be lower-cased. Since font selection isn't case sensitive, this +// shouldn't ever be a problem. +Token +Tokenizer::parseIdentifier() { + std::string identifier; + auto flags = CharData::Nmstart; + auto start = position_; + + while (position_ < input_.length()) { + char c = peek(); + + if (c == '\\') { + advance(); + if (!parseEscape(identifier)) { + position_ = start; + return Token(Token::Type::Invalid); + } + flags = CharData::Nmchar; + } else if (charData[static_cast(c)] & flags) { + identifier += advance() + (c >= 'A' && c <= 'Z' ? 32 : 0); + flags = CharData::Nmchar; + } else { + break; + } + } + + return Token(Token::Type::Identifier, identifier); +} + +uint32_t +Tokenizer::parseUnicode() { + uint32_t value = 0; + size_t count = 0; + + while (position_ < input_.length() && count < 6) { + char c = peek(); + uint32_t digit; + + if (c >= '0' && c <= '9') { + digit = c - '0'; + } else if (c >= 'a' && c <= 'f') { + digit = c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + digit = c - 'A' + 10; + } else { + break; + } + + value = value * 16 + digit; + advance(); + count++; + } + + // Optional whitespace after hex escape + char c = peek(); + if (c == '\r') { + advance(); + if (peek() == '\n') advance(); + } else if (isWhitespace(c)) { + advance(); + } + + return value; +} + +bool +Tokenizer::parseEscape(std::string& str) { + char c = peek(); + auto flags = charData[static_cast(c)]; + + if (flags & CharData::Hex) { + str += utf8Encode(parseUnicode()); + return true; + } else if (!(flags & CharData::Newline) && !(flags & CharData::Hex)) { + str += advance(); + return true; + } + + return false; +} + +Token +Tokenizer::parseString(char quote) { + advance(); + std::string value; + auto start = position_; + + while (position_ < input_.length()) { + char c = peek(); + + if (c == quote) { + advance(); + return Token(Token::Type::QuotedString, value); + } else if (c == '\\') { + advance(); + c = peek(); + if (c == '\r') { + advance(); + if (peek() == '\n') advance(); + } else if (isNewline(c)) { + advance(); + } else { + if (!parseEscape(value)) { + position_ = start; + return Token(Token::Type::Invalid); + } + } + } else { + value += advance(); + } + } + + position_ = start; + return Token(Token::Type::Invalid); +} + +Token +Tokenizer::nextToken() { + if (position_ >= input_.length()) { + return Token(Token::Type::EndOfInput); + } + + char c = peek(); + auto flags = charData[static_cast(c)]; + + if (isWhitespace(c)) { + std::string whitespace; + while (position_ < input_.length() && isWhitespace(peek())) { + whitespace += advance(); + } + return Token(Token::Type::Whitespace, whitespace); + } + + if (flags & CharData::NumStart) { + Token token = parseNumber(); + if (token.type() != Token::Type::Invalid) return token; + } + + if (flags & CharData::Nmstart) { + Token token = parseIdentifier(); + if (token.type() != Token::Type::Invalid) return token; + } + + if (c == '"') { + Token token = parseString('"'); + if (token.type() != Token::Type::Invalid) return token; + } + + if (c == '\'') { + Token token = parseString('\''); + if (token.type() != Token::Type::Invalid) return token; + } + + switch (advance()) { + case '/': return Token(Token::Type::Slash); + case ',': return Token(Token::Type::Comma); + case '%': return Token(Token::Type::Percent); + default: return Token(Token::Type::Invalid); + } +} + +FontParser::FontParser(std::string_view input) + : tokenizer_(input) + , currentToken_(tokenizer_.nextToken()) + , nextToken_(tokenizer_.nextToken()) {} + +const std::unordered_map FontParser::weightMap = { + {"normal", 400}, + {"bold", 700}, + {"lighter", 100}, + {"bolder", 700} +}; + +const std::unordered_map FontParser::unitMap = { + {"cm", 37.8f}, + {"mm", 3.78f}, + {"in", 96.0f}, + {"pt", 96.0f / 72.0f}, + {"pc", 96.0f / 6.0f}, + {"em", 16.0f}, + {"px", 1.0f} +}; + +void +FontParser::advance() { + currentToken_ = nextToken_; + nextToken_ = tokenizer_.nextToken(); +} + +void +FontParser::skipWs() { + while (currentToken_.type() == Token::Type::Whitespace) advance(); +} + +bool +FontParser::check(Token::Type type) const { + return currentToken_.type() == type; +} + +bool +FontParser::checkWs() const { + return nextToken_.type() == Token::Type::Whitespace + || nextToken_.type() == Token::Type::EndOfInput; +} + +bool +FontParser::parseFontStyle(FontProperties& props) { + if (check(Token::Type::Identifier)) { + const auto& value = currentToken_.getString(); + if (value == "italic") { + props.fontStyle = FontStyle::Italic; + advance(); + return true; + } else if (value == "oblique") { + props.fontStyle = FontStyle::Oblique; + advance(); + return true; + } else if (value == "normal") { + props.fontStyle = FontStyle::Normal; + advance(); + return true; + } + } + + return false; +} + +bool +FontParser::parseFontVariant(FontProperties& props) { + if (check(Token::Type::Identifier)) { + const auto& value = currentToken_.getString(); + if (value == "small-caps") { + props.fontVariant = FontVariant::SmallCaps; + advance(); + return true; + } else if (value == "normal") { + props.fontVariant = FontVariant::Normal; + advance(); + return true; + } + } + + return false; +} + +bool +FontParser::parseFontWeight(FontProperties& props) { + if (check(Token::Type::Number)) { + double weightFloat = currentToken_.getNumber(); + int weight = static_cast(weightFloat); + if (weight < 1 || weight > 1000) return false; + props.fontWeight = static_cast(weight); + advance(); + return true; + } else if (check(Token::Type::Identifier)) { + const auto& value = currentToken_.getString(); + + if (auto it = weightMap.find(value); it != weightMap.end()) { + props.fontWeight = it->second; + advance(); + return true; + } + } + + return false; +} + +bool +FontParser::parseFontSize(FontProperties& props) { + if (!check(Token::Type::Number)) return false; + + props.fontSize = currentToken_.getNumber(); + advance(); + + double multiplier = 1.0f; + if (check(Token::Type::Identifier)) { + const auto& unit = currentToken_.getString(); + + if (auto it = unitMap.find(unit); it != unitMap.end()) { + multiplier = it->second; + advance(); + } else { + return false; + } + } else if (check(Token::Type::Percent)) { + multiplier = 16.0f / 100.0f; + advance(); + } else { + return false; + } + + // Technically if we consumed some tokens but couldn't parse the font-size, + // we should rewind the tokenizer, but I don't think the grammar allows for + // any valid alternates in this specific case + + props.fontSize *= multiplier; + return true; +} + +// line-height is not used by canvas ever, but should still parse +bool +FontParser::parseLineHeight(FontProperties& props) { + if (check(Token::Type::Slash)) { + advance(); + skipWs(); + if (check(Token::Type::Number)) { + advance(); + if (check(Token::Type::Percent)) { + advance(); + } else if (check(Token::Type::Identifier)) { + auto identifier = currentToken_.getString(); + if (auto it = unitMap.find(identifier); it != unitMap.end()) { + advance(); + } else { + return false; + } + } else { + return false; + } + } else if (check(Token::Type::Identifier) && currentToken_.getString() == "normal") { + advance(); + } else { + return false; + } + } + + return true; +} + +bool +FontParser::parseFontFamily(FontProperties& props) { + while (!check(Token::Type::EndOfInput)) { + std::string family = ""; + std::string trailingWs = ""; + bool found = false; + + while ( + check(Token::Type::QuotedString) || + check(Token::Type::Identifier) || + check(Token::Type::Whitespace) + ) { + if (check(Token::Type::Whitespace)) { + if (found) trailingWs += currentToken_.getString(); + } else { // Identifier, QuotedString + if (found) { + family += trailingWs; + trailingWs.clear(); + } + + family += currentToken_.getString(); + found = true; + } + + advance(); + } + + if (!found) return false; // only whitespace or non-id/string found + + props.fontFamily.push_back(family); + + if (check(Token::Type::Comma)) advance(); + } + + return true; +} + +FontProperties +FontParser::parse(const std::string& fontString, bool* success) { + FontParser parser(fontString); + auto result = parser.parseFont(); + if (success) *success = !parser.hasError_; + return result; +} + +FontProperties +FontParser::parseFont() { + FontProperties props; + uint8_t state = 0b111; + + skipWs(); + + for (size_t i = 0; i < 3 && checkWs(); i++) { + if ((state & 0b001) && parseFontStyle(props)) { + state &= 0b110; + goto match; + } + + if ((state & 0b010) && parseFontVariant(props)) { + state &= 0b101; + goto match; + } + + if ((state & 0b100) && parseFontWeight(props)) { + state &= 0b011; + goto match; + } + + break; // all attempts exhausted + match: skipWs(); // success: move to the next non-ws token + } + + if (parseFontSize(props)) { + skipWs(); + if (parseLineHeight(props) && parseFontFamily(props)) { + return props; + } + } + + hasError_ = true; + return props; +} diff --git a/src/FontParser.h b/src/FontParser.h new file mode 100644 index 000000000..c88802109 --- /dev/null +++ b/src/FontParser.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "CharData.h" + +enum class FontStyle { + Normal, + Italic, + Oblique +}; + +enum class FontVariant { + Normal, + SmallCaps +}; + +struct FontProperties { + double fontSize{16.0f}; + std::vector fontFamily; + uint16_t fontWeight{400}; + FontVariant fontVariant{FontVariant::Normal}; + FontStyle fontStyle{FontStyle::Normal}; +}; + +class Token { + public: + enum class Type { + Invalid, + Number, + Percent, + Identifier, + Slash, + Comma, + QuotedString, + Whitespace, + EndOfInput + }; + + Token(Type type, std::string value); + Token(Type type, double value); + Token(Type type); + + Type type() const { return type_; } + + const std::string& getString() const; + double getNumber() const; + + private: + Type type_; + std::variant value_; +}; + +class Tokenizer { + public: + Tokenizer(std::string_view input); + Token nextToken(); + + private: + std::string_view input_; + size_t position_{0}; + + // Util + std::string utf8Encode(uint32_t codepoint); + inline bool isWhitespace(char c) const { + return charData[static_cast(c)] & CharData::Whitespace; + } + inline bool isNewline(char c) const { + return charData[static_cast(c)] & CharData::Newline; + } + + // Moving through the string + char peek() const; + char advance(); + + // Tokenize + Token parseNumber(); + Token parseIdentifier(); + uint32_t parseUnicode(); + bool parseEscape(std::string& str); + Token parseString(char quote); +}; + +class FontParser { + public: + static FontProperties parse(const std::string& fontString, bool* success = nullptr); + + private: + static const std::unordered_map weightMap; + static const std::unordered_map unitMap; + + FontParser(std::string_view input); + + void advance(); + void skipWs(); + bool check(Token::Type type) const; + bool checkWs() const; + + bool parseFontStyle(FontProperties& props); + bool parseFontVariant(FontProperties& props); + bool parseFontWeight(FontProperties& props); + bool parseFontSize(FontProperties& props); + bool parseLineHeight(FontProperties& props); + bool parseFontFamily(FontProperties& props); + FontProperties parseFont(); + + Tokenizer tokenizer_; + Token currentToken_; + Token nextToken_; + bool hasError_{false}; +}; diff --git a/test/canvas.test.js b/test/canvas.test.js index 1a75ac031..75f15ed5a 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -14,7 +14,6 @@ const { createCanvas, createImageData, loadImage, - parseFont, registerFont, Canvas, deregisterAllFonts @@ -37,78 +36,6 @@ describe('Canvas', function () { 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' }, - `50px "Helvetica 'Neue'", "foo \\"bar\\" baz" , "Someone's weird \\'edge\\' case", sans-serif`, - { size: 50, unit: 'px', family: `Helvetica 'Neue',foo "bar" baz,Someone's weird 'edge' case,sans-serif` } - ] - - 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' }) 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) + }) + } +}) From da33bbed88946188385af6dc10368410ffede365 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Thu, 18 Jul 2024 12:44:34 +0100 Subject: [PATCH 097/128] Add link tags for pdfs Co-Authored-By: Caleb Hearon --- .gitignore | 1 + CHANGELOG.md | 2 ++ Readme.md | 20 ++++++++++++ examples/pdf-link.js | 20 ++++++++++++ index.d.ts | 2 ++ src/CanvasRenderingContext2d.cc | 56 ++++++++++++++++++++++++++++++++- src/CanvasRenderingContext2d.h | 4 +++ test/canvas.test.js | 37 ++++++++++++++++++++++ 8 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 examples/pdf-link.js diff --git a/.gitignore b/.gitignore index ff66b1103..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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9e0ca08..11d8a039d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ project adheres to [Semantic Versioning](http://semver.org/). * `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. ### Added +* Support for accessibility and links in PDFs + ### Fixed 3.0.1 diff --git a/Readme.md b/Readme.md index 73c65d369..4cb17701c 100644 --- a/Readme.md +++ b/Readme.md @@ -515,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/examples/pdf-link.js b/examples/pdf-link.js new file mode 100644 index 000000000..f6e40291b --- /dev/null +++ b/examples/pdf-link.js @@ -0,0 +1,20 @@ +const fs = require('fs') +const path = require('path') +const Canvas = require('..') + +const canvas = Canvas.createCanvas(400, 300, 'pdf') +const ctx = canvas.getContext('2d') + +ctx.beginTag('Link', 'uri=\'https://google.com\'') +ctx.font = '22px Helvetica' +ctx.fillText('Text link to Google', 110, 50) +ctx.endTag('Link') + +ctx.fillText('Rect link to node-canvas below!', 40, 180) + +ctx.beginTag('Link', 'uri=\'https://github.com/Automattic/node-canvas\' rect=[0 200 400 100]') +ctx.endTag('Link') + +fs.writeFile(path.join(__dirname, 'pdf-link.pdf'), canvas.toBuffer(), function (err) { + if (err) throw err +}) diff --git a/index.d.ts b/index.d.ts index ce21cef26..6458bc132 100644 --- a/index.d.ts +++ b/index.d.ts @@ -232,6 +232,8 @@ export class CanvasRenderingContext2D { 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. diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 1597d089a..f8f217ad9 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -135,6 +135,10 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { 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), @@ -419,7 +423,7 @@ Context2d::fill(bool preserve) { width = cairo_image_surface_get_width(patternSurface); height = y2 - y1; } - + cairo_new_path(_context); cairo_rectangle(_context, 0, 0, width, height); cairo_clip(_context); @@ -3348,3 +3352,53 @@ Context2d::Ellipse(const Napi::CallbackInfo& info) { } 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 745106e2d..a78788451 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -178,6 +178,10 @@ class Context2d : public Napi::ObjectWrap { 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); + #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0) + void BeginTag(const Napi::CallbackInfo& info); + void EndTag(const Napi::CallbackInfo& info); + #endif inline void setContext(cairo_t *ctx) { _context = ctx; } inline cairo_t *context(){ return _context; } inline Canvas *canvas(){ return _canvas; } diff --git a/test/canvas.test.js b/test/canvas.test.js index 75f15ed5a..ecadbc7ec 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -755,6 +755,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 () { @@ -2000,4 +2005,36 @@ describe('Canvas', function () { }) } }) + + 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', {}) }) + }) + }) }) From a0c80314687ed278803d3143d9a7f88c8575837f Mon Sep 17 00:00:00 2001 From: Philippe Plantier Date: Wed, 26 Oct 2022 11:47:40 +0200 Subject: [PATCH 098/128] getImageData fixes when rectangle is outside of canvas fix a crash in getImageData if the rectangle is outside the canvas return transparent black pixels when getting image data outside the canvas remove dead code, add comments Fixes #2024 Fixes #1849 --- CHANGELOG.md | 2 + src/CanvasRenderingContext2d.cc | 50 ++-- test/canvas.test.js | 478 ++++++++++++++++++++++++++++++++ test/public/tests.js | 22 ++ 4 files changed, 533 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d8a039d..ed1b43c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ project adheres to [Semantic Versioning](http://semver.org/). * Support for accessibility and links in PDFs ### 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 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index f8f217ad9..dfbcc17a0 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1011,21 +1011,26 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { sh = -sh; } - if (sx + sw > width) sw = width - sx; - if (sy + sh > height) sh = height - sy; - - // WebKit/moz functionality. node-canvas used to return in either case. - if (sw <= 0) sw = 1; - if (sh <= 0) sh = 1; - - // Non-compliant. "Pixels outside the canvas must be returned as transparent - // black." This instead clips the returned array to the canvas area. + // Width and height to actually copy + int cw = sw; + int ch = sh; + // Offsets in the destination image + int ox = 0; + int oy = 0; + + // 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; + + // 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; } @@ -1047,13 +1052,16 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { 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; @@ -1082,10 +1090,11 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { 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; @@ -1102,9 +1111,10 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { 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; @@ -1116,9 +1126,10 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { 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; @@ -1138,6 +1149,7 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { } } +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(); diff --git a/test/canvas.test.js b/test/canvas.test.js index ecadbc7ec..48abb57b8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1251,6 +1251,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 @@ -1274,6 +1698,60 @@ describe('Canvas', function () { 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 () { diff --git a/test/public/tests.js b/test/public/tests.js index c904cde7e..89d5ba67e 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2378,6 +2378,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) From 0b2edc1ba91303087dcd3584e97dfa90581b375d Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 12 Jan 2025 13:50:26 -0500 Subject: [PATCH 099/128] remove reference to old JS parseFont --- browser.js | 4 ---- index.d.ts | 3 --- 2 files changed, 7 deletions(-) diff --git a/browser.js b/browser.js index 0267a75a2..df6b3e533 100644 --- a/browser.js +++ b/browser.js @@ -1,9 +1,5 @@ /* globals document, ImageData */ -const parseFont = require('./lib/parse-font') - -exports.parseFont = parseFont - exports.createCanvas = function (width, height) { return Object.assign(document.createElement('canvas'), { width: width, height: height }) } diff --git a/index.d.ts b/index.d.ts index 6458bc132..4cce92c3e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -489,9 +489,6 @@ export class ImageData { readonly width: number; } -// This is marked private, but is exported... -// export function parseFont(description: string): object - // Not documented: backends /** Library version. */ From 52330b89b70ac1cdaf6fb3c8331d675b65aaa0cf Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 13 Jan 2025 22:45:21 -0500 Subject: [PATCH 100/128] support ctx.direction and textAlign start/end --- CHANGELOG.md | 2 ++ index.d.ts | 1 + src/Canvas.h | 1 - src/CanvasRenderingContext2d.cc | 46 ++++++++++++++++++++++++++++----- src/CanvasRenderingContext2d.h | 5 +++- test/canvas.test.js | 2 -- 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1b43c00..8aa4a39e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ project adheres to [Semantic Versioning](http://semver.org/). ### 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)) diff --git a/index.d.ts b/index.d.ts index 4cce92c3e..43ff107d0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -290,6 +290,7 @@ export class CanvasRenderingContext2D { textBaseline: CanvasTextBaseline; textAlign: CanvasTextAlign; canvas: Canvas; + direction: 'ltr' | 'rtl'; } export class CanvasGradient { diff --git a/src/Canvas.h b/src/Canvas.h index 5b039539a..eb19843e5 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -37,7 +37,6 @@ enum text_align_t : int8_t { TEXT_ALIGNMENT_LEFT = -1, TEXT_ALIGNMENT_CENTER = 0, TEXT_ALIGNMENT_RIGHT = 1, - // Currently same as LEFT and RIGHT without RTL support: TEXT_ALIGNMENT_START = -2, TEXT_ALIGNMENT_END = 2 }; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index dfbcc17a0..d294a4b35 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -161,7 +161,8 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { 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::GetTextAlign, &Context2d::SetTextAlign>("textAlign", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty) }); exports.Set("CanvasRenderingContext2d", ctor); @@ -230,6 +231,8 @@ Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrapfontDescription); @@ -762,6 +765,27 @@ Context2d::AddPage(const Napi::CallbackInfo& info) { 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; +} + /* * Put image data. * @@ -2451,6 +2475,9 @@ Context2d::paintText(const Napi::CallbackInfo&info, bool stroke) { pango_layout_set_text(layout, str.c_str(), -1); pango_cairo_update_layout(context(), layout); + PangoDirection pango_dir = state->direction == "ltr" ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL; + pango_context_set_base_dir(pango_layout_get_context(_layout), pango_dir); + if (argsNum == 3) { scaled_by = get_text_scale(layout, args[2]); cairo_save(context()); @@ -2522,18 +2549,26 @@ inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { void Context2d::setTextPath(double x, double y) { PangoRectangle logical_rect; + text_align_t alignment = state->textAlignment; - switch (state->textAlignment) { + // Convert start/end to left/right based on direction + if (alignment == TEXT_ALIGNMENT_START) { + alignment = (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT; + } else if (alignment == TEXT_ALIGNMENT_END) { + alignment = (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT; + } + + switch (alignment) { case TEXT_ALIGNMENT_CENTER: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width / 2; break; - case TEXT_ALIGNMENT_END: case TEXT_ALIGNMENT_RIGHT: pango_layout_get_pixel_extents(_layout, NULL, &logical_rect); x -= logical_rect.width; break; - default: ; + default: // TEXT_ALIGNMENT_LEFT + break; } y -= getBaselineAdjustment(_layout, state->textBaseline); @@ -2687,13 +2722,12 @@ Napi::Value Context2d::GetTextAlign(const Napi::CallbackInfo& info) { const char* align; switch (state->textAlignment) { - default: - // TODO the default is supposed to be "start" 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); } diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index a78788451..16cd42d34 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -31,10 +31,11 @@ struct canvas_state_t { cairo_filter_t patternQuality = CAIRO_FILTER_GOOD; float globalAlpha = 1.f; int shadowBlur = 0; - text_align_t textAlignment = TEXT_ALIGNMENT_LEFT; // TODO default is supposed to be START + 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"; canvas_state_t() { fontDescription = pango_font_description_from_string("sans"); @@ -182,6 +183,8 @@ class Context2d : public Napi::ObjectWrap { 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; } diff --git a/test/canvas.test.js b/test/canvas.test.js index 48abb57b8..d33bda63f 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -569,8 +569,6 @@ describe('Canvas', function () { const canvas = createCanvas(200, 200) const ctx = canvas.getContext('2d') - assert.equal('left', ctx.textAlign) // default TODO wrong default - ctx.textAlign = 'start' assert.equal('start', ctx.textAlign) ctx.textAlign = 'center' assert.equal('center', ctx.textAlign) From 88e965709234c5b3ebcedfad7405c56da658df88 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 18 Jan 2025 11:17:16 -0500 Subject: [PATCH 101/128] allow registerFont after a canvas has been created (#2483) Fixes #1921 --- CHANGELOG.md | 1 + Readme.md | 2 +- src/Canvas.cc | 7 ++++++- src/Canvas.h | 2 ++ src/CanvasRenderingContext2d.cc | 20 +++++++++++++++++++- src/CanvasRenderingContext2d.h | 2 ++ 6 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa4a39e4..33c2b1cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### 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 diff --git a/Readme.md b/Readme.md index 4cb17701c..d0429c520 100644 --- a/Readme.md +++ b/Readme.md @@ -163,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') diff --git a/src/Canvas.cc b/src/Canvas.cc index 7b208bec2..b4fe25120 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -38,7 +38,10 @@ using namespace std; -std::vector font_face_list; +std::vector Canvas::font_face_list; + +// Increases each time a font is (de)registered +int Canvas::fontSerial = 1; /* * Initialize Canvas. @@ -734,6 +737,7 @@ Canvas::RegisterFont(const Napi::CallbackInfo& info) { free(family); free(weight); free(style); + fontSerial++; } void @@ -749,6 +753,7 @@ Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) { }); font_face_list.clear(); + fontSerial++; if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException(); } diff --git a/src/Canvas.h b/src/Canvas.h index eb19843e5..3883c5137 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -91,9 +91,11 @@ class Canvas : public Napi::ObjectWrap { void resurface(Napi::Object This); Napi::Env env; + static int fontSerial; private: Backend* _backend; Napi::ObjectReference _jsBackend; Napi::FunctionReference ctor; + static std::vector font_face_list; }; diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index d294a4b35..449f5c07f 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2450,8 +2450,24 @@ get_text_scale(PangoLayout *layout, double maxWidth) { } } +/* + * Make sure the layout's font list is up-to-date + */ +void +Context2d::checkFonts() { + // If fonts have been registered, the PangoContext is using an outdated FontMap + if (canvas()->fontSerial != fontSerial) { + pango_context_set_font_map( + pango_layout_get_context(_layout), + pango_cairo_font_map_get_default() + ); + + fontSerial = canvas()->fontSerial; + } +} + void -Context2d::paintText(const Napi::CallbackInfo&info, bool stroke) { +Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { int argsNum = info.Length() >= 4 ? 3 : 2; if (argsNum == 3 && info[3].IsUndefined()) @@ -2472,6 +2488,7 @@ Context2d::paintText(const Napi::CallbackInfo&info, bool stroke) { PangoLayout *layout = this->layout(); + checkFonts(); pango_layout_set_text(layout, str.c_str(), -1); pango_cairo_update_layout(context(), layout); @@ -2775,6 +2792,7 @@ Context2d::MeasureText(const Napi::CallbackInfo& info) { PangoFontMetrics *metrics; PangoLayout *layout = this->layout(); + checkFonts(); pango_layout_set_text(layout, str.Utf8Value().c_str(), -1); pango_cairo_update_layout(ctx, layout); diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 16cd42d34..6b29b60f0 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -220,6 +220,7 @@ class Context2d : public Napi::ObjectWrap { void _setFillPattern(Napi::Value arg); void _setStrokeColor(Napi::Value arg); void _setStrokePattern(Napi::Value arg); + void checkFonts(); void paintText(const Napi::CallbackInfo&, bool); Napi::Reference _fillStyle; Napi::Reference _strokeStyle; @@ -227,4 +228,5 @@ class Context2d : public Napi::ObjectWrap { cairo_t *_context = nullptr; cairo_path_t *_path; PangoLayout *_layout = nullptr; + int fontSerial = 1; }; From 61e474e299b04babd4b5348bc15ba71bee42099e Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 21 Jan 2025 21:54:46 -0500 Subject: [PATCH 102/128] 3.1.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33c2b1cd0..b0383e8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ project adheres to [Semantic Versioning](http://semver.org/). (Unreleased) ================== ### Changed +### Added +### Fixed + +3.1.0 +================== * 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)) diff --git a/package.json b/package.json index 8d4133042..22b5f3f49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.0.1", + "version": "3.1.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 2f9de7c913e3903c6e9b3caefa6d352cd403bb62 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 22 Feb 2025 20:51:44 -0500 Subject: [PATCH 103/128] remove unused and un-freed GError docs say passing null is allowed --- src/Image.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 559f8a36c..e6af93474 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1471,9 +1471,8 @@ 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; } From d24a127b0c3529f8593fe06191a1e72423ee2a69 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 22 Feb 2025 20:53:08 -0500 Subject: [PATCH 104/128] double-free via g_object_unref that could crash Fixes #2486 --- CHANGELOG.md | 2 ++ src/Image.cc | 10 +--------- test/canvas.test.js | 9 +++++++++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0383e8c1..08d3d458a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added + ### Fixed +* Fix a crash when SVGs without width or height are loaded (#2486) 3.1.0 ================== diff --git a/src/Image.cc b/src/Image.cc index e6af93474..a70f67566 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1470,8 +1470,6 @@ cairo_status_t Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { _is_svg = true; - cairo_status_t status; - if (NULL == (_rsvg = rsvg_handle_new_from_data(buf, len, nullptr))) { return CAIRO_STATUS_READ_ERROR; } @@ -1484,13 +1482,7 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { width = naturalWidth = d_width; height = naturalHeight = d_height; - status = renderSVGToSurface(); - if (status != CAIRO_STATUS_SUCCESS) { - g_object_unref(_rsvg); - return status; - } - - return CAIRO_STATUS_SUCCESS; + return renderSVGToSurface(); } /* diff --git a/test/canvas.test.js b/test/canvas.test.js index d33bda63f..9dfe4d5a8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -2513,4 +2513,13 @@ describe('Canvas', function () { assert.throws(() => { ctx.beginTag('Link', {}) }) }) }) + + describe('loadImage', function () { + it('doesn\'t crash when you don\'t specify width and height', async function () { + await assert.rejects(async () => { + const svg = ``; + await loadImage(Buffer.from(svg)); + }); + }); + }); }) From 9d5f104cb52644f878eb3bac4c29198f73f5f956 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 22 Feb 2025 21:11:59 -0500 Subject: [PATCH 105/128] nicer error reporting when svg width/height missing --- src/Image.cc | 5 +++++ test/canvas.test.js | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Image.cc b/src/Image.cc index a70f67566..ce0b51996 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1482,6 +1482,11 @@ Image::loadSVGFromBuffer(uint8_t *buf, unsigned len) { width = naturalWidth = d_width; height = naturalHeight = d_height; + if (width <= 0 || height <= 0) { + this->errorInfo.set("Width and height must be set on the svg element"); + return CAIRO_STATUS_READ_ERROR; + } + return renderSVGToSurface(); } diff --git a/test/canvas.test.js b/test/canvas.test.js index 9dfe4d5a8..b45382a2a 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -2516,10 +2516,17 @@ describe('Canvas', function () { 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); }); }); }) From 367af8c01a9290102b3b59ae43a31a41d13f7927 Mon Sep 17 00:00:00 2001 From: Anton Gilgur Date: Sat, 29 Mar 2025 01:37:04 -0400 Subject: [PATCH 106/128] fix(deps): update `prebuild-install` to work on Node 22.14+ This installation error on Node 22.14+: ``` npm error prebuild-install warn This package does not support N-API version undefined npm error prebuild-install warn install No prebuilt binaries found (target=undefined runtime=napi arch=x64 libc= platform=linux) ``` is resolved by updating to newer `prebuild-install` 7.1.3, which has a newer version of `napi-build-utils` (2.0.0) with a bugfix for newer N-API version comparison --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d3d458a..88bff24f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### 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) 3.1.0 ================== diff --git a/package.json b/package.json index 22b5f3f49..2754cd381 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ ], "dependencies": { "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1" + "prebuild-install": "^7.1.3" }, "devDependencies": { "@types/node": "^10.12.18", From 494035d3d67b6117f5602ac57ce4aa271be9c4cb Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 3 Apr 2025 21:44:54 -0400 Subject: [PATCH 107/128] leak in sync toBuffer Claude found it right away Fixes #2490 --- src/Canvas.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index b4fe25120..cc5c4f96e 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -368,7 +368,6 @@ static void setPdfMetadata(Canvas* canvas, Napi::Object opts) { Napi::Value Canvas::ToBuffer(const Napi::CallbackInfo& info) { - EncodingWorker *worker = new EncodingWorker(info.Env()); cairo_status_t status; // Vector canvases, sync only @@ -434,7 +433,6 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { CairoError(ex).ThrowAsJavaScriptException(); } catch (const char* ex) { Napi::Error::New(env, ex).ThrowAsJavaScriptException(); - } return env.Undefined(); @@ -461,6 +459,7 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { // Make sure the surface exists since we won't have an isolate context in the async block: surface(); + EncodingWorker* worker = new EncodingWorker(env); worker->Init(&ToPngBufferAsync, closure); worker->Queue(); @@ -498,6 +497,7 @@ Canvas::ToBuffer(const Napi::CallbackInfo& info) { // Make sure the surface exists since we won't have an isolate context in the async block: surface(); + EncodingWorker* worker = new EncodingWorker(env); worker->Init(&ToJpegBufferAsync, closure); worker->Queue(); return env.Undefined(); From b50b392d569fd9fc3c564d99fe16ef803db70e94 Mon Sep 17 00:00:00 2001 From: truman126 Date: Mon, 17 Mar 2025 14:10:10 -0300 Subject: [PATCH 108/128] added a check to make sure maxWidth > 0 Fixes #2171 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88bff24f7..cd13a7f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### 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) 3.1.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 449f5c07f..06c31df75 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2496,6 +2496,7 @@ Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { pango_context_set_base_dir(pango_layout_get_context(_layout), pango_dir); if (argsNum == 3) { + if (args[2] <= 0) return; scaled_by = get_text_scale(layout, args[2]); cairo_save(context()); cairo_scale(context(), scaled_by, 1); From c9363611daf7768130f7d0a4d491defa8bb459b5 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Fri, 11 Apr 2025 18:43:42 -0400 Subject: [PATCH 109/128] add visual test for #2498 --- test/public/tests.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/public/tests.js b/test/public/tests.js index 89d5ba67e..165d847e6 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 @@ -2813,3 +2815,20 @@ tests['no exif orientation'] = function (ctx, 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(` + + + + `) +} From 1234a86f65b2318906334ce0790cd12401c67a25 Mon Sep 17 00:00:00 2001 From: Mike Gilfillan Date: Thu, 3 Apr 2025 12:51:41 +0100 Subject: [PATCH 110/128] Render at natural dimensions to fix scaling issues Fixes #2498 --- CHANGELOG.md | 1 + src/Image.cc | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd13a7f2a..4cb978ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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) 3.1.0 ================== diff --git a/src/Image.cc b/src/Image.cc index ce0b51996..973736505 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1506,9 +1506,6 @@ 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); From 772c464a4d6eaa8e9c9a06a69ba92e67d75d116a Mon Sep 17 00:00:00 2001 From: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Mon, 16 Jun 2025 03:31:52 +0300 Subject: [PATCH 111/128] fix(TextMetrics): rtl direction + start/end textAlign (#2510) * fix(TextMetrics): rtl direction + start/end textAlign * changelog * update tests * expose resolveTextAlignment * jest test --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 29 ++++++++++++++++++----------- src/CanvasRenderingContext2d.h | 1 + test/canvas.test.js | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cb978ce9..af2da3fe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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 3.1.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 06c31df75..15c5c80f5 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2557,6 +2557,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 @@ -2567,14 +2581,7 @@ inline double getBaselineAdjustment(PangoLayout* layout, short baseline) { void Context2d::setTextPath(double x, double y) { PangoRectangle logical_rect; - text_align_t alignment = state->textAlignment; - - // Convert start/end to left/right based on direction - if (alignment == TEXT_ALIGNMENT_START) { - alignment = (state->direction == "rtl") ? TEXT_ALIGNMENT_RIGHT : TEXT_ALIGNMENT_LEFT; - } else if (alignment == TEXT_ALIGNMENT_END) { - alignment = (state->direction == "rtl") ? TEXT_ALIGNMENT_LEFT : TEXT_ALIGNMENT_RIGHT; - } + text_align_t alignment = resolveTextAlignment(); switch (alignment) { case TEXT_ALIGNMENT_CENTER: @@ -2816,16 +2823,16 @@ Context2d::MeasureText(const Napi::CallbackInfo& info) { metrics = PANGO_LAYOUT_GET_METRICS(layout); + text_align_t alignment = resolveTextAlignment(); + double x_offset; - switch (state->textAlignment) { + switch (alignment) { case TEXT_ALIGNMENT_CENTER: x_offset = logical_rect.width / 2.; break; - case TEXT_ALIGNMENT_END: case TEXT_ALIGNMENT_RIGHT: x_offset = logical_rect.width; break; - case TEXT_ALIGNMENT_START: case TEXT_ALIGNMENT_LEFT: default: x_offset = 0.0; diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 6b29b60f0..341e5936d 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -222,6 +222,7 @@ class Context2d : public Napi::ObjectWrap { 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; diff --git a/test/canvas.test.js b/test/canvas.test.js index b45382a2a..01156d089 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1026,6 +1026,39 @@ describe('Canvas', function () { 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 () { From 05a53d83a7c55fd8d0dc4c42914f44d1a6dc78b0 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 15 Jun 2025 22:10:17 -0400 Subject: [PATCH 112/128] add node 24 to ci --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 83ddb105b..b523a3e33 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] steps: - uses: actions/setup-node@v4 with: @@ -33,7 +33,7 @@ jobs: runs-on: windows-2019 strategy: matrix: - node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] steps: - uses: actions/setup-node@v4 with: @@ -57,7 +57,7 @@ jobs: runs-on: macos-15 strategy: matrix: - node: [18.20.5, 20.18.1, 22.12.0, 23.3.0] + node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] steps: - uses: actions/setup-node@v4 with: From f5f103abb81783bba8390b09d7fdb42703e9f877 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 15 Jun 2025 22:39:08 -0400 Subject: [PATCH 113/128] report correct freed size when resurfacing A little kludgey to force 2 calls in canvas.cc but 2 calls are necessary when changing size anyways, so it's the more general usage. TODO remove backends anyways Fixes #2514 --- src/Canvas.cc | 3 ++- src/backend/Backend.cc | 13 ++++--------- src/backend/Backend.h | 1 - 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index cc5c4f96e..3d7984fdc 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -907,7 +907,8 @@ Canvas::resurface(Napi::Object This) { Napi::Value context; if (This.Get("context").UnwrapTo(&context) && context.IsObject()) { - backend()->recreateSurface(); + backend()->destroySurface(); + backend()->createSurface(); // Reset context Context2d *context2d = Context2d::Unwrap(context.As()); cairo_t *prev = context2d->context(); diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 14d67e7b5..1d2cff54b 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -22,13 +22,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; @@ -55,8 +48,9 @@ int Backend::getWidth() } void Backend::setWidth(int width_) { + this->destroySurface(); this->width = width_; - this->recreateSurface(); + this->createSurface(); } int Backend::getHeight() @@ -65,8 +59,9 @@ int Backend::getHeight() } void Backend::setHeight(int height_) { + this->destroySurface(); this->height = height_; - this->recreateSurface(); + this->createSurface(); } bool Backend::isSurfaceValid(){ diff --git a/src/backend/Backend.h b/src/backend/Backend.h index d23573b6e..0b3f92c15 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -30,7 +30,6 @@ class Backend void setCanvas(Canvas* canvas); virtual cairo_surface_t* createSurface() = 0; - virtual cairo_surface_t* recreateSurface(); DLL_PUBLIC cairo_surface_t* getSurface(); virtual void destroySurface(); From 3dca82adc782239fb2191f041fbae69591ba0aa7 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 17 Jun 2025 21:01:21 -0400 Subject: [PATCH 114/128] fix build / refactor backends 0. Most importantly this fixes the dumb mistake I made in the previous commit: not all backends implemented destroySurface. 1. Both Backend and Pdf/SvgBackend were cleaning up memory on exit. This responsibility should not be unclear; let's just do it all in the child class, since PdfBackend and SvgBackend have other stuff to cleanup. This is all still less code. 2. The only reason to destroy the surface is if it's dirty from e.g. setWidth (this is referring to isSurfaceValid) 3. Make createSurface idempotent. This allows us to merge it with getSurface() and makes it safer. Call it ensureSurface. --- src/Canvas.cc | 6 +++--- src/Canvas.h | 2 +- src/backend/Backend.cc | 29 ++--------------------------- src/backend/Backend.h | 9 ++------- src/backend/ImageBackend.cc | 20 ++++++++++++-------- src/backend/ImageBackend.h | 4 +++- src/backend/PdfBackend.cc | 27 +++++++++++++++------------ src/backend/PdfBackend.h | 5 +++-- src/backend/SvgBackend.cc | 36 ++++++++++++++++-------------------- src/backend/SvgBackend.h | 5 +++-- 10 files changed, 60 insertions(+), 83 deletions(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index 3d7984fdc..bc790add5 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -131,13 +131,13 @@ Canvas::Canvas(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), if (instance.IsJust()) backend = ImageBackend::Unwrap(jsBackend = instance.Unwrap()); } + backend->setCanvas(this); + if (!backend->isSurfaceValid()) { Napi::Error::New(env, backend->getError()).ThrowAsJavaScriptException(); return; } - backend->setCanvas(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 @@ -908,7 +908,7 @@ Canvas::resurface(Napi::Object This) { if (This.Get("context").UnwrapTo(&context) && context.IsObject()) { backend()->destroySurface(); - backend()->createSurface(); + backend()->ensureSurface(); // Reset context Context2d *context2d = Context2d::Unwrap(context.As()); cairo_t *prev = context2d->context(); diff --git a/src/Canvas.h b/src/Canvas.h index 3883c5137..7d0bc9d6d 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -76,7 +76,7 @@ class Canvas : public Napi::ObjectWrap { 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()); } diff --git a/src/backend/Backend.cc b/src/backend/Backend.cc index 1d2cff54b..4607fb646 100644 --- a/src/backend/Backend.cc +++ b/src/backend/Backend.cc @@ -11,31 +11,12 @@ Backend::Backend(std::string name, Napi::CallbackInfo& info) : name(name), env(i this->height = height; } -Backend::~Backend() -{ - Backend::destroySurface(); -} - void Backend::setCanvas(Canvas* _canvas) { this->canvas = _canvas; } -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() { @@ -50,7 +31,6 @@ void Backend::setWidth(int width_) { this->destroySurface(); this->width = width_; - this->createSurface(); } int Backend::getHeight() @@ -61,23 +41,18 @@ void Backend::setHeight(int height_) { this->destroySurface(); this->height = height_; - this->createSurface(); } -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; } diff --git a/src/backend/Backend.h b/src/backend/Backend.h index 0b3f92c15..d51eb7601 100644 --- a/src/backend/Backend.h +++ b/src/backend/Backend.h @@ -17,7 +17,6 @@ class Backend protected: int width; int height; - cairo_surface_t* surface = nullptr; Canvas* canvas = nullptr; Backend(std::string name, Napi::CallbackInfo& info); @@ -25,14 +24,10 @@ class Backend public: Napi::Env env; - virtual ~Backend(); - void setCanvas(Canvas* canvas); - virtual cairo_surface_t* createSurface() = 0; - - DLL_PUBLIC cairo_surface_t* getSurface(); - virtual void destroySurface(); + virtual cairo_surface_t* ensureSurface() = 0; + virtual void destroySurface() = 0; DLL_PUBLIC std::string getName(); diff --git a/src/backend/ImageBackend.cc b/src/backend/ImageBackend.cc index 682c56b18..1fede0736 100644 --- a/src/backend/ImageBackend.cc +++ b/src/backend/ImageBackend.cc @@ -3,8 +3,10 @@ #include #include -ImageBackend::ImageBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("image", info) -{ +ImageBackend::ImageBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("image", info) {} + +ImageBackend::~ImageBackend() { + destroySurface(); } // This returns an approximate value only, suitable for @@ -29,11 +31,12 @@ int32_t ImageBackend::approxBytesPerPixel() { } } -cairo_surface_t* ImageBackend::createSurface() { - assert(!surface); - surface = cairo_image_surface_create(format, width, height); - assert(surface); - Napi::MemoryManagement::AdjustExternalMemory(env, 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; } @@ -50,7 +53,8 @@ cairo_format_t ImageBackend::getFormat() { } void ImageBackend::setFormat(cairo_format_t _format) { - this->format = _format; + this->destroySurface(); + this->format = _format; } Napi::FunctionReference ImageBackend::constructor; diff --git a/src/backend/ImageBackend.h b/src/backend/ImageBackend.h index 032907f0f..14946c7b9 100644 --- a/src/backend/ImageBackend.h +++ b/src/backend/ImageBackend.h @@ -6,12 +6,14 @@ 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(Napi::CallbackInfo& info); + ~ImageBackend(); cairo_format_t getFormat(); void setFormat(cairo_format_t format); diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index ce214a044..b9e7c3665 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -5,26 +5,29 @@ #include "../Canvas.h" #include "../closure.h" -PdfBackend::PdfBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("pdf", info) { - PdfBackend::createSurface(); -} +PdfBackend::PdfBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("pdf", info) {} PdfBackend::~PdfBackend() { - cairo_surface_finish(surface); - if (_closure) delete _closure; destroySurface(); } -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; +void PdfBackend::destroySurface() { + if (surface) { + cairo_surface_destroy(surface); + surface = nullptr; + if (_closure) { + delete _closure; + _closure = nullptr; + } + } } void diff --git a/src/backend/PdfBackend.h b/src/backend/PdfBackend.h index 59aa0fedd..6ae8415c8 100644 --- a/src/backend/PdfBackend.h +++ b/src/backend/PdfBackend.h @@ -7,8 +7,9 @@ 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; diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index 530d0b571..e1f0b8d0e 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -9,36 +9,32 @@ using namespace Napi; -SvgBackend::SvgBackend(Napi::CallbackInfo& info) : Napi::ObjectWrap(info), Backend("svg", info) { - SvgBackend::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(); } -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_destroy(surface); + surface = nullptr; + if (_closure) { + delete _closure; + _closure = nullptr; + } + } } - void SvgBackend::Initialize(Napi::Object target) { Napi::Env env = target.Env(); diff --git a/src/backend/SvgBackend.h b/src/backend/SvgBackend.h index 301ec831c..f44842690 100644 --- a/src/backend/SvgBackend.h +++ b/src/backend/SvgBackend.h @@ -7,8 +7,9 @@ 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; From 627c6018fb47066a5015ce4d4693fed16b0bd4a1 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 17 Jun 2025 21:30:34 -0400 Subject: [PATCH 115/128] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af2da3fe6..2c8bbb470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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 ================== From 48ecd5a6249712a2a23cf778f1c40ced9fda1ecd Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Tue, 17 Jun 2025 21:38:02 -0400 Subject: [PATCH 116/128] further backend cleanup, add asserts --- src/backend/PdfBackend.cc | 8 ++++---- src/backend/SvgBackend.cc | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index b9e7c3665..5c1e8fa5b 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -1,6 +1,7 @@ #include "PdfBackend.h" #include +#include #include "../InstanceData.h" #include "../Canvas.h" #include "../closure.h" @@ -23,10 +24,9 @@ void PdfBackend::destroySurface() { if (surface) { cairo_surface_destroy(surface); surface = nullptr; - if (_closure) { - delete _closure; - _closure = nullptr; - } + assert(_closure); + delete _closure; + _closure = nullptr; } } diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index e1f0b8d0e..f4bf2b9c3 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -28,10 +28,9 @@ void SvgBackend::destroySurface() { if (surface) { cairo_surface_destroy(surface); surface = nullptr; - if (_closure) { - delete _closure; - _closure = nullptr; - } + assert(_closure); + delete _closure; + _closure = nullptr; } } From 7a942d484fe10544432a3a9a21034f3e811e7995 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Thu, 19 Jun 2025 08:33:10 -0400 Subject: [PATCH 117/128] 3.1.1 --- CHANGELOG.md | 4 ++++ package.json | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8bbb470..341358def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added +### Fixed +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) @@ -20,6 +23,7 @@ project adheres to [Semantic Versioning](http://semver.org/). 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)) diff --git a/package.json b/package.json index 2754cd381..bf4eaaf9b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.1.0", + "version": "3.1.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", @@ -63,7 +63,9 @@ "node": "^18.12.0 || >= 20.9.0" }, "binary": { - "napi_versions": [7] + "napi_versions": [ + 7 + ] }, "license": "MIT" } From 2738a707afe7a3f0f6daf95c0f50d326cda2a288 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 25 Jun 2025 21:41:16 -0400 Subject: [PATCH 118/128] fix resurface crash with png, svg canvases Regressed in 3dca82adc782239fb2191f041fbae69591ba0aa7 This was originally added in cfc6dfd714772af87e814a687230ae16b4690182, but I don't think that assessment is right at least for the current bug: cairo_destroy, called after the backend's SetWidth, still holds a reference to the surface and calls the closure (presumably for leading or something?). I can't get it to happen consistently enough to write a test, sadly. Fixes #2520 --- src/backend/PdfBackend.cc | 1 + src/backend/SvgBackend.cc | 1 + 2 files changed, 2 insertions(+) diff --git a/src/backend/PdfBackend.cc b/src/backend/PdfBackend.cc index 5c1e8fa5b..4eb46168c 100644 --- a/src/backend/PdfBackend.cc +++ b/src/backend/PdfBackend.cc @@ -22,6 +22,7 @@ cairo_surface_t* PdfBackend::ensureSurface() { void PdfBackend::destroySurface() { if (surface) { + cairo_surface_finish(surface); cairo_surface_destroy(surface); surface = nullptr; assert(_closure); diff --git a/src/backend/SvgBackend.cc b/src/backend/SvgBackend.cc index f4bf2b9c3..475c07dea 100644 --- a/src/backend/SvgBackend.cc +++ b/src/backend/SvgBackend.cc @@ -26,6 +26,7 @@ cairo_surface_t* SvgBackend::ensureSurface() { void SvgBackend::destroySurface() { if (surface) { + cairo_surface_finish(surface); cairo_surface_destroy(surface); surface = nullptr; assert(_closure); From a862af8040c03593bd9376fe2464a73867a0924d Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 25 Jun 2025 22:43:14 -0400 Subject: [PATCH 119/128] 3.1.2 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 341358def..98cba4534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed +3.1.2 +================== +### Fixed +* Fix crash when setting width/height on PDF, SVG canvas (#2520) + 3.1.1 ================== ### Fixed diff --git a/package.json b/package.json index bf4eaaf9b..5ad174990 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.1.1", + "version": "3.1.2", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 6353deb01147e37d76a6b4a01400e179db84776f Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 16 Aug 2025 12:09:45 -0400 Subject: [PATCH 120/128] ci: windows-2025 image (2019 was retired) --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b523a3e33..963d21721 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: Windows: name: Test on Windows - runs-on: windows-2019 + runs-on: windows-2025 strategy: matrix: node: [18.20.5, 20.18.1, 22.12.0, 23.3.0, 24.2.0] @@ -45,6 +45,7 @@ jobs: 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 From d548caadb12ad4023fe689e541f0d66da0cf45cb Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Mon, 11 Aug 2025 21:26:43 -0400 Subject: [PATCH 121/128] use memcpy to avoid misaligned pointer UB compiles to the same thing before/after at any optimization level --- src/bmp/BMPParser.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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; } From 6ce963d278d535665e671c4c5a5761565e1bf4da Mon Sep 17 00:00:00 2001 From: Nick Doiron Date: Sun, 17 Aug 2025 13:57:44 -0500 Subject: [PATCH 122/128] Set pango language through ctx.lang (#2526) Co-authored-by: Caleb Hearon --- CHANGELOG.md | 1 + index.d.ts | 1 + src/CanvasRenderingContext2d.cc | 30 ++++++++++++++++++++++++++++-- src/CanvasRenderingContext2d.h | 4 ++++ test/canvas.test.js | 3 ++- 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98cba4534..79119fecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added +* Added `ctx.lang` to set the ISO language code for text ### Fixed 3.1.2 diff --git a/index.d.ts b/index.d.ts index 43ff107d0..27ab0c341 100644 --- a/index.d.ts +++ b/index.d.ts @@ -291,6 +291,7 @@ export class CanvasRenderingContext2D { textAlign: CanvasTextAlign; canvas: Canvas; direction: 'ltr' | 'rtl'; + lang: string; } export class CanvasGradient { diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 15c5c80f5..2342a57e5 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -43,7 +43,7 @@ constexpr double twoPi = M_PI * 2.; #define PANGO_LAYOUT_GET_METRICS(LAYOUT) pango_context_get_metrics( \ pango_layout_get_context(LAYOUT), \ pango_layout_get_font_description(LAYOUT), \ - pango_context_get_language(pango_layout_get_context(LAYOUT))) + pango_language_from_string(state->lang.c_str())) inline static bool checkArgs(const Napi::CallbackInfo&info, double *args, int argsNum, int offset = 0){ Napi::Env env = info.Env(); @@ -162,7 +162,8 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { 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::GetDirection, &Context2d::SetDirection>("direction", napi_default_jsproperty), + InstanceAccessor<&Context2d::GetLanguage, &Context2d::SetLanguage>("lang", napi_default_jsproperty) }); exports.Set("CanvasRenderingContext2d", ctor); @@ -786,6 +787,25 @@ Context2d::SetDirection(const Napi::CallbackInfo& info, const Napi::Value& value 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; +} + /* * Put image data. * @@ -2490,6 +2510,9 @@ Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { checkFonts(); pango_layout_set_text(layout, str.c_str(), -1); + if (state->lang != "") { + pango_context_set_language(pango_layout_get_context(_layout), pango_language_from_string(state->lang.c_str())); + } pango_cairo_update_layout(context(), layout); PangoDirection pango_dir = state->direction == "ltr" ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL; @@ -2802,6 +2825,9 @@ Context2d::MeasureText(const Napi::CallbackInfo& info) { 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 diff --git a/src/CanvasRenderingContext2d.h b/src/CanvasRenderingContext2d.h index 341e5936d..1d9548895 100644 --- a/src/CanvasRenderingContext2d.h +++ b/src/CanvasRenderingContext2d.h @@ -36,6 +36,7 @@ struct canvas_state_t { 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"); @@ -61,6 +62,7 @@ struct canvas_state_t { fontDescription = pango_font_description_copy(other.fontDescription); font = other.font; imageSmoothingEnabled = other.imageSmoothingEnabled; + lang = other.lang; } ~canvas_state_t() { @@ -157,6 +159,7 @@ class Context2d : public Napi::ObjectWrap { 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); @@ -179,6 +182,7 @@ class Context2d : public Napi::ObjectWrap { 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); diff --git a/test/canvas.test.js b/test/canvas.test.js index 01156d089..4655c31c7 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -2490,7 +2490,8 @@ describe('Canvas', function () { ['patternQuality', 'best'], // ['quality', 'best'], // doesn't do anything, TODO remove ['textDrawingMode', 'glyph'], - ['antialias', 'gray'] + ['antialias', 'gray'], + ['lang', 'eu'] ] for (const [k, v] of state) { From 9bcf3631b41c422ad832118186ee9f02bbde2810 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sun, 17 Aug 2025 15:22:38 -0400 Subject: [PATCH 123/128] 3.2.0 --- CHANGELOG.md | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79119fecd..940894033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,13 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Changed ### Added -* Added `ctx.lang` to set the ISO language code for text ### Fixed +3.2.0 +================== +### Added +* Added `ctx.lang` to set the ISO language code for text + 3.1.2 ================== ### Fixed diff --git a/package.json b/package.json index 5ad174990..3dd7e1329 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.1.2", + "version": "3.2.0", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js", From 418f555e1645a2d0fc7e0a9e86265c69c7ddbfde Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sat, 6 Sep 2025 21:44:30 -0700 Subject: [PATCH 124/128] bug: incorrect roundRect() with large radii Fixes #2400 --- CHANGELOG.md | 1 + src/CanvasRenderingContext2d.cc | 6 +++--- test/public/tests.js | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 940894033..b16096f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* `roundRect()` shape incorrect when radii were large relative to rectangle size (#2400) 3.2.0 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 2342a57e5..3f52c1fdd 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -3175,10 +3175,10 @@ Context2d::RoundRect(const Napi::CallbackInfo& info) { upperLeft.x *= scale; upperLeft.y *= scale; upperRight.x *= scale; - upperRight.x *= scale; - lowerLeft.y *= scale; + upperRight.y *= scale; + lowerLeft.x *= scale; lowerLeft.y *= scale; - lowerRight.y *= scale; + lowerRight.x *= scale; lowerRight.y *= scale; } } diff --git a/test/public/tests.js b/test/public/tests.js index 165d847e6..582c0ce28 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -132,6 +132,11 @@ tests['roundRect()'] = function (ctx) { 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) { From 616859b50294d859d6d59929a766afe4e4f43ec9 Mon Sep 17 00:00:00 2001 From: Ian Chien Date: Sun, 7 Sep 2025 21:54:36 +0800 Subject: [PATCH 125/128] fix: reject loadImage when src is null or invalid (#2518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description: - To handle non-string/non-Buffer sources while fetching image. - Add test cases for '', null, and undefined inputs to loadImage() Test Result: ``` npm run test ... ✔ rejects when loadImage is called with null ✔ rejects when loadImage is called with undefined ✔ rejects when loadImage is called with empty string 291 passing (302ms) 6 pending ``` --- CHANGELOG.md | 1 + lib/image.js | 4 ++++ test/image.test.js | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b16096f69..aafa96fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed * `roundRect()` shape incorrect when radii were large relative to rectangle size (#2400) +* Reject loadImage when src is null or invalid (#2304) 3.2.0 ================== diff --git a/lib/image.js b/lib/image.js index 9ffa3c794..72243439c 100644 --- a/lib/image.js +++ b/lib/image.js @@ -63,6 +63,10 @@ Object.defineProperty(Image.prototype, 'src', { } } 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/test/image.test.js b/test/image.test.js index a5d6f415c..edae78beb 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -516,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) From 7f34c9bec84c9637b3dec216ae7f4a83a8022fdf Mon Sep 17 00:00:00 2001 From: martinwcf Date: Fri, 10 Oct 2025 15:45:11 +0200 Subject: [PATCH 126/128] Fix error message HTTP response status code in image src setter (#2532) --- CHANGELOG.md | 1 + lib/image.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aafa96fe8..59fad2f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Fix error message HTTP response status code in image src setter * `roundRect()` shape incorrect when radii were large relative to rectangle size (#2400) * Reject loadImage when src is null or invalid (#2304) diff --git a/lib/image.js b/lib/image.js index 72243439c..a6c81ba83 100644 --- a/lib/image.js +++ b/lib/image.js @@ -50,7 +50,7 @@ Object.defineProperty(Image.prototype, 'src', { }) .then(res => { if (!res.ok) { - throw new Error(`Server responded with ${res.statusCode}`) + throw new Error(`Server responded with ${res.status}`) } return res.arrayBuffer() }) From f2c570d6e12fa3667148b4c7a6e6211b9051646c Mon Sep 17 00:00:00 2001 From: skooch Date: Wed, 17 Dec 2025 02:11:51 +1100 Subject: [PATCH 127/128] Fix compilation on GCC 15 by including (#2545) (#2546) As per the GCC 15 porting guide, section "Header dependency changes": https://gcc.gnu.org/gcc-15/porting_to.html#header-dep-changes > Some C++ Standard Library headers have been changed to no longer include other headers that were being used internally by the library. As such, C++ programs that used standard library components without including the right headers will no longer compile. > > In particular, the following headers are used less widely within libstdc++ and may need to be included explicitly when compiling with GCC 15: > > `` (for `int8_t`, `int32_t` etc.) and `` (for `std::int8_t`, `std::int32_t` etc.) > `` (for `std::endl`, `std::flush` etc.) `` must now be explicitly included to use these types and was missing from `CharData.h`. --- CHANGELOG.md | 1 + src/CharData.h | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59fad2f10..c47018705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ project adheres to [Semantic Versioning](http://semver.org/). * 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 ================== diff --git a/src/CharData.h b/src/CharData.h index ebc2dd5e1..00fc4effc 100644 --- a/src/CharData.h +++ b/src/CharData.h @@ -3,6 +3,8 @@ #pragma once +#include + namespace CharData { static constexpr uint8_t Whitespace = 0x1; static constexpr uint8_t Newline = 0x2; From 41adf083176071e82d4049c77e74c3d42dd9e6e6 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Sat, 10 Jan 2026 15:39:07 -0500 Subject: [PATCH 128/128] v3.2.1 --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c47018705..4a4deae39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ 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) diff --git a/package.json b/package.json index 3dd7e1329..7ba228af8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "canvas", "description": "Canvas graphics API backed by Cairo", - "version": "3.2.0", + "version": "3.2.1", "author": "TJ Holowaychuk ", "main": "index.js", "browser": "browser.js",