-
-
Save apollo13/66c3eca2ae6f860aa8cdfeb97e904965 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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