Skip to content

Commit 9351f5c

Browse files
committed
Fix base64 image import bug in extracted area in InpaintMaskMenuItemsExtractMaskedArea.tsx and removed unused locales entries in en.json
1 parent 01edf81 commit 9351f5c

File tree

2 files changed

+120
-116
lines changed

2 files changed

+120
-116
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2105,9 +2105,6 @@
21052105
"newSession": "New Session",
21062106
"clearCaches": "Clear Caches",
21072107
"recalculateRects": "Recalculate Rects",
2108-
"canvasIsEmpty": "Canvas is empty",
2109-
"extractMaskedAreaError": "Unable to extract masked area",
2110-
"extractMaskedAreaDataMissing": "Cannot extract: image or mask data is missing.",
21112108
"clipToBbox": "Clip Strokes to Bbox",
21122109
"extractRegion": "Extract Region",
21132110
"outputOnlyMaskedRegions": "Output Only Generated Regions",

invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsExtractMaskedArea.tsx

Lines changed: 120 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { logger } from 'app/logging/logger';
33
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
44
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
55
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
6-
import { canvasToImageData, getPrefixedId } from 'features/controlLayers/konva/util';
6+
import { canvasToBlob, canvasToImageData } from 'features/controlLayers/konva/util';
77
import type { CanvasImageState, Rect } from 'features/controlLayers/store/types';
8+
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
89
import { toast } from 'features/toast/toast';
910
import { memo, useCallback } from 'react';
1011
import { useTranslation } from 'react-i18next';
1112
import { PiSelectionBackgroundBold } from 'react-icons/pi';
1213
import { serializeError } from 'serialize-error';
14+
import { uploadImage } from 'services/api/endpoints/images';
1315

1416
const log = logger('canvas');
1517

