mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): restore save/copy/download/merge functionality
This commit is contained in:
parent
d95fe5925a
commit
d2c9140e69
@ -15,6 +15,10 @@ import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
|
|||||||
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
|
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
|
||||||
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
|
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
|
||||||
import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage';
|
import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage';
|
||||||
|
import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGallery';
|
||||||
|
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
|
||||||
|
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
|
||||||
|
import { addCanvasMergedListener } from './listeners/canvasMerged';
|
||||||
|
|
||||||
export const listenerMiddleware = createListenerMiddleware();
|
export const listenerMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
@ -43,3 +47,8 @@ addUserInvokedCanvasListener();
|
|||||||
addUserInvokedNodesListener();
|
addUserInvokedNodesListener();
|
||||||
addUserInvokedTextToImageListener();
|
addUserInvokedTextToImageListener();
|
||||||
addUserInvokedImageToImageListener();
|
addUserInvokedImageToImageListener();
|
||||||
|
|
||||||
|
addCanvasSavedToGalleryListener();
|
||||||
|
addCanvasDownloadedAsImageListener();
|
||||||
|
addCanvasCopiedToClipboardListener();
|
||||||
|
addCanvasMergedListener();
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
|
||||||
|
import { startAppListening } from '..';
|
||||||
|
import { log } from 'app/logging/useLogger';
|
||||||
|
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||||
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
import { copyBlobToClipboard } from 'features/canvas/util/copyBlobToClipboard';
|
||||||
|
|
||||||
|
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
|
||||||
|
|
||||||
|
export const addCanvasCopiedToClipboardListener = () => {
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: canvasCopiedToClipboard,
|
||||||
|
effect: async (action, { dispatch, getState }) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const blob = await getBaseLayerBlob(state);
|
||||||
|
|
||||||
|
if (!blob) {
|
||||||
|
moduleLog.error('Problem getting base layer blob');
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title: 'Problem Copying Canvas',
|
||||||
|
description: 'Unable to export base layer',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyBlobToClipboard(blob);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,33 @@
|
|||||||
|
import { canvasDownloadedAsImage } from 'features/canvas/store/actions';
|
||||||
|
import { startAppListening } from '..';
|
||||||
|
import { log } from 'app/logging/useLogger';
|
||||||
|
import { downloadBlob } from 'features/canvas/util/downloadBlob';
|
||||||
|
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||||
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
|
||||||
|
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||||
|
|
||||||
|
export const addCanvasDownloadedAsImageListener = () => {
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: canvasDownloadedAsImage,
|
||||||
|
effect: async (action, { dispatch, getState }) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const blob = await getBaseLayerBlob(state);
|
||||||
|
|
||||||
|
if (!blob) {
|
||||||
|
moduleLog.error('Problem getting base layer blob');
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title: 'Problem Downloading Canvas',
|
||||||
|
description: 'Unable to export base layer',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBlob(blob, 'mergedCanvas.png');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,88 @@
|
|||||||
|
import { canvasMerged } from 'features/canvas/store/actions';
|
||||||
|
import { startAppListening } from '..';
|
||||||
|
import { log } from 'app/logging/useLogger';
|
||||||
|
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||||
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
import { imageUploaded } from 'services/thunks/image';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||||
|
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
|
|
||||||
|
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
|
||||||
|
|
||||||
|
export const addCanvasMergedListener = () => {
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: canvasMerged,
|
||||||
|
effect: async (action, { dispatch, getState, take }) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const blob = await getBaseLayerBlob(state, true);
|
||||||
|
|
||||||
|
if (!blob) {
|
||||||
|
moduleLog.error('Problem getting base layer blob');
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title: 'Problem Merging Canvas',
|
||||||
|
description: 'Unable to export base layer',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
|
if (!canvasBaseLayer) {
|
||||||
|
moduleLog.error('Problem getting canvas base layer');
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title: 'Problem Merging Canvas',
|
||||||
|
description: 'Unable to export base layer',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseLayerRect = canvasBaseLayer.getClientRect({
|
||||||
|
relativeTo: canvasBaseLayer.getParent(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const filename = `mergedCanvas_${uuidv4()}.png`;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
imageUploaded({
|
||||||
|
imageType: 'intermediates',
|
||||||
|
formData: {
|
||||||
|
file: new File([blob], filename, { type: 'image/png' }),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const [{ payload }] = await take(
|
||||||
|
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
|
||||||
|
imageUploaded.fulfilled.match(action) &&
|
||||||
|
action.meta.arg.formData.file.name === filename
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedCanvasImage = deserializeImageResponse(payload.response);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setMergedCanvas({
|
||||||
|
kind: 'image',
|
||||||
|
layer: 'base',
|
||||||
|
image: mergedCanvasImage,
|
||||||
|
...baseLayerRect,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title: 'Canvas Merged',
|
||||||
|
status: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
import { canvasSavedToGallery } from 'features/canvas/store/actions';
|
||||||
|
import { startAppListening } from '..';
|
||||||
|
import { log } from 'app/logging/useLogger';
|
||||||
|
import { imageUploaded } from 'services/thunks/image';
|
||||||
|
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||||
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
|
||||||
|
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||||
|
|
||||||
|
export const addCanvasSavedToGalleryListener = () => {
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: canvasSavedToGallery,
|
||||||
|
effect: async (action, { dispatch, getState }) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const blob = await getBaseLayerBlob(state);
|
||||||
|
|
||||||
|
if (!blob) {
|
||||||
|
moduleLog.error('Problem getting base layer blob');
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title: 'Problem Saving Canvas',
|
||||||
|
description: 'Unable to export base layer',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
imageUploaded({
|
||||||
|
imageType: 'results',
|
||||||
|
formData: {
|
||||||
|
file: new File([blob], 'mergedCanvas.png', { type: 'image/png' }),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -6,6 +6,7 @@ import { imageUploaded } from 'services/thunks/image';
|
|||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { resultAdded } from 'features/gallery/store/resultsSlice';
|
||||||
|
|
||||||
export const addImageUploadedListener = () => {
|
export const addImageUploadedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
@ -14,24 +15,31 @@ export const addImageUploadedListener = () => {
|
|||||||
action.payload.response.image_type !== 'intermediates',
|
action.payload.response.image_type !== 'intermediates',
|
||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
const { response } = action.payload;
|
const { response } = action.payload;
|
||||||
|
const { imageType } = action.meta.arg;
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const image = deserializeImageResponse(response);
|
const image = deserializeImageResponse(response);
|
||||||
|
|
||||||
dispatch(uploadAdded(image));
|
if (imageType === 'uploads') {
|
||||||
|
dispatch(uploadAdded(image));
|
||||||
|
|
||||||
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
||||||
|
|
||||||
if (state.gallery.shouldAutoSwitchToNewImages) {
|
if (state.gallery.shouldAutoSwitchToNewImages) {
|
||||||
dispatch(imageSelected(image));
|
dispatch(imageSelected(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.meta.arg.activeTabName === 'img2img') {
|
||||||
|
dispatch(initialImageSelected(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.meta.arg.activeTabName === 'unifiedCanvas') {
|
||||||
|
dispatch(setInitialCanvasImage(image));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.meta.arg.activeTabName === 'img2img') {
|
if (imageType === 'results') {
|
||||||
dispatch(initialImageSelected(image));
|
dispatch(resultAdded(image));
|
||||||
}
|
|
||||||
|
|
||||||
if (action.meta.arg.activeTabName === 'unifiedCanvas') {
|
|
||||||
dispatch(setInitialCanvasImage(image));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
CanvasState,
|
CanvasState,
|
||||||
isCanvasMaskLine,
|
isCanvasMaskLine,
|
||||||
} from 'features/canvas/store/canvasTypes';
|
} from 'features/canvas/store/canvasTypes';
|
||||||
import generateMask from 'features/canvas/util/generateMask';
|
import createMaskStage from 'features/canvas/util/generateMask';
|
||||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
import type {
|
import type {
|
||||||
FacetoolType,
|
FacetoolType,
|
||||||
@ -257,7 +257,7 @@ export const frontendToBackendParameters = (
|
|||||||
...boundingBoxDimensions,
|
...boundingBoxDimensions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { dataURL: maskDataURL, imageData: maskImageData } = generateMask(
|
const { dataURL: maskDataURL, imageData: maskImageData } = createMaskStage(
|
||||||
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
|
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
|
||||||
boundingBox
|
boundingBox
|
||||||
);
|
);
|
||||||
|
@ -44,6 +44,12 @@ import IAICanvasRedoButton from './IAICanvasRedoButton';
|
|||||||
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
||||||
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
|
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
|
||||||
import IAICanvasUndoButton from './IAICanvasUndoButton';
|
import IAICanvasUndoButton from './IAICanvasUndoButton';
|
||||||
|
import {
|
||||||
|
canvasCopiedToClipboard,
|
||||||
|
canvasDownloadedAsImage,
|
||||||
|
canvasMerged,
|
||||||
|
canvasSavedToGallery,
|
||||||
|
} from 'features/canvas/store/actions';
|
||||||
|
|
||||||
export const selector = createSelector(
|
export const selector = createSelector(
|
||||||
[systemSelector, canvasSelector, isStagingSelector],
|
[systemSelector, canvasSelector, isStagingSelector],
|
||||||
@ -70,14 +76,8 @@ export const selector = createSelector(
|
|||||||
|
|
||||||
const IAICanvasToolbar = () => {
|
const IAICanvasToolbar = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const {
|
const { isProcessing, isStaging, isMaskEnabled, layer, tool } =
|
||||||
isProcessing,
|
useAppSelector(selector);
|
||||||
isStaging,
|
|
||||||
isMaskEnabled,
|
|
||||||
layer,
|
|
||||||
tool,
|
|
||||||
shouldCropToBoundingBoxOnSave,
|
|
||||||
} = useAppSelector(selector);
|
|
||||||
const canvasBaseLayer = getCanvasBaseLayer();
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -183,42 +183,19 @@ const IAICanvasToolbar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMergeVisible = () => {
|
const handleMergeVisible = () => {
|
||||||
dispatch(
|
dispatch(canvasMerged());
|
||||||
mergeAndUploadCanvas({
|
|
||||||
cropVisible: false,
|
|
||||||
shouldSetAsInitialImage: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveToGallery = () => {
|
const handleSaveToGallery = () => {
|
||||||
dispatch(
|
dispatch(canvasSavedToGallery());
|
||||||
mergeAndUploadCanvas({
|
|
||||||
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
|
||||||
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
|
||||||
shouldSaveToGallery: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyImageToClipboard = () => {
|
const handleCopyImageToClipboard = () => {
|
||||||
dispatch(
|
dispatch(canvasCopiedToClipboard());
|
||||||
mergeAndUploadCanvas({
|
|
||||||
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
|
||||||
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
|
||||||
shouldCopy: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadAsImage = () => {
|
const handleDownloadAsImage = () => {
|
||||||
dispatch(
|
dispatch(canvasDownloadedAsImage());
|
||||||
mergeAndUploadCanvas({
|
|
||||||
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
|
||||||
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
|
||||||
shouldDownload: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeLayer = (e: ChangeEvent<HTMLSelectElement>) => {
|
const handleChangeLayer = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
13
invokeai/frontend/web/src/features/canvas/store/actions.ts
Normal file
13
invokeai/frontend/web/src/features/canvas/store/actions.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
|
||||||
|
|
||||||
|
export const canvasCopiedToClipboard = createAction(
|
||||||
|
'canvas/canvasCopiedToClipboard'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const canvasDownloadedAsImage = createAction(
|
||||||
|
'canvas/canvasDownloadedAsImage'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const canvasMerged = createAction('canvas/canvasMerged');
|
@ -0,0 +1,9 @@
|
|||||||
|
export const blobToDataURL = (blob: Blob): Promise<string> => {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (_e) => resolve(reader.result as string);
|
||||||
|
reader.onerror = (_e) => reject(reader.error);
|
||||||
|
reader.onabort = (_e) => reject(new Error('Read aborted'));
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Copies a blob to the clipboard by calling navigator.clipboard.write().
|
||||||
|
*/
|
||||||
|
export const copyBlobToClipboard = (blob: Blob) => {
|
||||||
|
navigator.clipboard.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
[blob.type]: blob,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
import { CanvasMaskLine } from 'features/canvas/store/canvasTypes';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { IRect } from 'konva/lib/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a stage from array of mask objects.
|
||||||
|
* We cannot just convert the mask layer to a blob because it uses a texture with transparent areas.
|
||||||
|
* So instead we create a new stage with the mask layer and composite it onto a white background.
|
||||||
|
*/
|
||||||
|
const createMaskStage = async (
|
||||||
|
lines: CanvasMaskLine[],
|
||||||
|
boundingBox: IRect
|
||||||
|
): Promise<Konva.Stage> => {
|
||||||
|
// create an offscreen canvas and add the mask to it
|
||||||
|
const { width, height } = boundingBox;
|
||||||
|
|
||||||
|
const offscreenContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const maskStage = new Konva.Stage({
|
||||||
|
container: offscreenContainer,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseLayer = new Konva.Layer();
|
||||||
|
const maskLayer = new Konva.Layer();
|
||||||
|
|
||||||
|
// composite the image onto the mask layer
|
||||||
|
baseLayer.add(
|
||||||
|
new Konva.Rect({
|
||||||
|
...boundingBox,
|
||||||
|
fill: 'white',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
lines.forEach((line) =>
|
||||||
|
maskLayer.add(
|
||||||
|
new Konva.Line({
|
||||||
|
points: line.points,
|
||||||
|
stroke: 'black',
|
||||||
|
strokeWidth: line.strokeWidth * 2,
|
||||||
|
tension: 0,
|
||||||
|
lineCap: 'round',
|
||||||
|
lineJoin: 'round',
|
||||||
|
shadowForStrokeEnabled: false,
|
||||||
|
globalCompositeOperation:
|
||||||
|
line.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
maskStage.add(baseLayer);
|
||||||
|
maskStage.add(maskLayer);
|
||||||
|
|
||||||
|
// you'd think we can't do this until we finish with the maskStage, but we can
|
||||||
|
offscreenContainer.remove();
|
||||||
|
|
||||||
|
return maskStage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createMaskStage;
|
@ -0,0 +1,11 @@
|
|||||||
|
/** Download a blob as a file */
|
||||||
|
export const downloadBlob = (blob: Blob, fileName: string) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
a.remove();
|
||||||
|
};
|
@ -1,170 +0,0 @@
|
|||||||
// import { CanvasMaskLine } from 'features/canvas/store/canvasTypes';
|
|
||||||
// import Konva from 'konva';
|
|
||||||
// import { Stage } from 'konva/lib/Stage';
|
|
||||||
// import { IRect } from 'konva/lib/types';
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Generating a mask image from InpaintingCanvas.tsx is not as simple
|
|
||||||
// * as calling toDataURL() on the canvas, because the mask may be represented
|
|
||||||
// * by colored lines or transparency, or the user may have inverted the mask
|
|
||||||
// * display.
|
|
||||||
// *
|
|
||||||
// * So we need to regenerate the mask image by creating an offscreen canvas,
|
|
||||||
// * drawing the mask and compositing everything correctly to output a valid
|
|
||||||
// * mask image.
|
|
||||||
// */
|
|
||||||
// export const getStageDataURL = (stage: Stage, boundingBox: IRect): string => {
|
|
||||||
// // create an offscreen canvas and add the mask to it
|
|
||||||
// // const { stage, offscreenContainer } = buildMaskStage(lines, boundingBox);
|
|
||||||
|
|
||||||
// const dataURL = stage.toDataURL({ ...boundingBox });
|
|
||||||
|
|
||||||
// // const imageData = stage
|
|
||||||
// // .toCanvas()
|
|
||||||
// // .getContext('2d')
|
|
||||||
// // ?.getImageData(
|
|
||||||
// // boundingBox.x,
|
|
||||||
// // boundingBox.y,
|
|
||||||
// // boundingBox.width,
|
|
||||||
// // boundingBox.height
|
|
||||||
// // );
|
|
||||||
|
|
||||||
// // offscreenContainer.remove();
|
|
||||||
|
|
||||||
// // return { dataURL, imageData };
|
|
||||||
|
|
||||||
// return dataURL;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export const getStageImageData = (
|
|
||||||
// stage: Stage,
|
|
||||||
// boundingBox: IRect
|
|
||||||
// ): ImageData | undefined => {
|
|
||||||
// const imageData = stage
|
|
||||||
// .toCanvas()
|
|
||||||
// .getContext('2d')
|
|
||||||
// ?.getImageData(
|
|
||||||
// boundingBox.x,
|
|
||||||
// boundingBox.y,
|
|
||||||
// boundingBox.width,
|
|
||||||
// boundingBox.height
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return imageData;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export const buildMaskStage = (
|
|
||||||
// lines: CanvasMaskLine[],
|
|
||||||
// boundingBox: IRect
|
|
||||||
// ): { stage: Stage; offscreenContainer: HTMLDivElement } => {
|
|
||||||
// // create an offscreen canvas and add the mask to it
|
|
||||||
// const { width, height } = boundingBox;
|
|
||||||
|
|
||||||
// const offscreenContainer = document.createElement('div');
|
|
||||||
|
|
||||||
// const stage = new Konva.Stage({
|
|
||||||
// container: offscreenContainer,
|
|
||||||
// width: width,
|
|
||||||
// height: height,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const baseLayer = new Konva.Layer();
|
|
||||||
// const maskLayer = new Konva.Layer();
|
|
||||||
|
|
||||||
// // composite the image onto the mask layer
|
|
||||||
// baseLayer.add(
|
|
||||||
// new Konva.Rect({
|
|
||||||
// ...boundingBox,
|
|
||||||
// fill: 'white',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
|
|
||||||
// lines.forEach((line) =>
|
|
||||||
// maskLayer.add(
|
|
||||||
// new Konva.Line({
|
|
||||||
// points: line.points,
|
|
||||||
// stroke: 'black',
|
|
||||||
// strokeWidth: line.strokeWidth * 2,
|
|
||||||
// tension: 0,
|
|
||||||
// lineCap: 'round',
|
|
||||||
// lineJoin: 'round',
|
|
||||||
// shadowForStrokeEnabled: false,
|
|
||||||
// globalCompositeOperation:
|
|
||||||
// line.tool === 'brush' ? 'source-over' : 'destination-out',
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
|
|
||||||
// stage.add(baseLayer);
|
|
||||||
// stage.add(maskLayer);
|
|
||||||
|
|
||||||
// return { stage, offscreenContainer };
|
|
||||||
// };
|
|
||||||
|
|
||||||
import { CanvasMaskLine } from 'features/canvas/store/canvasTypes';
|
|
||||||
import Konva from 'konva';
|
|
||||||
import { IRect } from 'konva/lib/types';
|
|
||||||
import { canvasToBlob } from './canvasToBlob';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generating a mask image from InpaintingCanvas.tsx is not as simple
|
|
||||||
* as calling toDataURL() on the canvas, because the mask may be represented
|
|
||||||
* by colored lines or transparency, or the user may have inverted the mask
|
|
||||||
* display.
|
|
||||||
*
|
|
||||||
* So we need to regenerate the mask image by creating an offscreen canvas,
|
|
||||||
* drawing the mask and compositing everything correctly to output a valid
|
|
||||||
* mask image.
|
|
||||||
*/
|
|
||||||
const generateMask = async (lines: CanvasMaskLine[], boundingBox: IRect) => {
|
|
||||||
// create an offscreen canvas and add the mask to it
|
|
||||||
const { width, height } = boundingBox;
|
|
||||||
|
|
||||||
const offscreenContainer = document.createElement('div');
|
|
||||||
|
|
||||||
const stage = new Konva.Stage({
|
|
||||||
container: offscreenContainer,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseLayer = new Konva.Layer();
|
|
||||||
const maskLayer = new Konva.Layer();
|
|
||||||
|
|
||||||
// composite the image onto the mask layer
|
|
||||||
baseLayer.add(
|
|
||||||
new Konva.Rect({
|
|
||||||
...boundingBox,
|
|
||||||
fill: 'white',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
lines.forEach((line) =>
|
|
||||||
maskLayer.add(
|
|
||||||
new Konva.Line({
|
|
||||||
points: line.points,
|
|
||||||
stroke: 'black',
|
|
||||||
strokeWidth: line.strokeWidth * 2,
|
|
||||||
tension: 0,
|
|
||||||
lineCap: 'round',
|
|
||||||
lineJoin: 'round',
|
|
||||||
shadowForStrokeEnabled: false,
|
|
||||||
globalCompositeOperation:
|
|
||||||
line.tool === 'brush' ? 'source-over' : 'destination-out',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
stage.add(baseLayer);
|
|
||||||
stage.add(maskLayer);
|
|
||||||
|
|
||||||
const maskDataURL = stage.toDataURL(boundingBox);
|
|
||||||
|
|
||||||
const maskBlob = await canvasToBlob(stage.toCanvas(boundingBox));
|
|
||||||
|
|
||||||
offscreenContainer.remove();
|
|
||||||
|
|
||||||
return { maskDataURL, maskBlob };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default generateMask;
|
|
@ -0,0 +1,38 @@
|
|||||||
|
import { getCanvasBaseLayer } from './konvaInstanceProvider';
|
||||||
|
import { RootState } from 'app/store/store';
|
||||||
|
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||||
|
|
||||||
|
export const getBaseLayerBlob = async (
|
||||||
|
state: RootState,
|
||||||
|
withoutBoundingBox?: boolean
|
||||||
|
) => {
|
||||||
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
|
if (!canvasBaseLayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
shouldCropToBoundingBoxOnSave,
|
||||||
|
boundingBoxCoordinates,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
} = state.canvas;
|
||||||
|
|
||||||
|
const clonedBaseLayer = canvasBaseLayer.clone();
|
||||||
|
|
||||||
|
clonedBaseLayer.scale({ x: 1, y: 1 });
|
||||||
|
|
||||||
|
const absPos = clonedBaseLayer.getAbsolutePosition();
|
||||||
|
|
||||||
|
const boundingBox =
|
||||||
|
shouldCropToBoundingBoxOnSave && !withoutBoundingBox
|
||||||
|
? {
|
||||||
|
x: boundingBoxCoordinates.x + absPos.x,
|
||||||
|
y: boundingBoxCoordinates.y + absPos.y,
|
||||||
|
width: boundingBoxDimensions.width,
|
||||||
|
height: boundingBoxDimensions.height,
|
||||||
|
}
|
||||||
|
: clonedBaseLayer.getClientRect();
|
||||||
|
|
||||||
|
return konvaNodeToBlob(clonedBaseLayer, boundingBox);
|
||||||
|
};
|
@ -2,17 +2,15 @@ import { RootState } from 'app/store/store';
|
|||||||
import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider';
|
import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider';
|
||||||
import { isCanvasMaskLine } from '../store/canvasTypes';
|
import { isCanvasMaskLine } from '../store/canvasTypes';
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import {
|
import createMaskStage from './createMaskStage';
|
||||||
areAnyPixelsBlack,
|
import { konvaNodeToImageData } from './konvaNodeToImageData';
|
||||||
getImageDataTransparency,
|
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||||
} from 'common/util/arrayBuffer';
|
|
||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
|
||||||
import generateMask from './generateMask';
|
|
||||||
import { dataURLToImageData } from './dataURLToImageData';
|
|
||||||
import { canvasToBlob } from './canvasToBlob';
|
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'getCanvasDataURLs' });
|
const moduleLog = log.child({ namespace: 'getCanvasDataURLs' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets Blob and ImageData objects for the base and mask layers
|
||||||
|
*/
|
||||||
export const getCanvasData = async (state: RootState) => {
|
export const getCanvasData = async (state: RootState) => {
|
||||||
const canvasBaseLayer = getCanvasBaseLayer();
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
const canvasStage = getCanvasStage();
|
const canvasStage = getCanvasStage();
|
||||||
@ -27,10 +25,6 @@ export const getCanvasData = async (state: RootState) => {
|
|||||||
boundingBoxCoordinates,
|
boundingBoxCoordinates,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
isMaskEnabled,
|
isMaskEnabled,
|
||||||
shouldPreserveMaskedArea,
|
|
||||||
boundingBoxScaleMethod: boundingBoxScale,
|
|
||||||
scaledBoundingBoxDimensions,
|
|
||||||
stageCoordinates,
|
|
||||||
} = state.canvas;
|
} = state.canvas;
|
||||||
|
|
||||||
const boundingBox = {
|
const boundingBox = {
|
||||||
@ -38,18 +32,10 @@ export const getCanvasData = async (state: RootState) => {
|
|||||||
...boundingBoxDimensions,
|
...boundingBoxDimensions,
|
||||||
};
|
};
|
||||||
|
|
||||||
// generationParameters.fit = false;
|
// Clone the base layer so we don't affect the visible base layer
|
||||||
|
|
||||||
// generationParameters.strength = img2imgStrength;
|
|
||||||
|
|
||||||
// generationParameters.invert_mask = shouldPreserveMaskedArea;
|
|
||||||
|
|
||||||
// generationParameters.bounding_box = boundingBox;
|
|
||||||
|
|
||||||
// clone the base layer so we don't affect the actual canvas during scaling
|
|
||||||
const clonedBaseLayer = canvasBaseLayer.clone();
|
const clonedBaseLayer = canvasBaseLayer.clone();
|
||||||
|
|
||||||
// scale to 1 so we get an uninterpolated image
|
// Scale it to 100% so we get full resolution
|
||||||
clonedBaseLayer.scale({ x: 1, y: 1 });
|
clonedBaseLayer.scale({ x: 1, y: 1 });
|
||||||
|
|
||||||
// absolute position is needed to get the bounding box coords relative to the base layer
|
// absolute position is needed to get the bounding box coords relative to the base layer
|
||||||
@ -62,73 +48,25 @@ export const getCanvasData = async (state: RootState) => {
|
|||||||
height: boundingBox.height,
|
height: boundingBox.height,
|
||||||
};
|
};
|
||||||
|
|
||||||
// get a dataURL of the bbox'd region (will convert this to an ImageData to check its transparency)
|
// For the base layer, use the offset boundingBox
|
||||||
const baseDataURL = clonedBaseLayer.toDataURL(offsetBoundingBox);
|
const baseBlob = await konvaNodeToBlob(clonedBaseLayer, offsetBoundingBox);
|
||||||
|
const baseImageData = await konvaNodeToImageData(
|
||||||
// get a blob (will upload this as the canvas intermediate)
|
clonedBaseLayer,
|
||||||
const baseBlob = await canvasToBlob(
|
offsetBoundingBox
|
||||||
clonedBaseLayer.toCanvas(offsetBoundingBox)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// build a new mask layer and get its dataURL and blob
|
// For the mask layer, use the normal boundingBox
|
||||||
const { maskDataURL, maskBlob } = await generateMask(
|
const maskStage = await createMaskStage(
|
||||||
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
|
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [], // only include mask lines, and only if mask is enabled
|
||||||
boundingBox
|
boundingBox
|
||||||
);
|
);
|
||||||
|
const maskBlob = await konvaNodeToBlob(maskStage, boundingBox);
|
||||||
// convert to ImageData (via pure jank)
|
const maskImageData = await konvaNodeToImageData(maskStage, boundingBox);
|
||||||
const baseImageData = await dataURLToImageData(
|
|
||||||
baseDataURL,
|
|
||||||
boundingBox.width,
|
|
||||||
boundingBox.height
|
|
||||||
);
|
|
||||||
|
|
||||||
// convert to ImageData (via pure jank)
|
|
||||||
const maskImageData = await dataURLToImageData(
|
|
||||||
maskDataURL,
|
|
||||||
boundingBox.width,
|
|
||||||
boundingBox.height
|
|
||||||
);
|
|
||||||
|
|
||||||
// check transparency
|
|
||||||
const {
|
|
||||||
isPartiallyTransparent: baseIsPartiallyTransparent,
|
|
||||||
isFullyTransparent: baseIsFullyTransparent,
|
|
||||||
} = getImageDataTransparency(baseImageData.data);
|
|
||||||
|
|
||||||
// check mask for black
|
|
||||||
const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data);
|
|
||||||
|
|
||||||
if (state.system.enableImageDebugging) {
|
|
||||||
openBase64ImageInTab([
|
|
||||||
{ base64: maskDataURL, caption: 'mask b64' },
|
|
||||||
{ base64: baseDataURL, caption: 'image b64' },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// generationParameters.init_img = imageDataURL;
|
|
||||||
// generationParameters.progress_images = false;
|
|
||||||
|
|
||||||
// if (boundingBoxScale !== 'none') {
|
|
||||||
// generationParameters.inpaint_width = scaledBoundingBoxDimensions.width;
|
|
||||||
// generationParameters.inpaint_height = scaledBoundingBoxDimensions.height;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// generationParameters.seam_size = seamSize;
|
|
||||||
// generationParameters.seam_blur = seamBlur;
|
|
||||||
// generationParameters.seam_strength = seamStrength;
|
|
||||||
// generationParameters.seam_steps = seamSteps;
|
|
||||||
// generationParameters.tile_size = tileSize;
|
|
||||||
// generationParameters.infill_method = infillMethod;
|
|
||||||
// generationParameters.force_outpaint = false;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseDataURL,
|
|
||||||
baseBlob,
|
baseBlob,
|
||||||
maskDataURL,
|
baseImageData,
|
||||||
maskBlob,
|
maskBlob,
|
||||||
baseIsPartiallyTransparent,
|
maskImageData,
|
||||||
baseIsFullyTransparent,
|
|
||||||
doesMaskHaveBlackPixels,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
areAnyPixelsBlack,
|
||||||
|
getImageDataTransparency,
|
||||||
|
} from 'common/util/arrayBuffer';
|
||||||
|
|
||||||
|
export const getCanvasGenerationMode = (
|
||||||
|
baseImageData: ImageData,
|
||||||
|
maskImageData: ImageData
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
isPartiallyTransparent: baseIsPartiallyTransparent,
|
||||||
|
isFullyTransparent: baseIsFullyTransparent,
|
||||||
|
} = getImageDataTransparency(baseImageData.data);
|
||||||
|
|
||||||
|
// check mask for black
|
||||||
|
const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data);
|
||||||
|
|
||||||
|
if (baseIsPartiallyTransparent) {
|
||||||
|
if (baseIsFullyTransparent) {
|
||||||
|
return 'txt2img';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'outpaint';
|
||||||
|
} else {
|
||||||
|
if (doesMaskHaveBlackPixels) {
|
||||||
|
return 'inpaint';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'img2img';
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
import Konva from 'konva';
|
||||||
|
import { IRect } from 'konva/lib/types';
|
||||||
|
import { canvasToBlob } from './canvasToBlob';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Konva node to a Blob
|
||||||
|
* @param node - The Konva node to convert to a Blob
|
||||||
|
* @param boundingBox - The bounding box to crop to
|
||||||
|
* @returns A Promise that resolves with Blob of the node cropped to the bounding box
|
||||||
|
*/
|
||||||
|
export const konvaNodeToBlob = async (
|
||||||
|
node: Konva.Node,
|
||||||
|
boundingBox: IRect
|
||||||
|
): Promise<Blob> => {
|
||||||
|
return await canvasToBlob(node.toCanvas(boundingBox));
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
import Konva from 'konva';
|
||||||
|
import { IRect } from 'konva/lib/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Konva node to a dataURL
|
||||||
|
* @param node - The Konva node to convert to a dataURL
|
||||||
|
* @param boundingBox - The bounding box to crop to
|
||||||
|
* @returns A dataURL of the node cropped to the bounding box
|
||||||
|
*/
|
||||||
|
export const konvaNodeToDataURL = (
|
||||||
|
node: Konva.Node,
|
||||||
|
boundingBox: IRect
|
||||||
|
): string => {
|
||||||
|
// get a dataURL of the bbox'd region
|
||||||
|
return node.toDataURL(boundingBox);
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import Konva from 'konva';
|
||||||
|
import { IRect } from 'konva/lib/types';
|
||||||
|
import { dataURLToImageData } from './dataURLToImageData';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Konva node to an ImageData object
|
||||||
|
* @param node - The Konva node to convert to an ImageData object
|
||||||
|
* @param boundingBox - The bounding box to crop to
|
||||||
|
* @returns A Promise that resolves with ImageData object of the node cropped to the bounding box
|
||||||
|
*/
|
||||||
|
export const konvaNodeToImageData = async (
|
||||||
|
node: Konva.Node,
|
||||||
|
boundingBox: IRect
|
||||||
|
): Promise<ImageData> => {
|
||||||
|
// get a dataURL of the bbox'd region
|
||||||
|
const dataURL = node.toDataURL(boundingBox);
|
||||||
|
|
||||||
|
return await dataURLToImageData(
|
||||||
|
dataURL,
|
||||||
|
boundingBox.width,
|
||||||
|
boundingBox.height
|
||||||
|
);
|
||||||
|
};
|
@ -14,9 +14,11 @@ import { buildRangeNode } from '../nodeBuilders/buildRangeNode';
|
|||||||
import { buildIterateNode } from '../nodeBuilders/buildIterateNode';
|
import { buildIterateNode } from '../nodeBuilders/buildIterateNode';
|
||||||
import { buildEdges } from '../edgeBuilders/buildEdges';
|
import { buildEdges } from '../edgeBuilders/buildEdges';
|
||||||
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
||||||
import { getGenerationMode } from '../getGenerationMode';
|
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { buildInpaintNode } from '../nodeBuilders/buildInpaintNode';
|
import { buildInpaintNode } from '../nodeBuilders/buildInpaintNode';
|
||||||
|
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
||||||
|
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||||
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'buildCanvasGraph' });
|
const moduleLog = log.child({ namespace: 'buildCanvasGraph' });
|
||||||
|
|
||||||
@ -61,37 +63,25 @@ export const buildCanvasGraphAndBlobs = async (
|
|||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
> => {
|
> => {
|
||||||
const c = await getCanvasData(state);
|
const canvasData = await getCanvasData(state);
|
||||||
|
|
||||||
if (!c) {
|
if (!canvasData) {
|
||||||
moduleLog.error('Unable to create canvas graph');
|
moduleLog.error('Unable to create canvas data');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { baseBlob, baseImageData, maskBlob, maskImageData } = canvasData;
|
||||||
baseBlob,
|
|
||||||
maskBlob,
|
|
||||||
baseIsPartiallyTransparent,
|
|
||||||
baseIsFullyTransparent,
|
|
||||||
doesMaskHaveBlackPixels,
|
|
||||||
} = c;
|
|
||||||
|
|
||||||
moduleLog.debug(
|
const generationMode = getCanvasGenerationMode(baseImageData, maskImageData);
|
||||||
{
|
|
||||||
data: {
|
|
||||||
baseIsPartiallyTransparent,
|
|
||||||
baseIsFullyTransparent,
|
|
||||||
doesMaskHaveBlackPixels,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'Built canvas data'
|
|
||||||
);
|
|
||||||
|
|
||||||
const generationMode = getGenerationMode(
|
if (state.system.enableImageDebugging) {
|
||||||
baseIsPartiallyTransparent,
|
const baseDataURL = await blobToDataURL(baseBlob);
|
||||||
baseIsFullyTransparent,
|
const maskDataURL = await blobToDataURL(maskBlob);
|
||||||
doesMaskHaveBlackPixels
|
openBase64ImageInTab([
|
||||||
);
|
{ base64: maskDataURL, caption: 'mask b64' },
|
||||||
|
{ base64: baseDataURL, caption: 'image b64' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
moduleLog.debug(`Generation mode: ${generationMode}`);
|
moduleLog.debug(`Generation mode: ${generationMode}`);
|
||||||
|
|
||||||
@ -104,8 +94,14 @@ export const buildCanvasGraphAndBlobs = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (baseNode.type === 'inpaint') {
|
if (baseNode.type === 'inpaint') {
|
||||||
const { seamSize, seamBlur, seamSteps, seamStrength, tileSize } =
|
const {
|
||||||
state.generation;
|
seamSize,
|
||||||
|
seamBlur,
|
||||||
|
seamSteps,
|
||||||
|
seamStrength,
|
||||||
|
tileSize,
|
||||||
|
infillMethod,
|
||||||
|
} = state.generation;
|
||||||
|
|
||||||
// generationParameters.invert_mask = shouldPreserveMaskedArea;
|
// generationParameters.invert_mask = shouldPreserveMaskedArea;
|
||||||
// if (boundingBoxScale !== 'none') {
|
// if (boundingBoxScale !== 'none') {
|
||||||
@ -117,7 +113,7 @@ export const buildCanvasGraphAndBlobs = async (
|
|||||||
baseNode.seam_strength = seamStrength;
|
baseNode.seam_strength = seamStrength;
|
||||||
baseNode.seam_steps = seamSteps;
|
baseNode.seam_steps = seamSteps;
|
||||||
baseNode.tile_size = tileSize;
|
baseNode.tile_size = tileSize;
|
||||||
// baseNode.infill_method = infillMethod;
|
baseNode.infill_method = infillMethod as InpaintInvocation['infill_method'];
|
||||||
// baseNode.force_outpaint = false;
|
// baseNode.force_outpaint = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas';
|
|
||||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -16,10 +16,6 @@ export default function UnifiedCanvasCopyToClipboard() {
|
|||||||
(state: RootState) => state.system.isProcessing
|
(state: RootState) => state.system.isProcessing
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldCropToBoundingBoxOnSave = useAppSelector(
|
|
||||||
(state: RootState) => state.canvas.shouldCropToBoundingBoxOnSave
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -36,13 +32,7 @@ export default function UnifiedCanvasCopyToClipboard() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleCopyImageToClipboard = () => {
|
const handleCopyImageToClipboard = () => {
|
||||||
dispatch(
|
dispatch(canvasCopiedToClipboard());
|
||||||
mergeAndUploadCanvas({
|
|
||||||
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
|
||||||
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
|
||||||
shouldCopy: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { canvasDownloadedAsImage } from 'features/canvas/store/actions';
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas';
|
|
||||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -16,14 +15,6 @@ export default function UnifiedCanvasDownloadImage() {
|
|||||||
|
|
||||||
const isStaging = useAppSelector(isStagingSelector);
|
const isStaging = useAppSelector(isStagingSelector);
|
||||||
|
|
||||||
const isProcessing = useAppSelector(
|
|
||||||
(state: RootState) => state.system.isProcessing
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldCropToBoundingBoxOnSave = useAppSelector(
|
|
||||||
(state: RootState) => state.canvas.shouldCropToBoundingBoxOnSave
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
['shift+d'],
|
['shift+d'],
|
||||||
() => {
|
() => {
|
||||||
@ -33,18 +24,13 @@ export default function UnifiedCanvasDownloadImage() {
|
|||||||
enabled: () => !isStaging,
|
enabled: () => !isStaging,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
[canvasBaseLayer, isProcessing]
|
[canvasBaseLayer]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDownloadAsImage = () => {
|
const handleDownloadAsImage = () => {
|
||||||
dispatch(
|
dispatch(canvasDownloadedAsImage());
|
||||||
mergeAndUploadCanvas({
|
|
||||||
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
|
||||||
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
|
||||||
shouldDownload: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { canvasMerged } from 'features/canvas/store/actions';
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas';
|
import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas';
|
||||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
@ -30,12 +31,7 @@ export default function UnifiedCanvasMergeVisible() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleMergeVisible = () => {
|
const handleMergeVisible = () => {
|
||||||
dispatch(
|
dispatch(canvasMerged());
|
||||||
mergeAndUploadCanvas({
|
|
||||||
cropVisible: false,
|
|
||||||
shouldSetAsInitialImage: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { canvasSavedToGallery } from 'features/canvas/store/actions';
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas';
|
|
||||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -14,9 +14,6 @@ export default function UnifiedCanvasSaveToGallery() {
|
|||||||
const isProcessing = useAppSelector(
|
const isProcessing = useAppSelector(
|
||||||
(state: RootState) => state.system.isProcessing
|
(state: RootState) => state.system.isProcessing
|
||||||
);
|
);
|
||||||
const shouldCropToBoundingBoxOnSave = useAppSelector(
|
|
||||||
(state: RootState) => state.canvas.shouldCropToBoundingBoxOnSave
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -34,14 +31,9 @@ export default function UnifiedCanvasSaveToGallery() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSaveToGallery = () => {
|
const handleSaveToGallery = () => {
|
||||||
dispatch(
|
dispatch(canvasSavedToGallery());
|
||||||
mergeAndUploadCanvas({
|
|
||||||
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
|
||||||
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
|
||||||
shouldSaveToGallery: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||||
|
Loading…
Reference in New Issue
Block a user