Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor PDF exports for headless printing #891

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
17 changes: 10 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,20 @@ configurations {
dependencies {
implementation "org.megamek:megamek${mmBranchTag}:${version}"

implementation 'org.apache.xmlgraphics:batik-dom:1.10'
implementation 'org.apache.xmlgraphics:batik-codec:1.10'
implementation 'org.apache.xmlgraphics:batik-rasterizer:1.10'
implementation ('org.apache.xmlgraphics:batik-bridge:1.10') {
implementation 'org.apache.xmlgraphics:batik-dom:1.13'
implementation 'org.apache.xmlgraphics:batik-codec:1.13'
implementation 'org.apache.xmlgraphics:batik-rasterizer:1.13'
implementation ('org.apache.xmlgraphics:batik-bridge:1.13') {
// We don't need the python and javascript engine taking up space
exclude group: 'org.python', module: 'jython'
exclude group: 'org.mozilla', module: 'rhino'
}
implementation 'org.apache.xmlgraphics:batik-svggen:1.10'
implementation 'org.apache.xmlgraphics:fop:2.3'
implementation 'org.apache.pdfbox:pdfbox:2.0.19'
implementation 'org.apache.xmlgraphics:batik-svggen:1.13'
implementation ('org.apache.xmlgraphics:fop:2.5') {
// We don't need this proprietary module
exclude group: 'com.sun.media', module: 'jai-codec'
}
implementation 'org.apache.pdfbox:pdfbox:2.0.22'

jarbundler 'com.ultramixer.jarbundler:jarbundler-core:3.3.0'
}
Expand Down
120 changes: 120 additions & 0 deletions src/megameklab/com/printing/PdfRecordSheetExporter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package megameklab.com.printing;

import java.awt.print.PageFormat;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.fop.configuration.Configuration;
import org.apache.fop.configuration.ConfigurationException;
import org.apache.fop.configuration.DefaultConfigurationBuilder;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.xml.sax.SAXException;

import megamek.common.annotations.Nullable;

/**
* Exports {@link RecordSheetBook} instances to a PDF.
*/
public class PdfRecordSheetExporter {
private final MemoryUsageSetting memoryUsageSetting;
private final Configuration cfg;

/**
* Creates a new exporter using temp files for memory management.
*/
public PdfRecordSheetExporter() {
this(MemoryUsageSetting.setupTempFileOnly(), null);
}

/**
* Creates a new exporter using the supplied memory usage settings and optional configuration.
* @param memoryUsageSetting The {@link MemoryUsageSetting} to use when exporting the PDF.
* @param cfg The {@link Configuration} to use when exporting the PDF, or {@code null} if the
* default configuration should be used.
*/
public PdfRecordSheetExporter(MemoryUsageSetting memoryUsageSetting, @Nullable Configuration cfg) {
this.memoryUsageSetting = Objects.requireNonNull(memoryUsageSetting);
this.cfg = cfg;
}

/**
* Exports a {@link RecordSheetBook} to a PDF.
* @param book The {@link RecordSheetBook} to export.
* @param pageFormat The {@link PageFormat} to use with the resulting PDF.
* @param fileName The file name to save the resulting PDF.
* @throws IOException
* @throws ConfigurationException
* @throws TranscoderException
* @throws SAXException
*/
public void exportToFile(RecordSheetBook book, PageFormat pageFormat, String fileName)
throws IOException, ConfigurationException, TranscoderException, SAXException {
PDFMergerUtility merger = new PDFMergerUtility();
merger.setDestinationFileName(fileName);

Map<Integer, List<String>> bookmarkNames = new HashMap<>();
addSheetsToPdf(merger, book, pageFormat, bookmarkNames);

// Load newly created document, add an outline, then write back to the file.
File file = new File(fileName);
try (PDDocument doc = PDDocument.load(file)) {
addBookmarksToDocument(bookmarkNames, doc);
doc.save(file);
}
}

private void addBookmarksToDocument(Map<Integer, List<String>> bookmarkNames, PDDocument doc) {
PDDocumentOutline outline = new PDDocumentOutline();
doc.getDocumentCatalog().setDocumentOutline(outline);
for (Map.Entry<Integer, List<String>> entry : bookmarkNames.entrySet()) {
for (String name : entry.getValue()) {
PDOutlineItem bookmark = new PDOutlineItem();
bookmark.setDestination(doc.getPage(entry.getKey()));
bookmark.setTitle(name);
outline.addLast(bookmark);
}
}

outline.openNode();
}

private void addSheetsToPdf(PDFMergerUtility merger, RecordSheetBook book, PageFormat pageFormat,
Map<Integer, List<String>> bookmarkNames)
throws TranscoderException, SAXException, IOException, ConfigurationException {
Iterator<PrintRecordSheet> sheets = book.takeSheets().iterator();
Configuration configuration = getOrCreateConfiguration();
while (sheets.hasNext()) {
PrintRecordSheet rs = sheets.next();

// Ensure we do not hold onto the PrintRecordSheet instance any longer than necessary
sheets.remove();

bookmarkNames.put(rs.getFirstPage(), rs.getBookmarkNames());
for (int i = 0; i < rs.getPageCount(); i++) {
merger.addSource(rs.exportPDF(i, pageFormat, configuration));
}
}

merger.mergeDocuments(memoryUsageSetting);
}

private Configuration getOrCreateConfiguration()
throws ConfigurationException, SAXException, IOException {
if (cfg != null) {
return cfg;
} else {
DefaultConfigurationBuilder cfgBuilder = new DefaultConfigurationBuilder();
return cfgBuilder.build(getClass().getResourceAsStream("fop-config.xml"));
}
}
}
12 changes: 6 additions & 6 deletions src/megameklab/com/printing/PrintEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -453,17 +453,17 @@ protected void drawFluffImage() {
private void drawEraIcon() {
File iconFile;
if (getEntity().getYear() < 2781) {
iconFile = new File("data/images/recordsheets/era_starleague.png");
iconFile = new File(CConfig.getRecordSheetsPath(), "era_starleague.png");
} else if (getEntity().getYear() < 3050) {
iconFile = new File("data/images/recordsheets/era_sw.png");
iconFile = new File(CConfig.getRecordSheetsPath(), "era_sw.png");
} else if (getEntity().getYear() < 3061) {
iconFile = new File("data/images/recordsheets/era_claninvasion.png");
iconFile = new File(CConfig.getRecordSheetsPath(), "era_claninvasion.png");
} else if (getEntity().getYear() < 3068) {
iconFile = new File("data/images/recordsheets/era_civilwar.png");
iconFile = new File(CConfig.getRecordSheetsPath(), "era_civilwar.png");
} else if (getEntity().getYear() < 3086) {
iconFile = new File("data/images/recordsheets/era_jihad.png");
iconFile = new File(CConfig.getRecordSheetsPath(), "era_jihad.png");
} else {
iconFile = new File("data/images/recordsheets/era_darkage.png");
iconFile = new File(CConfig.getRecordSheetsPath(), "era_darkage.png");
}
Element rect = getSVGDocument().getElementById(ERA_ICON);
if (rect instanceof SVGRectElement) {
Expand Down
7 changes: 4 additions & 3 deletions src/megameklab/com/printing/PrintMech.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

import megamek.common.annotations.Nullable;
import megameklab.com.MegaMekLab;
import megameklab.com.util.CConfig;
import megameklab.com.util.ImageHelper;
import megameklab.com.util.UnitUtil;

Expand Down Expand Up @@ -289,7 +290,7 @@ private boolean loadArmorPips(int loc, boolean rear) {
}
}

NodeList nl = loadPipSVG(String.format("data/images/recordsheets/biped_pips/Armor_%s_%d_Humanoid.svg",
NodeList nl = loadPipSVG(String.format("biped_pips/Armor_%s_%d_Humanoid.svg",
locAbbr, mech.getOArmor(loc, rear)));
if (null == nl) {
return false;
Expand All @@ -298,7 +299,7 @@ private boolean loadArmorPips(int loc, boolean rear) {
}

private boolean loadISPips() {
NodeList nl = loadPipSVG(String.format("data/images/recordsheets/biped_pips/BipedIS%d.svg",
NodeList nl = loadPipSVG(String.format("biped_pips/BipedIS%d.svg",
(int) mech.getWeight()));
if (null == nl) {
return false;
Expand All @@ -320,7 +321,7 @@ private boolean copyPipPattern(NodeList nl, String parentName) {
}

private @Nullable NodeList loadPipSVG(String filename) {
File f = new File(filename);
File f = new File(CConfig.getRecordSheetsPath(), filename);
if (!f.exists()) {
return null;
}
Expand Down
14 changes: 9 additions & 5 deletions src/megameklab/com/printing/PrintRecordSheet.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
import megameklab.com.MegaMekLab;
import megameklab.com.printing.reference.ReferenceTable;
import megameklab.com.util.CConfig;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
import org.apache.fop.configuration.Configuration;
import org.apache.fop.configuration.ConfigurationException;
import org.apache.fop.configuration.DefaultConfigurationBuilder;
import org.apache.batik.anim.dom.SVGDOMImplementation;
import org.apache.batik.anim.dom.SVGLocatableSupport;
import org.apache.batik.bridge.BridgeContext;
Expand Down Expand Up @@ -342,9 +342,13 @@ public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) {
}

public InputStream exportPDF(int pageNumber, PageFormat pageFormat) throws TranscoderException, SAXException, IOException, ConfigurationException {
createDocument(pageNumber + firstPage, pageFormat, true);
DefaultConfigurationBuilder cfgBuilder = new DefaultConfigurationBuilder();
Configuration cfg = cfgBuilder.build(getClass().getResourceAsStream("fop-config.xml"));
return exportPDF(pageNumber, pageFormat, cfg);
}

public InputStream exportPDF(int pageNumber, PageFormat pageFormat, Configuration cfg) throws TranscoderException, SAXException, IOException, ConfigurationException {
createDocument(pageNumber + firstPage, pageFormat, true);
PDFTranscoder transcoder = new PDFTranscoder();
transcoder.configure(cfg);
transcoder.addTranscodingHint(PDFTranscoder.KEY_AUTO_FONTS, false);
Expand Down Expand Up @@ -402,7 +406,7 @@ protected void processImage(int pageNum, PageFormat pageFormat) {
}

String getSVGDirectoryName() {
return "data/images/recordsheets/" + options.getPaperSize().dirName;
return new File(CConfig.getRecordSheetsPath(), options.getPaperSize().dirName).getPath();
}

/**
Expand Down
89 changes: 89 additions & 0 deletions src/megameklab/com/printing/RecordSheetBook.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package megameklab.com.printing;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

import megamek.common.Entity;

/**
* Represents a book of {@link PrintRecordSheet} instances.
*/
public class RecordSheetBook {
private List<PrintRecordSheet> sheets = new ArrayList<>();
private List<Entity> unprintable = new ArrayList<>();
private int pageCount;

/**
* Takes the {@link PrintRecordSheet} entries out of the book
* and resets the sheets and unprintable entities. Callers
* should save the list of unprintable entities prior to
* calling {@code takeSheets}.
*
* @return The list of {@link PrintRecordSheet} entries from this
* book. The caller now owns this reference.
*/
public List<PrintRecordSheet> takeSheets() {
List<PrintRecordSheet> taken = sheets;

sheets = new ArrayList<>();
unprintable = new ArrayList<>();
pageCount = 0;

return taken;
}

/**
* Executes an action on each {@link PrintRecordSheet} in the book.
*
* @param consumer The action to execute on each sheet in the book.
*/
public void forEachSheet(Consumer<PrintRecordSheet> consumer) {
for (PrintRecordSheet sheet : sheets) {
consumer.accept(sheet);
}
}

/**
* Gets the total page count.
*/
public int getPageCount() {
return pageCount;
}

/**
* Adds a record sheet to the book.
* @param recordSheet The {@link PrintRecordSheet} to add to the book.
*/
public void addSheet(PrintRecordSheet recordSheet) {
sheets.add(Objects.requireNonNull(recordSheet));
pageCount += recordSheet.getPageCount();
}

/**
* Gets a value indicating whether or not the book
* included at least one unprintable entity.
*/
public boolean hasUnprintableEntities() {
return !unprintable.isEmpty();
}

/**
* Gets a list of the unprintable entities.
* @return A list of unprintable entities.
*/
public List<Entity> getUnprintableEntities() {
return Collections.unmodifiableList(unprintable);
}

/**
* Adds an unprintable entity to the book.
* @param entity An {@link Entity} which could not be
* converted into a {@link PrintRecordSheet}.
*/
public void addUnprintableEntity(Entity entity) {
unprintable.add(Objects.requireNonNull(entity));
}
}
Loading