Создайте новый проект Java с Maven

Начнем с создания нового проекта Java с именем pdf_utils с помощью Maven:

mvn archetype:generate \
    -DgroupId=com.pdf.pdf_utils \
    -DartifactId=pdf_utils \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DarchetypeVersion=1.4 \
    -DinteractiveMode=false

Затем откройте файл pdf_utils/pom.xml и добавьте зависимость к PDFBox в разделе dependencies:

<dependencies>
   ...
    <dependency>
      <groupId>org.apache.pdfbox</groupId>
      <artifactId>pdfbox</artifactId>
      <version>2.0.27</version>
    </dependency>
    ...
</dependencies>

Также измените целевую и исходную версии компилятора:

 <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>17</source>
          <target>17</target>
        </configuration>
      </plugin>
</plugins>

Затем переименуйте сгенерированный класс src/main/java/com/pdf/pdf_utils/App.java в PDFUtils.

Как обрезать PDF

Теперь мы разработаем функцию Java, которая обрезает каждую страницу в документе PDF и сохраняет обрезанное содержимое в новом файле PDF. Координаты обрезки, указанные в миллиметрах, будут служить входными данными для функции.

  1. Добавьте вспомогательные функции для преобразования между единицами мм ‹-›
  2. Перевести миллиметры в единицы
  3. Извлеките имя файла документа
  4. Обрезать каждую страницу
  5. Сохраните результат и добавьте «-cropped» к имени выходного файла.
public String cropPDFMM(float x, float y, float width, float height, String srcFilePath) throws IOException {
    // helper functions to convert between mm <-> units
    Function<Float, Float> mmToUnits = (Float a) -> a / 0.352778f;
    Function<Float, Float> unitsToMm = (Float a) -> a * 0.352778f;

    // convert mm to units
    float xUnits = mmToUnits.apply(x);
    float yUnits = mmToUnits.apply(y);
    float widthUnits = mmToUnits.apply(width);
    float heightUnits = mmToUnits.apply(height);

    // extract the doc's file name
    File srcFile = new File(srcFilePath);
    String fileName = srcFile.getName();
    int dotIndex = fileName.lastIndexOf('.');
    String fileNameWithoutExtension =  (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex);

    // crop each page
    PDDocument doc = PDDocument.load(srcFile);
    int nrOfPages = doc.getNumberOfPages();
    PDRectangle newBox = new PDRectangle(
            xUnits,
            yUnits,
            widthUnits,
            heightUnits);
    for (int i = 0; i < nrOfPages; i++) {
        doc.getPage(i).setCropBox(newBox);
    }

    // save the result & append -cropped to the file name
    File outFile = new File(fileNameWithoutExtension + "-cropped.pdf");
    doc.save(outFile);
    doc.close();
    return outFile.getCanonicalPath();
}

Давайте протестируем cropPDFMM, вызвав его из функции main:

public static void main( String[] args )
    {
    String srcFilePath = "/Users/user/.../file.pdf";
    PDFUtils app = new PDFUtils();

    try {
        ///// crop pdf
        float x = 18f;
        float y = 20f;
        float width = 140f;
        float height = 210f;
        String resultFilePath = app.cropPDFMM(x, y, width, height, srcFilePath);

        System.out.println( "Done!" );
    } catch (Exception e) {
        System.out.println(e);
    }
}

Вы должны увидеть файл с именем file-cropped.pdf в текущем каталоге.

Как удалить страницы из PDF

Чтобы удалить определенные страницы из документа, мы можем использовать массив целочисленных диапазонов. Каждый диапазон состоит из начальной и конечной страниц ([startPage1, endPage1, startPage2, endPage2, …]). Функция перебирает каждую страницу документа и проверяет, не выходит ли номер страницы за пределы любого из указанных диапазонов в массиве. Если страница не находится ни в одном диапазоне, она добавляется в новый документ.

  1. Добавьте вспомогательную функцию, чтобы проверить, находится ли страница в диапазоне
  2. Проверьте номер каждой страницы -> добавьте его к временному файлу. документ
  3. Сохраните темп. док. -› перезаписать входной файл
