Java визуализирует файл docx и создает PDF с водяным знаком

Java задняя часть внешний интерфейс регулярное выражение

Оригинальный адрес:Java визуализирует файл docx и создает PDF с водяным знаком

Недавно сделал более интересное требование, да и реализация поинтереснее.

необходимость:

  1. Пользователь загружает файл docx с несколькими заполнителями в документе, который идентифицируется как шаблон документа.
  2. На переднем конце пользователи могут перетаскивать теги на шаблоны для замены заполнителей.
  3. Бэкенд получает содержимое этикетки в соответствии с этикеткой, генерирует pdf-документ и добавляет водяной знак.

Трудности реализации требований:

  1. Файлы шаблонов поступают из таких ролей, как бизнес, финансы, исполнительная власть и т. д., и невозможно использовать языки разметки шаблонов, обычно используемые аналогичными (freemark, speed, Thymeleaf) технологиями.
  2. После того, как документ загружен, его необходимо проанализировать, чтобы сгенерировать html для внешнего интерфейса, чтобы перетаскивать теги, и окончательный документ, который нужно отобразить, — это pdf. Поскольку сгенерированный PDF-файл является официальным документом, формат должен быть строго гарантирован.
  3. Если интерфейс напрямую использует редактор форматированного текста, в настоящее время нет удовлетворительной реализации с открытым исходным кодом, а самостоятельно разработанный форматированный текст требует чрезвычайно высокого технического содержания. Так что не рассматривайте возможность редактора форматированного текста.

Исследование технологий и выбор технологий (технологический стек Java):

1. Преобразование формата документа docx:

После поиска в Google я нашел этот ответ на StackOverflow:Converting docx into pdf in javaИспользуйте следующий пакет jar:

Apache POI 3.15
org.apache.poi.xwpf.converter.core-1.0.6.jar
org.apache.poi.xwpf.converter.pdf-1.0.6.jar
fr.opensagres.xdocreport.itext.extension-2.0.0.jar
itext-2.1.7.jar
ooxml-schemas-1.3.jar

На самом деле, после написания демо-теста я обнаружил, что это сочетание и неисправность не могут быть дружественными к сложным документам docx, код не является строгим, время от времени выбрасываются исключения Nullpoint и возникают необъяснимые конфликты пакетов jar. проблема в том, что формат строго не гарантируется. Различные проблемы могут возникнуть со сложными серийными номерами. проходят.

Второй способ, используяLibreOffice, LibreOffice предоставляет набор API-интерфейсов, которые могут быть предоставлены для вызовов программ Java. так что используйтеjodconverterдля вызова LibreOffice. Учебники, которые я нашел в Интернете, давно устарели. jodconverter уже давно выпустил версию 4.2. Самый надежный документ — смотреть прямо на официальный.wiki.

2. Визуализация шаблона

Первая идея состоит в том, чтобы заменить docx простым текстовым форматом html, а затем использовать существующий механизм шаблонов Java (freemark, speed) для отображения содержимого. Однако при преобразовании файлов docx в html все равно будет большая потеря формата. проходят.

Второй способ мышления. Прямое управление документами docx Замените заполнители содержимым непосредственно в документе docx. Это гарантирует, что форматирование не потеряется, но не существует готового механизма шаблонов, поддерживающего рендеринг docx. Вам нужно реализовать это самостоятельно.

3. Водяной знак

Это относительно просто, используйте напрямуюitextpdfБесплатная версия решит проблему. Следует отметить проблему с китайским шрифтом, ниже будет объяснено шаг за шагом.

Ключевые технологии реализации технологии реализации:

Использование jodconverter + libreoffice

jodconverterПолный наборspring-bootрешение, простоpom.xmlДобавьте следующую конфигурацию в:

<dependency>
    <groupId>org.jodconverter</groupId>
    <artifactId>jodconverter-local</artifactId>
    <version>4.2.0</version>
