Skip to content

Commit d50abd8

Browse files
fix(ui): image quality degradation while saving images
The HTML Canvas context has an `imageSmoothingEnabled` property which defaults to `true`. This causes the browser canvas API to, well, apply image smoothing - everything gets antialiased when drawn. This is, of course, problematic when our goal is to be pixel-perfect. When the same image is drawn multiple times, we get progressive image degradation. In `CanvasEntityObjectRenderer.cloneObjectGroup()`, where we use Konva's `Node.cache()` method to create a canvas from the entity's objects. Here, we were not setting `imageSmoothingEnabled` to false. This method is used very often by the compositor and we end up feeding back antialiased versions of the image data back into the canvas or generation backend. Disabling smoothing here appears to fix the issue. I've also disabled image smoothing everywhere else we interact with a canvas rendering context.
1 parent ddfa32d commit d50abd8

File tree

3 files changed

+13
-3
lines changed

3 files changed

+13
-3
lines changed

invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ export class CanvasCompositorModule extends CanvasModuleBase {
126126
const ctx = canvas.getContext('2d');
127127
assert(ctx !== null, 'Canvas 2D context is null');
128128

129+
ctx.imageSmoothingEnabled = false;
130+
129131
for (const id of this.getCompositeRasterLayerEntityIds()) {
130132
const adapter = this.manager.adapters.rasterLayers.get(id);
131133
if (!adapter) {
@@ -288,6 +290,8 @@ export class CanvasCompositorModule extends CanvasModuleBase {
288290
const ctx = canvas.getContext('2d');
289291
assert(ctx !== null);
290292

293+
ctx.imageSmoothingEnabled = false;
294+
291295
for (const id of this.getCompositeInpaintMaskEntityIds()) {
292296
const adapter = this.manager.adapters.inpaintMasks.get(id);
293297
if (!adapter) {

invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
452452
if (attrs) {
453453
clone.setAttrs(attrs);
454454
}
455-
clone.cache();
455+
clone.cache({ pixelRatio: 1, imageSmoothingEnabled: false });
456456
return clone;
457457
};
458458

invokeai/frontend/web/src/features/controlLayers/konva/util.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ export function imageDataToDataURL(imageData: ImageData): string {
233233
if (!ctx) {
234234
throw new Error('Unable to get canvas context');
235235
}
236+
ctx.imageSmoothingEnabled = false;
236237
ctx.putImageData(imageData, 0, 0);
237238

238239
// Convert the canvas to a data URL (base64)
@@ -251,6 +252,7 @@ export function imageDataToBlob(imageData: ImageData): Promise<Blob | null> {
251252
return Promise.resolve(null);
252253
}
253254

255+
ctx.imageSmoothingEnabled = false;
254256
ctx.putImageData(imageData, 0, 0);
255257

256258
return new Promise<Blob | null>((resolve) => {
@@ -281,14 +283,16 @@ export const dataURLToImageData = (dataURL: string, width: number, height: numbe
281283
canvas.width = width;
282284
canvas.height = height;
283285
const ctx = canvas.getContext('2d');
284-
const image = new Image();
285286

286287
if (!ctx) {
287288
canvas.remove();
288289
reject('Unable to get context');
289290
return;
290291
}
291292

293+
ctx.imageSmoothingEnabled = false;
294+
295+
const image = new Image();
292296
image.onload = function () {
293297
ctx.drawImage(image, 0, 0);
294298
canvas.remove();
@@ -306,7 +310,7 @@ export const dataURLToImageData = (dataURL: string, width: number, height: numbe
306310

307311
export const konvaNodeToCanvas = (arg: { node: Konva.Node; rect?: Rect; bg?: string }): HTMLCanvasElement => {
308312
const { node, rect, bg } = arg;
309-
const canvas = node.toCanvas({ ...(rect ?? {}) });
313+
const canvas = node.toCanvas({ ...(rect ?? {}), imageSmoothingEnabled: false });
310314

311315
if (!bg) {
312316
return canvas;
@@ -318,6 +322,7 @@ export const konvaNodeToCanvas = (arg: { node: Konva.Node; rect?: Rect; bg?: str
318322
bgCanvas.height = canvas.height;
319323
const bgCtx = bgCanvas.getContext('2d');
320324
assert(bgCtx !== null, 'bgCtx is null');
325+
bgCtx.imageSmoothingEnabled = false;
321326
bgCtx.fillStyle = bg;
322327
bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
323328
bgCtx.drawImage(canvas, 0, 0);
@@ -344,6 +349,7 @@ export const canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => {
344349
export const canvasToImageData = (canvas: HTMLCanvasElement): ImageData => {
345350
const ctx = canvas.getContext('2d');
346351
assert(ctx, 'ctx is null');
352+
ctx.imageSmoothingEnabled = false;
347353
return ctx.getImageData(0, 0, canvas.width, canvas.height);
348354
};
349355

0 commit comments

Comments
 (0)