mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 21:25:04 -05:00
Comments
This commit is contained in:
committed by
psychedelicious
parent
e4aae1a591
commit
11fe3b6332
@@ -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 (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { writePsd } from 'ag-psd';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectRasterLayerEntities } from 'features/controlLayers/store/selectors';
|
||||
import { downloadBlob } from 'features/controlLayers/konva/util';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -12,7 +11,6 @@ const log = logger('canvas');
|
||||
|
||||
export const useExportCanvasToPSD = () => {
|
||||
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<typeof item> => 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 };
|
||||
};
|
||||
|
||||
@@ -321,6 +321,7 @@ export const downloadBlob = (blob: Blob, fileName: string) => {
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
24
invokeai/frontend/web/src/types/ag-psd.d.ts
vendored
Normal file
24
invokeai/frontend/web/src/types/ag-psd.d.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user