Skip to content

Commit

Permalink
GH-234 basic text redaction tool functionality.
Browse files Browse the repository at this point in the history
  • Loading branch information
Patrick Corless committed Aug 17, 2023
1 parent 8306555 commit 5bfac8b
Show file tree
Hide file tree
Showing 34 changed files with 602 additions and 202 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ public abstract class Annotation extends Dictionary {
public static final Name SUBTYPE_POLYGON = new Name("Polygon");
public static final Name SUBTYPE_POLYLINE = new Name("PolyLine");
public static final Name SUBTYPE_HIGHLIGHT = new Name("Highlight");
public static final Name SUBTYPE_REDACT = new Name("Redact");
public static final Name SUBTYPE_POPUP = new Name("Popup");
public static final Name SUBTYPE_WIDGET = new Name("Widget");
public static final Name SUBTYPE_INK = new Name("Ink");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ else if (TextMarkupAnnotation.isTextMarkupAnnotation(subType)) {
return TextAnnotation.getInstance(library, rect);
} else if (subType.equals(Annotation.SUBTYPE_POPUP)) {
return PopupAnnotation.getInstance(library, rect);
} else if (subType.equals(Annotation.SUBTYPE_REDACT)) {
return RedactionAnnotation.getInstance(library, rect);
} else {
logger.warning("Unsupported Annotation type. ");
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,6 @@ public class CircleAnnotation extends MarkupAnnotation {
private static final Logger logger =
Logger.getLogger(CircleAnnotation.class.toString());

/**
* (Optional; PDF 1.4) An array of numbers in the range 0.0 to 1.0 specifying
* the interior color that shall be used to fill the annotation’s line endings
* (see Table 176). The number of array elements shall determine the colour
* space in which the colour is defined:
* 0 - No colour; transparent
* 1 - DeviceGray
* 3 - DeviceRGB
* 4 - DeviceCMYK
*/
public static final Name IC_KEY = new Name("IC");

// state properties for generate the content stream and shapes representation.
// of the annnotation state.
private Color fillColor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,7 @@ public class LineAnnotation extends MarkupAnnotation {
* Default value: 0 (no leader line extensions).
*/
public static final Name LLE_KEY = new Name("LLE");
/**
* (Optional; PDF 1.4) An array of numbers in the range 0.0 to 1.0 specifying
* the interior color that shall be used to fill the annotation’s line endings
* (see Table 176). The number of array elements shall determine the colour
* space in which the colour is defined:
* 0 - No colour; transparent
* 1 - DeviceGray
* 3 - DeviceRGB
* 4 - DeviceCMYK
*/
public static final Name IC_KEY = new Name("IC");

/**
* (Optional; PDF 1.6) If true, the text specified by the Contents or RC
* entries shall be replicated as a caption in the appearance of the line,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
import org.icepdf.core.pobjects.graphics.GraphicsState;
import org.icepdf.core.util.Library;

import java.awt.*;
import java.awt.geom.GeneralPath;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
Expand Down Expand Up @@ -161,6 +164,33 @@ public abstract class MarkupAnnotation extends Annotation {
*/
public static final Name EXT_GSTATE_NAME = new Name("ip1");

/**
* (Optional; PDF 1.4) An array of numbers in the range 0.0 to 1.0 specifying
* the interior color that shall be used to fill the annotation’s line endings
* (see Table 176). The number of array elements shall determine the colour
* space in which the colour is defined:
* 0 - No colour; transparent
* 1 - DeviceGray
* 3 - DeviceRGB
* 4 - DeviceCMYK
*/
public static final Name IC_KEY = new Name("IC");

/**
* (Required) An array of 8 × n numbers specifying the coordinates of
* n quadrilaterals in default user space. Each quadrilateral shall encompasses
* a word or group of contiguous words in the text underlying the annotation.
* The coordinates for each quadrilateral shall be given in the order
* x1 y1 x2 y2 x3 y3 x4 y4
* specifying the quadrilateral’s four vertices in counterclockwise order
* (see Figure 64). The text shall be oriented with respect to the edge
* connecting points (x1, y1) and (x2, y2).
* <br>
* The annotation dictionary’s AP entry, if present, shall take precedence
* over QuadPoints; see Table 168 and 12.5.5, "Appearance Streams."
*/
public static final Name KEY_QUAD_POINTS = new Name("QuadPoints");

protected String titleText;
protected PopupAnnotation popupAnnotation;
protected float opacity = 1.0f;
Expand All @@ -172,6 +202,13 @@ public abstract class MarkupAnnotation extends Annotation {
protected Name intent;
// exData not implemented

/**
* Converted Quad points.
*/
protected Shape[] quadrilaterals;
protected GeneralPath markupPath;
protected ArrayList<Shape> markupBounds;

public MarkupAnnotation(Library library, DictionaryEntries dictionaryEntries) {
super(library, dictionaryEntries);
}
Expand Down Expand Up @@ -224,6 +261,22 @@ public synchronized void init() throws InterruptedException {
}
}

protected static DictionaryEntries createCommonMarkupDictionary(Name subType, Rectangle rect) {
DictionaryEntries entries = new DictionaryEntries();
// set default link annotation values.
entries.put(Dictionary.TYPE_KEY, Annotation.TYPE_VALUE);
entries.put(Dictionary.SUBTYPE_KEY, subType);
entries.put(Annotation.FLAG_KEY, 4);
// coordinates
if (rect != null) {
entries.put(Annotation.RECTANGLE_KEY,
PRectangle.getPRectangleVector(rect));
} else {
entries.put(Annotation.RECTANGLE_KEY, new Rectangle(10, 10, 50, 100));
}
return entries;
}


public String getTitleText() {
if (titleText == null) {
Expand Down Expand Up @@ -406,4 +459,24 @@ public void setSubject(String subject) {
public String toString() {
return getPObjectReference() + " - " + getTitleText() + " - " + getContents();
}

public void setMarkupPath(GeneralPath markupPath) {
this.markupPath = markupPath;
}

public GeneralPath getMarkupPath() {
return markupPath;
}

public void setMarkupBounds(ArrayList<Shape> markupBounds) {
this.markupBounds = markupBounds;
}

// public Color getMarkupColor() {
// return color;
// }
//
// public void setMarkupColor(Color textMarkupColor) {
// this.color = textMarkupColor;
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,6 @@ public class PolyAnnotation extends MarkupAnnotation {
*/
public static final Name BE_KEY = new Name("BE");

/**
* (Optional; PDF 1.4) An array of numbers in the range 0.0 to 1.0 specifying
* the interior color that shall be used to fill the annotation’s line endings
* (see Table 176). The number of array elements shall determine the colour
* space in which the colour is defined:
* 0 - No colour; transparent
* 1 - DeviceGray
* 3 - DeviceRGB
* 4 - DeviceCMYK
* TODO consolidate duplication across the line type shapes to new base class.
*/
public static final Name IC_KEY = new Name("IC");

/**
* (Optional; PDF 1.6) A name that shall describe the intent of the polygon
* or polyline annotation (see also Table 170). The following values shall be
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package org.icepdf.core.pobjects.annotations;

import org.icepdf.core.pobjects.*;
import org.icepdf.core.pobjects.graphics.Shapes;
import org.icepdf.core.pobjects.graphics.commands.*;
import org.icepdf.core.util.ColorUtil;
import org.icepdf.core.util.Defs;
import org.icepdf.core.util.Library;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.icepdf.core.pobjects.annotations.utils.QuadPoints.buildQuadPoints;
import static org.icepdf.core.pobjects.annotations.utils.QuadPoints.parseQuadPoints;

/**
* RedactionAnnotations allow an area of content to be marked by the user for redaction. This annotation type
* does not actually remove the content instead it acts as a marker. The content will only be removed if the
* document is saved to an output stream using WriteMode.FULL_UPDATE.
*
* @since 7.2.0
*/
public class RedactionAnnotation extends MarkupAnnotation {

private static final Logger logger =
Logger.getLogger(RedactionAnnotation.class.toString());

private static Color redactionColor;

static {
// sets annotation selected redaction colour, generally it's always going to be black, but can be configured
try {
String color = Defs.sysProperty(
"org.icepdf.core.views.page.annotation.redactionColor.highlight.color", "#000000");
int colorValue = ColorUtil.convertColor(color);
redactionColor = new Color(colorValue >= 0 ? colorValue : Integer.parseInt("000000", 16));
} catch (NumberFormatException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("Error reading Text Markup Annotation redaction colour");
}
}
}

/*
* (Optional) A form XObject specifying the overlay appearance for this redaction annotation. After this redaction
* is applied and the affected content has been removed, the overlay appearance should be drawn such that its
* origin lines up with the lower-left corner of the annotation rectangle. This form XObject is not necessarily
* related to other annotation appearances, and may or may not be present in the AP dictionary. This entry takes
* precedence over the IC, OverlayText, DA, and Q entries.
*/
public static final Name RO_KEY = new Name("RO");

/*
* (Optional) A text string specifying the overlay text that should be drawn over the redacted region after the
* affected content has been removed. This entry is ignored if the RO entry is present.
*/
public static final Name OVERLAY_TEXT_KEY = new Name("OverlayText");

/*
* (Optional) If true, then the text specified by OverlayText should be repeated to fill the redacted region after
* the affected content has been removed. This entry is ignored if the RO entry is present.
* Default value: false.
*/
public static final Name REPEAT_KEY = new Name("Repeat");

/*
* (Required if OverlayText is present, ignored otherwise) The appearance string to be used in formatting the
* overlay text when it is drawn after the affected content has been removed (see 12.7.3.3, “Variable Text”).
* This entry is ignored if the RO entry is present.
*/
public static final Name DA_KEY = new Name("DA");

/**
* (Optional) A code specifying the form of quadding
* (justification) that shall be used in displaying the annotation’s text:
* 0 - Left-justified
* 1 - Centered
* 2 - Right-justified
* Default value: 0 (left-justified).
*/
public static final Name Q_KEY = new Name("Q");

public RedactionAnnotation(Library library, DictionaryEntries dictionaryEntries) {
super(library, dictionaryEntries);
}

/**
* Gets an instance of a TextMarkupAnnotation that has valid Object Reference.
*
* @param library document library
* @param rect bounding rectangle in user space
* @return new RedactAnnotation Instance.
*/
public static RedactionAnnotation getInstance(Library library, Rectangle rect) {
// state manager
StateManager stateManager = library.getStateManager();

// create a new entries to hold the annotation properties
DictionaryEntries entries = createCommonMarkupDictionary(Annotation.SUBTYPE_REDACT, rect);

RedactionAnnotation redactAnnotation = null;
try {
redactAnnotation = new RedactionAnnotation(library, entries);
redactAnnotation.init();
entries.put(NM_KEY, new LiteralStringObject(String.valueOf(redactAnnotation.hashCode())));
redactAnnotation.setPObjectReference(stateManager.getNewReferenceNumber());
redactAnnotation.setNew(true);
redactAnnotation.setModifiedDate(PDate.formatDateTime(new Date()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.fine("Text markup annotation instance creation was interrupted");
}
return redactAnnotation;
}

public void init() throws InterruptedException {
super.init();
// collect the quad points.
quadrilaterals = parseQuadPoints(library, entries);
color = getColor() == null ? redactionColor : getColor();

// for editing purposes grab anny shapes from the AP Stream and
// store them as markupBounds and markupPath. This works ok but
// perhaps a better way would be to reapply the bound box
Appearance appearance = appearances.get(currentAppearance);
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
Shapes shapes = appearanceState.getShapes();
if (shapes != null) {
markupBounds = new ArrayList<>();
markupPath = new GeneralPath();

ShapeDrawCmd shapeDrawCmd;
for (DrawCmd cmd : shapes.getShapes()) {
if (cmd instanceof ShapeDrawCmd) {
shapeDrawCmd = (ShapeDrawCmd) cmd;
markupBounds.add(shapeDrawCmd.getShape());
markupPath.append(shapeDrawCmd.getShape(), false);
}
}

}
// try and generate an appearance stream.
resetNullAppearanceStream();
}

@Override
public void resetAppearanceStream(double dx, double dy, AffineTransform pageSpace, boolean isNew) {
// check if we have anything to reset.
if (markupBounds == null) {
return;
}

// todo: Revisit as there is a lot of duplicate code here also found in textMarkupAnnotations, however
// this may change when image redaction code is added.

Appearance appearance = appearances.get(currentAppearance);
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
appearanceState.setShapes(new Shapes());

Rectangle2D bbox = appearanceState.getBbox();
AffineTransform matrix = appearanceState.getMatrix();
Shapes shapes = appearanceState.getShapes();

// set up the stroke from the border settings.
BasicStroke stroke = new BasicStroke(1f);
shapes.add(new StrokeDrawCmd(stroke));
shapes.add(new GraphicsStateCmd(EXT_GSTATE_NAME));
shapes.add(new AlphaDrawCmd(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)));
shapes.add(new ShapeDrawCmd(markupPath));
shapes.add(new ColorDrawCmd(color));
shapes.add(new FillDrawCmd());
shapes.add(new AlphaDrawCmd(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)));

// create the quad points
entries.put(KEY_QUAD_POINTS, buildQuadPoints(markupBounds));
setModifiedDate(PDate.formatDateTime(new Date()));

// update the appearance stream
// create/update the appearance stream of the xObject.
Form form = updateAppearanceStream(shapes, bbox, matrix,
PostScriptEncoder.generatePostScript(shapes.getShapes()), isNew);
generateExternalGraphicsState(form, opacity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,6 @@ public class SquareAnnotation extends MarkupAnnotation {
private static final Logger logger =
Logger.getLogger(SquareAnnotation.class.toString());

/**
* (Optional; PDF 1.4) An array of numbers in the range 0.0 to 1.0 specifying
* the interior color that shall be used to fill the annotation’s line endings
* (see Table 176). The number of array elements shall determine the colour
* space in which the colour is defined:
* 0 - No colour; transparent
* 1 - DeviceGray
* 3 - DeviceRGB
* 4 - DeviceCMYK
*/
public static final Name IC_KEY = new Name("IC");

private Color fillColor;
private boolean isFillColor;
private Rectangle rectangle;
Expand Down
Loading

0 comments on commit 5bfac8b

Please sign in to comment.