PdfExporter.java
package org.fresnel.optics;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* PDF export with optional sheet splitting (A4 / A3 / A2 / custom roll).
*
* <p>The rendered image is placed at its physical size in millimetres. For images
* larger than the chosen page size the image is split across multiple pages with
* a configurable overlap, and 5-mm registration tick marks are drawn at the page
* corners on every page so a user can tile the printed pages back together.
*
* <p>PDF coordinate units are points (1 pt = 1/72 inch). Conversions:
* {@code mm = pt · 25.4 / 72}.
*/
public final class PdfExporter {
/** Standard sheet sizes (width × height in mm, portrait). */
public enum SheetSize {
A4(210.0, 297.0),
A3(297.0, 420.0),
A2(420.0, 594.0),
A1(594.0, 841.0),
A0(841.0, 1189.0),
/** Single page sized exactly to the image; never split. */
FIT(0.0, 0.0);
public final double widthMm;
public final double heightMm;
SheetSize(double w, double h) { this.widthMm = w; this.heightMm = h; }
}
private static final double MM_PER_PT = 25.4 / 72.0;
private static final double PT_PER_MM = 72.0 / 25.4;
/** Overlap between adjacent split pages, in mm. */
public static final double DEFAULT_OVERLAP_MM = 5.0;
/** Crop-mark length in mm. */
public static final double CROP_MARK_MM = 5.0;
private PdfExporter() {}
/** Convenience: write to byte array. */
public static byte[] toPdfBytes(RenderResult r, SheetSize sheet) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writePdf(r, sheet, DEFAULT_OVERLAP_MM, baos);
return baos.toByteArray();
}
/**
* Write the rendered image as PDF, splitting onto pages of the given sheet size.
*
* @param r rendered image with physical pixel size
* @param sheet target page size (or {@link SheetSize#FIT} to use one custom page)
* @param overlapMm overlap between adjacent split pages, in millimetres
*/
public static void writePdf(RenderResult r, SheetSize sheet, double overlapMm, OutputStream out)
throws IOException {
BufferedImage img = r.image();
double imgWMm = r.widthMm();
double imgHMm = r.heightMm();
try (PDDocument doc = new PDDocument()) {
if (sheet == SheetSize.FIT
|| (imgWMm <= sheet.widthMm + 1e-6 && imgHMm <= sheet.heightMm + 1e-6)) {
// Single page sized to the image (or to the sheet if it fits).
double pageWMm = sheet == SheetSize.FIT ? imgWMm : sheet.widthMm;
double pageHMm = sheet == SheetSize.FIT ? imgHMm : sheet.heightMm;
PDPage page = new PDPage(new PDRectangle(
(float) (pageWMm * PT_PER_MM), (float) (pageHMm * PT_PER_MM)));
doc.addPage(page);
PDImageXObject pdImage = LosslessFactory.createFromImage(doc, img);
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
// Centre the image on the page.
float xPt = (float) ((pageWMm - imgWMm) / 2.0 * PT_PER_MM);
float yPt = (float) ((pageHMm - imgHMm) / 2.0 * PT_PER_MM);
cs.drawImage(pdImage,
xPt, yPt,
(float) (imgWMm * PT_PER_MM),
(float) (imgHMm * PT_PER_MM));
drawCornerCropMarks(cs, pageWMm, pageHMm);
}
} else {
// Multi-page tiling.
double overlap = Math.max(0.0, overlapMm);
double pageWMm = sheet.widthMm;
double pageHMm = sheet.heightMm;
double tileWMm = pageWMm - 2.0 * overlap;
double tileHMm = pageHMm - 2.0 * overlap;
if (tileWMm <= 0 || tileHMm <= 0)
throw new IllegalArgumentException("overlap too large for sheet");
int nx = (int) Math.ceil(imgWMm / tileWMm);
int ny = (int) Math.ceil(imgHMm / tileHMm);
PDImageXObject pdImage = LosslessFactory.createFromImage(doc, img);
for (int j = 0; j < ny; j++) {
for (int i = 0; i < nx; i++) {
PDPage page = new PDPage(new PDRectangle(
(float) (pageWMm * PT_PER_MM), (float) (pageHMm * PT_PER_MM)));
doc.addPage(page);
// Image position so that tile (i,j) is centred in the page (with overlap margin).
// Image origin in mm = -i·tileW + overlap (left edge of tile-image area).
double xMm = -i * tileWMm + overlap;
// PDF y-axis points up; image origin is its top-left, but we draw with bottom-left.
// Place so tile-row j of the image (from top) maps to the page area.
double yMm = pageHMm - imgHMm + j * tileHMm - overlap;
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
cs.drawImage(pdImage,
(float) (xMm * PT_PER_MM),
(float) (yMm * PT_PER_MM),
(float) (imgWMm * PT_PER_MM),
(float) (imgHMm * PT_PER_MM));
drawCornerCropMarks(cs, pageWMm, pageHMm);
}
}
}
}
doc.save(out);
}
}
/** Draw 5-mm tick marks at the four page corners pointing inward. */
private static void drawCornerCropMarks(PDPageContentStream cs, double pageWMm, double pageHMm)
throws IOException {
float w = (float) (pageWMm * PT_PER_MM);
float h = (float) (pageHMm * PT_PER_MM);
float t = (float) (CROP_MARK_MM * PT_PER_MM);
cs.setStrokingColor(0f, 0f, 0f);
cs.setLineWidth(0.5f);
// Bottom-left
cs.moveTo(0, 0); cs.lineTo(t, 0); cs.stroke();
cs.moveTo(0, 0); cs.lineTo(0, t); cs.stroke();
// Bottom-right
cs.moveTo(w, 0); cs.lineTo(w - t, 0); cs.stroke();
cs.moveTo(w, 0); cs.lineTo(w, t); cs.stroke();
// Top-left
cs.moveTo(0, h); cs.lineTo(t, h); cs.stroke();
cs.moveTo(0, h); cs.lineTo(0, h - t); cs.stroke();
// Top-right
cs.moveTo(w, h); cs.lineTo(w - t, h); cs.stroke();
cs.moveTo(w, h); cs.lineTo(w, h - t); cs.stroke();
}
}