mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 05:18:19 +00:00
Replace the image layer type with an Image node (#948)
* Use builder pattern for widgets * Arguments to new function * Add node graph when dragging in image * Fix duplicate import * Skip processing under node graph frame if unused * Reduce node graph rerenders * DUPLICATE ALL frontend changes into other frontend * DUPLICATE more changes to another frontend * Code review * Allow importing SVG files as bitmaps Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
66e8325362
commit
64e62699fc
32 changed files with 444 additions and 261 deletions
|
@ -228,7 +228,7 @@
|
|||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
import { textInputCleanup } from "@/utility-functions/keyboard-entry";
|
||||
import { rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
||||
import { extractPixelData, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
||||
import { type DisplayEditableTextbox, type MouseCursorIcon, type XY } from "@/wasm-communication/messages";
|
||||
|
||||
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@/components/floating-menus/EyedropperPreview.vue";
|
||||
|
@ -300,10 +300,9 @@ export default defineComponent({
|
|||
Array.from(dataTransfer.items).forEach(async (item) => {
|
||||
const file = item.getAsFile();
|
||||
if (file?.type.startsWith("image")) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const u8Array = new Uint8Array(buffer);
|
||||
const imageData = await extractPixelData(file);
|
||||
|
||||
this.editor.instance.pasteImage(file.type, u8Array, e.clientX, e.clientY);
|
||||
this.editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height, e.clientX, e.clientY);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@ import { type FullscreenState } from "@/state-providers/fullscreen";
|
|||
import { type PortfolioState } from "@/state-providers/portfolio";
|
||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@/utility-functions/keyboard-entry";
|
||||
import { platformIsMac } from "@/utility-functions/platform";
|
||||
import { extractPixelData } from "@/utility-functions/rasterization";
|
||||
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import { TriggerPaste } from "@/wasm-communication/messages";
|
||||
|
@ -270,10 +271,8 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
|||
|
||||
const file = item.getAsFile();
|
||||
if (file?.type.startsWith("image")) {
|
||||
file.arrayBuffer().then((buffer): void => {
|
||||
const u8Array = new Uint8Array(buffer);
|
||||
|
||||
editor.instance.pasteImage(file.type, u8Array);
|
||||
extractPixelData(file).then((imageData): void => {
|
||||
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -317,10 +316,11 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
|||
if (imageType) {
|
||||
const blob = await item.getType(imageType);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (): void => {
|
||||
const u8Array = new Uint8Array(reader.result as ArrayBuffer);
|
||||
|
||||
editor.instance.pasteImage(imageType, u8Array);
|
||||
reader.onload = async (): Promise<void> => {
|
||||
if (reader.result instanceof ArrayBuffer) {
|
||||
const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
|
||||
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { reactive, readonly } from "vue";
|
|||
|
||||
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
|
||||
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, updateBackendImage } from "@/utility-functions/imaginate";
|
||||
import { rasterizeSVG, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
||||
import { extractPixelData, rasterizeSVG, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import {
|
||||
type FrontendDocumentDetails,
|
||||
|
@ -45,7 +45,8 @@ export function createPortfolioState(editor: Editor) {
|
|||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
||||
const data = await upload("image/*", "data");
|
||||
editor.instance.pasteImage(data.type, Uint8Array.from(data.content));
|
||||
const imageData = await extractPixelData(new Blob([data.content], { type: data.type }));
|
||||
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFileDownload, (triggerFileDownload) => {
|
||||
downloadFileText(triggerFileDownload.name, triggerFileDownload.document);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { replaceBlobURLsWithBase64 } from "@/utility-functions/files";
|
||||
|
||||
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
|
||||
// Rasterize the string of an SVG document at a given width and height and return the canvas it was drawn onto during the rasterization process
|
||||
export async function rasterizeSVGCanvas(svg: string, width: number, height: number, backgroundColor?: string): Promise<HTMLCanvasElement> {
|
||||
// A canvas to render our SVG to in order to get a raster image
|
||||
const canvas = document.createElement("canvas");
|
||||
|
@ -22,6 +22,7 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
|
|||
const svgBlob = new Blob([svgWithBase64Images], { type: "image/svg+xml;charset=utf-8" });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
// Load the Image from the URL and wait until it's done
|
||||
const image = new Image();
|
||||
image.src = url;
|
||||
await new Promise<void>((resolve) => {
|
||||
|
@ -37,6 +38,7 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
|
|||
return canvas;
|
||||
}
|
||||
|
||||
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
|
||||
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
||||
const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);
|
||||
|
||||
|
@ -51,3 +53,49 @@ export async function rasterizeSVG(svg: string, width: number, height: number, m
|
|||
|
||||
return blob;
|
||||
}
|
||||
|
||||
/// Convert an image source (e.g. PNG document) into pixel data, a width and a height
|
||||
export async function extractPixelData(imageData: ImageBitmapSource): Promise<ImageData> {
|
||||
// Special handling to rasterize an SVG file
|
||||
let svgImageData;
|
||||
if (imageData instanceof File && imageData.type === "image/svg+xml") {
|
||||
const svgSource = await imageData.text();
|
||||
const svgElement = new DOMParser().parseFromString(svgSource, "image/svg+xml").querySelector("svg");
|
||||
if (!svgElement) throw new Error("Error reading SVG file");
|
||||
|
||||
let bounds = svgElement.viewBox.baseVal;
|
||||
|
||||
// If the bounds are zero (which will happen if the `viewBox` is not provided), set bounds to the artwork's bounding box
|
||||
if (bounds.width === 0 || bounds.height === 0) {
|
||||
// It's necessary to measure while the element is in the DOM, otherwise the dimensions are zero
|
||||
const toRemove = document.body.insertAdjacentElement("beforeend", svgElement);
|
||||
bounds = svgElement.getBBox();
|
||||
toRemove?.remove();
|
||||
}
|
||||
|
||||
svgImageData = await rasterizeSVGCanvas(svgSource, bounds.width, bounds.height);
|
||||
}
|
||||
|
||||
// Decode the image file binary data
|
||||
const image = await createImageBitmap(svgImageData || imageData);
|
||||
|
||||
// Halve the image size until the editor lag is somewhat usable
|
||||
// TODO: Fix lag so this can be removed
|
||||
const MAX_IMAGE_SIZE = 512;
|
||||
let { width, height } = image;
|
||||
while (width > MAX_IMAGE_SIZE || height > MAX_IMAGE_SIZE) {
|
||||
width /= 2;
|
||||
height /= 2;
|
||||
}
|
||||
width = Math.floor(width);
|
||||
height = Math.floor(height);
|
||||
|
||||
// Render image to canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) throw new Error("Could not create canvas context");
|
||||
context.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
|
||||
return context.getImageData(0, 0, width, height);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue