From ea5a1249ee390a28bc7a3abb08d65171fafffdcf Mon Sep 17 00:00:00 2001 From: DustyShoe Date: Sun, 3 May 2026 04:09:31 +0300 Subject: [PATCH] fix(text-tool): preserve logical size when committing HiDPI text --- .../konva/CanvasTool/CanvasTextToolModule.ts | 9 +++--- .../controlLayers/text/textCommit.test.ts | 28 +++++++++++++++++++ .../features/controlLayers/text/textCommit.ts | 18 ++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/text/textCommit.test.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/text/textCommit.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasTextToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasTextToolModule.ts index 592da1ffdaa..7308aa6e7d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasTextToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasTextToolModule.ts @@ -4,7 +4,7 @@ import type { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/C import { canvasToBlob, getPrefixedId } from 'features/controlLayers/konva/util'; import { type CanvasTextSettingsState, selectCanvasTextSlice } from 'features/controlLayers/store/canvasTextSlice'; import type { Coordinate, RgbaColor } from 'features/controlLayers/store/types'; -import { imageDTOToImageObject } from 'features/controlLayers/store/util'; +import { buildCommittedTextImageState, getCommittedTextImageDimensions } from 'features/controlLayers/text/textCommit'; import { getFontStackById, TEXT_RASTER_PADDING } from 'features/controlLayers/text/textConstants'; import { buildFontDescriptor, @@ -418,7 +418,8 @@ export class CanvasTextToolModule extends CanvasModuleBase { is_intermediate: true, silent: true, }); - const imageState = imageDTOToImageObject(imageDTO); + const { width: committedWidth, height: committedHeight } = getCommittedTextImageDimensions(totalWidth, totalHeight); + const imageState = buildCommittedTextImageState(imageDTO, totalWidth, totalHeight); const extraLeftPadding = Math.ceil(textSettings.fontSize * 0.12); const fallbackPosition = calculateLayerPosition( @@ -434,8 +435,8 @@ export class CanvasTextToolModule extends CanvasModuleBase { rotation === 0 ? basePosition : { - x: basePosition.x + baseSize.width / 2 - totalWidth / 2, - y: basePosition.y + baseSize.height / 2 - totalHeight / 2, + x: basePosition.x + baseSize.width / 2 - committedWidth / 2, + y: basePosition.y + baseSize.height / 2 - committedHeight / 2, }; const selectedAdapter = this.manager.stateApi.getSelectedEntityAdapter(); diff --git a/invokeai/frontend/web/src/features/controlLayers/text/textCommit.test.ts b/invokeai/frontend/web/src/features/controlLayers/text/textCommit.test.ts new file mode 100644 index 00000000000..d83e5c65e64 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/text/textCommit.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; + +import { buildCommittedTextImageState, getCommittedTextImageDimensions } from './textCommit'; + +describe('text commit helpers', () => { + it('preserves logical dimensions when committing a hidpi render', () => { + const imageDTO = { + image_name: 'canvas-text.png', + width: 300, + height: 150, + }; + + const imageState = buildCommittedTextImageState(imageDTO as never, 200, 100); + + expect(imageState.image).toEqual({ + image_name: 'canvas-text.png', + width: 200, + height: 100, + }); + }); + + it('rounds up fractional logical dimensions and clamps to at least one pixel', () => { + expect(getCommittedTextImageDimensions(99.1, 0.2)).toEqual({ + width: 100, + height: 1, + }); + }); +}); diff --git a/invokeai/frontend/web/src/features/controlLayers/text/textCommit.ts b/invokeai/frontend/web/src/features/controlLayers/text/textCommit.ts new file mode 100644 index 00000000000..a94990abe5b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/text/textCommit.ts @@ -0,0 +1,18 @@ +import { imageDTOToImageObject } from 'features/controlLayers/store/util'; +import type { ImageDTO } from 'services/api/types'; + +export const getCommittedTextImageDimensions = (width: number, height: number) => ({ + width: Math.max(1, Math.ceil(width)), + height: Math.max(1, Math.ceil(height)), +}); + +export const buildCommittedTextImageState = (imageDTO: ImageDTO, width: number, height: number) => { + const committedDimensions = getCommittedTextImageDimensions(width, height); + return imageDTOToImageObject(imageDTO, { + image: { + image_name: imageDTO.image_name, + width: committedDimensions.width, + height: committedDimensions.height, + }, + }); +};