diff --git a/packages/node/src/base64.ts b/packages/node/src/base64.ts index c8b79542..5b022343 100644 --- a/packages/node/src/base64.ts +++ b/packages/node/src/base64.ts @@ -55,6 +55,8 @@ export const downloadImageFromURLAsBase64 = async (url: string): Promise const isBuffer = (file: string | Buffer): file is Buffer => file instanceof Buffer; +const base64DataUriPrefix = /^data:[^,]*;base64,/; + const fileToBase64 = (file: string | Buffer): Promise => isFilePromise(file).then((isFile) => isFile @@ -76,10 +78,12 @@ const fileToBase64 = (file: string | Buffer): Promise => /** * Converts media into a base64 string so that it can be sent to Weaviate. * - * @param {Media} media The media as a base64 string, a file path, a URL, a `Uint8Array` (including `Buffer`), or a `Blob`. A base64 string is returned as is. + * @param {Media} media The media as a base64 string, a base64 data URI, a file path, a URL, a `Uint8Array` (including `Buffer`), or a `Blob`. A base64 string is returned as is and a data URI has its `data:;base64,` prefix removed. * @returns {Promise} The base64 string. */ export const toBase64FromMedia = async (media: Media): Promise => { + if (typeof media === 'string' && base64DataUriPrefix.test(media)) + return media.replace(base64DataUriPrefix, ''); if (media instanceof Blob) return Buffer.from(await media.arrayBuffer()).toString('base64'); if (media instanceof Uint8Array) return Buffer.from(media.buffer, media.byteOffset, media.byteLength).toString('base64'); diff --git a/packages/test/node/base64/unit.test.ts b/packages/test/node/base64/unit.test.ts new file mode 100644 index 00000000..577b74db --- /dev/null +++ b/packages/test/node/base64/unit.test.ts @@ -0,0 +1,19 @@ +import { toBase64FromMedia } from '@weaviate/node'; +import { describe, expect, it } from 'vitest'; + +describe('Unit testing of toBase64FromMedia in the node client', () => { + const short = Buffer.from('image bytes').toString('base64'); + const long = Buffer.alloc(6000, 7).toString('base64'); + + it.each([ + ['a short png data URI', `data:image/png;base64,${short}`, short], + ['a long jpeg data URI', `data:image/jpeg;base64,${long}`, long], + ['a data URI with media type parameters', `data:image/png;name=img.png;base64,${short}`, short], + ])('should strip the prefix from %s', async (_, media, expected) => { + expect(await toBase64FromMedia(media)).toEqual(expected); + }); + + it('should return a base64 string as is', async () => { + expect(await toBase64FromMedia(long)).toEqual(long); + }); +}); diff --git a/packages/test/web/base64.test.ts b/packages/test/web/base64.test.ts new file mode 100644 index 00000000..d8063bbe --- /dev/null +++ b/packages/test/web/base64.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { toBase64FromMedia } from '../../web/src/base64.js'; + +describe('Unit testing of toBase64FromMedia in the web client', () => { + const b64 = Buffer.from('image bytes').toString('base64'); + + it.each([ + ['a png data URI', `data:image/png;base64,${b64}`], + ['a data URI with media type parameters', `data:image/png;name=img.png;base64,${b64}`], + ])('should strip the prefix from %s', async (_, media) => { + expect(await toBase64FromMedia(media)).toEqual(b64); + }); + + it('should return a base64 string as is', async () => { + expect(await toBase64FromMedia(b64)).toEqual(b64); + }); +}); diff --git a/packages/web/src/base64.ts b/packages/web/src/base64.ts index 9e3262da..1e4c615a 100644 --- a/packages/web/src/base64.ts +++ b/packages/web/src/base64.ts @@ -25,6 +25,8 @@ const blobToBase64 = (blob: Blob): Promise => reader.readAsDataURL(blob); }); +const base64DataUriPrefix = /^data:[^,]*;base64,/; + export const downloadImageFromURLAsBase64 = async (url: string): Promise => { if (!isUrl(url)) { throw new Error('Invalid URL'); @@ -43,13 +45,17 @@ export const downloadImageFromURLAsBase64 = async (url: string): Promise * Converts media input into a base64 string. * Accepts: * - A base64 string (returns it unchanged) + * - A base64 data URI (returns it without the `data:;base64,` prefix) * - A URL (fetches the image and converts to base64) * - A `Blob` or `File` (uses FileReader) * - A `Uint8Array` (copied into a `Blob`, which rejects `SharedArrayBuffer`-backed views) */ export const toBase64FromMedia = (media: Media): Promise => { if (typeof media === 'string') { - if (media.startsWith('data:') || /^[A-Za-z0-9+/=]+$/.test(media)) { + if (base64DataUriPrefix.test(media)) { + // Base64 data URI, strip the data:mime/type;base64, prefix + return Promise.resolve(media.replace(base64DataUriPrefix, '')); + } else if (media.startsWith('data:') || /^[A-Za-z0-9+/=]+$/.test(media)) { // Already base64 string return Promise.resolve(media); } else if (isUrl(media)) {