package org.alastairmailer;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Logger;
import org.jpedal.PdfDecoder;
import org.jpedal.grouping.DefaultSearchListener;
import org.jpedal.grouping.PdfGroupingAlgorithms;
import org.jpedal.grouping.SearchListener;
import org.jpedal.grouping.SearchType;
import org.jpedal.objects.PdfPageData;
public class PDFImageCacher {
private Map<File, Vector<Future<PDFHighlightImage>>> cache = new HashMap<File, Vector<Future<PDFHighlightImage>>>();
public ExecutorService threadPool;
private static final int CACHE_LIMIT = 30;
private static final int THREAD_LIMIT = 2;
private int cacheCount = 0;
private final Logger log = Logger.getLogger(PDFImageCacher.class.getCanonicalName());
public PDFImageCacher() {
threadPool = Executors.newFixedThreadPool(THREAD_LIMIT);
}
public void requestCache(File f, int pageNum, String[] highlight) {
log.fine("Cache requested: " + f + ' ' + pageNum);
if (cache.containsKey(f)) {
Vector<Future<PDFHighlightImage>> pages = cache.get(f);
if (pages.size() < pageNum) { pages.setSize(pageNum); }
if (pages.get(pageNum - 1) == null) {
log.fine("No copy in cache, caching");
if (cacheCount < CACHE_LIMIT) {
GetPDFImage request = new GetPDFImage(f, pageNum, highlight);
Future<PDFHighlightImage> result = threadPool.submit(request);
pages.set(pageNum - 1, result);
cacheCount++;
} else {
log.fine("Cache overflow");
}
}
} else {
Vector<Future<PDFHighlightImage>> fileEntry = new Vector<Future<PDFHighlightImage>>();
if (fileEntry.size() < pageNum) { fileEntry.setSize(pageNum); }
if (cacheCount < CACHE_LIMIT) {
GetPDFImage request = new GetPDFImage(f, pageNum, highlight);
Future<PDFHighlightImage> result = threadPool.submit(request);
fileEntry.set(pageNum - 1, result);
cacheCount++;
cache.put(f, fileEntry);
log.fine("Added " + f.toString() + ' ' + pageNum);
} else {
log.fine("Cache overflow");
}
}
}
public void decache(File f) {
if (cache.containsKey(f)) {
log.fine("Decaching " + f);
Vector<Future<PDFHighlightImage>> pages = cache.get(f);
for (Future<PDFHighlightImage> future : pages) {
if (future != null) { future.cancel(true); }
}
cacheCount -= cache.get(f).size();
cache.remove(f);
}
}
public void decache(File f, int pageNum) {
if (cache.containsKey(f)) {
Vector<Future<PDFHighlightImage>> pages = cache.get(f);
pages.get(pageNum).cancel(true);
cacheCount--;
pages.remove(pageNum);
}
}
public void decacheExceptFirst(File f) {
if (cache.containsKey(f)) {
log.fine("Decaching " + f + " except first page");
Vector<Future<PDFHighlightImage>> pages = cache.get(f);
for (Future<PDFHighlightImage> future : pages) {
if (future != null && future != pages.firstElement()) {
future.cancel(true);
}
}
cacheCount -= pages.size() - 1;
pages.retainAll(Collections.singletonList(pages.firstElement()));
}
}
public PDFHighlightImage getImage(File f, int pageNum) {
PDFHighlightImage image = null;
if (cache.containsKey(f)) {
Future<PDFHighlightImage> imageF = cache.get(f).get(pageNum - 1);
if (imageF != null) {
try {
image = imageF.get();
}
catch (CancellationException e) { }
catch (ExecutionException e) {
log.warning("Exception encountered getting page " + pageNum + " of " + f.toString() + ": " + e.getMessage());
}
catch (InterruptedException e) { }
}
}
return image;
}
}
class GetPDFImage implements Callable<PDFHighlightImage> {
File pdfFile;
int pageNum;
BufferedImage im;
String[] highlightTerms;
public GetPDFImage(File f, int pageNo, String[] highlights) {
pdfFile = f;
pageNum = pageNo;
highlightTerms = highlights;
}
float zoom = 1.25f;
@SuppressWarnings("unchecked")
public PDFHighlightImage call() throws Exception {
PdfDecoder pdf = new PdfDecoder();
PdfDecoder.setFontReplacements(pdf);
pdf.openPdfFile(pdfFile.toString());
pdf.setPageParameters(zoom, pageNum);
pdf.decodePage(pageNum);
BufferedImage pdfImage = pdf.getPageAsImage(pageNum);
PdfGroupingAlgorithms grouping = pdf.getGroupingObject();
PdfPageData page = pdf.getPdfPageData();
int x1 = page.getMediaBoxX(pageNum);
int x2 = page.getMediaBoxWidth(pageNum);
int y1 = page.getMediaBoxY(pageNum);
int y2 = page.getMediaBoxHeight(pageNum);
final SearchListener listener = new DefaultSearchListener();
List<Rectangle> highlights = grouping.findMultipleTermsInRectangle(
x1, y1, x2, y2,
page.getRotation(pageNum),
pageNum,
highlightTerms,
true, SearchType.DEFAULT, listener);
List<Rectangle> rectangles = new Vector<Rectangle>();
for (Rectangle r: highlights) {
int rX = Math.round((r.x - x1) * zoom);
// 0.25f is to account for descender height, which JPedal does not seem to do
int rY = Math.round((y2 - r.y - r.height + y1 + r.height * 0.2f) * zoom);
int rW = Math.round(r.width * zoom);
int rH = Math.round(r.height * zoom);
Rectangle pdfR = new Rectangle(rX, rY, rW, rH);
rectangles.add(pdfR);
}
pdf.closePdfFile();
pdf = null;
return new PDFHighlightImage(pdfImage, rectangles, pageNum);
}
}
class PDFHighlightImage {
public BufferedImage pdfImage;
public List<Rectangle> highlights;
public int pageNum;
private static final Color selectColor = new Color(255,255,0,125);
private static final Color otherColor = new Color(0,0,255,125);
public PDFHighlightImage(BufferedImage pdfImage, List<Rectangle> highlights, int pageNum) {
this.pdfImage = pdfImage;
this.highlights = highlights;
this.pageNum = pageNum;
}
public BufferedImage getHighlightedImage(Rectangle highlighted) {
WritableRaster wr = pdfImage.copyData(null);
BufferedImage imNew = new BufferedImage(pdfImage.getColorModel(), wr, pdfImage.isAlphaPremultiplied(), null);
Graphics2D g = imNew.createGraphics();
for (Rectangle r : highlights) {
g.setColor(r == highlighted ? selectColor : otherColor);
g.fill(r);
}
g.dispose();
return imNew;
}
}