package edu.ncsu.dlf.model;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.imageio.ImageIO;
import javax.servlet.ServletContext;
import edu.ncsu.dlf.model.PdfComment.Tag;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.color.PDGamma;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationMarkup;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationRubberStamp;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationText;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup;
/**
* Puts a wrapper around the PDF library
*
*/
public class Pdf {
private static final int BORDER_WIDTH = 30;
private static final float SCALE_UP_FACTOR = 2.0f;
private static final int DEFAULT_SIZE = 72;
private static final PDGamma ORANGE = new PDGamma();
private static final PDGamma GREEN = new PDGamma();
private static final PDGamma YELLOW = new PDGamma();
static {
ORANGE.setR(0.9921568627f);
ORANGE.setG(0.5333333333f);
ORANGE.setB(0.1803921568f);
GREEN.setR(0);
GREEN.setG(1);
GREEN.setB(0);
YELLOW.setR(1);
YELLOW.setG(1);
YELLOW.setB(0);
}
private PDDocument doc;
private Color highlightColor = new Color(234, 249, 35, 140);
public static final String pathToCommentBoxImage = "/images/comment_box.PNG";
private BufferedImage commentBoxImage;
private boolean DEBUG = Boolean.parseBoolean(System.getenv("DEBUG"));
private BufferedImage pageImage;
Pdf(InputStream pdfInputStream, InputStream commentBoxInputStream) throws IOException {
this.doc = PDDocument.load(pdfInputStream);
if (commentBoxInputStream != null)
this.commentBoxImage = ImageIO.read(commentBoxInputStream);
}
public Pdf(InputStream input, ServletContext servletContext) throws IOException {
this(input, servletContext.getResourceAsStream(pathToCommentBoxImage));
if (this.commentBoxImage == null) {
System.out.println(servletContext.getRealPath(pathToCommentBoxImage));
}
}
public List<PdfComment> getPDFComments() {
List<PdfComment> comments = new ArrayList<>();
@SuppressWarnings("unchecked")
List<PDPage> pages = doc.getDocumentCatalog().getAllPages();
for (PDPage page : pages) {
try {
pageImage = null;
List<PDAnnotation> annotations = page.getAnnotations();
// erase highlight and popup annotations from page to avoid them blotting out text
page.setAnnotations(nonBlockingAnnotations(annotations));
int size = Math.round(DEFAULT_SIZE * SCALE_UP_FACTOR);
for (PDAnnotation anno : annotations) {
if (pageImage == null) {
pageImage = page.convertToImage(BufferedImage.TYPE_INT_RGB, size);
}
PdfComment pdfComment = turnAnnotationIntoPDFComment(anno);
if (pdfComment != null) {
comments.add(pdfComment);
}
}
// restore annotations
page.setAnnotations(annotations);
} catch (IOException e) {
e.printStackTrace();
}
}
return comments;
}
private PdfComment turnAnnotationIntoPDFComment(PDAnnotation anno) {
PdfComment pdfComment = null;
if (anno instanceof PDAnnotationTextMarkup) {
PDAnnotationTextMarkup comment = (PDAnnotationTextMarkup) anno;
String writtenComment = comment.getContents();
if (writtenComment == null || writtenComment.isEmpty()) {
writtenComment = "[blank]";
}
if (PDAnnotationTextMarkup.SUB_TYPE_STRIKEOUT.equals(comment.getSubtype())) {
writtenComment = "delete this";
}
pdfComment = new PdfComment(writtenComment);
if (PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT.equals(comment.getSubtype())) {
pdfComment.setImage(makeHighlightedSubImage(pageImage, comment.getQuadPoints()));
} else {
pdfComment.setImage(makePlainSubImage(pageImage, comment.getRectangle()));
}
}
else if (anno instanceof PDAnnotationText) {
String writtenComment = anno.getContents();
if (writtenComment == null || writtenComment.isEmpty()) {
writtenComment = "[blank]";
}
pdfComment = new PdfComment(writtenComment);
pdfComment.setImage(makePopupSubImage(pageImage, anno.getRectangle()));
}
else if (anno.getContents() != null || anno.getAppearance() != null) {
String writtenComment = anno.getContents();
if (writtenComment == null || writtenComment.isEmpty()) {
writtenComment = "[blank]";
}
pdfComment = new PdfComment(writtenComment);
pdfComment.setImage(makePlainSubImage(pageImage, anno.getRectangle()));
}
return pdfComment;
}
private BufferedImage makePlainSubImage(BufferedImage image, PDRectangle r) {
float[] convertedQuadPoints = rectToQuadArray(r);
return makeSubImage(image, convertedQuadPoints, PostExtractMarkup.NONE);
}
private List<PDAnnotation> nonBlockingAnnotations(List<PDAnnotation> annotations) {
//filters out annotations that pdfbox draws poorly so they don't blot the text out and
//make the images hard to see. This includes hightlight textMarkups and Popups
List<PDAnnotation> annotationsThatAreNotTextMarkupOrPopup = new ArrayList<>();
for(PDAnnotation annotation: annotations) {
if (annotation instanceof PDAnnotationTextMarkup) {
if (!PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT.equals(annotation.getSubtype())) {
annotationsThatAreNotTextMarkupOrPopup.add(annotation);
}
}
else if (annotation.getClass() == PDAnnotationMarkup.class || annotation.getClass() == PDAnnotationRubberStamp.class) {
annotationsThatAreNotTextMarkupOrPopup.add(annotation);
}
}
return annotationsThatAreNotTextMarkupOrPopup;
}
private BufferedImage makeHighlightedSubImage(BufferedImage img, float[] quadPoints) {
return makeSubImage(img, quadPoints, PostExtractMarkup.HIGHLIGHTS);
}
private BufferedImage makePopupSubImage(BufferedImage img, PDRectangle r) {
float[] convertedQuadPoints = rectToQuadArray(r);
return makeSubImage(img, convertedQuadPoints, PostExtractMarkup.POPUP);
}
private float[] rectToQuadArray(PDRectangle r) {
float[] convertedQuadPoints = new float[]{r.getLowerLeftX(),r.getLowerLeftY(),
r.getUpperRightX(), r.getLowerLeftY(), r.getLowerLeftX(), r.getUpperRightY(),
r.getUpperRightX(), r.getUpperRightY()};
return convertedQuadPoints;
}
private enum PostExtractMarkup {
NONE(1), HIGHLIGHTS(1), POPUP(2);
public final float subImageContextMultiplier;
PostExtractMarkup(float subImageContextMultiplier) {
this.subImageContextMultiplier = subImageContextMultiplier;
}
}
/* adapted the specs of a pdf tool http://www.pdf-technologies.com/api/html/P_PDFTech_PDFMarkupAnnotation_QuadPoints.htm
* The QuadPoints array must contain 8*n elements specifying the coordinates of n quadrilaterals.
* Each quadrilateral encompasses a word or group of continuous words in the text underlying the annotation.
* The coordinates for each quadrilateral are given in the order x4 y4 x3 y3 x1 y1 x2 y2 specifying the quadrilateral's
* four vertices x1 is upper left and numbering goes clockwise.
*
* I assume all quadrilaterals are rectangles. No guarantees on multi-column selects
*
*/
private BufferedImage makeSubImage(BufferedImage img, float[] quadPoints, PostExtractMarkup markup) {
if (quadPoints.length < 8) {
return null;
}
Rectangle subImageRect = scaleAndTransformSubImageQuad(quadPoints, img, markup);
BufferedImage subImage = img.getSubimage(subImageRect.x, subImageRect.y, subImageRect.width, subImageRect.height);
BufferedImage newImage = new BufferedImage(subImage.getWidth(), subImage.getHeight(), img.getType());
Graphics2D g2 = newImage.createGraphics();
g2.drawImage(subImage, 0, 0, null);
if (markup == PostExtractMarkup.HIGHLIGHTS) {
for (int n = 0; n < quadPoints.length; n += 8) {
float[] oneQuad = Arrays.copyOfRange(quadPoints, n, n + 8);
paintHighlight(g2, scaleAndTransformAnnotationQuad(oneQuad, subImageRect, img.getHeight()));
}
} else if (markup == PostExtractMarkup.POPUP) {
//we know quadPoints will be only one quad because that's how makePopupSubImage defines it.
paintCommentBox(g2, scaleAndTransformAnnotationQuad(quadPoints, subImageRect, img.getHeight()));
}
g2.dispose();
if (DEBUG) {
try {
// for debugging
File output = new File("test"+Math.random()+".png");
System.out.println("Saving image to disk "+output.getAbsolutePath());
ImageIO.write(newImage, "png", output);
} catch (IOException e) {
e.printStackTrace();
}
}
return newImage;
}
private Rectangle scaleAndTransformSubImageQuad(float[] quadPoints, RenderedImage img, PostExtractMarkup markup) {
// we find the upper left corner
int minX = getMinXFromQuadPoints(quadPoints);
int minY = getMinYFromQuadPoints(quadPoints);
// allows us to make
float scaledBorder = BORDER_WIDTH * markup.subImageContextMultiplier;
int x = Math.round((minX - scaledBorder) * SCALE_UP_FACTOR);
x = Math.max(x, 0); // keep subimage on screen
int y = Math.round((minY - scaledBorder) * SCALE_UP_FACTOR);
y = Math.max(y, 0); // keep subimage on screen
int width = Math.round((getMaxXFromQuadPoints(quadPoints) - minX + 2 * scaledBorder) * SCALE_UP_FACTOR);
width = Math.min(width, img.getWidth() - x); // clamp width
int height = Math.round((getMaxYFromQuadPoints(quadPoints) - minY + 2 * scaledBorder) * SCALE_UP_FACTOR);
height = Math.min(height, img.getHeight() - y); // clamp height
// the y is counted from the bottom, so we have to flip our coordinate
y = (img.getHeight() - y - height);
return new Rectangle(x, y, width, height);
}
private Rectangle scaleAndTransformAnnotationQuad(float[] oneQuad, Rectangle boundingRect, int imageHeight) {
int x = getMinXFromQuadPoints(oneQuad);
int y = getMinYFromQuadPoints(oneQuad);
int width = Math.round((getMaxXFromQuadPoints(oneQuad) - x) * SCALE_UP_FACTOR);
int height = Math.round((getMaxYFromQuadPoints(oneQuad) - y) * SCALE_UP_FACTOR);
x *= SCALE_UP_FACTOR;
y *= SCALE_UP_FACTOR;
x -= boundingRect.x;
// invert y again
y = imageHeight - y - boundingRect.y - height;
return new Rectangle(x, y, width, height);
}
private void paintCommentBox(Graphics2D g2, Rectangle rect) {
if (commentBoxImage != null) {
g2.drawImage(commentBoxImage, rect.x, rect.y, rect.width, rect.height, null);
} else {
g2.setColor(highlightColor);
g2.setStroke(new BasicStroke(2 * SCALE_UP_FACTOR));
g2.drawRect(rect.x, rect.y , rect.width, rect.height);
}
}
private void paintHighlight(Graphics2D g2, Rectangle rect) {
g2.setColor(highlightColor);
g2.fillRect(rect.x, rect.y , rect.width, rect.height);
}
//x values are on the even integers
private static int getMinXFromQuadPoints(float[] quadPoints) {
int min = Integer.MAX_VALUE;
for(int i = 0; i< quadPoints.length; i += 2) {
if (quadPoints[i] < min) {
min = (int)quadPoints[i];
}
}
return min;
}
//y values are on the even integers
private static int getMinYFromQuadPoints(float[] quadPoints) {
int min = Integer.MAX_VALUE;
for(int i = 1; i< quadPoints.length; i += 2) {
if (quadPoints[i] < min) {
min = (int)quadPoints[i];
}
}
return min;
}
//x values are on the even integers
private static int getMaxXFromQuadPoints(float[] quadPoints) {
int max = 0;
for(int i = 0; i< quadPoints.length; i += 2) {
if (quadPoints[i] > max) {
max = (int)quadPoints[i];
}
}
return max;
}
//y values are on the even integers
private static int getMaxYFromQuadPoints(float[] quadPoints) {
int max = 0;
for(int i = 1; i< quadPoints.length; i += 2) {
if (quadPoints[i] > max) {
max = (int)quadPoints[i];
}
}
return max;
}
public void updateCommentsWithColorsAndLinks(List<PdfComment> comments, Repo repo) {
@SuppressWarnings("unchecked")
List<PDPage> pages = doc.getDocumentCatalog().getAllPages();
int commentOn = 0;
for(PDPage page : pages) {
try {
List<PDAnnotation> newList = new ArrayList<PDAnnotation>();
List<PDAnnotation> annotations = page.getAnnotations();
for(int i=0; i<annotations.size(); i++) {
PDAnnotation anno = annotations.get(i);
if (anno instanceof PDAnnotationTextMarkup) {
PdfComment userComment = comments.get(commentOn);
commentOn++;
String newMessage = userComment.getMessageWithLink(repo);
PDAnnotationTextMarkup newComment = makeNewAnnotation((PDAnnotationTextMarkup) anno, userComment, newMessage);
newList.add(newComment);
} else {
newList.add(anno);
}
}
page.setAnnotations(newList);
} catch(IOException e) {
e.printStackTrace();
} finally {
//page.clear();
page.updateLastModified();
}
}
}
//Makes a brand new text annotation that is almost exactly like the one passed in.
// this is the only way I could get the annotations to actually change color.
private PDAnnotationTextMarkup makeNewAnnotation(PDAnnotationTextMarkup comment, PdfComment userComment, String messageWithLink) {
PDAnnotationTextMarkup newComment = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
List<Tag> tags = userComment.getTags();
if (tags.contains(Tag.CONSIDER_FIX) || tags.contains(Tag.POSITIVE)) {
newComment.setColour(GREEN);
} else if (tags.contains(Tag.MUST_FIX)) {
newComment.setColour(ORANGE);
} else {
newComment.setColour(YELLOW);
}
newComment.setContents(messageWithLink);
newComment.setRectangle(comment.getRectangle()); //both rectangle and quadpoints are needed... don't know why
newComment.setQuadPoints(comment.getQuadPoints());
newComment.setSubject(comment.getSubject());
newComment.setTitlePopup(comment.getTitlePopup()); //author name
return newComment;
}
public PDDocument getDoc() {
return doc;
}
public void close() throws IOException {
doc.close();
}
@SuppressWarnings("unused")
private static void main(String[] args) throws Exception{
FileInputStream fos = new FileInputStream("test.pdf");
Pdf pdf = new Pdf(fos, new FileInputStream("src/main/webapp/images/comment_box.PNG"));
System.out.println(pdf.getPDFComments());
}
}