image.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import decodePng from "png-chunks-extract";
  2. import tEXt from "png-chunk-text";
  3. import encodePng from "png-chunks-encode";
  4. import { stringToBase64, encode, decode, base64ToString } from "./encode";
  5. import { MIME_TYPES } from "../constants";
  6. // -----------------------------------------------------------------------------
  7. // PNG
  8. // -----------------------------------------------------------------------------
  9. const blobToArrayBuffer = (blob: Blob): Promise<ArrayBuffer> => {
  10. if ("arrayBuffer" in blob) {
  11. return blob.arrayBuffer();
  12. }
  13. // Safari
  14. return new Promise((resolve, reject) => {
  15. const reader = new FileReader();
  16. reader.onload = (event) => {
  17. if (!event.target?.result) {
  18. return reject(new Error("couldn't convert blob to ArrayBuffer"));
  19. }
  20. resolve(event.target.result as ArrayBuffer);
  21. };
  22. reader.readAsArrayBuffer(blob);
  23. });
  24. };
  25. export const getTEXtChunk = async (
  26. blob: Blob,
  27. ): Promise<{ keyword: string; text: string } | null> => {
  28. const chunks = decodePng(new Uint8Array(await blobToArrayBuffer(blob)));
  29. const metadataChunk = chunks.find((chunk) => chunk.name === "tEXt");
  30. if (metadataChunk) {
  31. return tEXt.decode(metadataChunk.data);
  32. }
  33. return null;
  34. };
  35. export const encodePngMetadata = async ({
  36. blob,
  37. metadata,
  38. }: {
  39. blob: Blob;
  40. metadata: string;
  41. }) => {
  42. const chunks = decodePng(new Uint8Array(await blobToArrayBuffer(blob)));
  43. const metadataChunk = tEXt.encode(
  44. MIME_TYPES.excalidraw,
  45. JSON.stringify(
  46. await encode({
  47. text: metadata,
  48. compress: true,
  49. }),
  50. ),
  51. );
  52. // insert metadata before last chunk (iEND)
  53. chunks.splice(-1, 0, metadataChunk);
  54. return new Blob([encodePng(chunks)], { type: "image/png" });
  55. };
  56. export const decodePngMetadata = async (blob: Blob) => {
  57. const metadata = await getTEXtChunk(blob);
  58. if (metadata?.keyword === MIME_TYPES.excalidraw) {
  59. try {
  60. const encodedData = JSON.parse(metadata.text);
  61. if (!("encoded" in encodedData)) {
  62. // legacy, un-encoded scene JSON
  63. if ("type" in encodedData && encodedData.type === "excalidraw") {
  64. return metadata.text;
  65. }
  66. throw new Error("FAILED");
  67. }
  68. return await decode(encodedData);
  69. } catch (error) {
  70. console.error(error);
  71. throw new Error("FAILED");
  72. }
  73. }
  74. throw new Error("INVALID");
  75. };
  76. // -----------------------------------------------------------------------------
  77. // SVG
  78. // -----------------------------------------------------------------------------
  79. export const encodeSvgMetadata = async ({ text }: { text: string }) => {
  80. const base64 = await stringToBase64(
  81. JSON.stringify(await encode({ text })),
  82. true /* is already byte string */,
  83. );
  84. let metadata = "";
  85. metadata += `<!-- payload-type:${MIME_TYPES.excalidraw} -->`;
  86. metadata += `<!-- payload-version:2 -->`;
  87. metadata += "<!-- payload-start -->";
  88. metadata += base64;
  89. metadata += "<!-- payload-end -->";
  90. return metadata;
  91. };
  92. export const decodeSvgMetadata = async ({ svg }: { svg: string }) => {
  93. if (svg.includes(`payload-type:${MIME_TYPES.excalidraw}`)) {
  94. const match = svg.match(/<!-- payload-start -->(.+?)<!-- payload-end -->/);
  95. if (!match) {
  96. throw new Error("INVALID");
  97. }
  98. const versionMatch = svg.match(/<!-- payload-version:(\d+) -->/);
  99. const version = versionMatch?.[1] || "1";
  100. const isByteString = version !== "1";
  101. try {
  102. const json = await base64ToString(match[1], isByteString);
  103. const encodedData = JSON.parse(json);
  104. if (!("encoded" in encodedData)) {
  105. // legacy, un-encoded scene JSON
  106. if ("type" in encodedData && encodedData.type === "excalidraw") {
  107. return json;
  108. }
  109. throw new Error("FAILED");
  110. }
  111. return await decode(encodedData);
  112. } catch (error) {
  113. console.error(error);
  114. throw new Error("FAILED");
  115. }
  116. }
  117. throw new Error("INVALID");
  118. };