public void removePages(String srcFilePath, Integer[] pageRanges) throws IOException {
     // a helper function to test if a page is within a range
     BiPredicate<Integer, Integer[]> pageInInterval = (Integer page, Integer[] allPages) -> {
         for (int j = 0; j < allPages.length; j+=2) {
             int startPage = allPages[j];
             int endPage = allPages[j+1];
             if (page >= startPage-1 && page < endPage) {
                 return true;
             }
         }
         return false;
     };

     File srcFile = new File(srcFilePath);
     PDDocument pdfDocument = PDDocument.load(srcFile);
     PDDocument tmpDoc = new PDDocument();

     // test if a page is within a range
     // if not, append the page to a temp. doc.
     for (int i = 0; i < pdfDocument.getNumberOfPages(); i++) {
         if (pageInInterval.test(i, pageRanges)) {
             continue;
         }
         tmpDoc.addPage(pdfDocument.getPage(i));
     }

     // save the temporary doc.
     tmpDoc.save(new File(srcFilePath));
     tmpDoc.close();
     pdfDocument.close();
 }

Давайте проверим это, вызвав removePages в основной функции:

 ///// remove pages
app.removePages(resultFilePath, new Integer[] {1, 21, 376, 428});

Он перезапишет входной (обрезанный) файл.

Как разделить PDF

Теперь мы представим функцию, которая позволяет разбивать PDF-файл на несколько отдельных PDF-файлов, каждый из которых будет содержать определенное количество страниц. Функция ожидает два входа: путь к исходному PDF-документу и желаемое количество страниц в каждом разделенном файле.

  1. Извлечь имя исходного файла
  2. для каждого nrOfPages
  3. добавить их во временный документ
  4. сохранить временный документ с именем исходного файла + индекс
public void splitPDF(String srcFilePath, int nrOfPages) throws IOException {
    // extract file's name
    File srcFile = new File(srcFilePath);
    String fileName = srcFile.getName();
    int dotIndex = fileName.lastIndexOf('.');
    String fileNameWithoutExtension =  (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex);

    PDDocument pdfDocument = PDDocument.load(srcFile);

    // extract every nrOfPages to a temporary document
    // append an index to its name and save it
    for (int i = 1; i < pdfDocument.getNumberOfPages(); i+=nrOfPages) {
        Splitter splitter = new Splitter();

        int fromPage = i;
        int toPage = i+nrOfPages;
        splitter.setStartPage(fromPage);
        splitter.setEndPage(toPage);
        splitter.setSplitAtPage(toPage - fromPage );

        List<PDDocument> lst = splitter.split(pdfDocument);

        PDDocument pdfDocPartial = lst.get(0);
        File f = new File(fileNameWithoutExtension + "-" + i + ".pdf");
        pdfDocPartial.save(f);
        pdfDocPartial.close();
    }
    pdfDocument.close();
}

Вот полная функция main():

public static void main( String[] args ){
    String srcFilePath = "/Users/user/pdfs/file.pdf";
    PDFUtils app = new PDFUtils();

    try {
         ///// crop pdf
        float x = 18f;
        float y = 20f;
        float width = 140f;
        float height = 210f;
        String resultFilePath = app.cropPDFMM(x, y, width, height, srcFilePath);

        ///// remove pages
        app.removePages(resultFilePath, new Integer[] {1, 21, 376, 428});
        
        ///// split pages
        app.splitPDF(resultFilePath, 20);

        System.out.println( "Done!" );
    } catch (Exception e) {
        System.out.println(e);
    }
}

Объединить 2 страницы на листе (2 на одном листе)

Эта функция основана на https://stackoverflow.com/questions/12093408/pdfbox-merge-2-portrait-pages-onto-a-single-side-by-side-landscape-page.

  1. Создать временный документ
  2. Перебирайте страницы исходного документа. -› получить левую и правую страницы
  3. Создайте новую «выходную» страницу с правильными размерами
  4. Добавить левую страницу в (0,0)
  5. Добавить правую страницу, переведенную на (ширина левой страницы, 0)
  6. Сохраните темп. doc -> перезаписать исходный файл
