diff --git a/lib/media/data-utils.js b/lib/media/data-utils.js new file mode 100644 --- /dev/null +++ b/lib/media/data-utils.js @@ -0,0 +1,92 @@ +// @flow + +import { fileInfoFromData } from './file-utils.js'; + +/** + * Returns a hex string representation of the given Uint8Array. + */ +function uintArrayToHexString(data: Uint8Array): string { + return Array.from(data) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); +} + +/** + * Returns a Uint8Array representation of the given hex string. + */ +function hexToUintArray(hex: string): Uint8Array { + const result = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + result[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return result; +} + +/** + * Returns a data URL representation of the given Uint8Array. + * Returns `null` if MIME type cannot be determined by reading the data header. + */ +function uintArrayToDataURL(bytes: Uint8Array): ?string { + const base64 = base64FromIntArray(bytes); + const { mime } = fileInfoFromData(bytes); + if (!mime) { + return null; + } + return `data:${mime};base64,${base64}`; +} + +/** + * Converts a Uint8Array to a base64 string. This is a temporary workaround + * until we can use implement native method to directly save Uint8Array to file. + * + * It is ~4-6x faster than using + * ```js + * let base64 = await blobToDataURI(new Blob([bytes])); + * base64 = base64.substring(base64.indexOf(',') + 1); + * ``` + * + * This function based on + * - https://developer.mozilla.org/en-US/docs/Glossary/Base64 + * - https://gist.github.com/jonleighton/958841 + * - https://stackoverflow.com/a/12713326 + */ +function base64FromIntArray(bytes: Uint8Array): string { + let base64 = ''; + const alphabet = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + const remainder = bytes.length % 3; + const mainLength = bytes.length - remainder; + + let a, b, c, d; + let chunk; + for (let i = 0; i < mainLength; i += 3) { + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; + a = (chunk & 16515072) >> 18; + b = (chunk & 258048) >> 12; + c = (chunk & 4032) >> 6; + d = chunk & 63; + base64 += alphabet[a] + alphabet[b] + alphabet[c] + alphabet[d]; + } + + if (remainder === 1) { + chunk = bytes[mainLength]; + a = (chunk & 252) >> 2; + b = (chunk & 3) << 4; + base64 += alphabet[a] + alphabet[b] + '=='; + } else if (remainder === 2) { + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; + a = (chunk & 64512) >> 10; + b = (chunk & 1008) >> 4; + c = (chunk & 15) << 2; + base64 += alphabet[a] + alphabet[b] + alphabet[c] + '='; + } + return base64; +} + +export { + uintArrayToHexString, + hexToUintArray, + base64FromIntArray, + uintArrayToDataURL, +}; diff --git a/lib/media/data-utils.test.js b/lib/media/data-utils.test.js new file mode 100644 --- /dev/null +++ b/lib/media/data-utils.test.js @@ -0,0 +1,42 @@ +// @flow +import { + uintArrayToHexString, + hexToUintArray, + uintArrayToDataURL, +} from './data-utils.js'; + +describe('uintArrayToHexString', () => { + it('converts Uint8Array to hex string', () => { + const data = new Uint8Array([0x00, 0x01, 0x02, 0x1a, 0x2b, 0xff]); + expect(uintArrayToHexString(data)).toStrictEqual('0001021a2bff'); + }); +}); + +describe('hexToUintArray', () => { + it('converts hex string to Uint8Array', () => { + const hex = '0001021a2bff'; + expect(hexToUintArray(hex)).toStrictEqual( + new Uint8Array([0x00, 0x01, 0x02, 0x1a, 0x2b, 0xff]), + ); + }); +}); + +describe('uintArrayToDataURL', () => { + it('converts JPEG Uint8Array to data URL', () => { + const jpegMagicBytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); + const jpegData = new Uint8Array(jpegMagicBytes.length + 10); + jpegData.set(jpegMagicBytes); + jpegData.set( + new Uint8Array([0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]), + jpegMagicBytes.length, + ); + + const jpegBase64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAAA='; + expect(uintArrayToDataURL(jpegData)).toStrictEqual(jpegBase64); + }); + + it('returns null if MIME type cannot be determined', () => { + const data = new Uint8Array([0x00, 0x01, 0x02, 0x1a, 0x2b, 0xff]); + expect(uintArrayToDataURL(data)).toBeNull(); + }); +});