Skip to content

Commit

Permalink
GH-173 printable popup annotations (#265)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcorless authored May 8, 2023
1 parent ec88862 commit 416b29b
Show file tree
Hide file tree
Showing 9 changed files with 404 additions and 129 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.icepdf.core.pobjects;

import org.icepdf.core.pobjects.annotations.Annotation;
import org.icepdf.core.pobjects.annotations.MarkupAnnotation;
import org.icepdf.core.pobjects.annotations.MarkupGluePainter;
import org.icepdf.core.pobjects.annotations.PopupAnnotation;
import org.icepdf.core.util.Library;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

public class MarkupGlueAnnotation extends Annotation {

protected MarkupAnnotation markupAnnotation;
protected PopupAnnotation popupAnnotation;

public MarkupGlueAnnotation(Library l, DictionaryEntries h) {
super(l, h);
}

public MarkupGlueAnnotation(Library l, MarkupAnnotation markupAnnotation, PopupAnnotation popupAnnotation) {
super(l, new DictionaryEntries());
this.markupAnnotation = markupAnnotation;
this.popupAnnotation = popupAnnotation;
}

protected void renderAppearanceStream(Graphics2D g2d) {
if (this.popupAnnotation == null || this.markupAnnotation == null) return;

GraphicsConfiguration graphicsConfiguration = g2d.getDeviceConfiguration();
boolean isPrintingAllowed = this.markupAnnotation.getFlagPrint();
if (graphicsConfiguration.getDevice().getType() == GraphicsDevice.TYPE_PRINTER &&
this.popupAnnotation.isOpen() && isPrintingAllowed) {
AffineTransform oldTransform = g2d.getTransform();
new MarkupGluePainter(markupAnnotation, popupAnnotation, this).paint(g2d);
g2d.setTransform(oldTransform);
}
}

public Rectangle2D.Float getUserSpaceRectangle() {
// make sure we always update this to get the correct clip during painting
if (this.markupAnnotation != null && this.popupAnnotation != null) {
Rectangle rect = this.markupAnnotation.getUserSpaceRectangle().getBounds().union(
this.popupAnnotation.getUserSpaceRectangle().getBounds());
userSpaceRectangle = new Rectangle2D.Float(rect.x, rect.y, rect.width, rect.height);
}
return userSpaceRectangle;
}

public boolean allowPrintNormalMode() {
return allowScreenOrPrintRenderingOrInteraction() && this.markupAnnotation.getFlagPrint();
}

@Override
public void resetAppearanceStream(double dx, double dy, AffineTransform pageSpace, boolean isNew) {

}

public MarkupAnnotation getMarkupAnnotation() {
return markupAnnotation;
}
}
52 changes: 50 additions & 2 deletions core/core-awt/src/main/java/org/icepdf/core/pobjects/Page.java
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ else if (annotObj instanceof DictionaryEntries) { // HashMap lacks "Type"->"Anno
// add any found annotations to the vector.
annotations.add(a);
}
// create synthetic annotation to paint the glue between a markup annotation and the popup
// this is only used for print purposes. A similar pattern is also used in the Viewer RI
createPrintableMarkupAnnotationGlue(a);
}
} catch (IllegalStateException e) {
Annotation finalA = a;
Expand Down Expand Up @@ -809,15 +812,33 @@ public Shape getPageShape(int boundary, float userRotation, float userZoom) {
return path.createTransformedShape(at);
}

private void createPrintableMarkupAnnotationGlue(Annotation annotation) {
// create synthetic annotation to paint the glue between a markup annotation and the popup
// this is only used for print purposes. A similar pattern is also used in the Viewer RI
if (annotation instanceof PopupAnnotation) {
PopupAnnotation popupAnnotation = (PopupAnnotation) annotation;
MarkupAnnotation markupAnnotation = popupAnnotation.getParent();
// insert glue before popup is painted, so we don't over paint
Annotation annot;
for (int i = 0; i < annotations.size(); i++) {
annot = annotations.get(i);
if (annot instanceof PopupAnnotation && annot.equals(popupAnnotation)) {
annotations.add(i, new MarkupGlueAnnotation(library, markupAnnotation, popupAnnotation));
break;
}
}
}
}

/**
* Adds an annotation that was previously added to the document. It is
* assumed that the annotation has a valid object reference. This
* is commonly used with the undo/redo state manager in the RI. Use
* the method @link{#createAnnotation} for creating new annotations.
*
* @param newAnnotation annotation object to add
* @param isNew annotation is new and should be added to stateManager, otherwise change will be part of the document
* but not yet added to the stateManager as the change was likely a missing content stream or popup.
* @param isNew annotation is new and should be added to stateManager, otherwise change will be part of the document
* but not yet added to the stateManager as the change was likely a missing content stream or popup.
* @return reference to annotation that was added.
*/
@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -883,6 +904,9 @@ public Annotation addAnnotation(Annotation newAnnotation, boolean isNew) {
// add the annotations to the parsed annotations list
this.annotations.add(newAnnotation);

// add visual glue for markup annotation
createPrintableMarkupAnnotationGlue(newAnnotation);

// add the new annotations to the library
library.addObject(newAnnotation, newAnnotation.getPObjectReference());

Expand Down Expand Up @@ -971,6 +995,30 @@ else if (annot.isNew()) {
if (annotations != null) {
annotations.remove(annot);
}
// todo clean up orphaned popup annotations
// remove any corresponding popup annotation.
if (annot instanceof MarkupAnnotation) {
MarkupAnnotation markupAnnotation = (MarkupAnnotation) annot;
for (Annotation annotation : annotations) {
if (annotation instanceof PopupAnnotation &&
annotation.equals(markupAnnotation.getPopupAnnotation())) {
annotations.remove(annotation);
break;
}
}
}
// remove any markupGlue so that it doesn't get painted. Glue is never added to the document, it created
// dynamically for print purposes.
if (annot instanceof MarkupAnnotation) {
MarkupAnnotation markupAnnotation = (MarkupAnnotation) annot;
for (Annotation annotation : annotations) {
if (annotation instanceof MarkupGlueAnnotation &&
((MarkupGlueAnnotation) annotation).getMarkupAnnotation().equals(markupAnnotation)) {
annotations.remove(annotation);
break;
}
}
}
// finally remove it from the library to free up the memory
library.removeObject(annot.getPObjectReference());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1308,7 +1308,10 @@ public void render(Graphics2D origG, int renderHintType,
origG.setRenderingHints(grh.getRenderingHints(renderHintType));
origG.setTransform(at);
Shape preAppearanceStreamClip = origG.getClip();
origG.clip(deriveDrawingRectangle());
Rectangle2D.Float derivedClip = deriveDrawingRectangle();
if (derivedClip != null) {
origG.clip(deriveDrawingRectangle());
}

renderAppearanceStream(origG);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
import org.icepdf.core.pobjects.graphics.GraphicsState;
import org.icepdf.core.util.Library;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

/**
Expand Down Expand Up @@ -301,6 +305,25 @@ public PDate getCreationDate() {
return creationDate;
}

/**
* Format the creation date using the given FormatStyle
* @param formatStyle date output style used by DateTimeFormatter
* @return formatted creation date if available, empty String otherwise
*/
public String getFormattedCreationDate(FormatStyle formatStyle) {
LocalDateTime creationDate = getCreationDate().asLocalDateTime();
if (creationDate == null) return "";
DateTimeFormatter formatter = DateTimeFormatter
.ofLocalizedDateTime(formatStyle)
.withLocale(Locale.getDefault());
return creationDate.format(formatter);
}

public String getFormattedTitleText() {
String titleText = getTitleText();
return titleText != null ? titleText : "";
}

public boolean isInReplyTo() {
return library.getObject(entries, IRT_KEY) != null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.icepdf.core.pobjects.annotations;

import org.icepdf.core.pobjects.MarkupGlueAnnotation;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;

/**
* MarkupGluePainter allows for a visual associating between a markup annotation, and it's popup annotation
* when open. This painter code is called from the component used in the Viewer RI as well as painting popups
* for printing purposes.
*
* @since 7.1
*/
public class MarkupGluePainter {

protected MarkupAnnotation markupAnnotation;
protected PopupAnnotation popupAnnotation;

protected MarkupGlueAnnotation markupGlueAnnotation;

public MarkupGluePainter(MarkupAnnotation markupAnnotation, PopupAnnotation popupAnnotation,
MarkupGlueAnnotation markupGlueAnnotation) {
this.markupAnnotation = markupAnnotation;
this.popupAnnotation = popupAnnotation;
this.markupGlueAnnotation = markupGlueAnnotation;
}

public void paint(Graphics g) {
if (popupAnnotation.isOpen()) {
Rectangle popupBounds = popupAnnotation.getUserSpaceRectangle().getBounds();
Rectangle markupBounds = markupAnnotation.getUserSpaceRectangle().getBounds();
Rectangle glueBounds = markupGlueAnnotation.getUserSpaceRectangle().getBounds();
MarkupGluePainter.paintGlue(
g, markupBounds, popupBounds, glueBounds, markupAnnotation.getColor());
}
}

public static void paintGlue(Graphics g, Rectangle markupBounds, Rectangle popupBounds, Rectangle glueBounds,
Color color) {
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(color);
g2d.setStroke(new BasicStroke(1));
GeneralPath path = new GeneralPath();
path.moveTo(0, 0);

// in order to draw the curvy shape we need to determine which of the 8 surrounding regions
// the popup is relative to the markup annotation.
int popupX = popupBounds.x;
int popupY = popupBounds.y;
int popupXC = (int) popupBounds.getCenterX();
int popupYC = (int) popupBounds.getCenterY();
int popupW = popupBounds.width;
int popupH = popupBounds.height;

int markupXC = (int) markupBounds.getCenterX();
int markupYC = (int) markupBounds.getCenterY();

float angle = (float) Math.toDegrees(Math.atan2(markupYC - popupYC, markupXC - popupXC));
if (angle < 0) {
angle += 360;
}
// N
if (angle >= 67.5 && angle < 112.5) {
path.moveTo(markupXC, markupYC);
path.quadTo(popupXC, popupY + popupH, popupX, popupY + popupH);
path.lineTo(popupX + popupW, popupY + popupH);
path.quadTo(popupXC, popupY + popupH, markupXC, markupYC);
}
// NE
else if (angle >= 112.5 && angle < 157.5) {
path.moveTo(markupXC, markupYC);
path.quadTo(popupX, popupY + popupH, popupX, popupYC);
path.lineTo(popupXC, popupY + popupH);
path.quadTo(popupX, popupY + popupH, markupXC, markupYC);
}
// E
else if (angle >= 157.5 && angle < 202.5) {
path.moveTo(markupXC, markupYC);
path.quadTo(popupX, popupYC, popupX, popupY);
path.lineTo(popupX, popupY + popupH);
path.quadTo(popupX, popupYC, markupXC, markupYC);
}
// SE
else if (angle >= 202.5 && angle < 247.5) {
path.moveTo(markupXC, markupYC);
path.quadTo(popupX, popupY, popupXC, popupY);
path.lineTo(popupX, popupYC);
path.quadTo(popupX, popupY, markupXC, markupYC);
}
// S
else if (angle >= 247.5 && angle < 292.5) {
path.moveTo(markupXC, markupYC);
path.quadTo(popupXC, popupY, popupX, popupY);
path.lineTo(popupX + popupW, popupY);
path.quadTo(popupXC, popupY, markupXC, markupYC);
} else if (angle >= 292.5 && angle < 315) {
path.moveTo(markupXC, markupYC);
path.quadTo(popupX + popupW, popupY, popupXC, popupY);
path.lineTo(popupX + popupW, popupYC);
path.quadTo(popupX + popupW, popupY, markupXC, markupYC);
}
// W
else if (angle >= 315 || angle < 22.5) {
path.moveTo(markupXC, markupYC);
path.quadTo(popupX + popupW, popupYC, popupX + popupW, popupY);
path.lineTo(popupX + popupW, popupY + popupH);
path.quadTo(popupX + popupW, popupYC, markupXC, markupYC);
}
// NW
else if (angle >= 22.5 && angle < 67.5) {
path.moveTo(markupXC, markupYC);
path.quadTo(popupX + popupW, popupY + popupH, popupX + popupW, popupYC);
path.lineTo(popupXC, popupY + popupH);
path.quadTo(popupX + popupW, popupY + popupH, markupXC, markupYC);
}
// translate to this components space.
path.transform(new AffineTransform(1, 0, 0, 1, -glueBounds.x, -glueBounds.y));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
g2d.fill(path);
}
}
Loading

0 comments on commit 416b29b

Please sign in to comment.