From 2e7515ac6867fb8c78f177a6e25a578a6ed8db37 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 25 Feb 2025 09:02:01 +0100 Subject: [PATCH 01/11] feat: add base64 string encoding --- src/save/encodeBase64.ts | 52 ++++++++++++++++++++++++++++++++++++++++ src/save/index.ts | 1 + 2 files changed, 53 insertions(+) create mode 100644 src/save/encodeBase64.ts diff --git a/src/save/encodeBase64.ts b/src/save/encodeBase64.ts new file mode 100644 index 000000000..c02bd7d48 --- /dev/null +++ b/src/save/encodeBase64.ts @@ -0,0 +1,52 @@ + +import type { Image } from '../Image.js'; + +import {encode} from './encode.js'; +import type {ImageFormat} from './encode.js'; + + +/** + * Converts image into a base64 URL string. + * @param image - Image to get base64 encoding from. + * @param format - Image format. + * @returns base64 string. + */ +export function encodeBase64(image:Image,format:ImageFormat){ + const isNode = typeof window === 'undefined' && typeof document === 'undefined'; + if(isNode){ + return encodeBase64InNode(image,format); + }else{ + return encodeBase64InBrowser(image,format); + } +} + + +/** + * Converts image into a base64 URL string in NodeJs. + * @param image - Image to get base64 encoding from. + * @param format - Image format. + * @returns base64 string. + */ +function encodeBase64InNode(image:Image,format:ImageFormat){ + const encodedData = encode(image,{format}); + return `data:image/${format};base64,${(Buffer.from(encodedData) + .toString('base64'))}` +} +/** + * Converts image into a base64 URL string in browser. + * @param image - Image to get base64 encoding from. + * @param format - Image format. + * @returns base64 string. + */ +function encodeBase64InBrowser(image:Image,format:ImageFormat){ + const buffer = encode(image,{format}); + let binaryString = ''; + for(const el of buffer){ + binaryString += String.fromCodePoint(el); + } + const base64String = btoa(binaryString); + const dataURL = `data:image/${format};base64,${base64String}`; + return dataURL; +} + + diff --git a/src/save/index.ts b/src/save/index.ts index b2a09f6fe..949cb9ca9 100644 --- a/src/save/index.ts +++ b/src/save/index.ts @@ -3,3 +3,4 @@ export * from './encodePng.js'; export * from './encodeJpeg.js'; export * from './write.js'; export * from './writeCanvas.js'; +export * from './encodeBase64.js'; From cc01564731508e7d35328bed5a793fcffc848c07 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 25 Feb 2025 09:02:15 +0100 Subject: [PATCH 02/11] test: add testing cases --- src/save/__tests__/encodeBase64.test.ts | 100 ++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/save/__tests__/encodeBase64.test.ts diff --git a/src/save/__tests__/encodeBase64.test.ts b/src/save/__tests__/encodeBase64.test.ts new file mode 100644 index 000000000..602d15eca --- /dev/null +++ b/src/save/__tests__/encodeBase64.test.ts @@ -0,0 +1,100 @@ + +import { vi } from "vitest"; + +import { createRgbaImage } from "../../../test/testUtils.js"; +import { encode } from "../encode.js"; +import { encodeBase64 } from "../encodeBase64.js"; + +test("basic image (png)",()=>{ + const image = testUtils.createGreyImage([ + [0, 0, 0, 0, 0], + [0, 255, 255, 255, 0], + [0, 255, 0, 255, 0], + [0, 255, 255, 255, 0], + [255, 0, 255, 0, 255], + ]); + const base64Url = encodeBase64(image,'png'); + + expect(base64Url).toBe("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAAAAACoBHk5AAAAFklEQVR4XmNggID///+DSCCEskHM/wCAnQr2TY5mOAAAAABJRU5ErkJggg=="); +}); + +test("basic image 2 (jpeg)",()=>{ + const image = testUtils.createGreyImage([ + [255, 255, 255, 255, 255], + [255, 0, 0, 0, 255], + [255, 0, 0, 0, 255], + [255, 0, 0, 0, 255], + [255, 255, 255, 255, 255], + ]); + const format = 'jpeg' + const base64 = encodeBase64(image,format); + const base64Data = Buffer.from(encode(image,{format})) + .toString('base64'); + expect(typeof base64).toBe('string'); + expect(base64Data).toBe(base64.slice( base64.indexOf(',') + 1)); +}); + +test("legacy image-js test",()=>{ + const image = createRgbaImage([[ + 255, + 0, + 0, + 255, // red + 0, + 255, + 0, + 255,// green + 0, + 0, + 255, + 255 // blue + ],[255, + 255, + 0, + 255,// yellow + 255, + 0, + 255, + 255,// magenta + 0, + 255, + 255, + 255,// cyan + ],[ 0, + 0, + 0, + 255,// black + 255, + 255, + 255, + 255, // white + 127, + 127, + 127, + 255,//grey + ]]); + const format = 'jpeg'; + const url = encodeBase64(image,format); + const base64Data = Buffer.from(encode(image,{format})) + .toString('base64') + expect(typeof url).toBe('string'); + expect(base64Data).toBe(url.slice(url.indexOf(',') + 1)); +}) +test("browser testing",()=>{ + const image = testUtils.createGreyImage([ + [255, 255, 255, 255, 255], + [255, 0, 0, 0, 255], + [255, 0, 0, 0, 255], + [255, 0, 0, 0, 255], + [255, 255, 255, 255, 255], + ]); + const base64Node = encodeBase64(image,'jpg'); + vi.stubGlobal('window',()=>{ + const base64Browser = encodeBase64(image,'jpg'); + expect(base64Browser).not.toBe(base64Node); + }) + vi.stubGlobal('document',()=>{ + const base64Browser = encodeBase64(image,'jpg'); + expect(base64Browser).not.toBe(base64Node); + }) +}) From 4831b6fb9e2c67f2ac41cce0e766bf0d48278add Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 25 Feb 2025 09:15:22 +0100 Subject: [PATCH 03/11] chore: fix prettier errors --- src/save/__tests__/encodeBase64.test.ts | 137 ++++++++++++------------ src/save/encodeBase64.ts | 41 ++++--- 2 files changed, 90 insertions(+), 88 deletions(-) diff --git a/src/save/__tests__/encodeBase64.test.ts b/src/save/__tests__/encodeBase64.test.ts index 602d15eca..b68c6340c 100644 --- a/src/save/__tests__/encodeBase64.test.ts +++ b/src/save/__tests__/encodeBase64.test.ts @@ -1,11 +1,10 @@ +import { vi } from 'vitest'; -import { vi } from "vitest"; +import { createRgbaImage } from '../../../test/testUtils.js'; +import { encode } from '../encode.js'; +import { encodeBase64 } from '../encodeBase64.js'; -import { createRgbaImage } from "../../../test/testUtils.js"; -import { encode } from "../encode.js"; -import { encodeBase64 } from "../encodeBase64.js"; - -test("basic image (png)",()=>{ +test('basic image (png)', () => { const image = testUtils.createGreyImage([ [0, 0, 0, 0, 0], [0, 255, 255, 255, 0], @@ -13,12 +12,14 @@ test("basic image (png)",()=>{ [0, 255, 255, 255, 0], [255, 0, 255, 0, 255], ]); - const base64Url = encodeBase64(image,'png'); + const base64Url = encodeBase64(image, 'png'); - expect(base64Url).toBe("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAAAAACoBHk5AAAAFklEQVR4XmNggID///+DSCCEskHM/wCAnQr2TY5mOAAAAABJRU5ErkJggg=="); + expect(base64Url).toBe( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAAAAACoBHk5AAAAFklEQVR4XmNggID///+DSCCEskHM/wCAnQr2TY5mOAAAAABJRU5ErkJggg==', + ); }); -test("basic image 2 (jpeg)",()=>{ +test('basic image 2 (jpeg)', () => { const image = testUtils.createGreyImage([ [255, 255, 255, 255, 255], [255, 0, 0, 0, 255], @@ -26,61 +27,65 @@ test("basic image 2 (jpeg)",()=>{ [255, 0, 0, 0, 255], [255, 255, 255, 255, 255], ]); - const format = 'jpeg' - const base64 = encodeBase64(image,format); - const base64Data = Buffer.from(encode(image,{format})) - .toString('base64'); + const format = 'jpeg'; + const base64 = encodeBase64(image, format); + const base64Data = Buffer.from(encode(image, { format })).toString('base64'); expect(typeof base64).toBe('string'); - expect(base64Data).toBe(base64.slice( base64.indexOf(',') + 1)); + expect(base64Data).toBe(base64.slice(base64.indexOf(',') + 1)); }); -test("legacy image-js test",()=>{ - const image = createRgbaImage([[ - 255, - 0, - 0, - 255, // red - 0, - 255, - 0, - 255,// green - 0, - 0, - 255, - 255 // blue - ],[255, +test('legacy image-js test', () => { + const image = createRgbaImage([ + [ + 255, + 0, + 0, + 255, // red + 0, + 255, + 0, + 255, // green + 0, + 0, + 255, + 255, // blue + ], + [ 255, - 0, - 255,// yellow 255, 0, - 255, - 255,// magenta + 255, // yellow + 255, 0, 255, - 255, - 255,// cyan - ],[ 0, - 0, - 0, - 255,// black - 255, - 255, - 255, - 255, // white - 127, - 127, - 127, - 255,//grey - ]]); - const format = 'jpeg'; - const url = encodeBase64(image,format); - const base64Data = Buffer.from(encode(image,{format})) - .toString('base64') - expect(typeof url).toBe('string'); - expect(base64Data).toBe(url.slice(url.indexOf(',') + 1)); -}) -test("browser testing",()=>{ + 255, // magenta + 0, + 255, + 255, + 255, // cyan + ], + [ + 0, + 0, + 0, + 255, // black + 255, + 255, + 255, + 255, // white + 127, + 127, + 127, + 255, //grey + ], + ]); + const format = 'jpeg'; + const url = encodeBase64(image, format); + const base64Data = Buffer.from(encode(image, { format })).toString('base64'); + expect(typeof url).toBe('string'); + expect(base64Data).toBe(url.slice(url.indexOf(',') + 1)); +}); +test('browser testing', () => { const image = testUtils.createGreyImage([ [255, 255, 255, 255, 255], [255, 0, 0, 0, 255], @@ -88,13 +93,13 @@ test("browser testing",()=>{ [255, 0, 0, 0, 255], [255, 255, 255, 255, 255], ]); - const base64Node = encodeBase64(image,'jpg'); - vi.stubGlobal('window',()=>{ - const base64Browser = encodeBase64(image,'jpg'); - expect(base64Browser).not.toBe(base64Node); - }) - vi.stubGlobal('document',()=>{ - const base64Browser = encodeBase64(image,'jpg'); - expect(base64Browser).not.toBe(base64Node); - }) -}) + const base64Node = encodeBase64(image, 'jpg'); + vi.stubGlobal('window', () => { + const base64Browser = encodeBase64(image, 'jpg'); + expect(base64Browser).not.toBe(base64Node); + }); + vi.stubGlobal('document', () => { + const base64Browser = encodeBase64(image, 'jpg'); + expect(base64Browser).not.toBe(base64Node); + }); +}); diff --git a/src/save/encodeBase64.ts b/src/save/encodeBase64.ts index c02bd7d48..b2b11d44c 100644 --- a/src/save/encodeBase64.ts +++ b/src/save/encodeBase64.ts @@ -1,9 +1,7 @@ - import type { Image } from '../Image.js'; -import {encode} from './encode.js'; -import type {ImageFormat} from './encode.js'; - +import { encode } from './encode.js'; +import type { ImageFormat } from './encode.js'; /** * Converts image into a base64 URL string. @@ -11,26 +9,27 @@ import type {ImageFormat} from './encode.js'; * @param format - Image format. * @returns base64 string. */ -export function encodeBase64(image:Image,format:ImageFormat){ - const isNode = typeof window === 'undefined' && typeof document === 'undefined'; - if(isNode){ - return encodeBase64InNode(image,format); - }else{ - return encodeBase64InBrowser(image,format); +export function encodeBase64(image: Image, format: ImageFormat) { + const isNode = + typeof window === 'undefined' && typeof document === 'undefined'; + if (isNode) { + return encodeBase64InNode(image, format); + } else { + return encodeBase64InBrowser(image, format); } } - /** * Converts image into a base64 URL string in NodeJs. * @param image - Image to get base64 encoding from. * @param format - Image format. * @returns base64 string. */ -function encodeBase64InNode(image:Image,format:ImageFormat){ - const encodedData = encode(image,{format}); - return `data:image/${format};base64,${(Buffer.from(encodedData) - .toString('base64'))}` +function encodeBase64InNode(image: Image, format: ImageFormat) { + const encodedData = encode(image, { format }); + return `data:image/${format};base64,${Buffer.from(encodedData).toString( + 'base64', + )}`; } /** * Converts image into a base64 URL string in browser. @@ -38,15 +37,13 @@ function encodeBase64InNode(image:Image,format:ImageFormat){ * @param format - Image format. * @returns base64 string. */ -function encodeBase64InBrowser(image:Image,format:ImageFormat){ - const buffer = encode(image,{format}); +function encodeBase64InBrowser(image: Image, format: ImageFormat) { + const buffer = encode(image, { format }); let binaryString = ''; - for(const el of buffer){ + for (const el of buffer) { binaryString += String.fromCodePoint(el); } const base64String = btoa(binaryString); - const dataURL = `data:image/${format};base64,${base64String}`; - return dataURL; + const dataURL = `data:image/${format};base64,${base64String}`; + return dataURL; } - - From a3ffd35a9f8a44233e44b9048733ad7e00f78dde Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 25 Feb 2025 11:41:08 +0100 Subject: [PATCH 04/11] perf: change algorithm to improve performance --- src/save/encodeBase64.ts | 39 ++++----------------------------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/src/save/encodeBase64.ts b/src/save/encodeBase64.ts index b2b11d44c..2b0ed1222 100644 --- a/src/save/encodeBase64.ts +++ b/src/save/encodeBase64.ts @@ -10,40 +10,9 @@ import type { ImageFormat } from './encode.js'; * @returns base64 string. */ export function encodeBase64(image: Image, format: ImageFormat) { - const isNode = - typeof window === 'undefined' && typeof document === 'undefined'; - if (isNode) { - return encodeBase64InNode(image, format); - } else { - return encodeBase64InBrowser(image, format); - } +const buffer = encode(image, { format }); +const binaryString = new TextDecoder('latin1').decode(Uint8Array.from(buffer)); +const base64String = btoa(binaryString); +return `data:image/${format};base64,${base64String}`; } -/** - * Converts image into a base64 URL string in NodeJs. - * @param image - Image to get base64 encoding from. - * @param format - Image format. - * @returns base64 string. - */ -function encodeBase64InNode(image: Image, format: ImageFormat) { - const encodedData = encode(image, { format }); - return `data:image/${format};base64,${Buffer.from(encodedData).toString( - 'base64', - )}`; -} -/** - * Converts image into a base64 URL string in browser. - * @param image - Image to get base64 encoding from. - * @param format - Image format. - * @returns base64 string. - */ -function encodeBase64InBrowser(image: Image, format: ImageFormat) { - const buffer = encode(image, { format }); - let binaryString = ''; - for (const el of buffer) { - binaryString += String.fromCodePoint(el); - } - const base64String = btoa(binaryString); - const dataURL = `data:image/${format};base64,${base64String}`; - return dataURL; -} From c623c5faecf43d4b52f3bc7b39c37fe21f0dd708 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 25 Feb 2025 11:41:19 +0100 Subject: [PATCH 05/11] test: modify tests --- .../__snapshots__/encodeBase64.test.ts.snap | 3 ++ src/save/__tests__/encodeBase64.test.ts | 28 +++++-------------- 2 files changed, 10 insertions(+), 21 deletions(-) create mode 100644 src/save/__tests__/__snapshots__/encodeBase64.test.ts.snap diff --git a/src/save/__tests__/__snapshots__/encodeBase64.test.ts.snap b/src/save/__tests__/__snapshots__/encodeBase64.test.ts.snap new file mode 100644 index 000000000..93dacbe31 --- /dev/null +++ b/src/save/__tests__/__snapshots__/encodeBase64.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`basic image 2 (jpeg) 1`] = `"/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2MBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAAUABQMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AOu0zTbmyvL+efUZbpLmTfHE+cQDLHaMk+oHbpQB/9k="`; diff --git a/src/save/__tests__/encodeBase64.test.ts b/src/save/__tests__/encodeBase64.test.ts index b68c6340c..e69667556 100644 --- a/src/save/__tests__/encodeBase64.test.ts +++ b/src/save/__tests__/encodeBase64.test.ts @@ -1,9 +1,10 @@ -import { vi } from 'vitest'; import { createRgbaImage } from '../../../test/testUtils.js'; import { encode } from '../encode.js'; import { encodeBase64 } from '../encodeBase64.js'; - +/** + * @vitest-environment jsdom + */ test('basic image (png)', () => { const image = testUtils.createGreyImage([ [0, 0, 0, 0, 0], @@ -31,7 +32,7 @@ test('basic image 2 (jpeg)', () => { const base64 = encodeBase64(image, format); const base64Data = Buffer.from(encode(image, { format })).toString('base64'); expect(typeof base64).toBe('string'); - expect(base64Data).toBe(base64.slice(base64.indexOf(',') + 1)); + expect(base64Data).toMatchSnapshot(); }); test('legacy image-js test', () => { @@ -85,21 +86,6 @@ test('legacy image-js test', () => { expect(typeof url).toBe('string'); expect(base64Data).toBe(url.slice(url.indexOf(',') + 1)); }); -test('browser testing', () => { - const image = testUtils.createGreyImage([ - [255, 255, 255, 255, 255], - [255, 0, 0, 0, 255], - [255, 0, 0, 0, 255], - [255, 0, 0, 0, 255], - [255, 255, 255, 255, 255], - ]); - const base64Node = encodeBase64(image, 'jpg'); - vi.stubGlobal('window', () => { - const base64Browser = encodeBase64(image, 'jpg'); - expect(base64Browser).not.toBe(base64Node); - }); - vi.stubGlobal('document', () => { - const base64Browser = encodeBase64(image, 'jpg'); - expect(base64Browser).not.toBe(base64Node); - }); -}); + + + From 8a3792f04a650ae63f8292398c2c243e6b6f0eac Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 25 Feb 2025 11:59:51 +0100 Subject: [PATCH 06/11] chore: formatting fixes --- src/save/__tests__/encodeBase64.test.ts | 8 +------- src/save/encodeBase64.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/save/__tests__/encodeBase64.test.ts b/src/save/__tests__/encodeBase64.test.ts index e69667556..0a6f3806c 100644 --- a/src/save/__tests__/encodeBase64.test.ts +++ b/src/save/__tests__/encodeBase64.test.ts @@ -1,10 +1,7 @@ - import { createRgbaImage } from '../../../test/testUtils.js'; import { encode } from '../encode.js'; import { encodeBase64 } from '../encodeBase64.js'; -/** - * @vitest-environment jsdom - */ + test('basic image (png)', () => { const image = testUtils.createGreyImage([ [0, 0, 0, 0, 0], @@ -86,6 +83,3 @@ test('legacy image-js test', () => { expect(typeof url).toBe('string'); expect(base64Data).toBe(url.slice(url.indexOf(',') + 1)); }); - - - diff --git a/src/save/encodeBase64.ts b/src/save/encodeBase64.ts index 2b0ed1222..17f0ccf81 100644 --- a/src/save/encodeBase64.ts +++ b/src/save/encodeBase64.ts @@ -10,9 +10,11 @@ import type { ImageFormat } from './encode.js'; * @returns base64 string. */ export function encodeBase64(image: Image, format: ImageFormat) { -const buffer = encode(image, { format }); -const binaryString = new TextDecoder('latin1').decode(Uint8Array.from(buffer)); -const base64String = btoa(binaryString); -return `data:image/${format};base64,${base64String}`; -} + const buffer = encode(image, { format }); + const binaryString = new TextDecoder('latin1').decode( + Uint8Array.from(buffer), + ); + const base64String = btoa(binaryString); + return `data:image/${format};base64,${base64String}`; +} From 5d3ba91b167534dda3281727579675be4f69761e Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 25 Feb 2025 12:26:25 +0100 Subject: [PATCH 07/11] fix: get more reliable algo version --- src/save/encodeBase64.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/save/encodeBase64.ts b/src/save/encodeBase64.ts index 17f0ccf81..1dd8045ad 100644 --- a/src/save/encodeBase64.ts +++ b/src/save/encodeBase64.ts @@ -11,10 +11,12 @@ import type { ImageFormat } from './encode.js'; */ export function encodeBase64(image: Image, format: ImageFormat) { const buffer = encode(image, { format }); - const binaryString = new TextDecoder('latin1').decode( - Uint8Array.from(buffer), - ); - + const binaryArray = []; + for (const el of buffer) { + binaryArray.push(String.fromCodePoint(el)); + } + const binaryString = binaryArray.join(''); const base64String = btoa(binaryString); - return `data:image/${format};base64,${base64String}`; + const dataURL = `data:image/${format};base64,${base64String}`; + return dataURL; } From 73a299de3daa8b7ac8ccf5324d40798475597cb8 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Fri, 28 Feb 2025 09:15:46 +0100 Subject: [PATCH 08/11] feat: add package for performant base64 encoding --- package.json | 1 + src/save/__tests__/encodeBase64.test.ts | 3 +++ src/save/encodeBase64.ts | 10 +++------- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 0e5077c1e..b0f4da8d9 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ }, "homepage": "https://github.com/image-js/image-js#readme", "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", "bresenham-zingl": "^0.2.0", "colord": "^2.9.3", "fast-bmp": "^2.0.1", diff --git a/src/save/__tests__/encodeBase64.test.ts b/src/save/__tests__/encodeBase64.test.ts index 0a6f3806c..31d5560e2 100644 --- a/src/save/__tests__/encodeBase64.test.ts +++ b/src/save/__tests__/encodeBase64.test.ts @@ -2,6 +2,9 @@ import { createRgbaImage } from '../../../test/testUtils.js'; import { encode } from '../encode.js'; import { encodeBase64 } from '../encodeBase64.js'; +/** + * @vitest-environment jsdom + */ test('basic image (png)', () => { const image = testUtils.createGreyImage([ [0, 0, 0, 0, 0], diff --git a/src/save/encodeBase64.ts b/src/save/encodeBase64.ts index 1dd8045ad..44667918f 100644 --- a/src/save/encodeBase64.ts +++ b/src/save/encodeBase64.ts @@ -1,8 +1,9 @@ +import { toBase64 } from '@jsonjoy.com/base64'; + import type { Image } from '../Image.js'; import { encode } from './encode.js'; import type { ImageFormat } from './encode.js'; - /** * Converts image into a base64 URL string. * @param image - Image to get base64 encoding from. @@ -11,12 +12,7 @@ import type { ImageFormat } from './encode.js'; */ export function encodeBase64(image: Image, format: ImageFormat) { const buffer = encode(image, { format }); - const binaryArray = []; - for (const el of buffer) { - binaryArray.push(String.fromCodePoint(el)); - } - const binaryString = binaryArray.join(''); - const base64String = btoa(binaryString); + const base64String = toBase64(buffer); const dataURL = `data:image/${format};base64,${base64String}`; return dataURL; } From de882ba15a79f17fa7baf6c56d5f61036239621a Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Fri, 28 Feb 2025 09:20:42 +0100 Subject: [PATCH 09/11] fix: remove vitest jsdom comments --- src/save/__tests__/encodeBase64.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/save/__tests__/encodeBase64.test.ts b/src/save/__tests__/encodeBase64.test.ts index 31d5560e2..0a6f3806c 100644 --- a/src/save/__tests__/encodeBase64.test.ts +++ b/src/save/__tests__/encodeBase64.test.ts @@ -2,9 +2,6 @@ import { createRgbaImage } from '../../../test/testUtils.js'; import { encode } from '../encode.js'; import { encodeBase64 } from '../encodeBase64.js'; -/** - * @vitest-environment jsdom - */ test('basic image (png)', () => { const image = testUtils.createGreyImage([ [0, 0, 0, 0, 0], From fea99ac06ba174024c2ef117826e92c24fa86197 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Fri, 28 Feb 2025 15:39:32 +0100 Subject: [PATCH 10/11] fix: resolve conversations --- package.json | 3 +- ...est.ts.snap => encodeDataURL.test.ts.snap} | 0 ...deBase64.test.ts => encodeDataURL.test.ts} | 60 ++++--------------- src/save/encode.ts | 2 +- src/save/encodeBase64.ts | 18 ------ src/save/encodeDataURL.ts | 25 ++++++++ src/save/index.ts | 2 +- 7 files changed, 40 insertions(+), 70 deletions(-) rename src/save/__tests__/__snapshots__/{encodeBase64.test.ts.snap => encodeDataURL.test.ts.snap} (100%) rename src/save/__tests__/{encodeBase64.test.ts => encodeDataURL.test.ts} (58%) delete mode 100644 src/save/encodeBase64.ts create mode 100644 src/save/encodeDataURL.ts diff --git a/package.json b/package.json index b0f4da8d9..e1b1adfcc 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ }, "homepage": "https://github.com/image-js/image-js#readme", "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", + "bresenham-zingl": "^0.2.0", "colord": "^2.9.3", "fast-bmp": "^2.0.1", @@ -94,6 +94,7 @@ "rimraf": "^6.0.1", "tailwindcss": "^3.4.17", "typescript": "~5.7.3", + "uint8-base64": "^0.1.1", "vite": "^6.0.8", "vitest": "^2.1.8" } diff --git a/src/save/__tests__/__snapshots__/encodeBase64.test.ts.snap b/src/save/__tests__/__snapshots__/encodeDataURL.test.ts.snap similarity index 100% rename from src/save/__tests__/__snapshots__/encodeBase64.test.ts.snap rename to src/save/__tests__/__snapshots__/encodeDataURL.test.ts.snap diff --git a/src/save/__tests__/encodeBase64.test.ts b/src/save/__tests__/encodeDataURL.test.ts similarity index 58% rename from src/save/__tests__/encodeBase64.test.ts rename to src/save/__tests__/encodeDataURL.test.ts index 0a6f3806c..d21ea4a93 100644 --- a/src/save/__tests__/encodeBase64.test.ts +++ b/src/save/__tests__/encodeDataURL.test.ts @@ -1,6 +1,5 @@ -import { createRgbaImage } from '../../../test/testUtils.js'; import { encode } from '../encode.js'; -import { encodeBase64 } from '../encodeBase64.js'; +import { encodeDataURL } from '../encodeDataURL.js'; test('basic image (png)', () => { const image = testUtils.createGreyImage([ @@ -10,7 +9,7 @@ test('basic image (png)', () => { [0, 255, 255, 255, 0], [255, 0, 255, 0, 255], ]); - const base64Url = encodeBase64(image, 'png'); + const base64Url = encodeDataURL(image); expect(base64Url).toBe( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAAAAACoBHk5AAAAFklEQVR4XmNggID///+DSCCEskHM/wCAnQr2TY5mOAAAAABJRU5ErkJggg==', @@ -26,59 +25,22 @@ test('basic image 2 (jpeg)', () => { [255, 255, 255, 255, 255], ]); const format = 'jpeg'; - const base64 = encodeBase64(image, format); + const base64 = encodeDataURL(image, { format }); const base64Data = Buffer.from(encode(image, { format })).toString('base64'); expect(typeof base64).toBe('string'); expect(base64Data).toMatchSnapshot(); }); test('legacy image-js test', () => { - const image = createRgbaImage([ - [ - 255, - 0, - 0, - 255, // red - 0, - 255, - 0, - 255, // green - 0, - 0, - 255, - 255, // blue - ], - [ - 255, - 255, - 0, - 255, // yellow - 255, - 0, - 255, - 255, // magenta - 0, - 255, - 255, - 255, // cyan - ], - [ - 0, - 0, - 0, - 255, // black - 255, - 255, - 255, - 255, // white - 127, - 127, - 127, - 255, //grey - ], - ]); + const image = testUtils.createRgbaImage( + ` + 255 0 0 / 255 | 0 255 0 / 255 | 0 0 255 / 255 + 255 255 0 / 255 | 255 0 255 / 255 | 0 255 255 / 255 + 0 0 0 / 255 | 255 255 255 / 255 | 127 127 127 / 255 + `, + ); const format = 'jpeg'; - const url = encodeBase64(image, format); + const url = encodeDataURL(image, { format }); const base64Data = Buffer.from(encode(image, { format })).toString('base64'); expect(typeof url).toBe('string'); expect(base64Data).toBe(url.slice(url.indexOf(',') + 1)); diff --git a/src/save/encode.ts b/src/save/encode.ts index 3f99f3b6c..3bd5e769c 100644 --- a/src/save/encode.ts +++ b/src/save/encode.ts @@ -29,7 +29,7 @@ export interface EncodeOptionsJpeg { export interface EncodeOptionsBmp { format: 'bmp'; } -const defaultPng: EncodeOptionsPng = { format: 'png' }; +export const defaultPng: EncodeOptionsPng = { format: 'png' }; /** * Encodes the image to the specified format diff --git a/src/save/encodeBase64.ts b/src/save/encodeBase64.ts deleted file mode 100644 index 44667918f..000000000 --- a/src/save/encodeBase64.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { toBase64 } from '@jsonjoy.com/base64'; - -import type { Image } from '../Image.js'; - -import { encode } from './encode.js'; -import type { ImageFormat } from './encode.js'; -/** - * Converts image into a base64 URL string. - * @param image - Image to get base64 encoding from. - * @param format - Image format. - * @returns base64 string. - */ -export function encodeBase64(image: Image, format: ImageFormat) { - const buffer = encode(image, { format }); - const base64String = toBase64(buffer); - const dataURL = `data:image/${format};base64,${base64String}`; - return dataURL; -} diff --git a/src/save/encodeDataURL.ts b/src/save/encodeDataURL.ts new file mode 100644 index 000000000..013002322 --- /dev/null +++ b/src/save/encodeDataURL.ts @@ -0,0 +1,25 @@ +import { encode as uint8encode } from 'uint8-base64'; + +import type { Image } from '../Image.js'; + +import type { + EncodeOptionsBmp, + EncodeOptionsJpeg, + EncodeOptionsPng, +} from './encode.js'; +import { encode, defaultPng } from './encode.js'; +/** + * Converts image into Data URL string. + * @param image - Image to get base64 encoding from. + * @param options - Encoding options. + * @returns base64 string. + */ +export function encodeDataURL( + image: Image, + options: EncodeOptionsBmp | EncodeOptionsPng | EncodeOptionsJpeg = defaultPng, +) { + const buffer = encode(image, options); + const base64 = uint8encode(buffer); + const base64Data = new TextDecoder().decode(base64); + return `data:image/${options.format};base64,${base64Data}`; +} diff --git a/src/save/index.ts b/src/save/index.ts index 949cb9ca9..c9572c576 100644 --- a/src/save/index.ts +++ b/src/save/index.ts @@ -3,4 +3,4 @@ export * from './encodePng.js'; export * from './encodeJpeg.js'; export * from './write.js'; export * from './writeCanvas.js'; -export * from './encodeBase64.js'; +export * from './encodeDataURL.js'; From 5bd614b208630e732a295859f0953affef717dea Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 4 Mar 2025 08:55:06 +0100 Subject: [PATCH 11/11] chore: update uint8-base64 --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index e1b1adfcc..4661e10be 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ }, "homepage": "https://github.com/image-js/image-js#readme", "dependencies": { - "bresenham-zingl": "^0.2.0", "colord": "^2.9.3", "fast-bmp": "^2.0.1", @@ -94,7 +93,7 @@ "rimraf": "^6.0.1", "tailwindcss": "^3.4.17", "typescript": "~5.7.3", - "uint8-base64": "^0.1.1", + "uint8-base64": "^1.0.0", "vite": "^6.0.8", "vitest": "^2.1.8" }