From d5ac76d4eadf7b9100abd3a4ed9c159ca991345d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arno=C5=A1t=20Pleskot?= Date: Fri, 16 Jun 2023 22:36:24 +0200 Subject: [PATCH] feat: working export with pngjs --- package.json | 2 + src/data/blob.ts | 111 +++++++++++++++++++++++------------------------ src/global.d.ts | 4 ++ yarn.lock | 12 +++++ 4 files changed, 72 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index 91a4400b6..b2be70aa3 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "png-chunk-text": "1.0.0", "png-chunks-encode": "1.0.0", "png-chunks-extract": "1.0.0", + "pngjs": "7.0.0", "points-on-curve": "0.2.0", "pwacompat": "2.0.17", "react": "18.2.0", @@ -74,6 +75,7 @@ "@types/lodash.throttle": "4.1.7", "@types/pako": "1.0.3", "@types/pica": "5.1.3", + "@types/pngjs": "6.0.1", "@types/react": "18.0.15", "@types/react-dom": "18.0.6", "@types/resize-observer-browser": "0.1.7", diff --git a/src/data/blob.ts b/src/data/blob.ts index 2fc44ec6c..47e587ec6 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -13,6 +13,7 @@ import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem"; import { isValidExcalidrawData, isValidLibrary } from "./json"; import { restore, restoreLibraryItems } from "./restore"; import { ImportedLibraryData } from "./types"; +import { PNG } from "pngjs/browser"; const parseFileContents = async (blob: Blob | File) => { let contents: string; @@ -233,75 +234,71 @@ const _canvasToBlob = async (canvas: HTMLCanvasElement): Promise => { export const canvasToBlob = async ( canvas: HTMLCanvasElement, ): Promise => { - const chunkSize = 8000; // Adjust the chunk size according to your requirements + const tileWidth = 1000; + const tileHeight = 1000; + const tileDataArray: Uint8ClampedArray[][] = []; // Two-dimensional array to store tile data - const chunkBlobs: Blob[] = []; // Array to hold the generated image chunk Blobs + const { width: canvasWidth, height: canvasHeight } = canvas; - console.log("canvas", canvas.width, canvas.height); + const ctx = canvas.getContext("2d"); - // Split the canvas into chunks and generate the image chunks - for (let x = 0; x < canvas.width; x += chunkSize) { - for (let y = 0; y < canvas.height; y += chunkSize) { - const chunkCanvas = document.createElement("canvas"); - chunkCanvas.width = chunkSize; - chunkCanvas.height = chunkSize; - const chunkContext = chunkCanvas.getContext("2d"); + if (!ctx) { + throw new Error("No canvas context"); + } - if (!chunkContext) { - throw new Error("Could not get context"); - } + // Function to process each tile + function processTile(tileX: number, tileY: number) { + // Calculate the starting and ending coordinates for the tile + const startX = tileX * tileWidth; + const startY = tileY * tileHeight; + const endX = Math.min(startX + tileWidth, canvasWidth); + const endY = Math.min(startY + tileHeight, canvasHeight); - // Copy the portion of the main canvas into the chunk canvas - chunkContext.drawImage( - canvas, - x, - y, - chunkSize, - chunkSize, - 0, - 0, - chunkSize, - chunkSize, - ); + // Get the image data for the tile directly from the main canvas + const imageData = ctx!.getImageData( + startX, + startY, + endX - startX, + endY - startY, + ).data; - console.log(x, y, chunkSize); + // Store the tile data in the two-dimensional array + tileDataArray[tileY] = tileDataArray[tileY] || []; + tileDataArray[tileY][tileX] = imageData; + } - // Convert the chunk canvas to a Blob - const blob = await _canvasToBlob(chunkCanvas); - chunkBlobs.push(blob); - - chunkCanvas.remove(); + // Iterate over the tiles and process each one + for (let tileY = 0; tileY < canvasHeight / tileHeight; tileY++) { + for (let tileX = 0; tileX < canvasWidth / tileWidth; tileX++) { + processTile(tileX, tileY); } } - // Convert each Blob into an ArrayBuffer and concatenate them - const arrayBuffers = await Promise.all( - chunkBlobs.map((blob) => { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => { - if (reader.result instanceof ArrayBuffer) { - resolve(reader.result); - } else { - throw new Error("Failed to read ArrayBuffer"); - } - }; - reader.readAsArrayBuffer(blob); - }); - }), - ); - const totalLength = arrayBuffers.reduce( - (length, buffer) => length + buffer.byteLength, - 0, - ); - const concatenatedBuffer = new Uint8Array(totalLength); - let offset = 0; - for (const buffer of arrayBuffers) { - concatenatedBuffer.set(new Uint8Array(buffer), offset); - offset += buffer.byteLength; + // Create a new PNG image with the final dimensions + const finalImage = new PNG({ width: canvasWidth, height: canvasHeight }); + + // Merge the tiles into the final image + for (let tileY = 0; tileY < canvasHeight / tileHeight; tileY++) { + for (let tileX = 0; tileX < canvasWidth / tileWidth; tileX++) { + const imageData = tileDataArray[tileY][tileX]; + const destX = tileX * tileWidth; + const destY = tileY * tileHeight; + for (let y = 0; y < tileHeight; y++) { + for (let x = 0; x < tileWidth; x++) { + const index = (y * tileWidth + x) * 4; + const destIndex = ((destY + y) * canvasWidth + destX + x) * 4; + finalImage.data[destIndex] = imageData[index]; + finalImage.data[destIndex + 1] = imageData[index + 1]; + finalImage.data[destIndex + 2] = imageData[index + 2]; + finalImage.data[destIndex + 3] = imageData[index + 3]; + } + } + } } - return new Blob([concatenatedBuffer], { type: "image/png" }); + const buffer = PNG.sync.write(finalImage); + + return new Blob([buffer], { type: "image/png" }); }; /** generates SHA-1 digest from supplied file (if not supported, falls back diff --git a/src/global.d.ts b/src/global.d.ts index 3a666e11a..feb8c2f46 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -120,3 +120,7 @@ declare module "image-blob-reduce" { const reduce: ImageBlobReduce.ImageBlobReduceStatic; export = reduce; } + +declare module "pngjs/browser" { + export { PNG } from "pngjs"; +} diff --git a/yarn.lock b/yarn.lock index 720e61a3e..2dbe1dea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2714,6 +2714,13 @@ resolved "https://registry.yarnpkg.com/@types/pica/-/pica-5.1.3.tgz#5ef64529a1f83f7d6586a8bf75a8a00be32aca02" integrity sha512-13SEyETRE5psd9bE0AmN+0M1tannde2fwHfLVaVIljkbL9V0OfFvKwCicyeDvVYLkmjQWEydbAlsDsmjrdyTOg== +"@types/pngjs@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.1.tgz#c711ec3fbbf077fed274ecccaf85dd4673130072" + integrity sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg== + dependencies: + "@types/node" "*" + "@types/prettier@^2.1.5": version "2.7.2" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" @@ -8227,6 +8234,11 @@ png-chunks-extract@1.0.0: dependencies: crc-32 "^0.3.0" +pngjs@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26" + integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow== + points-on-curve@0.2.0, points-on-curve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/points-on-curve/-/points-on-curve-0.2.0.tgz#7dbb98c43791859434284761330fa893cb81b4d1"