package jnpf.fileStorage.dms; import com.google.zxing.BinaryBitmap; import com.google.zxing.MultiFormatReader; import com.google.zxing.RGBLuminanceSource; import com.google.zxing.common.HybridBinarizer; import jnpf.dmsEntity.preview.model.DmsWatermarkPositionSnapshot; import jnpf.dmsEntity.preview.model.DmsWatermarkSnapshot; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.wp.usermodel.HeaderFooterType; import org.junit.Test; import javax.imageio.ImageIO; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.regex.Pattern; import java.util.function.Consumer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; public class DmsDocxWatermarkServiceTest { static { System.setProperty("java.awt.headless", "true"); } @Test public void addsRepeatedTextWatermarksWithoutChangingBodyText() throws Exception { byte[] source = sourceDocument(); DmsWatermarkSnapshot watermark = watermark("TILED"); byte[] result = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)) .apply(source, Collections.singletonList(watermark)); Path sample = Path.of("target", "watermark-preview", "dms-onlyoffice-watermark.docx"); Files.createDirectories(sample.getParent()); Files.write(sample, result); try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(result))) { assertEquals("受控文件正文", document.getParagraphArray(0).getText()); assertEquals(1, document.getHeaderList().size()); String headerXml = document.getHeaderList().get(0)._getHdrFtr().xmlText(); assertTrue(headerXml.contains("DMS_WATERMARK_")); assertTrue(headerXml.contains("behindDoc=\"true\"")); assertTrue(document.getHeaderList().get(0).getText().contains("原始页眉")); assertTrue(document.getFooterList().get(0).getText().contains("原始页脚")); assertEquals(1, document.getPackage().getPartsByName(Pattern.compile("/word/media/.*")).size()); } } @Test @SuppressWarnings("unchecked") public void loadsImageWatermarkFromFileCommon() throws Exception { DmsCommonFileStorage storage = mock(DmsCommonFileStorage.class); byte[] image = imageBytes(); doAnswer(invocation -> { Consumer consumer = invocation.getArgument(2); consumer.accept(new ByteArrayInputStream(image)); return null; }).when(storage).download(eq("annex"), eq("company-logo.png"), any()); DmsWatermarkSnapshot watermark = watermark("POSITION"); watermark.setWatermarkType("IMAGE"); watermark.setImageUrl("/api/file/Image/annex/company-logo.png"); byte[] result = new DmsDocxWatermarkService(storage) .apply(sourceDocument(), Collections.singletonList(watermark)); try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(result))) { assertEquals(1, document.getPackage().getPartsByName(Pattern.compile("/word/media/.*")).size()); assertTrue(document.getHeaderList().get(0)._getHdrFtr().xmlText().contains("DMS_WATERMARK_")); } verify(storage).download(eq("annex"), eq("company-logo.png"), any()); } @Test public void addsQrCodeAsHeaderImage() throws Exception { DmsWatermarkSnapshot watermark = watermark("POSITION"); watermark.setWatermarkType("BARCODE"); watermark.setBarcodeFormat("QR_CODE"); watermark.setContent("玻思韬-TMS需求文档.docx-D(00)"); byte[] result = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)) .apply(sourceDocument(), Collections.singletonList(watermark)); try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(result))) { assertEquals(1, document.getPackage().getPartsByName(Pattern.compile("/word/media/.*")).size()); document.getHeaderList().forEach(header -> { String headerXml = header._getHdrFtr().xmlText(); assertTrue(headerXml.contains("DMS_WATERMARK_")); assertTrue(headerXml.contains("relationships")); }); } } @Test public void chineseQrCodeRetainsContentAndSmallCodesAreNotCropped() throws Exception { DmsDocxWatermarkService service = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)); DmsWatermarkSnapshot watermark = watermark("POSITION"); watermark.setBarcodeFormat("QR_CODE"); watermark.setContent("玻思韬-TMS需求文档.docx-D(00)"); assertEquals(watermark.getContent(), decode(service.barcode(watermark, 454, 113))); assertEquals(watermark.getContent(), decode(service.barcode(watermark, 6, 6))); watermark.setBarcodeFormat("CODE_128"); watermark.setContent("QC-SOP-01200-D(00)"); assertEquals(watermark.getContent(), decode(service.barcode(watermark, 454, 113))); } @Test public void displaysBarcodeTextBelowQrCodeWhenEnabled() throws Exception { DmsDocxWatermarkService service = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)); DmsWatermarkSnapshot watermark = watermark("POSITION"); watermark.setWatermarkType("BARCODE"); watermark.setBarcodeFormat("QR_CODE"); watermark.setContent("QC-SOP-01200"); watermark.setShowBarcodeText(true); BufferedImage image = service.barcode(watermark, 454, 113); assertEquals(watermark.getContent(), decode(image)); assertTrue(darkPixels(image, image.getHeight() - 12) > 0); } @Test public void preservesPageVariablesAsLiveFieldsInAllHeaderTypes() throws Exception { byte[] source; try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(sourceDocument())); ByteArrayOutputStream output = new ByteArrayOutputStream()) { document.createHeader(HeaderFooterType.FIRST).createParagraph().createRun().setText("首页页眉"); document.createHeader(HeaderFooterType.EVEN).createParagraph().createRun().setText("偶数页页眉"); document.getDocument().getBody().getSectPr().addNewTitlePg(); document.setEvenAndOddHeadings(true); document.createParagraph().setPageBreak(true); document.createParagraph().createRun().setText("第二页正文"); document.write(output); source = output.toByteArray(); } DmsWatermarkSnapshot watermark = watermark("POSITION"); watermark.setContent("第 {/CurrPage} 页 / 共 {/TotalPage} 页 <&>"); watermark.setRotationAngle(0); byte[] result = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)) .apply(source, Collections.singletonList(watermark)); try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(result))) { assertEquals(3, document.getHeaderList().size()); for (org.apache.poi.xwpf.usermodel.XWPFHeader header : document.getHeaderList()) { String xml = header._getHdrFtr().xmlText(); assertTrue(xml.contains(" PAGE ")); assertTrue(xml.contains(" NUMPAGES ")); assertTrue(xml.contains("txbxContent")); assertTrue(javax.xml.parsers.DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(new ByteArrayInputStream(xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))) .getDocumentElement().getTextContent().contains("<&>")); assertTrue(!xml.contains("{/CurrPage}") && !xml.contains("{/TotalPage}")); assertTrue(xml.contains("70000")); // 30% visible means 70% transparent } assertEquals(0, document.getPackage().getPartsByName(Pattern.compile("/word/media/.*")).size()); assertEquals("受控文件正文", document.getParagraphArray(0).getText()); assertEquals("第二页正文", document.getParagraphs().get(document.getParagraphs().size() - 1).getText()); assertTrue(document.getFooterList().get(0).getText().contains("原始页脚")); } } @Test public void textInkStartsAtConfiguredPageCoordinates() throws Exception { DmsDocxWatermarkService service = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)); DmsWatermarkSnapshot watermark = watermark("POSITION"); watermark.setContent("I"); watermark.setFontFamily("SansSerif"); watermark.setFontSize(14); watermark.setRotationAngle(0); watermark.setOpacity(100); watermark.setMarkWidthMm(new BigDecimal("100")); watermark.setMarkHeightMm(new BigDecimal("20")); DmsWatermarkPositionSnapshot position = watermark.getPositions().get(0); position.setPortraitXMm(BigDecimal.ZERO); position.setPortraitYMm(BigDecimal.ZERO); BufferedImage pdfOverlay = ImageIO.read(new ByteArrayInputStream( service.renderPdfOverlay(210, 297, List.of(watermark)))); assertInkStartsAt(pdfOverlay, 0, 0); BufferedImage image = new BufferedImage(2100, 2970, BufferedImage.TYPE_INT_ARGB); service.applyImageWatermarks(image, List.of(watermark)); assertInkStartsAt(image, 0, 0); position.setPortraitXMm(new BigDecimal("10")); position.setPortraitYMm(new BigDecimal("15")); BufferedImage shifted = ImageIO.read(new ByteArrayInputStream( service.renderPdfOverlay(210, 297, List.of(watermark)))); assertInkStartsAt(shifted, (int) Math.round(10 * 144 / 25.4), (int) Math.round(15 * 144 / 25.4)); } @Test public void textInkStartsAtPageEdgeInDocxAndSelectedPageOverlays() throws Exception { DmsDocxWatermarkService service = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)); DmsWatermarkSnapshot watermark = watermark("POSITION"); watermark.setContent("I"); watermark.setFontFamily("SansSerif"); watermark.setFontSize(14); watermark.setRotationAngle(0); watermark.setOpacity(100); watermark.setMarkWidthMm(new BigDecimal("100")); watermark.setMarkHeightMm(new BigDecimal("20")); watermark.getPositions().get(0).setPortraitXMm(BigDecimal.ZERO); watermark.getPositions().get(0).setPortraitYMm(BigDecimal.ZERO); byte[] docx = service.apply(sourceDocument(), List.of(watermark)); Path sample = Path.of("target", "watermark-preview", "dms-text-origin.docx"); Files.createDirectories(sample.getParent()); Files.write(sample, docx); try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(docx))) { List pictures = document.getPackage() .getPartsByName(Pattern.compile("/word/media/.*")); assertEquals(1, pictures.size()); assertInkStartsAt(ImageIO.read(pictures.get(0).getInputStream()), 0, 0); } watermark.setRenderPages(List.of(1)); jnpf.onlyoffice.PageWatermarkRequest.Variant variant = service.pageOverlays( sourceDocument(), List.of(watermark)).get(0).getVariants().get(0); assertEquals(0D, variant.getOffsetXMm(), 0D); assertEquals(0D, variant.getOffsetYMm(), 0D); assertInkStartsAt(ImageIO.read(new ByteArrayInputStream( Base64.getDecoder().decode(variant.getPngBase64()))), 0, 0); } @Test public void pageNumberTextBoxStartsAtItsPageAnchor() throws Exception { DmsWatermarkSnapshot watermark = watermark("POSITION"); watermark.setContent("{/CurrPage}/{/TotalPage}"); watermark.setFontFamily("SansSerif"); watermark.setRotationAngle(0); watermark.getPositions().get(0).setPortraitXMm(BigDecimal.ZERO); watermark.getPositions().get(0).setPortraitYMm(BigDecimal.ZERO); byte[] docx = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)) .apply(sourceDocument(), List.of(watermark)); Path sample = Path.of("target", "watermark-preview", "dms-page-field-origin.docx"); Files.createDirectories(sample.getParent()); Files.write(sample, docx); try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(docx))) { String xml = document.getHeaderList().get(0)._getHdrFtr().xmlText(); javax.xml.parsers.DocumentBuilderFactory factory = javax.xml.parsers.DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); org.w3c.dom.Document header = factory.newDocumentBuilder().parse( new ByteArrayInputStream(xml.getBytes(java.nio.charset.StandardCharsets.UTF_8))); org.w3c.dom.Element textbox = (org.w3c.dom.Element) header .getElementsByTagNameNS("*", "txbxContent").item(0); org.w3c.dom.NodeList alignment = textbox.getElementsByTagNameNS("*", "jc"); assertTrue(alignment.getLength() > 0); for (int i = 0; i < alignment.getLength(); i++) { assertEquals("left", ((org.w3c.dom.Element) alignment.item(i)).getAttributeNS( "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "val")); } org.w3c.dom.Element body = (org.w3c.dom.Element) header.getElementsByTagNameNS("*", "bodyPr").item(0); assertEquals("t", body.getAttribute("anchor")); assertEquals("0", body.getAttribute("lIns")); assertEquals("0", body.getAttribute("tIns")); } } @Test public void tiledPageFieldsCoexistWithStaticWatermarks() throws Exception { DmsWatermarkSnapshot pages = watermark("TILED"); pages.setContent("第{/CurrPage}页,共{/TotalPage}页"); byte[] result = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)) .apply(sourceDocument(), java.util.Arrays.asList(watermark("POSITION"), pages)); Path sample = Path.of("target", "watermark-preview", "dms-page-fields.docx"); Files.createDirectories(sample.getParent()); Files.write(sample, result); try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(result))) { String xml = document.getHeaderList().get(0)._getHdrFtr().xmlText(); assertTrue(xml.split(" PAGE ", -1).length > 2); assertTrue(xml.contains("19800000")); // -30 degrees normalized for DrawingML assertTrue(!xml.contains("{/CurrPage}")); assertEquals(1, document.getPackage().getPartsByName(Pattern.compile("/word/media/.*")).size()); } } @Test public void positionWatermarkIsKeptInsidePageBounds() throws Exception { DmsWatermarkSnapshot watermark = watermark("POSITION"); watermark.setContent("{/CurrPage}"); watermark.setOpacity(100); watermark.getPositions().get(0).setPortraitXMm(new BigDecimal("165")); watermark.getPositions().get(0).setPortraitYMm(new BigDecimal("290")); byte[] result = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)) .apply(sourceDocument(), Collections.singletonList(watermark)); try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(result))) { String xml = document.getHeaderList().get(0)._getHdrFtr().xmlText(); java.util.regex.Matcher horizontal = Pattern.compile( "positionH[^>]*>.*?posOffset>(-?\\d+)", Pattern.DOTALL).matcher(xml); java.util.regex.Matcher vertical = Pattern.compile( "positionV[^>]*>.*?posOffset>(-?\\d+)", Pattern.DOTALL).matcher(xml); assertTrue(horizontal.find()); assertTrue(vertical.find()); assertTrue(Long.parseLong(horizontal.group(1)) >= 0 && Long.parseLong(horizontal.group(1)) < 5940000L); // less than configured 165 mm assertTrue(Long.parseLong(vertical.group(1)) >= 0 && Long.parseLong(vertical.group(1)) < 10440000L); // less than configured 290 mm assertTrue(xml.contains(" PAGE ")); assertTrue(Pattern.compile("alpha[^>]*val=\\\"0\\\"").matcher(xml).find()); } } @Test public void selectsA4AndA3CoordinatesForBothPageOrientations() throws Exception { DmsWatermarkSnapshot mark = watermark("POSITION"); mark.setContent("CHECK"); mark.setFontFamily("SansSerif"); mark.setRotationAngle(0); mark.setOpacity(100); mark.setMarkWidthMm(new BigDecimal("30")); mark.setMarkHeightMm(new BigDecimal("15")); DmsWatermarkPositionSnapshot a4 = mark.getPositions().get(0); a4.setPortraitXMm(new BigDecimal("10")); a4.setPortraitYMm(new BigDecimal("10")); a4.setLandscapeXMm(new BigDecimal("200")); a4.setLandscapeYMm(new BigDecimal("10")); DmsWatermarkPositionSnapshot a3 = new DmsWatermarkPositionSnapshot(); a3.setPaperType("A3"); a3.setPaperWidthMm(new BigDecimal("297")); a3.setPaperHeightMm(new BigDecimal("420")); a3.setPortraitXMm(new BigDecimal("10")); a3.setPortraitYMm(new BigDecimal("360")); a3.setLandscapeXMm(new BigDecimal("350")); a3.setLandscapeYMm(new BigDecimal("10")); mark.setPositions(List.of(a4, a3)); DmsDocxWatermarkService service = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)); double[][] cases = { {210, 297, 10, 10}, {297, 210, 200, 10}, {297, 420, 10, 360}, {420, 297, 350, 10} }; for (double[] page : cases) { BufferedImage result = ImageIO.read(new ByteArrayInputStream( service.renderPdfOverlay(page[0], page[1], List.of(mark)))); int x = (int) Math.round(page[2] * 144 / 25.4); int y = (int) Math.round(page[3] * 144 / 25.4); int width = (int) Math.round(30 * 144 / 25.4); int height = (int) Math.round(15 * 144 / 25.4); int visiblePixels = 0; for (int row = 0; row < result.getHeight(); row++) { for (int column = 0; column < result.getWidth(); column++) { if ((result.getRGB(column, row) >>> 24) == 0) continue; visiblePixels++; assertTrue("Watermark used the wrong paper size or orientation", column >= x && column < x + width && row >= y && row < y + height); } } assertTrue("Expected a visible watermark", visiblePixels > 0); } } @Test public void independentDisplayTextChangesOnlyTheLabelForBothBarcodeFormats() throws Exception { DmsDocxWatermarkService service = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)); for (String format : List.of("QR_CODE", "CODE_128")) { DmsWatermarkSnapshot mark = watermark("POSITION"); mark.setWatermarkType("BARCODE"); mark.setBarcodeFormat(format); mark.setContent("REC-2026-0004"); mark.setShowBarcodeText(true); mark.setBarcodeText("Page four label"); BufferedImage first = service.barcode(mark, 454, 227); mark.setBarcodeText("另一个独立文字"); BufferedImage second = service.barcode(mark, 454, 227); assertEquals("REC-2026-0004", decode(first)); assertEquals("REC-2026-0004", decode(second)); assertTrue("Changing the display text must change the rendered label", !Arrays.equals( first.getRGB(0, 0, first.getWidth(), first.getHeight(), null, 0, first.getWidth()), second.getRGB(0, 0, second.getWidth(), second.getHeight(), null, 0, second.getWidth()))); } } @Test public void repeatedPageEntriesRemainSeparateDrawingsWithTheirOwnPayloads() throws Exception { DmsDocxWatermarkService service = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)); DmsWatermarkSnapshot first = watermark("POSITION"); first.setWatermarkType("BARCODE"); first.setBarcodeFormat("QR_CODE"); first.setContent("REC-004"); first.setBarcodeText("First on page four"); first.setShowBarcodeText(true); first.setRenderPages(List.of(4)); first.setRotationAngle(0); first.setOpacity(100); first.setMarkWidthMm(new BigDecimal("40")); first.setMarkHeightMm(new BigDecimal("45")); DmsWatermarkSnapshot second = new DmsWatermarkSnapshot(); org.springframework.beans.BeanUtils.copyProperties(first, second); second.setContent("SECOND-004"); second.setBarcodeText("Second on page four"); List overlays = service.pageOverlays(sourceDocument(), List.of(first, second, first)); assertEquals(3, overlays.size()); for (int index = 0; index < overlays.size(); index++) { assertEquals(List.of(4), overlays.get(index).getPages()); jnpf.onlyoffice.PageWatermarkRequest.Variant variant = overlays.get(index).getVariants().get(0); assertEquals(210D, variant.getWidthMm(), 0.2D); assertEquals(297D, variant.getHeightMm(), 0.2D); assertEquals(40D, variant.getDrawingWidthMm(), 0.01D); assertEquals(45D, variant.getDrawingHeightMm(), 0.01D); assertEquals(65D, variant.getOffsetXMm(), 0.2D); assertEquals(138D, variant.getOffsetYMm(), 0.2D); BufferedImage image = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(variant.getPngBase64()))); assertEquals(index == 1 ? "SECOND-004" : "REC-004", decode(image)); } } private String decode(BufferedImage transparent) throws Exception { BufferedImage white = new BufferedImage(transparent.getWidth() * 4, transparent.getHeight() * 4, BufferedImage.TYPE_INT_RGB); Graphics2D graphics = white.createGraphics(); graphics.setColor(Color.WHITE); graphics.fillRect(0, 0, white.getWidth(), white.getHeight()); graphics.drawImage(transparent, 0, 0, white.getWidth(), white.getHeight(), null); graphics.dispose(); int[] pixels = white.getRGB(0, 0, white.getWidth(), white.getHeight(), null, 0, white.getWidth()); return new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer( new RGBLuminanceSource(white.getWidth(), white.getHeight(), pixels)))).getText(); } private int darkPixels(BufferedImage image, int startY) { int count = 0; for (int y = Math.max(0, startY); y < image.getHeight(); y++) { for (int x = 0; x < image.getWidth(); x++) { int argb = image.getRGB(x, y); if ((argb >>> 24) != 0 && (argb & 0x00ffffff) == 0) { count++; } } } return count; } private void assertInkStartsAt(BufferedImage image, int expectedX, int expectedY) { int left = image.getWidth(); int top = image.getHeight(); for (int y = 0; y < image.getHeight(); y++) { for (int x = 0; x < image.getWidth(); x++) { if ((image.getRGB(x, y) >>> 24) > 0) { left = Math.min(left, x); top = Math.min(top, y); } } } assertEquals("First visible text pixel should start at configured X", expectedX, left); assertEquals("First visible text pixel should start at configured Y", expectedY, top); } @Test public void returnsOriginalBytesWhenNoWatermarkApplies() { byte[] source = new byte[]{1, 2, 3}; byte[] result = new DmsDocxWatermarkService(mock(DmsCommonFileStorage.class)) .apply(source, Collections.emptyList()); assertTrue(source == result); } private DmsWatermarkSnapshot watermark(String layout) { DmsWatermarkSnapshot watermark = new DmsWatermarkSnapshot(); watermark.setWatermarkType("TEXT"); watermark.setContent("张三 预览受控文件"); watermark.setFontFamily("SimHei"); watermark.setFontColor("#666666"); watermark.setFontSize(24); watermark.setRotationAngle(-30); watermark.setOpacity(30); watermark.setLayoutMode(layout); watermark.setMarkWidthMm(new BigDecimal("80")); watermark.setMarkHeightMm(new BigDecimal("20")); watermark.setRepeatSpacingXMm(new BigDecimal("20")); watermark.setRepeatSpacingYMm(new BigDecimal("20")); DmsWatermarkPositionSnapshot position = new DmsWatermarkPositionSnapshot(); position.setPaperType("A4"); position.setPaperWidthMm(new BigDecimal("210")); position.setPaperHeightMm(new BigDecimal("297")); position.setPortraitXMm(new BigDecimal("65")); position.setPortraitYMm(new BigDecimal("138")); position.setLandscapeXMm(new BigDecimal("108")); position.setLandscapeYMm(new BigDecimal("95")); watermark.setPositions(Collections.singletonList(position)); return watermark; } private byte[] sourceDocument() throws Exception { try (XWPFDocument document = new XWPFDocument(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { document.createParagraph().createRun().setText("受控文件正文"); document.createHeader(HeaderFooterType.DEFAULT).createParagraph() .createRun().setText("原始页眉"); document.createFooter(HeaderFooterType.DEFAULT).createParagraph() .createRun().setText("原始页脚"); for (int i = 1; i <= 20; i++) { document.createParagraph().createRun().setText("现行版文件预览内容示例 " + i); } document.write(output); return output.toByteArray(); } } private byte[] imageBytes() throws Exception { BufferedImage image = new BufferedImage(160, 80, BufferedImage.TYPE_INT_ARGB); Graphics2D graphics = image.createGraphics(); try { graphics.setColor(new Color(30, 120, 150)); graphics.fillRect(0, 0, image.getWidth(), image.getHeight()); } finally { graphics.dispose(); } ByteArrayOutputStream output = new ByteArrayOutputStream(); ImageIO.write(image, "png", output); return output.toByteArray(); } }