From 11fe3b6332d75f02a49e2fd425bfb16a10bf975d Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Sun, 29 Jun 2025 21:44:31 -0400 Subject: [PATCH] Comments --- invokeai/frontend/web/public/locales/en.json | 2 + .../RasterLayerExportPSDButton.tsx | 9 - .../hooks/useExportCanvasToPSD.ts | 211 ++++-------------- .../src/features/controlLayers/konva/util.ts | 1 + invokeai/frontend/web/src/types/ag-psd.d.ts | 24 ++ 5 files changed, 72 insertions(+), 175 deletions(-) create mode 100644 invokeai/frontend/web/src/types/ag-psd.d.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 13e14c0108..b15f7e243d 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1350,6 +1350,8 @@ "noRasterLayersDesc": "Create at least one raster layer to export to PSD", "noActiveRasterLayers": "No Active Raster Layers", "noActiveRasterLayersDesc": "Enable at least one raster layer to export to PSD", + "noVisibleRasterLayers": "No Visible Raster Layers", + "noVisibleRasterLayersDesc": "Enable at least one raster layer to export to PSD", "failedToProcessLayers": "Failed to Process Layers", "psdExportSuccess": "PSD Export Complete", "psdExportSuccessDesc": "Successfully exported {{count}} layers to PSD file", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerExportPSDButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerExportPSDButton.tsx index a32f151ffa..1a5aec96e5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerExportPSDButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerExportPSDButton.tsx @@ -1,8 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { useExportCanvasToPSD } from 'features/controlLayers/hooks/useExportCanvasToPSD'; -import { selectRasterLayerEntities } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiFileArrowDownBold } from 'react-icons/pi'; @@ -10,19 +8,12 @@ import { PiFileArrowDownBold } from 'react-icons/pi'; export const RasterLayerExportPSDButton = memo(() => { const { t } = useTranslation(); const isBusy = useCanvasIsBusy(); - const rasterLayers = useAppSelector(selectRasterLayerEntities); const { exportCanvasToPSD } = useExportCanvasToPSD(); const onClick = useCallback(() => { exportCanvasToPSD(); }, [exportCanvasToPSD]); - const hasActiveLayers = rasterLayers.some((layer) => layer.isEnabled); - - if (!hasActiveLayers) { - return null; - } - return ( { const { t } = useTranslation(); - const rasterLayers = useAppSelector(selectRasterLayerEntities); const canvasManager = useCanvasManagerSafe(); const exportCanvasToPSD = useCallback(async () => { @@ -26,123 +24,26 @@ export const useExportCanvasToPSD = () => { return; } - if (rasterLayers.length === 0) { + // Get visible raster layer adapters using the compositor module + const adapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer'); + + if (adapters.length === 0) { toast({ - id: 'NO_RASTER_LAYERS', - title: t('toast.noRasterLayers'), - description: t('toast.noRasterLayersDesc'), + id: 'NO_VISIBLE_RASTER_LAYERS', + title: t('toast.noVisibleRasterLayers'), + description: t('toast.noVisibleRasterLayersDesc'), status: 'warning', }); return; } - // Get active (enabled) raster layers only - const activeLayers = rasterLayers.filter((layer) => layer.isEnabled); + log.debug(`Exporting ${adapters.length} visible raster layers to PSD`); - if (activeLayers.length === 0) { - toast({ - id: 'NO_ACTIVE_RASTER_LAYERS', - title: t('toast.noActiveRasterLayers'), - description: t('toast.noActiveRasterLayersDesc'), - status: 'warning', - }); - return; - } - - log.debug(`Exporting ${activeLayers.length} active raster layers to PSD`); - - // Ensure all rect calculations are up to date before proceeding - const rectCalculationPromises = activeLayers.map(async (layer) => { - const adapter = canvasManager.getAdapter({ id: layer.id, type: 'raster_layer' }); - if (adapter && adapter.type === 'raster_layer_adapter') { - await adapter.transformer.requestRectCalculation(); - } - }); - - // Wait for all rect calculations to complete with a timeout - try { - await Promise.race([ - Promise.all(rectCalculationPromises), - new Promise((_, reject) => { - setTimeout(() => reject(new Error('Rect calculation timeout')), 5000); - }), - ]); - } catch (error) { - log.warn({ error: serializeError(error as Error) }, 'Rect calculation timeout or error, proceeding anyway'); - } - - // Find canvas dimensions by getting the maximum bounds of all layers - let maxRight = 0; - let maxBottom = 0; - let minLeft = Infinity; - let minTop = Infinity; - - // Get layer adapters and calculate bounds - const layerAdapters = activeLayers - .map((layer) => { - const adapter = canvasManager.getAdapter({ id: layer.id, type: 'raster_layer' }); - log.debug(`Layer "${layer.name}": adapter found = ${!!adapter}, type = ${adapter?.type}`); - - if (adapter && adapter.type === 'raster_layer_adapter') { - // Get the actual pixel bounds of the layer content from the transformer - const pixelRect = adapter.transformer.$pixelRect.get(); - const layerPosition = adapter.state.position; - - log.debug( - `Layer "${layer.name}": pixelRect=${JSON.stringify(pixelRect)}, position=${JSON.stringify(layerPosition)}` - ); - - // Alternative approach: use full canvas and adjust positioning - const canvas = adapter.getCanvas(); - const left = layerPosition.x; - const top = layerPosition.y; - const right = left + canvas.width; - const bottom = top + canvas.height; - - log.debug( - `Layer "${layer.name}": canvas size = ${canvas.width}x${canvas.height}, position = ${left},${top}` - ); - - // Skip layers with invalid canvas dimensions - if (canvas.width === 0 || canvas.height === 0) { - log.debug(`Layer "${layer.name}": skipping due to invalid canvas dimensions`); - return null; - } - - minLeft = Math.min(minLeft, left); - minTop = Math.min(minTop, top); - maxRight = Math.max(maxRight, right); - maxBottom = Math.max(maxBottom, bottom); - - // Temporarily remove the empty bounds filter to see what's happening - // if (pixelRect.width === 0 || pixelRect.height === 0) { - // log.debug(`Layer "${layer.name}": skipping due to empty bounds`); - // return null; - // } - - return { adapter, canvas, rect: { x: left, y: top, width: canvas.width, height: canvas.height }, layer }; - } - return null; - }) - .filter((item): item is NonNullable => item !== null); - - log.debug(`Found ${layerAdapters.length} valid layer adapters out of ${activeLayers.length} active layers`); - - if (layerAdapters.length === 0) { - toast({ - id: 'NO_VALID_LAYER_ADAPTERS', - title: t('toast.noValidLayerAdapters'), - status: 'error', - }); - return; - } - - // Default canvas size if no valid bounds found - const canvasWidth = maxRight > minLeft ? Math.ceil(maxRight - minLeft) : 1024; - const canvasHeight = maxBottom > minTop ? Math.ceil(maxBottom - minTop) : 1024; + // Get the union rect of all adapters using the compositor module + const visibleRect = canvasManager.compositor.getRectOfAdapters(adapters); // Validate canvas dimensions - if (canvasWidth <= 0 || canvasHeight <= 0) { + if (visibleRect.width <= 0 || visibleRect.height <= 0) { toast({ id: 'INVALID_CANVAS_DIMENSIONS', title: t('toast.invalidCanvasDimensions'), @@ -151,7 +52,7 @@ export const useExportCanvasToPSD = () => { return; } - if (canvasWidth > 30000 || canvasHeight > 30000) { + if (visibleRect.width > 30000 || visibleRect.height > 30000) { toast({ id: 'CANVAS_TOO_LARGE', title: t('toast.canvasTooLarge'), @@ -161,65 +62,50 @@ export const useExportCanvasToPSD = () => { return; } - log.debug(`PSD canvas dimensions: ${canvasWidth}x${canvasHeight}`); + log.debug(`PSD canvas dimensions: ${visibleRect.width}x${visibleRect.height}`); - // Create PSD layers from active raster layers + // Create PSD layers from visible raster layer adapters const psdLayers = await Promise.all( - layerAdapters.map((layerData, index) => { - try { - const { adapter: _adapter, canvas, rect, layer } = layerData; + adapters.map((adapter, index) => { + const layer = adapter.state; + const canvas = adapter.getCanvas(); + const layerPosition = adapter.state.position; - const layerDataPSD = { - name: layer.name || `Layer ${index + 1}`, - left: Math.floor(rect.x - minLeft), - top: Math.floor(rect.y - minTop), - right: Math.floor(rect.x - minLeft + rect.width), - bottom: Math.floor(rect.y - minTop + rect.height), - opacity: Math.floor(layer.opacity * 255), - hidden: false, - blendMode: 'normal' as const, - canvas: canvas, - }; + const layerDataPSD = { + name: layer.name || `Layer ${index + 1}`, + left: Math.floor(layerPosition.x - visibleRect.x), + top: Math.floor(layerPosition.y - visibleRect.y), + right: Math.floor(layerPosition.x - visibleRect.x + canvas.width), + bottom: Math.floor(layerPosition.y - visibleRect.y + canvas.height), + opacity: Math.floor(layer.opacity * 255), + hidden: false, + blendMode: 'normal' as const, + canvas: canvas, + }; - log.debug( - `Layer "${layerDataPSD.name}": ${layerDataPSD.left},${layerDataPSD.top} to ${layerDataPSD.right},${layerDataPSD.bottom}` - ); + log.debug( + `Layer "${layerDataPSD.name}": ${layerDataPSD.left},${layerDataPSD.top} to ${layerDataPSD.right},${layerDataPSD.bottom}` + ); - return layerDataPSD; - } catch (error) { - log.error({ error: serializeError(error as Error) }, `Error processing layer ${layerData.layer.name}`); - return null; - } + return layerDataPSD; }) ); - // Filter out any failed layers - const validLayers = psdLayers.filter((layer) => layer !== null); - - if (validLayers.length === 0) { - toast({ - id: 'FAILED_TO_PROCESS_LAYERS', - title: t('toast.failedToProcessLayers'), - status: 'error', - }); - return; - } - // Create PSD document const psd = { - width: canvasWidth, - height: canvasHeight, + width: visibleRect.width, + height: visibleRect.height, channels: 3, // RGB bitsPerChannel: 8, colorMode: 3, // RGB mode - children: validLayers, + children: psdLayers, }; log.debug( { - layerCount: validLayers.length, - canvasDimensions: { width: canvasWidth, height: canvasHeight }, - layers: validLayers.map((l) => ({ + layerCount: psdLayers.length, + canvasDimensions: { width: visibleRect.width, height: visibleRect.height }, + layers: psdLayers.map((l) => ({ name: l.name, bounds: { left: l.left, top: l.top, right: l.right, bottom: l.bottom }, })), @@ -230,22 +116,15 @@ export const useExportCanvasToPSD = () => { // Generate PSD file const buffer = writePsd(psd); - // Create blob and download + // Create blob and download using the utility function const blob = new Blob([buffer], { type: 'application/octet-stream' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = `canvas-layers-${new Date().toISOString().slice(0, 10)}.psd`; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(url); - document.body.removeChild(a); + const fileName = `canvas-layers-${new Date().toISOString().slice(0, 10)}.psd`; + downloadBlob(blob, fileName); toast({ id: 'PSD_EXPORT_SUCCESS', title: t('toast.psdExportSuccess'), - description: t('toast.psdExportSuccessDesc', { count: validLayers.length }), + description: t('toast.psdExportSuccessDesc', { count: psdLayers.length }), status: 'success', }); @@ -259,7 +138,7 @@ export const useExportCanvasToPSD = () => { status: 'error', }); } - }, [rasterLayers, canvasManager, t]); + }, [canvasManager, t]); return { exportCanvasToPSD }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index b4a7b56ae7..4bb4d06f7a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -321,6 +321,7 @@ export const downloadBlob = (blob: Blob, fileName: string) => { a.click(); document.body.removeChild(a); a.remove(); + URL.revokeObjectURL(url); }; /** diff --git a/invokeai/frontend/web/src/types/ag-psd.d.ts b/invokeai/frontend/web/src/types/ag-psd.d.ts new file mode 100644 index 0000000000..09e9ad430a --- /dev/null +++ b/invokeai/frontend/web/src/types/ag-psd.d.ts @@ -0,0 +1,24 @@ +declare module 'ag-psd' { + export interface PsdLayer { + name: string; + left: number; + top: number; + right: number; + bottom: number; + opacity: number; + hidden: boolean; + blendMode: 'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'colorDodge' | 'colorBurn' | 'hardLight' | 'softLight' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity'; + canvas: HTMLCanvasElement; + } + + export interface PsdDocument { + width: number; + height: number; + channels: number; + bitsPerChannel: number; + colorMode: number; + children: PsdLayer[]; + } + + export function writePsd(psd: PsdDocument): ArrayBuffer; +} \ No newline at end of file