public void mergePages(String srcFilePath) throws IOException {

    // SOURCE: https://stackoverflow.com/questions/12093408/pdfbox-merge-2-portrait-pages-onto-a-single-side-by-side-landscape-page
    File srcFile = new File(srcFilePath);
    PDDocument pdfDocument = PDDocument.load(srcFile);
    PDDocument outPdf = new PDDocument();

    for (int i = 0; i < pdfDocument.getNumberOfPages(); i+=2) {
        PDPage page1 = pdfDocument.getPage(i);
        PDPage page2 = pdfDocument.getPage(i+1);
        PDRectangle pdf1Frame = page1.getCropBox();
        PDRectangle pdf2Frame = page2.getCropBox();
        PDRectangle outPdfFrame = new PDRectangle(pdf1Frame.getWidth()+pdf2Frame.getWidth(), Math.max(pdf1Frame.getHeight(), pdf2Frame.getHeight()));

        // Create output page with calculated frame and add it to the document
        COSDictionary dict = new COSDictionary();
        dict.setItem(COSName.TYPE, COSName.PAGE);
        dict.setItem(COSName.MEDIA_BOX, outPdfFrame);
        dict.setItem(COSName.CROP_BOX, outPdfFrame);
        dict.setItem(COSName.ART_BOX, outPdfFrame);
        PDPage newP = new PDPage(dict);
        outPdf.addPage(newP);

        // Source PDF pages has to be imported as form XObjects to be able to insert them at a specific point in the output page
        LayerUtility layerUtility = new LayerUtility(outPdf);
        PDFormXObject formPdf1 = layerUtility.importPageAsForm(pdfDocument, page1);
        PDFormXObject formPdf2 = layerUtility.importPageAsForm(pdfDocument, page2);

        AffineTransform afLeft = new AffineTransform();
        layerUtility.appendFormAsLayer(newP, formPdf1, afLeft, "left" + i);
        AffineTransform afRight = AffineTransform.getTranslateInstance(pdf1Frame.getWidth(), 0.0);
        layerUtility.appendFormAsLayer(newP, formPdf2, afRight, "right" + i);
    }

    outPdf.save(srcFile);
    outPdf.close();
    pdfDocument.close();
}public void mergePages(String srcFilePath) throws IOException { // SOURCE: https://stackoverflow.com/questions/12093408/pdfbox-merge-2-portrait-pages-onto-a-single-side-by-side-landscape-page File srcFile = new File(srcFilePath); PDDocument pdfDocument = PDDocument.load(srcFile); PDDocument outPdf = new PDDocument(); for (int i = 0; i < pdfDocument.getNumberOfPages(); i+=2) { PDPage page1 = pdfDocument.getPage(i); PDPage page2 = pdfDocument.getPage(i+1); PDRectangle pdf1Frame = page1.getCropBox(); PDRectangle pdf2Frame = page2.getCropBox(); PDRectangle outPdfFrame = new PDRectangle(pdf1Frame.getWidth()+pdf2Frame.getWidth(), Math.max(pdf1Frame.getHeight(), pdf2Frame.getHeight())); // Create output page with calculated frame and add it to the document COSDictionary dict = new COSDictionary(); dict.setItem(COSName.TYPE, COSName.PAGE); dict.setItem(COSName.MEDIA_BOX, outPdfFrame); dict.setItem(COSName.CROP_BOX, outPdfFrame); dict.setItem(COSName.ART_BOX, outPdfFrame); PDPage newP = new PDPage(dict); outPdf.addPage(newP); // Source PDF pages has to be imported as form XObjects to be able to insert them at a specific point in the output page LayerUtility layerUtility = new LayerUtility(outPdf); PDFormXObject formPdf1 = layerUtility.importPageAsForm(pdfDocument, page1); PDFormXObject formPdf2 = layerUtility.importPageAsForm(pdfDocument, page2); AffineTransform afLeft = new AffineTransform(); layerUtility.appendFormAsLayer(newP, formPdf1, afLeft, "left" + i); AffineTransform afRight = AffineTransform.getTranslateInstance(pdf1Frame.getWidth(), 0.0); layerUtility.appendFormAsLayer(newP, formPdf2, afRight, "right" + i); } outPdf.save(srcFile); outPdf.close(); pdfDocument.close(); }

Обновите main(), чтобы протестировать его:

...
 ///// 2 pages per sheet
app.mergePages(resultFilePath);
...

Вот полный список импорта:

package com.pdf.pdf_utils;

import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.multipdf.Splitter;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;

import java.awt.geom.AffineTransform;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.function.BiPredicate;
import java.util.function.Function;

Исходный код доступен здесь.

Первоначально опубликовано на https://alexadam.dev.