</dependenc>
<dependency>
    <groupId>org.jodconverter</groupId>
    <artifactId>jodconverter-spring-boot-starter</artifactId>
    <version>4.2.0</version>
</dependency>

Добавьте класс конфигурации:


@Configuration
public class ApplicationConfig {
    @Autowired
    private OfficeManager officeManager;
    @Bean
    public DocumentConverter documentConverter(){
        return LocalConverter.builder()
                .officeManager(officeManager)
                .build();
    }
}

в файле конфигурацииapplication.propertiesдобавлять:

# libreoffice 安装目录
jodconverter.local.office-home=/Applications/LibreOffice.app/Contents 
# 开启jodconverter
jodconverter.local.enabled=true

Используйте напрямую:

@Autowired
private DocumentConverter documentConverter;
private byte[] docxToPDF(InputStream inputStream) {
    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
        documentConverter
                .convert(inputStream)
                .as(DefaultDocumentFormatRegistry.DOCX)
                .to(byteArrayOutputStream)
                .as(DefaultDocumentFormatRegistry.PDF)
                .execute();
        return byteArrayOutputStream.toByteArray();
    } catch (OfficeException | IOException e) {
        log.error("convert pdf error");
    }
    return null;
}    

Просто конвертируйте docx в pdf. Обратите внимание, что потоки должны быть закрыты, чтобы предотвратить утечку памяти.

Отрисовка шаблона:

Посмотрите прямо на код:


@Service
public class OfficeService{

    //占位符 {}
    private static final Pattern SymbolPattern = Pattern.compile("\\{(.+?)\\}", Pattern.CASE_INSENSITIVE);

    public byte[] replaceSymbol(InputStream inputStream,Map<String,String> symbolMap) throws IOException {
        XWPFDocument doc = new XWPFDocument(inputStream)        
        replaceSymbolInPara(doc,symbolMap);
        replaceInTable(doc,symbolMap)       
        try(ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            doc.write(os);
            return os.toByteArray();
        }finally {
            inputStream.close();
        }
    }


    private int replaceSymbolInPara(XWPFDocument doc,Map<String,String> symbolMap){
        XWPFParagraph para;
        Iterator<XWPFParagraph> iterator = doc.getParagraphsIterator();
        while(iterator.hasNext()){
            para = iterator.next();
            replaceInPara(para,symbolMap);
        }
    }

    //替换正文
    private void replaceInPara(XWPFParagraph para,Map<String,String> symbolMap) {

        List<XWPFRun> runs;
        if (symbolMatcher(para.getParagraphText()).find()) {
            String text = para.getParagraphText();
            Matcher matcher3 = SymbolPattern.matcher(text);
            while (matcher3.find()) {
                String group = matcher3.group(1);
                String symbol = symbolMap.get(group);
                if (StringUtils.isBlank(symbol)) {
                    symbol = " ";
                }
                text = matcher3.replaceFirst(symbol);
                matcher3 = SymbolPattern.matcher(text);
            }
            runs = para.getRuns();
            String fontFamily = runs.get(0).getFontFamily();
            int fontSize = runs.get(0).getFontSize();
            XWPFRun xwpfRun = para.insertNewRun(0);
            xwpfRun.setFontFamily(fontFamily);
            xwpfRun.setText(text);
            if(fontSize > 0) {
                xwpfRun.setFontSize(fontSize);
            }
            int max = runs.size();
            for (int i = 1; i < max; i++) {
                para.removeRun(1);
            }

        }
    }

    //替换表格
    private void replaceInTable(XWPFDocument doc,Map<String,String> symbolMap) {
        Iterator<XWPFTable> iterator = doc.getTablesIterator();
        XWPFTable table;
        List<XWPFTableRow> rows;
        List<XWPFTableCell> cells;
        List<XWPFParagraph> paras;
        while (iterator.hasNext()) {
            table = iterator.next();
            rows = table.getRows();
            for (XWPFTableRow row : rows) {
                cells = row.getTableCells();
                for (XWPFTableCell cell : cells) {
                    paras = cell.getParagraphs();
                    for (XWPFParagraph para : paras) {
                        replaceInPara(para,symbolMap);
                    }
                }
            }
        }
    }
}

