@@ -334,6 +334,70 @@ public function testBuildXObjectSignameAndDescriptionIncludesNameAndDescriptionB
334334 $ this ->assertStringContainsString ('102.00 ' , $ xObject ->stream );
335335 }
336336
337+ /**
338+ * Regression: GRAPHIC_ONLY mode must not render any text in the n2 xObject layer.
339+ * Before the fix, the method fell through to the description block and wrote text
340+ * into the stamp.
341+ */
342+ public function testBuildXObjectGraphicOnlyReturnsEmptyStream (): void {
343+ $ handler = $ this ->getHandlerWithMode (SignerElementsService::RENDER_MODE_GRAPHIC_ONLY );
344+ $ xObject = $ this ->callPrivateMethod (
345+ $ handler , 'buildXObject ' , 100 , 50 , SignerElementsService::RENDER_MODE_GRAPHIC_ONLY ,
346+ );
347+
348+ $ this ->assertSame ('' , $ xObject ->stream );
349+ $ this ->assertSame ([], $ xObject ->resources );
350+ }
351+
352+ /**
353+ * Regression: GRAPHIC_ONLY mode must assign the user's drawn image to the full bbox
354+ * (signatureImageFrame = null). Before the fix only GRAPHIC_AND_DESCRIPTION set
355+ * signatureImagePath, leaving GRAPHIC_ONLY with no image (blank stamp).
356+ */
357+ public function testBuildAppearanceForElementSetsSignatureImageInGraphicOnlyMode (): void {
358+ $ imagePath = realpath (__DIR__ . '/../../../../../img/app-dark.png ' );
359+ $ this ->assertNotFalse ($ imagePath , 'Test image must exist ' );
360+
361+ $ handler = $ this ->getHandlerWithMode (SignerElementsService::RENDER_MODE_GRAPHIC_ONLY );
362+ $ this ->signatureBackgroundService ->method ('isEnabled ' )->willReturn (false );
363+
364+ $ appearance = $ this ->callPrivateMethod (
365+ $ handler ,
366+ 'buildAppearanceForElement ' ,
367+ 10.0 , 20.0 , 110.0 , 70.0 , 800.0 , 0 , 100 , 50 ,
368+ $ imagePath ,
369+ );
370+
371+ $ this ->assertInstanceOf (SignatureAppearanceDto::class, $ appearance );
372+ // Image must fill the entire stamp bbox (no split)
373+ $ this ->assertSame ($ imagePath , $ appearance ->signatureImagePath );
374+ $ this ->assertNull ($ appearance ->signatureImageFrame );
375+ }
376+
377+ /**
378+ * Regression: in SIGNAME_AND_DESCRIPTION the signer name must be horizontally
379+ * centred within the left half of the stamp, not pinned to leftPadding (left edge).
380+ *
381+ * Layout math for width=200, height=80, fontSize=20, name="Al":
382+ * leftHalfW = 100.0
383+ * lineWidth = strlen("Al") * (20 * 0.52) = 2 * 10.4 = 20.8
384+ * nameX = max(2.0, (100 - 20.8) / 2) = 39.6 → "39.60"
385+ * nameStartY = (80 + 24) / 2 - 20 = 32.0 → "32.00"
386+ * Old (broken) code always used leftPadding=2.0 → "2.00 32.00 Td"
387+ */
388+ public function testBuildXObjectSignameAndDescriptionCentersNameInLeftHalf (): void {
389+ $ handler = $ this ->getHandlerWithMode (SignerElementsService::RENDER_MODE_SIGNAME_AND_DESCRIPTION );
390+ $ handler ->setSignatureParams (['SignerCommonName ' => 'Al ' ]);
391+ $ xObject = $ this ->callPrivateMethod (
392+ $ handler , 'buildXObject ' , 200 , 80 , SignerElementsService::RENDER_MODE_SIGNAME_AND_DESCRIPTION ,
393+ );
394+
395+ // Centred position must appear
396+ $ this ->assertStringContainsString ('39.60 32.00 Td ' , $ xObject ->stream );
397+ // Old left-aligned position must NOT appear
398+ $ this ->assertStringNotContainsString ('2.00 32.00 Td ' , $ xObject ->stream );
399+ }
400+
337401 public function testBuildXObjectSignameAndDescriptionWithEmptyNameOmitsNameBlock (): void {
338402 // When SignerCommonName is absent and certificate has no CN, no name block should appear
339403 $ engine = $ this ->createMock (\OCA \Libresign \Handler \CertificateEngine \IEngineHandler::class);
0 commit comments