package jnpf.fileStorage.dms; import jnpf.dmsEntity.preview.model.DmsWatermarkPositionSnapshot; import jnpf.dmsEntity.preview.model.DmsWatermarkSnapshot; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.junit.Test; import javax.imageio.ImageIO; import javax.xml.parsers.DocumentBuilderFactory; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.regex.Pattern; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; public class DmsWatermarkAlignmentTest { private final DmsDocxWatermarkService service = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)); @Test public void configurationPreviewUsesTheExportPixelsAndRejectsUnboundedCanvases() throws Exception { DmsWatermarkSnapshot watermark = mark(); watermark.setHorizontalAlign("CENTER"); watermark.setVerticalAlign("MIDDLE"); for (double[] page : List.of(new double[]{210, 297}, new double[]{297, 210})) { assertArrayEquals(service.renderPdfOverlay(page[0], page[1], List.of(watermark)), service.preview(page[0], page[1], watermark)); } assertThrows(jnpf.exception.DataException.class, () -> service.preview(Double.NaN, 297, watermark)); assertThrows(jnpf.exception.DataException.class, () -> service.preview(210, 10000, watermark)); watermark.setMarkWidthMm(new BigDecimal("10000")); assertThrows(jnpf.exception.DataException.class, () -> service.preview(210, 297, watermark)); } @Test public void allNineAlignmentsPlaceEachLineAndTheWholeBlockInsideTheRectangle() throws Exception { List legacyLines = lineInk(pdf(mark())); for (String horizontal : List.of("LEFT", "CENTER", "RIGHT")) { for (String vertical : List.of("TOP", "MIDDLE", "BOTTOM")) { DmsWatermarkSnapshot mark = mark(); mark.setHorizontalAlign(horizontal); mark.setVerticalAlign(vertical); BufferedImage image = pdf(mark); List lines = lineInk(image); assertEquals(2, lines.size()); Rectangle block = lines.get(0).union(lines.get(1)); assertAligned(block.y, image.getHeight() - block.y - block.height, vertical); for (int index = 0; index < lines.size(); index++) { Rectangle line = lines.get(index); if ("LEFT".equals(horizontal)) assertEquals(legacyLines.get(index).x, line.x); else assertAligned(line.x, image.getWidth() - line.x - line.width, horizontal); } } } } @Test public void missingFieldsAndTiledLayoutKeepLegacyPixels() throws Exception { DmsWatermarkSnapshot mark = mark(); int[] legacy = pixels(pdf(mark)); mark.setHorizontalAlign("LEFT"); mark.setVerticalAlign("TOP"); assertArrayEquals(legacy, pixels(pdf(mark))); mark.setLayoutMode("TILED"); int[] tiled = pixels(pdf(mark)); mark.setHorizontalAlign("RIGHT"); mark.setVerticalAlign("BOTTOM"); assertArrayEquals(tiled, pixels(pdf(mark))); } @Test public void emptyAndOversizedTextRemainBounded() throws Exception { DmsWatermarkSnapshot mark = mark(); mark.setHorizontalAlign("RIGHT"); mark.setVerticalAlign("BOTTOM"); mark.setContent("\n\n"); assertTrue(lineInk(pdf(mark)).isEmpty()); mark.setContent("W".repeat(200) + "\n" + "I\n".repeat(50)); BufferedImage image = pdf(mark); assertFalse(lineInk(image).isEmpty()); assertEquals((int) Math.round(60 * 144 / 25.4), image.getWidth()); assertEquals((int) Math.round(40 * 144 / 25.4), image.getHeight()); } @Test public void docxAndSelectedPagePicturesKeepAlignmentAndRectangleCoordinates() throws Exception { DmsWatermarkSnapshot mark = mark(); mark.setHorizontalAlign("RIGHT"); mark.setVerticalAlign("BOTTOM"); byte[] source = source(); BufferedImage expected = pdf(mark); try (XWPFDocument docx = new XWPFDocument(new ByteArrayInputStream(service.apply(source, List.of(mark))))) { var picture = docx.getPackage().getPartsByName(Pattern.compile("/word/media/.*")).get(0); BufferedImage page = ImageIO.read(picture.getInputStream()); Rectangle ink = union(lineInk(page)); assertEquals(union(lineInk(expected)), ink); assertEquals("Body stays unchanged", docx.getParagraphArray(0).getText()); } mark.setRenderPages(List.of(1)); var variant = service.pageOverlays(source, List.of(mark)).get(0).getVariants().get(0); assertEquals(0, variant.getOffsetXMm(), 0.001); assertEquals(0, variant.getOffsetYMm(), 0.001); BufferedImage selected = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(variant.getPngBase64()))); assertEquals(union(lineInk(expected)), union(lineInk(selected))); BufferedImage raster = new BufferedImage(expected.getWidth(), expected.getHeight(), BufferedImage.TYPE_INT_ARGB); service.applyImageWatermarks(raster, List.of(mark)); Rectangle rasterInk = union(lineInk(raster)); assertAligned(rasterInk.x, raster.getWidth() - rasterInk.x - rasterInk.width, "RIGHT"); assertAligned(rasterInk.y, raster.getHeight() - rasterInk.y - rasterInk.height, "BOTTOM"); } @Test public void pageFieldsStayLiveAndUseNativeHorizontalAndVerticalAlignment() throws Exception { String[] horizontal = {"LEFT", "CENTER", "RIGHT"}; String[] vertical = {"TOP", "MIDDLE", "BOTTOM"}; String[] anchors = {"t", "ctr", "b"}; for (String align : horizontal) { for (int index = 0; index < vertical.length; index++) { DmsWatermarkSnapshot mark = mark(); mark.setHorizontalAlign(align); mark.setVerticalAlign(vertical[index]); mark.setContent("Page {/CurrPage} / {/TotalPage}"); try (XWPFDocument docx = new XWPFDocument(new ByteArrayInputStream(service.apply(source(), List.of(mark))))) { String xml = docx.getHeaderList().get(0)._getHdrFtr().xmlText(); assertTrue(xml.contains(" PAGE ")); assertTrue(xml.contains(" NUMPAGES ")); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); var header = factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); var paragraphAlignment = (org.w3c.dom.Element) header.getElementsByTagNameNS("*", "jc").item(0); assertEquals(align.toLowerCase(java.util.Locale.ROOT), paragraphAlignment.getAttributeNS( "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "val")); var body = (org.w3c.dom.Element) header.getElementsByTagNameNS("*", "bodyPr").item(0); assertEquals(anchors[index], body.getAttribute("anchor")); } } } } private void assertAligned(int before, int after, String alignment) { if ("LEFT".equals(alignment) || "TOP".equals(alignment)) assertTrue(alignment + " margin " + before, before <= 1); else if ("RIGHT".equals(alignment) || "BOTTOM".equals(alignment)) assertTrue(alignment + " margin " + after, after <= 1); else assertTrue("Center margins differ: " + before + "/" + after, Math.abs(before - after) <= 2); } private BufferedImage pdf(DmsWatermarkSnapshot mark) throws Exception { return ImageIO.read(new ByteArrayInputStream(service.renderPdfOverlay(60, 40, List.of(mark)))); } private int[] pixels(BufferedImage image) { return image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth()); } private Rectangle union(List lines) { return lines.stream().reduce(Rectangle::union).orElseThrow(); } private List lineInk(BufferedImage image) { List lines = new ArrayList<>(); Rectangle line = null; for (int y = 0; y < image.getHeight(); y++) { Rectangle row = null; for (int x = 0; x < image.getWidth(); x++) { if ((image.getRGB(x, y) >>> 24) != 0) { Rectangle pixel = new Rectangle(x, y, 1, 1); row = row == null ? pixel : row.union(pixel); } } if (row != null) line = line == null ? row : line.union(row); else if (line != null) { lines.add(line); line = null; } } if (line != null) lines.add(line); return lines; } private DmsWatermarkSnapshot mark() { DmsWatermarkSnapshot mark = new DmsWatermarkSnapshot(); mark.setWatermarkType("TEXT"); mark.setContent("WWWW\nI"); mark.setFontFamily("SansSerif"); mark.setFontColor("#000000"); mark.setFontSize(14); mark.setOpacity(100); mark.setRotationAngle(0); mark.setLayoutMode("POSITION"); mark.setMarkWidthMm(new BigDecimal("60")); mark.setMarkHeightMm(new BigDecimal("40")); DmsWatermarkPositionSnapshot position = new DmsWatermarkPositionSnapshot(); position.setPaperWidthMm(new BigDecimal("40")); position.setPaperHeightMm(new BigDecimal("60")); position.setPortraitXMm(BigDecimal.ZERO); position.setPortraitYMm(BigDecimal.ZERO); position.setLandscapeXMm(BigDecimal.ZERO); position.setLandscapeYMm(BigDecimal.ZERO); mark.setPositions(List.of(position)); return mark; } private byte[] source() throws Exception { try (XWPFDocument docx = new XWPFDocument(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { docx.createParagraph().createRun().setText("Body stays unchanged"); docx.write(output); return output.toByteArray(); } } }