Здесь требуется особое внимание:

  1. В проанализированном документе,para.getParagraphText()Относится к получению абзацев,para.getRuns()Должен относиться к слову приобретения. Но вот проблема, разделение полученных тиражей — загадка. В настоящее время я не нашел правила, очень вероятно, что наши плейсхолдеры разделены на несколькоrun, если мы просто нацелимсяrunвыполните замену регулярных выражений, но сначала поместите всеrunsОбъедините их, а затем выполните обычную замену.
  2. вызовpara.insertNewRun()когдаrunСтиль шрифта и размер шрифта не будут поддерживаться, и их необходимо получить и установить вручную. Поскольку реализованы два приведенных выше медовых сока, можно написать только кусок кода медового сока, чтобы обеспечить регулярную замену и правильный формат.

Метод испытания:

@Test
public void replaceSymbol() throws IOException {
    File file = new File("symbol.docx");
    InputStream inputStream = new FileInputStream(file);

    File outputFile = new File("out.docx");
    FileOutputStream outputStream = new FileOutputStream(outputFile);
    Map<String,String> map = new HashMap<>();
    map.put("tableName","水果价目表");
    map.put("name","苹果");	
    map.put("price","1.5/斤");
    byte[] bytes = office.replaceSymbol(inputStream, map, );

    outputStream.write(bytes);
}

replaceSymbol()Метод принимает два параметра: один — поток данных входного файла docx, а другой — карта заполнителей и содержимого.

Перед использованием этого метода:

before

После использования:

after

Добавить водяной знак:

pom.xmlНужно добавить:

<!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf -->
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.5.13</version>
</dependency>

Код для добавления водяного знака:

    public byte[] addWatermark(InputStream inputStream,String watermark) throws IOException, DocumentException {

        PdfReader reader = new PdfReader(inputStream);
        try(ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            PdfStamper stamper = new PdfStamper(reader, os);
            int total = reader.getNumberOfPages() + 1;
            PdfContentByte content;
            // 设置字体
            BaseFont baseFont = BaseFont.createFont("simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            // 循环对每页插入水印
            for (int i = 1; i < total; i++) {
                // 水印的起始
                content = stamper.getUnderContent(i);
                // 开始
                content.beginText();
                // 设置颜色
                content.setColorFill(new BaseColor(244, 244, 244));
                // 设置字体及字号
                content.setFontAndSize(baseFont, 50);
                // 设置起始位置
                content.setTextMatrix(400, 780);
                for (int x = 0; x < 5; x++) {
                    for (int y = 0; y < 5; y++) {
                        content.showTextAlignedKerned(Element.ALIGN_CENTER,
                                watermark,
                                (100f + x * 350),
                                (40.0f + y * 150),
                                30);
                    }
                }
                content.endText();
            }
            stamper.close();
            return os.toByteArray();
        }finally {
            reader.close();
        }

    }


Шрифт:

  1. Шрифты одинаково важны при работе с документами, если вы используете шрифт, которого нет в libreOffice, например Arial. нужно поставить файл шрифтаxxx.ttf
cp xxx.ttc /usr/share/fonts
fc-cache -fv
  1. itextpdfКитайские иероглифы не поддерживаются, необходимо указать дополнительные шрифты:
//字体路径
String fontPath = "simsun.ttf"
//设置字体
BaseFont baseFont = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

постскриптум

В целом требование очень интересное, но когда я поискал, то обнаружил, что качество китайских документов действительно настораживает, либо крайне устаревшие, либо все плагиатят друг друга. Чтобы запросить техническую документацию проекта, лучший путь должен быть следующим:

Официальный сайт проекта Начало работы == github demo > StackOverflow >>CSDN >> Baidu знает

Добро пожаловать в мой публичный аккаунт WeChat

二维码