Skip to content

Instantly share code, notes, and snippets.

@apollo13
Created July 23, 2020 09:41
Show Gist options
  • Save apollo13/66c3eca2ae6f860aa8cdfeb97e904965 to your computer and use it in GitHub Desktop.
Save apollo13/66c3eca2ae6f860aa8cdfeb97e904965 to your computer and use it in GitHub Desktop.
package at.dt_i.pdfrenderer.impl;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.Objects;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StopWatch;
import com.artifex.mupdf.fitz.ColorSpace;
import com.artifex.mupdf.fitz.Document;
import com.artifex.mupdf.fitz.DrawDevice;
import com.artifex.mupdf.fitz.Matrix;
import com.artifex.mupdf.fitz.Page;
import com.artifex.mupdf.fitz.Pixmap;
import com.artifex.mupdf.fitz.Rect;
import at.dt_i.primesign.commons.logging.MDCAccessor;
import at.dt_i.primesign.commons.logging.MDCAccessor.MDCAutoCloseable;
import at.dt_i.primesign.commons.utils.RandomStringUtils;
import at.dt_i.primesign.core.pdfrenderer.PageInfo;
import at.dt_i.primesign.core.pdfrenderer.PdfRenderer;
import at.dt_i.primesign.core.pdfrenderer.PdfRendererException;
/**
* Renderer (wrapper) implementation for Artifex MuPDF.
*/
public class MuPdfRendererImpl implements PdfRenderer {
private final Logger log = LoggerFactory.getLogger(MuPdfRendererImpl.class);
private Document document;
private final int pagesCount;
private final String rendererId;
private final int memoryFootprint;
private static final String MDC_RENDERER = "rendererId";
public MuPdfRendererImpl(byte[] buffer) {
Objects.requireNonNull(buffer);
memoryFootprint = buffer.length;
rendererId = RandomStringUtils.randomStringEqBinaryOfBitLength(64);
try (MDCAutoCloseable mdcAutoCloseable = MDCAccessor.put(MDC_RENDERER, rendererId)) {
log.info("Initializing pdf renderer (memoryFootprint={}, rendererId={}).", FileUtils.byteCountToDisplaySize(buffer.length), rendererId);
StopWatch sw = new StopWatch();
sw.start();
document = Document.openDocument(buffer, "pdf");
sw.stop();
log.trace("Renderer successfully initialized with pdf document ({}ms)", sw.getTotalTimeMillis());
pagesCount = document.countPages();
if (pagesCount < 0) {
throw new IllegalStateException("Renderer returns invalid page count (" + pagesCount + ").");
}
}
}
@Override
public synchronized BufferedImage renderPage(int pageNum, float zoom) throws PdfRendererException {
try (MDCAutoCloseable mdcAutoCloseable = MDCAccessor.put(MDC_RENDERER, rendererId)) {
// make sure document cannot be rendered after destroy/dispose has been called
if (document == null) {
throw new PdfRendererException(
PdfRendererException.ERROR_RENDERER_ALREADY_DISPOSED,
"Unable to render page. Pdf renderer has already been disposed."
);
}
if (pageNum < 1 || pageNum > getPageCount()) {
throw new PdfRendererException(
PdfRendererException.ERROR_INVALID_PAGE_NO,
"Unable to render page no " + pageNum + ". Must be within [1," + getPageCount() + "]."
);
}
log.debug("Rendering document (rendererId={}), page {} with zoom {}", rendererId, pageNum, zoom);
StopWatch sw = new StopWatch();
sw.start();
Page page = document.loadPage(pageNum - 1);
Matrix ctm = new Matrix().scale(zoom);
BufferedImage img = imageFromPage(page, ctm);
sw.stop();
log.trace("Finished rendering document page returning image with dimension {}x{} ({}ms)", img.getWidth(), img.getHeight(), sw.getTotalTimeMillis());
return img;
}
}
@Override
public int getPageCount() throws PdfRendererException {
if (document == null) {
throw new PdfRendererException(
PdfRendererException.ERROR_RENDERER_ALREADY_DISPOSED,
"Unable to render page. Pdf renderer has already been disposed."
);
}
return pagesCount;
}
@Override
public synchronized PageInfo getPageInfo(int pageNum) throws PdfRendererException {
try (MDCAutoCloseable mdcAutoCloseable = MDCAccessor.put(MDC_RENDERER, rendererId)) {
if (document == null) {
throw new PdfRendererException(
PdfRendererException.ERROR_RENDERER_ALREADY_DISPOSED,
"Unable to retrieve page info. Pdf renderer has already been disposed."
);
}
log.debug("Assemblying page info for document (rendererId={}), page {}", rendererId, pageNum);
StopWatch sw = new StopWatch();
sw.start();
Page page = document.loadPage(pageNum - 1);
PageInfo pageInfo = getPageInfo(page);
sw.stop();
log.trace("Finished assembling page info ({}ms)", sw.getTotalTimeMillis());
return pageInfo;
}
}
private PageInfo getPageInfo(Page page) {
Rect bounds = page.getBounds();
PageInfo pageInfo = new PageInfo();
pageInfo.setWidth(Math.abs(bounds.x1 - bounds.x0));
pageInfo.setHeight(Math.abs(bounds.y1 - bounds.y0));
return pageInfo;
}
@Override
public void dispose() throws PdfRendererException {
try (MDCAutoCloseable mdcAutoCloseable = MDCAccessor.put(MDC_RENDERER, rendererId)) {
if (document!= null) {
// unlink document reference in order to encourage garbage collector cleanup
// ...and in order to prevent further use of renderer for privacy reasons
log.info("Clearing reference to mupdf renderer making it eligible for garbage collection: {}", this);
document = null;
}
// Important note: no further tasks are to be done here, especially NOT calling document.destroy()
// The underlying mupdf implementation's cleanup is automatically called upon garbage collector cleanup.
// Calling it twice will crash the java VM since the underlying native library is not "double-free-safe".
}
}
@Override
public void destroy() throws PdfRendererException {
dispose();
}
@Override
protected void finalize() throws Throwable {
dispose();
}
private BufferedImage imageFromPixmap(Pixmap pixmap) {
int w = pixmap.getWidth();
int h = pixmap.getHeight();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
int[] pixels = pixmap.getPixels();
int[] imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
System.arraycopy(pixels, 0, imgData, 0, pixels.length);
return image;
}
private BufferedImage imageFromPage(Page page, Matrix ctm) {
Rect bbox = page.getBounds().transform(ctm);
Pixmap pixmap = new Pixmap(ColorSpace.DeviceBGR, bbox, true);
pixmap.clear(255);
DrawDevice dev = new DrawDevice(pixmap);
page.run(dev, ctm, null);
dev.close();
// do not call dev.destroy()
// this calls the finalize() which will be called by the GC anyway (avoiding double-free issues)
BufferedImage image = imageFromPixmap(pixmap);
// do not call pixmap.destroy()
// this calls the finalize() which will be called by the GC anyway (avoiding double-free issues)
// do not call page.destroy();
// this calls the finalize() which will be called by the GC anyway (avoiding double-free issues)
return image;
}
@Override
public int getMemoryFootprint() {
return memoryFootprint;
}
@Override
public String toString() {
return String.format("MuPdfRendererImpl [rendererId=%s, memoryFootprint=%s, pagesCount=%s]", rendererId,
FileUtils.byteCountToDisplaySize(memoryFootprint), pagesCount);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment