Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add base64 image encoding #481

Merged
merged 11 commits into from
Mar 4, 2025
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -93,6 +93,7 @@
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.17",
"typescript": "~5.7.3",
"uint8-base64": "^1.0.0",
"vite": "^6.0.8",
"vitest": "^2.1.8"
}
3 changes: 3 additions & 0 deletions src/save/__tests__/__snapshots__/encodeDataURL.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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="`;
47 changes: 47 additions & 0 deletions src/save/__tests__/encodeDataURL.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { encode } from '../encode.js';
import { encodeDataURL } from '../encodeDataURL.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 = encodeDataURL(image);

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 = 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 = 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 = 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));
});
2 changes: 1 addition & 1 deletion src/save/encode.ts
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions src/save/encodeDataURL.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
1 change: 1 addition & 0 deletions src/save/index.ts
Original file line number Diff line number Diff line change
@@ -3,3 +3,4 @@ export * from './encodePng.js';
export * from './encodeJpeg.js';
export * from './write.js';
export * from './writeCanvas.js';
export * from './encodeDataURL.js';

Unchanged files with check annotations Beta

import Filters from './Filters.js';
import Home from './Home.js';
export default function App() {

Check warning on line 8 in demo/components/App.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
return (
<CameraProvider>
<HashRouter>
import UnavailableCamera from './UnavailableCamera.js';
export default function CameraFeed() {

Check warning on line 7 in demo/components/CameraFeed.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
const [{ selectedCamera }] = useCameraContext();
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
import { useCameraContext } from '../contexts/cameraContext.js';
export default function CameraSelector() {

Check warning on line 3 in demo/components/CameraSelector.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
const [{ cameras, selectedCamera }, dispatch] = useCameraContext();
if (cameras.length === 0) return null;
return (
canvasInputRef: RefObject<HTMLCanvasElement | null>;
}
export default function CameraSnapshotButton(props: CameraSnapshotButtonProps) {

Check warning on line 12 in demo/components/CameraSnapshotButton.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
const { setSnapshotUrl, snapshotImageRef, canvasInputRef } = props;
function handleClick() {
if (canvasInputRef.current) {
snapshotImageRef: RefObject<Image | null>;
}
export default function CameraTransform(props: CameraTransformProps) {

Check warning on line 24 in demo/components/CameraTransform.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
const { canvasInputRef, transform, snapshotUrl, snapshotImageRef } = props;
const [{ selectedCamera }] = useCameraContext();
const videoRef = useRef<HTMLVideoElement>(null);
title: string;
children: ReactNode;
}
export default function Container(props: ContainerProps) {

Check warning on line 9 in demo/components/Container.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
return (
<div>
<Navbar />
import type { ReactNode } from 'react';
export default function ErrorAlert(props: { children: ReactNode }) {

Check warning on line 3 in demo/components/ErrorAlert.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
return (
<div className="p-4 text-red-800 bg-red-200 rounded">{props.children}</div>
);
import Container from './Container.js';
export default function Filters() {

Check warning on line 3 in demo/components/Filters.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
return <Container title="Filters">Filters</Container>;
}
const testTransform: TransformFunction = testGetFastKeypoints;
export default function Home() {

Check warning on line 14 in demo/components/Home.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
const snapshotImageRef = useRef<Image>(null);
const canvasInputRef = useRef<HTMLCanvasElement>(null);
const [snapshotUrl, setSnapshotUrl] = useState('');
{ name: 'Filters', href: '/filters', current: false },
];
export default function Navbar() {

Check warning on line 9 in demo/components/Navbar.tsx

GitHub Actions / nodejs / lint-eslint

Missing JSDoc comment
const location = useLocation();
const currentNavigation = navigation.map((navItem) => ({
...navItem,