JavaによるWord文書のキーワード置換とPDF変換の実装

Word文書のキーワード置換とPDF変換の課題

Word文書内のキーワードを置換し、PDF形式に変換する処理において、主に以下の課題が存在します:

  • Wordの内部構造ではテキストが複数のRunコンポーネントに分割されるため、キーワードが複数のRunに跨って存在する可能性がある
  • 単純な文字列置換では元の書式情報が失われる
  • ExcelからコピーされたテーブルのPDF変換時にコンテンツ消失の問題が発生する

解決アプローチ

段落全体のテキストを取得し、キーワードの存在を確認した後、キーワードの開始位置と終了位置を含むRunを特定して処理します。先頭と末尾のRunは部分的な置換を行い、中間のRunは空文字列に置換することで、書式を保持したキーワード置換を実現します。

実装コード

public class WordPdfConverter {
    
    /**
     * Word文書をPDFに変換し、指定されたパラメータを置換
     */
    public static void convertWordToPdf(InputStream input, OutputStream output, 
                                       PdfOptions pdfOptions, Map<String, String> replacements) throws Exception {
        XWPFDocument document = new XWPFDocument(input);
        replaceKeywords(document, replacements);
        PdfConverter.getInstance().convert(document, output, pdfOptions);
    }
    
    /**
     * ドキュメント内のすべてのキーワードを置換
     */
    private static void replaceKeywords(XWPFDocument doc, Map<String, String> replacementMap) {
        // 段落内のキーワード置換
        for (XWPFParagraph paragraph : doc.getParagraphs()) {
            replaceInParagraph(paragraph, replacementMap);
        }
        
        // テーブルセル内のキーワード置換
        for (XWPFTable table : doc.getTables()) {
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    for (XWPFParagraph paragraph : cell.getParagraphs()) {
                        replaceInParagraph(paragraph, replacementMap);
                    }
                }
            }
        }
    }
    
    /**
     * 個別の段落内でキーワード置換を実行
     */
    private static void replaceInParagraph(XWPFParagraph para, Map<String, String> replacements) {
        for (Map.Entry<String, String> entry : replacements.entrySet()) {
            processKeywordReplacement(para, entry.getKey(), entry.getValue());
        }
    }
    
    /**
     * 特定のキーワードに対する置換処理
     */
    private static void processKeywordReplacement(XWPFParagraph para, String target, String replacement) {
        String paraText = para.getText();
        int startIndex = paraText.indexOf(target);
        
        while (startIndex >= 0) {
            int endIndex = startIndex + target.length();
            int currentPosition = 0;
            boolean foundStart = false;
            
            for (int i = 0; i < para.getRuns().size(); i++) {
                XWPFRun run = para.getRuns().get(i);
                String runContent = run.getText(run.getTextPosition());
                
                if (runContent == null) continue;
                
                if (!foundStart) {
                    if (currentPosition + runContent.length() > startIndex) {
                        // キーワード開始Runの処理
                        String prefix = runContent.substring(0, startIndex - currentPosition);
                        run.setText(prefix + replacement, 0);
                        foundStart = true;
                    }
                    currentPosition += runContent.length();
                } else {
                    if (currentPosition >= endIndex) break;
                    
                    if (currentPosition + runContent.length() <= endIndex) {
                        // 中間Runを空文字に置換
                        run.setText("", 0);
                        currentPosition += runContent.length();
                    } else {
                        // キーワード終了Runの処理
                        String suffix = runContent.substring(endIndex - currentPosition);
                        run.setText(suffix, 0);
                        break;
                    }
                }
            }
            startIndex = paraText.indexOf(target, startIndex + replacement.length());
        }
    }
    
    /**
     * デバッグ用:Runの位置情報を出力
     */
    private static void debugRunPositions(XWPFParagraph para) {
        int pos = 0;
        Map<Integer, XWPFRun> positionMap = new HashMap<>();
        
        for (XWPFRun run : para.getRuns()) {
            String text = run.getText(run.getTextPosition());
            if (text != null) {
                for (int i = 0; i < text.length(); i++) {
                    positionMap.put(pos + i, run);
                }
                pos += text.length();
            }
        }
        System.out.println(positionMap);
    }
}

使用例

public class Main {
    public static void main(String[] args) throws Exception {
        String inputPath = "/path/to/input.docx";
        String outputPath = "/path/to/output.pdf";
        
        InputStream source = new FileInputStream(inputPath);
        OutputStream target = new FileOutputStream(outputPath);
        
        Map<String, String> replacements = new HashMap<>();
        replacements.put("置換対象1", "新しい値1");
        replacements.put("置換対象2", "新しい値2");
        
        PdfOptions options = PdfOptions.create();
        WordPdfConverter.convertWordToPdf(source, target, options, replacements);
    }
}

必要な依存関係

<dependencies>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>3.17</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>3.17</version>
    </dependency>
    <dependency>
        <groupId>fr.opensagres.xdocreport</groupId>
        <artifactId>org.apache.poi.xwpf.converter.pdf</artifactId>
        <version>1.0.6</version>
    </dependency>
</dependencies>

注意事項

  • ExcelからコピーされたテーブルはPDF変換時に内容が消失する可能性がある
  • DOC形式のファイルは事前にDOCX形式に変換する必要がある
  • Runの削除操作が正常に機能しない場合は空文字列への置換で対応

タグ: Java Apache POI Word処理 PDF変換 テキスト置換

7月17日 18:50 投稿