image.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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 { EXPORT_DATA_TYPES, 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 (
  64. "type" in encodedData &&
  65. encodedData.type === EXPORT_DATA_TYPES.excalidraw
  66. ) {
  67. return metadata.text;
  68. }
  69. throw new Error("FAILED");
  70. }
  71. return await decode(encodedData);
  72. } catch (error) {
  73. console.error(error);
  74. throw new Error("FAILED");
  75. }
  76. }
  77. throw new Error("INVALID");
  78. };
  79. // -----------------------------------------------------------------------------
  80. // SVG
  81. // -----------------------------------------------------------------------------
  82. export const encodeSvgMetadata = async ({ text }: { text: string }) => {
  83. const base64 = await stringToBase64(
  84. JSON.stringify(await encode({ text })),
  85. true /* is already byte string */,
  86. );
  87. let metadata = "";
  88. metadata += `<!-- payload-type:${MIME_TYPES.excalidraw} -->`;
  89. metadata += `<!-- payload-version:2 -->`;
  90. metadata += "<!-- payload-start -->";
  91. metadata += base64;
  92. metadata += "<!-- payload-end -->";
  93. return metadata;
  94. };
  95. export const decodeSvgMetadata = async ({ svg }: { svg: string }) => {
  96. if (svg.includes(`payload-type:${MIME_TYPES.excalidraw}`)) {
  97. const match = svg.match(/<!-- payload-start -->(.+?)<!-- payload-end -->/);
  98. if (!match) {
  99. throw new Error("INVALID");
  100. }
  101. const versionMatch = svg.match(/<!-- payload-version:(\d+) -->/);
  102. const version = versionMatch?.[1] || "1";
  103. const isByteString = version !== "1";
  104. try {
  105. const json = await base64ToString(match[1], isByteString);
  106. const encodedData = JSON.parse(json);
  107. if (!("encoded" in encodedData)) {
  108. // legacy, un-encoded scene JSON
  109. if (
  110. "type" in encodedData &&
  111. encodedData.type === EXPORT_DATA_TYPES.excalidraw
  112. ) {
  113. return json;
  114. }
  115. throw new Error("FAILED");
  116. }
  117. return await decode(encodedData);
  118. } catch (error) {
  119. console.error(error);
  120. throw new Error("FAILED");
  121. }
  122. }
  123. throw new Error("INVALID");
  124. };