diff --git a/package.json b/package.json index 15dc1ba26..f4bbcb818 100644 --- a/package.json +++ b/package.json @@ -136,8 +136,10 @@ "@types/invariant": "^2.2.37", "@types/lodash.throttle": "^4.1.9", "@types/node": "^20.14.3", + "@uwx/exif-be-gone-web": "1.6.5", "array.prototype.findlast": "^1.2.3", "babel-plugin-transform-remove-console": "^6.9.4", + "base64-js": "^1.5.1", "bcp-47": "^2.1.0", "bcp-47-match": "^2.0.3", "chroma-js": "^3.2.0", diff --git a/src/state/gallery.ts b/src/state/gallery.ts index a482adbf3..ff43a1706 100644 --- a/src/state/gallery.ts +++ b/src/state/gallery.ts @@ -12,6 +12,8 @@ import { SaveFormat, } from 'expo-image-manipulator' import {type BlobRef} from '@atproto/api' +import {transformExif} from '@uwx/exif-be-gone-web' +import {toByteArray} from 'base64-js' import {nanoid} from 'nanoid/non-secure' import {POST_IMG_MAX} from '#/lib/constants' @@ -207,6 +209,35 @@ export function resetImageManipulation( async function bypassCompression( img: ComposerImage, ): Promise { + // TODO: use expo-file-system instead of working directly in memory + + function dataUriToUint8Array(uri: string) { + const base64 = uri.split(',')[1] + return toByteArray(base64) + } + + function toArrayBuffer(uint8: Uint8Array) { + if (uint8.buffer instanceof ArrayBuffer) { + if ( + uint8.byteOffset === 0 && + uint8.byteLength === uint8.buffer.byteLength + ) { + return uint8.buffer + } + + return uint8.buffer.slice( + uint8.byteOffset, + uint8.byteOffset + uint8.byteLength, + ) + } + + // Fallback for environments where Uint8Array.buffer is not an ArrayBuffer + const buffer = new ArrayBuffer(uint8.length) + const view = new Uint8Array(buffer) + view.set(uint8) + return buffer + } + const source = img.transformed || img.source if ( source.width > POST_IMG_MAX.width || @@ -227,31 +258,48 @@ async function bypassCompression( return undefined } + let data: Uint8Array + const path = source.path // convert path to data URI if it is not already if (!path.startsWith('data:')) { try { await fetch(path) const response = await fetch(path) - const blob = await response.blob() - if (blob.size > POST_IMG_MAX.size) { + data = new Uint8Array(await response.arrayBuffer()) + if (data.byteLength > POST_IMG_MAX.size) { return undefined } - // path = await blobToDataUri(blob) } catch (e) { // Fetch failed, likely due to CORS. Return undefined to trigger normal compression flow and error handling. return undefined } - } else if (getDataUriSize(path) > POST_IMG_MAX.size) { + } else { + if (getDataUriSize(path) > POST_IMG_MAX.size) { + return undefined + } + data = new Uint8Array(dataUriToUint8Array(path).buffer) + } + + try { + data = await transformExif(data) + } catch (err) { + console.error( + 'Failed to transform EXIF data, proceeding with original image', + err, + ) return undefined } + const dataUri = await blobToDataUri( + new Blob([toArrayBuffer(data)], {type: source.mime}), + ) return { - path: await moveIfNecessary(path), + path: dataUri, width: source.width, height: source.height, mime: source.mime, - size: getDataUriSize(path), + size: getDataUriSize(dataUri), } } diff --git a/yarn.lock b/yarn.lock index 07ccffa5d..9551860c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5105,6 +5105,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@sindresorhus/merge-streams@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz#abb11d99aeb6d27f1b563c38147a72d50058e339" + integrity sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ== + "@sinonjs/commons@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" @@ -5854,6 +5859,15 @@ "@urql/core" "^5.0.0" wonka "^6.3.2" +"@uwx/exif-be-gone-web@1.6.5": + version "1.6.5" + resolved "https://registry.yarnpkg.com/@uwx/exif-be-gone-web/-/exif-be-gone-web-1.6.5.tgz#4956529e3fa31b4f49b0627380de15e014606789" + integrity sha512-KLcTMfusQmYPzI9cvoacm8tX/z6259AN6dBIJf31pfhQCnE23IODY0nQte1Hxxo+3uVKHBFEmg+Y0EI+gnwtQA== + dependencies: + fflate "^0.8.2" + globby "^16.2.0" + hex-encoding "^2.0.3" + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -9309,6 +9323,17 @@ fast-glob@^3.3.2: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -9385,6 +9410,11 @@ fdir@^6.5.0: resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== +fflate@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" @@ -9867,6 +9897,18 @@ globby@^12.0.2: merge2 "^1.4.1" slash "^4.0.0" +globby@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-16.2.0.tgz#6ab1351fbac1d9b9e47ed423814c2ad41af308ea" + integrity sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q== + dependencies: + "@sindresorhus/merge-streams" "^4.0.0" + fast-glob "^3.3.3" + ignore "^7.0.5" + is-path-inside "^4.0.0" + slash "^5.1.0" + unicorn-magic "^0.4.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -10027,6 +10069,14 @@ hermes-parser@^0.25.1: dependencies: hermes-estree "0.25.1" +hex-encoding@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/hex-encoding/-/hex-encoding-2.0.3.tgz#59ecfa5c07170f29405db7c2a7633b1ad380df47" + integrity sha512-sSmMo4ij2Vejht+5UkzuzNGiYgIWTSUw0GQqxHIQXsuuXB1ozYnjCZJZ/nwWNYRoU8+92Dp736wqxmiAN6/fcw== + dependencies: + node-buffer-encoding "^1.0.3" + uint8-encoding "^2.0.1" + hls.js@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.2.tgz#02272bea644b5f61f71741256618d6b629ae7834" @@ -10624,6 +10674,11 @@ is-path-inside@^2.1.0: dependencies: path-is-inside "^1.0.2" +is-path-inside@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-4.0.0.tgz#805aeb62c47c1b12fc3fd13bfb3ed1e7430071db" + integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== + is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" @@ -12492,6 +12547,11 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-buffer-encoding@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/node-buffer-encoding/-/node-buffer-encoding-1.0.3.tgz#94bab4c82e8ace9e69c17771f9fe8269f9469ddd" + integrity sha512-9hJZNChhQoCN1rCFScJiEwtzvWEJw2wSnu2nhDLD/YOYl1Ce8GbtnorsnjwwjpSk4sWE7zSp2etX6j7+bO7fVA== + node-fetch@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -15023,7 +15083,7 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== -slash@^5.0.0: +slash@^5.0.0, slash@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== @@ -15949,6 +16009,11 @@ uc.micro@^2.0.0, uc.micro@^2.1.0: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== +uint8-encoding@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/uint8-encoding/-/uint8-encoding-2.0.1.tgz#f8011194c8e576f9629dfd751b2c36d468dcaf59" + integrity sha512-xZAjZ+3OvrDtjFLLgojrLmG6T0YwZEo0OTyqCBxFjlFimIKnLtFqyYk6z/jDOUlJFJE52Srtrv4W4x7t4Cn/dA== + uint8arrays@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" @@ -16014,6 +16079,11 @@ unicode-segmenter@0.14.5, unicode-segmenter@^0.14.0, unicode-segmenter@^0.14.5: resolved "https://registry.yarnpkg.com/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz#c658f6dd30de172cdcd94542adc205ba43fb63c6" integrity sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g== +unicorn-magic@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.4.0.tgz#78c6a090fd6d07abd2468b83b385603e00dfdb24" + integrity sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"