From 2111f252983c590750666cd51d17669e88e037b5 Mon Sep 17 00:00:00 2001 From: Loic Dal Zotto Date: Mon, 14 Sep 2026 14:06:03 +0200 Subject: [PATCH 1/3] test(ios): reproduce multiline text alignment issue --- .../ImageMarkerExampleUITests.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/example/ios/ImageMarkerExampleUITests/ImageMarkerExampleUITests.swift b/example/ios/ImageMarkerExampleUITests/ImageMarkerExampleUITests.swift index 873b9b87..effce81e 100644 --- a/example/ios/ImageMarkerExampleUITests/ImageMarkerExampleUITests.swift +++ b/example/ios/ImageMarkerExampleUITests/ImageMarkerExampleUITests.swift @@ -173,6 +173,48 @@ final class ImageMarkerExampleUITests: XCTestCase { bytes[index] > 24 || bytes[index + 1] > 24 || bytes[index + 2] > 24 } ) + + // Alignment must not move glyphs or their background away from the requested origin. + let alignmentStyle = try TextStyle(dicOpts: [:]) + let font = UIFont.monospacedSystemFont(ofSize: 24, weight: .regular) + let format = UIGraphicsImageRendererFormat.default() + format.scale = 1 + // Arbitrary nonzero coordinates verify that drawing respects the requested origin. + let origin = CGPoint(x: 13, y: 17) + for alignment in [NSTextAlignment.left, .center, .right] { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = alignment + let attributes: [NSAttributedString.Key: Any] = [ + .font: font, + // Distinct colors let us check glyph and background positioning separately. + .foregroundColor: UIColor.blue, + .backgroundColor: UIColor.red, + .paragraphStyle: paragraph, + ] + let alignedLayout = ImageMarkerTextLayout( + text: NSAttributedString(string: "Hello\nWorld", attributes: attributes), + maxWidth: 200, + style: alignmentStyle + ) + let image = UIGraphicsImageRenderer( + size: CGSize(width: 240, height: 80), + format: format + ).image { _ in + alignedLayout.draw(at: origin) + } + let bounds = try XCTUnwrap(bluePixelBounds(in: image)) + // Visible glyphs must span more than one line. + XCTAssertGreaterThan(bounds.height, font.lineHeight) + XCTAssertEqual(bounds.minX, origin.x, accuracy: 0.001) + let pixels = try XCTUnwrap(rgbaBytes(for: image)) + assertPixel( + pixels, + at: CGPoint(x: origin.x + 1, y: origin.y + 1), + width: Int(image.size.width), + matches: .red, + message: "Text background should start at the requested origin for alignment \(alignment.rawValue)" + ) + } } func testCore21ImageInfoReadsEncodedOrientation() throws { From d52943b2f7b57cc14be70a9e903fb8c59533ae80 Mon Sep 17 00:00:00 2001 From: Loic Dal Zotto Date: Mon, 14 Sep 2026 13:49:22 +0200 Subject: [PATCH 2/3] feat(example): add multiline text alignment reproduction mode --- example/src/ImageMarkerLab.tsx | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/example/src/ImageMarkerLab.tsx b/example/src/ImageMarkerLab.tsx index 2beb45c9..9bf1c981 100644 --- a/example/src/ImageMarkerLab.tsx +++ b/example/src/ImageMarkerLab.tsx @@ -797,6 +797,58 @@ function useViewModel(props: ImageMarkerLabProps) { ); } + async function runMultilineTextAlignmentFeature() { + setLastRun('Multiline text alignment'); + setUri(''); + setShow(false); + setFileSize('0 B'); + setResultContract(''); + setLoading(true); + + try { + const path = await resultUri( + Marker.markText({ + backgroundImage: { src: assets.bg }, + watermarkTexts: textAlignOptions.map((alignment, index) => ({ + text: 'Hello\nWorld', + position: { X: '10%', Y: `${10 + index * 25}%` }, + style: { + color: '#FFFFFF', + fontSize: 64, + lineHeight: 80, + textAlign: alignment, + maxWidth: '70%', + textBackgroundStyle: { + type: TextBackgroundType.none, + color: '#1E293B', + padding: 0, + }, + }, + })), + quality: 100, + saveFormat: ImageFormat.png, + }) + ); + + setUri(formatResultUri(path, ImageFormat.png)); + setShow(true); + setResultContract( + 'Top to bottom: left, center, right. Each row should show Hello and World ' + + 'inside its dark background. All three blocks should start at the same ' + + 'horizontal position, with no missing or clipped text. Tap the preview to enlarge.' + ); + await updateFileSize(path, ImageFormat.png); + } catch (error) { + Toast.show({ + type: 'error', + text1: 'multiline text alignment failed', + text2: error instanceof Error ? error.message : String(error), + }); + } finally { + setLoading(false); + } + } + function runImageOffsetFeature() { setBackgroundFormat('normal image'); runMark( @@ -1968,6 +2020,7 @@ function useViewModel(props: ImageMarkerLabProps) { pickImage, runMark, runTextOffsetFeature, + runMultilineTextAlignmentFeature, runImageOffsetFeature, runMixedWatermarkFeature, runBlendModeFeature, @@ -2083,6 +2136,14 @@ function ImageMarkerLab(props: ImageMarkerLabProps) { testID="feature-text-anchor-offset" onPress={actions.runTextOffsetFeature} /> + Date: Mon, 14 Sep 2026 12:37:44 +0200 Subject: [PATCH 3/3] fix(ios): offset text drawing origin for alignment --- ios/RCTImageMarker/TextLayout.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ios/RCTImageMarker/TextLayout.swift b/ios/RCTImageMarker/TextLayout.swift index 5fcd6370..5b8ed19f 100644 --- a/ios/RCTImageMarker/TextLayout.swift +++ b/ios/RCTImageMarker/TextLayout.swift @@ -47,13 +47,21 @@ final class ImageMarkerTextLayout { func draw(at origin: CGPoint) { let glyphRange = layoutManager.glyphRange(for: textContainer) + let textBounds = layoutManager.usedRect(for: textContainer) + + // Subtract textBounds.origin to counter the alignment offset applied by + // drawBackground and drawGlyphs. + let textContainerOrigin = CGPoint( + x: origin.x - textBounds.origin.x, + y: origin.y - textBounds.origin.y + ) layoutManager.drawBackground( forGlyphRange: glyphRange, - at: origin + at: textContainerOrigin ) layoutManager.drawGlyphs( forGlyphRange: glyphRange, - at: origin + at: textContainerOrigin ) } }