@@ -20,123 +22,128 @@ export const InpaintMaskMenuItemsExtractMaskedArea = memo(() => {
2022
const { t } = useTranslation();
2123

2224
const onExtract = useCallback(() => {
23-
// The active inpaint mask layer is required to build the mask used for extraction.
24-
const maskAdapter = canvasManager.getAdapter(entityIdentifier);
25-
if (!maskAdapter) {
26-
log.error({ entityIdentifier }, 'Inpaint mask adapter not found when extracting masked area');
27-
toast({ status: 'error', title: t('controlLayers.extractMaskedAreaError') });
28-
return;
29-
}
30-
31-
try {
32-
// Use the mask's bounding box in stage coordinates to constrain the extraction region.
33-
const maskPixelRect = maskAdapter.transformer.$pixelRect.get();
34-
const maskPosition = maskAdapter.state.position;
35-
const rect: Rect = {
36-
x: Math.floor(maskPosition.x + maskPixelRect.x),
37-
y: Math.floor(maskPosition.y + maskPixelRect.y),
38-
width: Math.floor(maskPixelRect.width),
39-
height: Math.floor(maskPixelRect.height),
40-
};
41-
42-
// Abort when the canvas is effectively empty—no pixels to extract.
43-
if (rect.width <= 0 || rect.height <= 0) {
44-
toast({ status: 'warning', title: t('controlLayers.canvasIsEmpty') });
25+
void (async () => {
26+
// The active inpaint mask layer is required to build the mask used for extraction.
27+
const maskAdapter = canvasManager.getAdapter(entityIdentifier);
28+
if (!maskAdapter) {
29+
log.error({ entityIdentifier }, 'Inpaint mask adapter not found when extracting masked area');
30+
toast({ status: 'error', title: 'Unable to extract masked area.' });
4531
return;
4632
}
4733

48-
// Gather the visible raster layer adapters so we can composite them into a single bitmap.
49-
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
50-
51-
let compositeImageData: ImageData;
52-
if (rasterAdapters.length === 0) {
53-
// No visible raster layers—create a transparent buffer that matches the canvas bounds.
54-
compositeImageData = new ImageData(rect.width, rect.height);
55-
} else {
56-
// Render the visible raster layers into an offscreen canvas restricted to the canvas bounds.
57-
const compositeCanvas = canvasManager.compositor.getCompositeCanvas(rasterAdapters, rect);
58-
compositeImageData = canvasToImageData(compositeCanvas);
59-
}
60-
61-
// Render the inpaint mask layer into a canvas so we have the alpha data that defines the mask.
62-
const maskCanvas = maskAdapter.getCanvas(rect);
63-
const maskImageData = canvasToImageData(maskCanvas);
64-
65-
if (
66-
maskImageData.width !== compositeImageData.width ||
67-
maskImageData.height !== compositeImageData.height
68-
) {
69-
// Bail out if the mask and composite buffers disagree on dimensions.
70-
log.error(
71-
{
72-
maskDimensions: { width: maskImageData.width, height: maskImageData.height },
73-
compositeDimensions: { width: compositeImageData.width, height: compositeImageData.height },
34+
try {
35+
// Use the mask's bounding box in stage coordinates to constrain the extraction region.
36+
const maskPixelRect = maskAdapter.transformer.$pixelRect.get();
37+
const maskPosition = maskAdapter.state.position;
38+
const rect: Rect = {
39+
x: Math.floor(maskPosition.x + maskPixelRect.x),
40+
y: Math.floor(maskPosition.y + maskPixelRect.y),
41+
width: Math.floor(maskPixelRect.width),
42+
height: Math.floor(maskPixelRect.height),
43+
};
44+
45+
// Abort when the canvas is effectively empty—no pixels to extract.
46+
if (rect.width <= 0 || rect.height <= 0) {
47+
toast({ status: 'warning', title: 'Canvas is empty.' });
48+
return;
49+
}
50+
51+
// Gather the visible raster layer adapters so we can composite them into a single bitmap.
52+
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
53+
54+
let compositeImageData: ImageData;
55+
if (rasterAdapters.length === 0) {
56+
// No visible raster layers—create a transparent buffer that matches the canvas bounds.
57+
compositeImageData = new ImageData(rect.width, rect.height);
58+
} else {
59+
// Render the visible raster layers into an offscreen canvas restricted to the canvas bounds.
60+
const compositeCanvas = canvasManager.compositor.getCompositeCanvas(rasterAdapters, rect);
61+
compositeImageData = canvasToImageData(compositeCanvas);
62+
}
63+
64+
// Render the inpaint mask layer into a canvas so we have the alpha data that defines the mask.
65+
const maskCanvas = maskAdapter.getCanvas(rect);
66+
const maskImageData = canvasToImageData(maskCanvas);
67+
68+
if (
69+
maskImageData.width !== compositeImageData.width ||
70+
maskImageData.height !== compositeImageData.height
71+
) {
72+
// Bail out if the mask and composite buffers disagree on dimensions.
73+
log.error(
74+
{
75+
maskDimensions: { width: maskImageData.width, height: maskImageData.height },
76+
compositeDimensions: { width: compositeImageData.width, height: compositeImageData.height },
77+
},
78+
'Mask and composite dimensions did not match when extracting masked area'
79+
);
80+
toast({ status: 'error', title: 'Unable to extract masked area.' });
81+
return;
82+
}
83+
84+
const compositeArray = compositeImageData.data;
85+
const maskArray = maskImageData.data;
86+
87+
if (!compositeArray || !maskArray) {
88+
toast({ status: 'error', title: 'Cannot extract: image or mask data is missing.' });
89+
return;
90+
}
91+
92+
const outputArray = new Uint8ClampedArray(compositeArray.length);
93+
94+
// Apply the mask alpha channel to each pixel in the composite, keeping RGB untouched and only masking alpha.
95+
for (let i = 0; i < compositeArray.length; i += 4) {
96+
const maskAlpha = ((maskArray[i + 3] ?? 0) / 255) || 0;
97+
outputArray[i] = compositeArray[i] ?? 0;
98+
outputArray[i + 1] = compositeArray[i + 1] ?? 0;
99+
outputArray[i + 2] = compositeArray[i + 2] ?? 0;
100+
outputArray[i + 3] = Math.round((compositeArray[i + 3] ?? 0) * maskAlpha);
101+
}
102+
103+
// Package the masked pixels into an ImageData and draw them to an offscreen canvas.
104+
const outputImageData = new ImageData(outputArray, rect.width, rect.height);
105+
const outputCanvas = document.createElement('canvas');
106+
outputCanvas.width = rect.width;
107+
outputCanvas.height = rect.height;
108+
const outputContext = outputCanvas.getContext('2d');
109+
110+
if (!outputContext) {
111+
throw new Error('Failed to create canvas context for masked extraction');
112+
}
113+
114+
outputContext.putImageData(outputImageData, 0, 0);
115+
116+
// Upload the extracted canvas region as a real image resource and returns image_name
117+
118+
const blob = await canvasToBlob(outputCanvas);
119+
120+
const imageDTO = await uploadImage({
121+
file: new File([blob], 'inpaint-extract.png', { type: 'image/png' }),
122+
image_category: 'general',
123+
is_intermediate: true,
124+
silent: true,
125+
});
126+
127+
// Convert the uploaded image DTO into the canvas image state to avoid serializing the PNG in client state.
128+
const imageState: CanvasImageState = imageDTOToImageObject(imageDTO);
129+
130+
// Insert the new raster layer just after the last existing raster layer so it appears above the mask.
131+
const addAfter = canvasManager.stateApi.getRasterLayersState().entities.at(-1)?.id;
132+
133+
canvasManager.stateApi.addRasterLayer({
134+
overrides: {
135+
objects: [imageState],
136+
position: { x: rect.x, y: rect.y },
74137
},
75-
'Mask and composite dimensions did not match when extracting masked area'
76-
);
77-
toast({ status: 'error', title: t('controlLayers.extractMaskedAreaError') });
78-
return;
79-
}
80-
81-
const compositeArray = compositeImageData.data;
82-
const maskArray = maskImageData.data;
83-
84-
if (!compositeArray || !maskArray) {
85-
toast({ status: 'error', title: t('controlLayers.extractMaskedAreaDataMissing') });
86-
return;
138+
isSelected: true,
139+
addAfter,
140+
});
141+
} catch (error) {
142+
log.error({ error: serializeError(error as Error) }, 'Failed to extract masked area to raster layer');
143+
toast({ status: 'error', title: 'Unable to extract masked area.' });
87144
}
88-
89-
const outputArray = new Uint8ClampedArray(compositeArray.length);
90-
91-
// Apply the mask alpha channel to each pixel in the composite, keeping RGB untouched and only masking alpha.
92-
for (let i = 0; i < compositeArray.length; i += 4) {
93-
const maskAlpha = ((maskArray[i + 3] ?? 0) / 255) || 0;
94-
outputArray[i] = compositeArray[i] ?? 0;
95-
outputArray[i + 1] = compositeArray[i + 1] ?? 0;
96-
outputArray[i + 2] = compositeArray[i + 2] ?? 0;
97-
outputArray[i + 3] = Math.round((compositeArray[i + 3] ?? 0) * maskAlpha);
98-
}
99-
100-
// Package the masked pixels into an ImageData and draw them to an offscreen canvas.
101-
const outputImageData = new ImageData(outputArray, rect.width, rect.height);
102-
const outputCanvas = document.createElement('canvas');
103-
outputCanvas.width = rect.width;
104-
outputCanvas.height = rect.height;
105-
const outputContext = outputCanvas.getContext('2d');
106-
107-
if (!outputContext) {
108-
throw new Error('Failed to create canvas context for masked extraction');
109-
}
110-
111-
outputContext.putImageData(outputImageData, 0, 0);
112-
113-
// Convert the offscreen canvas into an Invoke canvas image state for insertion into the layer stack.
114-
const imageState: CanvasImageState = {
115-
id: getPrefixedId('image'),
116-
type: 'image',
117-
image: {
118-
dataURL: outputCanvas.toDataURL('image/png'),
119-
width: rect.width,
120-
height: rect.height,
121-
},
122-
};
123-
124-
// Insert the new raster layer just after the last existing raster layer so it appears above the mask.
125-
const addAfter = canvasManager.stateApi.getRasterLayersState().entities.at(-1)?.id;
126-
127-
canvasManager.stateApi.addRasterLayer({
128-
overrides: {
129-
objects: [imageState],
130-
position: { x: rect.x, y: rect.y },
131-
},
132-
isSelected: true,
133-
addAfter,
134-
});
135-
} catch (error) {
136-
log.error({ error: serializeError(error as Error) }, 'Failed to extract masked area to raster layer');
137-
toast({ status: 'error', title: t('controlLayers.extractMaskedAreaError') });
138-
}
139-
}, [canvasManager, entityIdentifier, t]);
145+
})();
146+
}, [canvasManager, entityIdentifier]);
140147

141148
return (
142149
<MenuItem onClick={onExtract} icon={<PiSelectionBackgroundBold />} isDisabled={isBusy}>

0 commit comments

Comments
 (0)