com.openhtmltopdf.pdfboxout.PdfBoxOutputDevice Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openhtmltopdf-pdfbox Show documentation
Show all versions of openhtmltopdf-pdfbox Show documentation
Openhtmltopdf is a CSS 2.1 renderer written in Java. This artifact supports PDF output with Apache PDF-BOX 2.
/*
* {{{ header & license
* Copyright (c) 2006 Wisconsin Court System
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
* }}}
*/
package com.openhtmltopdf.pdfboxout;
import com.openhtmltopdf.bidi.BidiReorderer;
import com.openhtmltopdf.bidi.SimpleBidiReorderer;
import com.openhtmltopdf.css.constants.CSSName;
import com.openhtmltopdf.css.parser.FSCMYKColor;
import com.openhtmltopdf.css.parser.FSColor;
import com.openhtmltopdf.css.parser.FSRGBColor;
import com.openhtmltopdf.css.style.CalculatedStyle;
import com.openhtmltopdf.css.style.CssContext;
import com.openhtmltopdf.extend.FSImage;
import com.openhtmltopdf.extend.OutputDevice;
import com.openhtmltopdf.layout.SharedContext;
import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver.FontDescription;
import com.openhtmltopdf.pdfboxout.PdfBoxForm.CheckboxStyle;
import com.openhtmltopdf.render.*;
import com.openhtmltopdf.util.Configuration;
import com.openhtmltopdf.util.XRLog;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitHeightDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.RenderingHints.Key;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.regex.Pattern;
public class PdfBoxOutputDevice extends AbstractOutputDevice implements OutputDevice {
//
// A discussion on units:
// PDF points are defined as 1/72 inch.
// CSS pixels are defined as 1/96 inch.
// PDF text units are defined as 1/1000 of a PDF point.
// OpenHTMLtoPDF dots are defined as 1/20 of a CSS pixel.
// Therefore dots per point is 20 * 96/72 or about 26.66.
// Dividing by _dotsPerPoint will convert OpenHTMLtoPDF dots to PDF points.
// Theoretically, this is all configurable, but not tested at all with other values.
//
private static final int FILL = 1;
private static final int STROKE = 2;
private static final int CLIP = 3;
private static final AffineTransform IDENTITY = new AffineTransform();
private static final BasicStroke STROKE_ONE = new BasicStroke(1);
private static final boolean ROUND_RECT_DIMENSIONS_DOWN = Configuration.isTrue("xr.pdf.round.rect.dimensions.down", false);
// The current PDF page.
private PDPage _page;
// A wrapper around the IOException throwing content stream methods which only throws runtime exceptions.
// Created for every page.
private PdfContentStreamAdapter _cp;
// We need the page height because the project uses top down units which PDFs use bottom up units.
// This is in PDF points unit (1/72 inch).
private float _pageHeight;
// The desired font as set by setFont.
// This may not yet be set on the PDF text stream.
private PdfBoxFSFont _font;
// This transform is a scale and translate.
// It scales from internal dots to PDF points.
// It translates positions to implement page margins.
private AffineTransform _transform = new AffineTransform();
// A stack of currently in force transforms on the PDF graphics state.
// NOTE: Transforms are cumulative and order is important.
// After the graphics state is restored in setClip we must appropriately reapply the transforms
// that should be in effect.
private final Deque transformStack = new ArrayDeque();
// An index into the transformStack. When we save state we set this to the length of transformStack
// then we know we have to reapply those transforms set after saving state upon restoring state.
private int clipTransformIndex;
// We use these to keep track of where the current transform-origin is in absolute internal dots units.
private float _absoluteTransformOriginX = 0;
private float _absoluteTransformOriginY = 0;
// The desired color as set by setColor.
// To make sure this color is set on the PDF graphics stream call ensureFillColor or ensureStrokeColor.
private FSColor _color = FSRGBColor.BLACK;
// The actual fill and stroke colors set on the PDF graphics stream.
// We keep these so we don't bloat the PDF with unneeded color calls.
private FSColor _fillColor;
private FSColor _strokeColor;
// The currently set stroke. This will not yet be set on the PDF graphics stream.
// This is already transformed to PDF points units.
// Call setStrokeDiff to set this on the PDF graphics stream.
private Stroke _stroke = null;
// Same as _stroke, but not transformed. That is, it is in internal dots units.
private Stroke _originalStroke = null;
// The currently set stroke on the PDF graphics stream. When we call setStokeDiff
// this is compared with _stroke and only the differences are output to the graphics stream.
private Stroke _oldStroke = null;
// The clipped area, as set on the PDF graphics stream, in PDF points units.
private Area _clip;
// Essentially per-run global variables.
private SharedContext _sharedContext;
// The project internal dots per PDF point unit. See discussion of units above.
private float _dotsPerPoint;
// The PDF document. Note: We are not responsible for closing it.
private PDDocument _writer;
// The default destination for the current page.
// This is used to create bookmarks without a valid destination.
private PDDestination _defaultDestination;
// Contains a list of bookmarks for the document.
private final List _bookmarks = new ArrayList();
// Contains a list of metadata items for the document.
private final List _metadata = new ArrayList();
// We keep a map of forms for the document so we can add controls to the correct form as they are seen.
private final Map forms = new HashMap();
// The list of controls in the document. Control class contains all the info we need to output a control.
private final List controls = new ArrayList();
// A set of controls, so we don't double process a control.
private final Set seenControls = new HashSet();
// We keep a map of fonts to font resource name so we don't double add fonts needed for form controls.
private final Map controlFonts = new HashMap();
// The checkbox style to appearance stream map. We only create appearance streams on demand and once for a specific
// style so we store appearance streams created here.
final Map checkboxAppearances = new EnumMap(CheckboxStyle.class);
// Again, we only create these appearance streams as needed.
PDAppearanceStream checkboxOffAppearance;
PDAppearanceStream radioBoxOffAppearance;
PDAppearanceStream radioBoxOnAppearance;
// The root box in the document. We keep this so we can search for specific boxes below it
// such as links or form controls which we need to position.
private Box _root;
// In theory, we can append to a PDF document, rather than creating new. This keeps the start page
// so we can use it to offset when we need to know the PDF page number.
// NOTE: Not tested recently, this feature may be broken.
private int _startPageNo;
// Whether we are in test mode, currently not used here, but keep around in case we need it down the track.
@SuppressWarnings("unused")
private final boolean _testMode;
// Link manage handles a links. We add the link in paintBackground and then output links when the document is finished.
private PdfBoxLinkManager _linkManager;
// Not used currently.
@SuppressWarnings("unused")
private RenderingContext _renderingContext;
// The bidi reorderer is responsible for shaping Arabic text, deshaping and
// converting RTL text into its visual order.
private BidiReorderer _reorderer = new SimpleBidiReorderer();
public PdfBoxOutputDevice(float dotsPerPoint, boolean testMode) {
_dotsPerPoint = dotsPerPoint;
_testMode = testMode;
}
public void setWriter(PDDocument writer) {
_writer = writer;
}
public PDDocument getWriter() {
return _writer;
}
/**
* Start a page. A new PDF page starts a new content stream so all graphics state has to be
* set back to default.
*/
public void initializePage(PDPageContentStream currentPage, PDPage page, float height) {
_cp = new PdfContentStreamAdapter(currentPage);
_page = page;
_pageHeight = height;
// We call saveGraphics so we can get back to a raw (unclipped) state after we have clipped.
// restoreGraphics is only used by setClip and page finish.
_cp.saveGraphics();
_transform = new AffineTransform();
_transform.scale(1.0d / _dotsPerPoint, 1.0d / _dotsPerPoint);
_absoluteTransformOriginX = 0;
_absoluteTransformOriginY += height * _dotsPerPoint;
_stroke = transformStroke(STROKE_ONE);
_originalStroke = _stroke;
_oldStroke = _stroke;
setStrokeDiff(_stroke, null);
if (_defaultDestination == null) {
// Create a default destination to the top of the first page.
PDPageFitHeightDestination dest = new PDPageFitHeightDestination();
dest.setPage(page);
}
}
public void finishPage() {
_cp.restoreGraphics();
_cp.closeContent();
}
public void paintReplacedElement(RenderingContext c, BlockBox box) {
PdfBoxReplacedElement element = (PdfBoxReplacedElement) box.getReplacedElement();
element.paint(c, this, box);
}
/**
* We use paintBackground to do extra stuff such as processing links, forms and form controls.
*/
public void paintBackground(RenderingContext c, Box box) {
super.paintBackground(c, box);
_linkManager.processLinkLater(c, box, _page, _pageHeight, _transform);
if (box.getElement() != null && box.getElement().getNodeName().equals("form")) {
if (!forms.containsKey(box.getElement())) {
PdfBoxForm frm = PdfBoxForm.createForm(box.getElement());
forms.put(box.getElement(), frm);
}
} else if (box.getElement() != null &&
(box.getElement().getNodeName().equals("input") ||
box.getElement().getNodeName().equals("textarea") ||
box.getElement().getNodeName().equals("button") ||
box.getElement().getNodeName().equals("select") ||
box.getElement().getNodeName().equals("openhtmltopdf-combo"))) {
// Add controls to list to process later. We do this in case we paint a control background
// before its associated form.
if (!seenControls.contains(box.getElement())) {
controls.add(new PdfBoxForm.Control(box, _page, _transform, c, _pageHeight));
seenControls.add(box.getElement());
}
}
}
public void processControls() {
PDResources checkBoxFontResource = null;
for (PdfBoxForm.Control ctrl : controls) {
PdfBoxForm frm = findEnclosingForm(ctrl.box.getElement());
String fontName = null;
if (!(ctrl.box.getElement().getAttribute("type").equals("checkbox") ||
ctrl.box.getElement().getAttribute("type").equals("radio") ||
ctrl.box.getElement().getAttribute("type").equals("hidden"))) {
PDFont fnt = ((PdfBoxFSFont) _sharedContext.getFont(ctrl.box.getStyle().getFontSpecification())).getFontDescription().get(0).getFont();
if (!controlFonts.containsKey(fnt)) {
fontName = "OpenHTMLFont" + controlFonts.size();
controlFonts.put(fnt, fontName);
} else {
fontName = controlFonts.get(fnt);
}
} else if (ctrl.box.getElement().getAttribute("type").equals("checkbox")) {
CheckboxStyle style = CheckboxStyle.fromIdent(ctrl.box.getStyle().getIdent(CSSName.FS_CHECKBOX_STYLE));
if (checkBoxFontResource == null) {
checkBoxFontResource = new PDResources();
checkBoxFontResource.put(COSName.getPDFName("OpenHTMLZap"), PDType1Font.ZAPF_DINGBATS);
}
if (!checkboxAppearances.containsKey(style)) {
PDAppearanceStream strm = PdfBoxForm.createCheckboxAppearance(style, getWriter(), checkBoxFontResource);
checkboxAppearances.put(style, strm);
}
if (checkboxOffAppearance == null) {
checkboxOffAppearance = PdfBoxForm.createCheckboxAppearance("q\nQ\n", getWriter(), checkBoxFontResource);
}
} else if (ctrl.box.getElement().getAttribute("type").equals("radio")) {
if (checkBoxFontResource == null) {
checkBoxFontResource = new PDResources();
checkBoxFontResource.put(COSName.getPDFName("OpenHTMLZap"), PDType1Font.ZAPF_DINGBATS);
}
if (radioBoxOffAppearance == null) {
radioBoxOffAppearance = PdfBoxForm.createCheckboxAppearance("q\nQ\n", getWriter(), checkBoxFontResource);
}
if (radioBoxOnAppearance == null) {
radioBoxOnAppearance = PdfBoxForm.createCheckboxAppearance(CheckboxStyle.DIAMOND, getWriter(), checkBoxFontResource);
}
}
if (frm != null) {
frm.addControl(ctrl, fontName);
}
}
PDResources resources = new PDResources();
for (Map.Entry fnt : controlFonts.entrySet()) {
resources.put(COSName.getPDFName(fnt.getValue()), fnt.getKey());
}
if (forms.size() != 0) {
int start = 0;
PDAcroForm acro = new PDAcroForm(_writer);
acro.setNeedAppearances(Boolean.TRUE);
acro.setDefaultResources(resources);
_writer.getDocumentCatalog().setAcroForm(acro);
for (PdfBoxForm frm : forms.values()) {
try {
start = 1 + frm.process(acro, start, _root, this);
} catch (IOException e) {
throw new PdfContentStreamAdapter.PdfException("processControls", e);
}
}
}
}
/**
* Helper function to find an enclosing PdfBoxForm given a input or textarea element.
* @param e
* @return
*/
public PdfBoxForm findEnclosingForm(Node e) {
Node parent;
while ((parent = e.getParentNode()) != null) {
if (parent.getNodeType() == Node.ELEMENT_NODE &&
parent.getNodeName().equals("form")) {
Element frmElement = (Element) parent;
if (forms.containsKey(frmElement)) {
return forms.get(frmElement);
}
}
e = parent;
}
XRLog.general(Level.WARNING, "Found form control (" + e.getNodeName() + ") with no enclosing form. Ignoring.");
return null;
}
/**
* Given a value in dots units, converts to PDF points.
*/
public float getDeviceLength(float length) {
return length / _dotsPerPoint;
}
public void drawBorderLine(Shape bounds, int side, int lineWidth, boolean solid) {
draw(bounds);
}
public void setColor(FSColor color) {
if (color instanceof FSRGBColor) {
_color = color;
} else if (color instanceof FSCMYKColor) {
_color = color;
} else {
assert(_color instanceof FSRGBColor || _color instanceof FSCMYKColor);
}
}
public void draw(Shape s) {
followPath(s, STROKE);
}
protected void drawLine(int x1, int y1, int x2, int y2) {
Line2D line = new Line2D.Double(x1, y1, x2, y2);
draw(line);
}
public void drawRect(int x, int y, int width, int height) {
draw(new Rectangle(x, y, width, height));
}
public void drawOval(int x, int y, int width, int height) {
Ellipse2D oval = new Ellipse2D.Float(x, y, width, height);
draw(oval);
}
public void fill(Shape s) {
followPath(s, FILL);
}
public void fillRect(int x, int y, int width, int height) {
if (ROUND_RECT_DIMENSIONS_DOWN) {
fill(new Rectangle(x, y, width - 1, height - 1));
} else {
fill(new Rectangle(x, y, width, height));
}
}
public void fillOval(int x, int y, int width, int height) {
Ellipse2D oval = new Ellipse2D.Float(x, y, width, height);
fill(oval);
}
public void translate(double tx, double ty) {
_transform.translate(tx, ty);
}
public Object getRenderingHint(Key key) {
return null;
}
public void setRenderingHint(Key key, Object value) {
}
public void setFont(FSFont font) {
_font = ((PdfBoxFSFont) font);
}
/**
* This returns a matrix that will convert y values to bottom up coordinate space (as used by PDFs).
*/
private AffineTransform normalizeMatrix(AffineTransform current) {
double[] mx = new double[6];
AffineTransform result = new AffineTransform();
result.getMatrix(mx);
mx[3] = -1;
mx[5] = _pageHeight;
result = new AffineTransform(mx);
result.concatenate(current);
return result;
}
public void drawString(String s, float x, float y, JustificationInfo info) {
PDFont firstFont = _font.getFontDescription().get(0).getFont();
// First check if the string will print with the current font entirely.
try {
firstFont.getStringWidth(s);
// We got here, so all is good.
drawStringFast(s, x, y, info, _font.getFontDescription().get(0), _font.getSize2D());
return;
}
catch (Exception e) {
// Fallthrough, we'll have to process the string into font runs.
}
List fontRuns = PdfBoxTextRenderer.divideIntoFontRuns(_font, s, _reorderer);
float xOffset = 0f;
for (FontRun run : fontRuns) {
drawStringFast(run.str, x + xOffset, y, info, run.des, _font.getSize2D());
try {
xOffset += (run.des.getFont().getStringWidth(run.str) / 1000f) * _font.getSize2D();
} catch (Exception e) {
XRLog.render(Level.WARNING, "BUG. Font didn't contain expected character.", e);
}
}
}
public void drawStringFast(String s, float x, float y, JustificationInfo info, FontDescription desc, float fontSize) {
// TODO: Emulate bold and italic.
if (s.length() == 0)
return;
ensureFillColor();
AffineTransform at = (AffineTransform) getTransform().clone();
at.translate(x, y);
AffineTransform inverse = normalizeMatrix(at);
AffineTransform flipper = AffineTransform.getScaleInstance(1, -1);
inverse.concatenate(flipper);
inverse.scale(_dotsPerPoint, _dotsPerPoint);
double[] mx = new double[6];
inverse.getMatrix(mx);
float b = (float) mx[1];
float c = (float) mx[2];
fontSize = fontSize / _dotsPerPoint;
_cp.beginText();
_cp.setFont(desc.getFont(), fontSize);
_cp.setTextMatrix((float) mx[0], b, c, (float) mx[3], (float) mx[4], (float) mx[5]);
if (info != null ) {
// The JustificationInfo numbers need to be normalized using the current document DPI
_cp.setTextSpacing(info.getNonSpaceAdjust() / _dotsPerPoint);
_cp.setSpaceSpacing(info.getSpaceAdjust() / _dotsPerPoint);
} else {
_cp.setTextSpacing(0.0f);
_cp.setSpaceSpacing(0.0f);
}
_cp.drawString(s);
_cp.endText();
}
public static class FontRun {
String str;
FontDescription des;
}
/*
public void drawString(String s, float x, float y, JustificationInfo info) {
if (Configuration.isTrue("xr.renderer.replace-missing-characters", false)) {
s = replaceMissingCharacters(s);
}
if (s.length() == 0)
return;
ensureFillColor();
AffineTransform at = (AffineTransform) getTransform().clone();
at.translate(x, y);
AffineTransform inverse = normalizeMatrix(at);
AffineTransform flipper = AffineTransform.getScaleInstance(1, -1);
inverse.concatenate(flipper);
inverse.scale(_dotsPerPoint, _dotsPerPoint);
double[] mx = new double[6];
inverse.getMatrix(mx);
_cp.beginText();
// Check if bold or italic need to be emulated
boolean resetMode = false;
FontDescription desc = _font.getFontDescription();
float fontSize = _font.getSize2D() / _dotsPerPoint;
cb.setFontAndSize(desc.getFont(), fontSize);
float b = (float) mx[1];
float c = (float) mx[2];
FontSpecification fontSpec = getFontSpecification();
if (fontSpec != null) {
int need = ITextFontResolver.convertWeightToInt(fontSpec.fontWeight);
int have = desc.getWeight();
if (need > have) {
cb.setTextRenderingMode(PdfContentByte.TEXT_RENDER_MODE_FILL_STROKE);
float lineWidth = fontSize * 0.04f; // 4% of font size
cb.setLineWidth(lineWidth);
resetMode = true;
ensureStrokeColor();
}
if ((fontSpec.fontStyle == IdentValue.ITALIC) && (desc.getStyle() != IdentValue.ITALIC)) {
b = 0f;
c = 0.21256f;
}
}
cb.setTextMatrix((float) mx[0], b, c, (float) mx[3], (float) mx[4], (float) mx[5]);
if (info == null) {
_cp.drawString(s);
} else {
PdfTextArray array = makeJustificationArray(s, info);
cb.showText(array);
}
if (resetMode) {
cb.setTextRenderingMode(PdfContentByte.TEXT_RENDER_MODE_FILL);
cb.setLineWidth(1);
}
_cp.endText();
}
*/
/*
private PdfTextArray makeJustificationArray(String s, JustificationInfo info) {
PdfTextArray array = new PdfTextArray();
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
array.add(Character.toString(c));
if (i != len - 1) {
float offset;
if (c == ' ' || c == '\u00a0' || c == '\u3000') {
offset = info.getSpaceAdjust();
} else {
offset = info.getNonSpaceAdjust();
}
array.add((-offset / _dotsPerPoint) * 1000 / (_font.getSize2D() / _dotsPerPoint));
}
}
return array;
}
*/
private AffineTransform getTransform() {
return _transform;
}
private void ensureFillColor() {
if (!(_color.equals(_fillColor))) {
_fillColor = _color;
if (_fillColor instanceof FSRGBColor) {
FSRGBColor rgb = (FSRGBColor) _fillColor;
_cp.setFillColor(rgb.getRed(), rgb.getGreen(), rgb.getBlue());
} else if (_fillColor instanceof FSCMYKColor) {
FSCMYKColor cmyk = (FSCMYKColor) _fillColor;
_cp.setFillColor(cmyk.getCyan(), cmyk.getMagenta(), cmyk.getYellow(), cmyk.getBlack());
}
else {
assert(_fillColor instanceof FSRGBColor || _fillColor instanceof FSCMYKColor);
}
}
}
private void ensureStrokeColor() {
if (!(_color.equals(_strokeColor))) {
_strokeColor = _color;
if (_strokeColor instanceof FSRGBColor) {
FSRGBColor rgb = (FSRGBColor) _strokeColor;
_cp.setStrokingColor(rgb.getRed(), rgb.getGreen(), rgb.getBlue());
} else if (_strokeColor instanceof FSCMYKColor) {
FSCMYKColor cmyk = (FSCMYKColor) _strokeColor;
_cp.setStrokingColor(cmyk.getCyan(), cmyk.getMagenta(), cmyk.getYellow(), cmyk.getBlack());
}
else {
assert(_strokeColor instanceof FSRGBColor || _strokeColor instanceof FSCMYKColor);
}
}
}
public PdfContentStreamAdapter getCurrentPage() {
return _cp;
}
private void followPath(Shape s, int drawType) {
if (s == null)
return;
if (drawType == STROKE) {
if (!(_stroke instanceof BasicStroke)) {
s = _stroke.createStrokedShape(s);
followPath(s, FILL);
return;
}
}
if (drawType == STROKE) {
setStrokeDiff(_stroke, _oldStroke);
_oldStroke = _stroke;
ensureStrokeColor();
} else if (drawType == FILL) {
ensureFillColor();
}
PathIterator points;
if (drawType == CLIP) {
points = s.getPathIterator(IDENTITY);
} else {
points = s.getPathIterator(_transform);
}
float[] coords = new float[6];
int traces = 0;
while (!points.isDone()) {
++traces;
int segtype = points.currentSegment(coords);
normalizeY(coords);
switch (segtype) {
case PathIterator.SEG_CLOSE:
_cp.closeSubpath();
break;
case PathIterator.SEG_CUBICTO:
_cp.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
break;
case PathIterator.SEG_LINETO:
_cp.lineTo(coords[0], coords[1]);
break;
case PathIterator.SEG_MOVETO:
_cp.moveTo(coords[0], coords[1]);
break;
case PathIterator.SEG_QUADTO:
_cp.curveTo(coords[0], coords[1], coords[2], coords[3]);
break;
}
points.next();
}
switch (drawType) {
case FILL:
if (traces > 0) {
if (points.getWindingRule() == PathIterator.WIND_EVEN_ODD)
_cp.fillEvenOdd();
else
_cp.fillNonZero();
}
break;
case STROKE:
if (traces > 0)
_cp.stroke();
break;
default: // drawType==CLIP
if (traces == 0)
_cp.addRect(0, 0, 0, 0);
if (points.getWindingRule() == PathIterator.WIND_EVEN_ODD)
_cp.clipEvenOdd();
else
_cp.clipNonZero();
_cp.newPath();
}
}
/**
* Converts a top down unit to a bottom up PDF unit.
*/
private float normalizeY(float y) {
return _pageHeight - y;
}
private void normalizeY(float[] coords) {
coords[1] = normalizeY(coords[1]);
coords[3] = normalizeY(coords[3]);
coords[5] = normalizeY(coords[5]);
}
private void setStrokeDiff(Stroke newStroke, Stroke oldStroke) {
if (newStroke == oldStroke)
return;
if (!(newStroke instanceof BasicStroke))
return;
BasicStroke nStroke = (BasicStroke) newStroke;
boolean oldOk = (oldStroke instanceof BasicStroke);
BasicStroke oStroke = null;
if (oldOk)
oStroke = (BasicStroke) oldStroke;
if (!oldOk || nStroke.getLineWidth() != oStroke.getLineWidth())
_cp.setLineWidth(nStroke.getLineWidth());
if (!oldOk || nStroke.getEndCap() != oStroke.getEndCap()) {
switch (nStroke.getEndCap()) {
case BasicStroke.CAP_BUTT:
_cp.setLineCap(0);
break;
case BasicStroke.CAP_SQUARE:
_cp.setLineCap(2);
break;
default:
_cp.setLineCap(1);
}
}
if (!oldOk || nStroke.getLineJoin() != oStroke.getLineJoin()) {
switch (nStroke.getLineJoin()) {
case BasicStroke.JOIN_MITER:
_cp.setLineJoin(0);
break;
case BasicStroke.JOIN_BEVEL:
_cp.setLineJoin(2);
break;
default:
_cp.setLineJoin(1);
}
}
if (!oldOk || nStroke.getMiterLimit() != oStroke.getMiterLimit())
_cp.setMiterLimit(nStroke.getMiterLimit());
boolean makeDash;
if (oldOk) {
if (nStroke.getDashArray() != null) {
if (nStroke.getDashPhase() != oStroke.getDashPhase()) {
makeDash = true;
} else if (!java.util.Arrays.equals(nStroke.getDashArray(), oStroke.getDashArray())) {
makeDash = true;
} else
makeDash = false;
} else if (oStroke.getDashArray() != null) {
makeDash = true;
} else
makeDash = false;
} else {
makeDash = true;
}
if (makeDash) {
float dash[] = nStroke.getDashArray();
if (dash == null)
_cp.setLineDash(new float[] {}, 0);
else {
_cp.setLineDash(dash, nStroke.getDashPhase());
}
}
}
public void setStroke(Stroke s) {
_originalStroke = s;
this._stroke = transformStroke(s);
}
private Stroke transformStroke(Stroke stroke) {
if (!(stroke instanceof BasicStroke))
return stroke;
BasicStroke st = (BasicStroke) stroke;
float scale = (float) Math.sqrt(Math.abs(_transform.getDeterminant()));
float dash[] = st.getDashArray();
if (dash != null) {
for (int k = 0; k < dash.length; ++k)
dash[k] *= scale;
}
return new BasicStroke(st.getLineWidth() * scale, st.getEndCap(), st.getLineJoin(), st.getMiterLimit(), dash, st.getDashPhase()
* scale);
}
public void clip(Shape s) {
if (s != null) {
s = _transform.createTransformedShape(s);
if (_clip == null)
_clip = new Area(s);
else
_clip.intersect(new Area(s));
followPath(s, CLIP);
} else {
assert(s != null);
}
}
public Shape getClip() {
try {
return _transform.createInverse().createTransformedShape(_clip);
} catch (NoninvertibleTransformException e) {
return null;
}
}
public void setClip(Shape s) {
// Restore graphics to get back to a no-clip situation.
_cp.restoreGraphics();
// Reapply the transforms that are in effect.
reapplyTransforms();
// Save graphics so we can do this again.
_cp.saveGraphics();
// Set the index so we know which transforms have to be reapplied
// when we next restore graphics.
clipTransformIndex = transformStack.size();
if (s != null)
s = _transform.createTransformedShape(s);
if (s == null) {
_clip = null;
} else {
_clip = new Area(s);
followPath(s, CLIP);
}
_fillColor = null;
_strokeColor = null;
_oldStroke = null;
}
public Stroke getStroke() {
return _originalStroke;
}
public void realizeImage(PdfBoxImage img) {
PDImageXObject xobject;
try {
if (img.isJpeg()) {
xobject = JPEGFactory.createFromStream(_writer,
new ByteArrayInputStream(img.getBytes()));
} else {
BufferedImage buffered = ImageIO.read(new ByteArrayInputStream(
img.getBytes()));
xobject = LosslessFactory.createFromImage(_writer, buffered);
}
} catch (IOException e) {
throw new PdfContentStreamAdapter.PdfException("realizeImage", e);
}
img.clearBytes();
img.setXObject(xobject);
}
public void drawImage(FSImage fsImage, int x, int y) {
PdfBoxImage img = (PdfBoxImage) fsImage;
PDImageXObject xobject = img.getXObject();
AffineTransform transformer = (AffineTransform) getTransform().clone();
transformer.translate(x, y);
transformer.translate(0, img.getHeight());
AffineTransform normalized = normalizeMatrix(transformer);
normalized.scale(img.getWidth(), -img.getHeight());
double[] mx = new double[6];
normalized.getMatrix(mx);
_cp.drawImage(xobject, (float) mx[4], (float) mx[5], (float) mx[0],
(float) mx[3]);
}
/*
private void drawPDFAsImage(PDFAsImage image, int x, int y) {
URI uri = image.getURI();
PdfReader reader = null;
try {
reader = getReader(uri);
} catch (IOException e) {
throw new XRRuntimeException("Could not load " + uri + ": " + e.getMessage(), e);
}
PdfImportedPage page = getWriter().getImportedPage(reader, 1);
AffineTransform at = AffineTransform.getTranslateInstance(x, y);
at.translate(0, image.getHeightAsFloat());
at.scale(image.getWidthAsFloat(), image.getHeightAsFloat());
AffineTransform inverse = normalizeMatrix(_transform);
AffineTransform flipper = AffineTransform.getScaleInstance(1, -1);
inverse.concatenate(at);
inverse.concatenate(flipper);
double[] mx = new double[6];
inverse.getMatrix(mx);
mx[0] = image.scaleWidth();
mx[3] = image.scaleHeight();
_currentPage.restoreState();
_currentPage.addTemplate(page, (float) mx[0], (float) mx[1], (float) mx[2], (float) mx[3], (float) mx[4], (float) mx[5]);
_currentPage.saveState();
}
public PdfReader getReader(URI uri) throws IOException {
PdfReader result = (PdfReader) _readerCache.get(uri);
if (result == null) {
result = new PdfReader(getSharedContext().getUserAgentCallback().getBinaryResource(uri.toString()));
_readerCache.put(uri, result);
}
return result;
}
*/
public float getDotsPerPoint() {
return _dotsPerPoint;
}
public void start(Document doc) {
_linkManager = new PdfBoxLinkManager(_sharedContext, _dotsPerPoint, _root, this);
loadBookmarks(doc);
loadMetadata(doc);
}
public void finish(RenderingContext c, Box root) {
processControls();
_linkManager.processLinks();
writeOutline(c, root);
}
private void writeOutline(RenderingContext c, Box root) {
if (_bookmarks.size() > 0) {
// TODO: .setViewerPreferences(PdfWriter.PageModeUseOutlines);
PDDocumentOutline outline = new PDDocumentOutline();
_writer.getDocumentCatalog().setDocumentOutline( outline );
writeBookmarks(c, root, outline, _bookmarks);
}
}
private void writeBookmarks(RenderingContext c, Box root, PDOutlineNode parent, List bookmarks) {
for (Iterator i = bookmarks.iterator(); i.hasNext();) {
Bookmark bookmark = i.next();
writeBookmark(c, root, parent, bookmark);
}
}
int getPageRefY(Box box) {
if (box instanceof InlineLayoutBox) {
InlineLayoutBox iB = (InlineLayoutBox) box;
return iB.getAbsY() + iB.getBaseline();
} else {
return box.getAbsY();
}
}
private void writeBookmark(RenderingContext c, Box root, PDOutlineNode parent, Bookmark bookmark) {
String href = bookmark.getHRef();
PDPageXYZDestination target = null;
if (href.length() > 0 && href.charAt(0) == '#') {
Box box = _sharedContext.getBoxById(href.substring(1));
if (box != null) {
PageBox page = root.getLayer().getPage(c, getPageRefY(box));
int distanceFromTop = page.getMarginBorderPadding(c, CalculatedStyle.TOP);
distanceFromTop += box.getAbsY() - page.getTop();
target = new PDPageXYZDestination();
target.setTop((int) normalizeY(distanceFromTop / _dotsPerPoint));
target.setPage(_writer.getPage(_startPageNo + page.getPageNo()));
}
}
PDOutlineItem outline = new PDOutlineItem();
outline.setDestination(target == null ? _defaultDestination : target);
outline.setTitle(bookmark.getName());
parent.addLast(outline);
writeBookmarks(c, root, outline, bookmark.getChildren());
}
private void loadBookmarks(Document doc) {
Element head = DOMUtil.getChild(doc.getDocumentElement(), "head");
if (head != null) {
Element bookmarks = DOMUtil.getChild(head, "bookmarks");
if (bookmarks != null) {
List l = DOMUtil.getChildren(bookmarks, "bookmark");
if (l != null) {
for (Iterator i = l.iterator(); i.hasNext();) {
Element e = i.next();
loadBookmark(null, e);
}
}
}
}
}
private void loadBookmark(Bookmark parent, Element bookmark) {
Bookmark us = new Bookmark(bookmark.getAttribute("name"), bookmark.getAttribute("href"));
if (parent == null) {
_bookmarks.add(us);
} else {
parent.addChild(us);
}
List l = DOMUtil.getChildren(bookmark, "bookmark");
if (l != null) {
for (Iterator i = l.iterator(); i.hasNext();) {
Element e = i.next();
loadBookmark(us, e);
}
}
}
private static class Bookmark {
private final String _name;
private final String _HRef;
private List _children;
public Bookmark(String name, String href) {
_name = name;
_HRef = href;
}
public String getHRef() {
return _HRef;
}
public String getName() {
return _name;
}
public void addChild(Bookmark child) {
if (_children == null) {
_children = new ArrayList();
}
_children.add(child);
}
public List getChildren() {
return _children == null ? Collections.emptyList() : _children;
}
}
// Metadata methods
// Methods to load and search a document's metadata
/**
* Appends a name/content metadata pair to this output device. A name or
* content value of null will be ignored.
*
* @param name
* the name of the metadata element to add.
* @return the content value for this metadata.
*/
public void addMetadata(String name, String value) {
if ((name != null) && (value != null)) {
Metadata m = new Metadata(name, value);
_metadata.add(m);
}
}
/**
* Searches the metadata name/content pairs of the current document and
* returns the content value from the first pair with a matching name. The
* search is case insensitive.
*
* @param name
* the metadata element name to locate.
* @return the content value of the first found metadata element; otherwise
* null.
*/
public String getMetadataByName(String name) {
if (name != null) {
for (int i = 0, len = _metadata.size(); i < len; i++) {
Metadata m = (Metadata) _metadata.get(i);
if ((m != null) && m.getName().equalsIgnoreCase(name)) {
return m.getContent();
}
}
}
return null;
}
/**
* Searches the metadata name/content pairs of the current document and
* returns any content values with a matching name in an ArrayList. The
* search is case insensitive.
*
* @param name
* the metadata element name to locate.
* @return an ArrayList with matching content values; otherwise an empty
* list.
*/
public ArrayList getMetadataListByName(String name) {
ArrayList result = new ArrayList();
if (name != null) {
for (int i = 0, len = _metadata.size(); i < len; i++) {
Metadata m = (Metadata) _metadata.get(i);
if ((m != null) && m.getName().equalsIgnoreCase(name)) {
result.add(m.getContent());
}
}
}
return result;
}
/**
* Locates and stores all metadata values in the document head that contain
* name/content pairs. If there is no pair with a name of "title", any
* content in the title element is saved as a "title" metadata item.
*
* @param doc
* the Document level node of the parsed xhtml file.
*/
private void loadMetadata(Document doc) {
Element head = DOMUtil.getChild(doc.getDocumentElement(), "head");
if (head != null) {
List l = DOMUtil.getChildren(head, "meta");
if (l != null) {
for (Iterator i = l.iterator(); i.hasNext();) {
Element e = i.next();
String name = e.getAttribute("name");
if (name != null) { // ignore non-name metadata data
String content = e.getAttribute("content");
Metadata m = new Metadata(name, content);
_metadata.add(m);
}
}
}
// If there is no title meta data attribute, use the document title.
String title = getMetadataByName("title");
if (title == null) {
Element t = DOMUtil.getChild(head, "title");
if (t != null) {
title = DOMUtil.getText(t).trim();
Metadata m = new Metadata("title", title);
_metadata.add(m);
}
}
}
}
/**
* Replaces all copies of the named metadata with a single value. A a new
* value of null will result in the removal of all copies of the named
* metadata. Use addMetadata
to append additional values with
* the same name.
*
* @param name
* the metadata element name to locate.
* @return the new content value for this metadata (null to remove all
* instances).
*/
public void setMetadata(String name, String value) {
if (name != null) {
boolean remove = (value == null); // removing all instances of name?
int free = -1; // first open slot in array
for (int i = 0, len = _metadata.size(); i < len; i++) {
Metadata m = (Metadata) _metadata.get(i);
if (m != null) {
if (m.getName().equalsIgnoreCase(name)) {
if (!remove) {
remove = true; // remove all other instances
m.setContent(value);
} else {
_metadata.set(i, null);
}
}
} else if (free == -1) {
free = i;
}
}
if (!remove) { // not found?
Metadata m = new Metadata(name, value);
if (free == -1) { // no open slots?
_metadata.add(m);
} else {
_metadata.set(free, m);
}
}
}
}
// Class for storing metadata element name/content pairs from the head
// section of an xhtml document.
private static class Metadata {
private String _name;
private String _content;
public Metadata(String name, String content) {
_name = name;
_content = content;
}
public String getContent() {
return _content;
}
public void setContent(String content) {
_content = content;
}
public String getName() {
return _name;
}
public void setName(String name) {
_name = name;
}
}
// Metadata end
public SharedContext getSharedContext() {
return _sharedContext;
}
public void setSharedContext(SharedContext sharedContext) {
_sharedContext = sharedContext;
sharedContext.getCss().setSupportCMYKColors(true);
}
public void setRoot(Box root) {
_root = root;
}
public int getStartPageNo() {
return _startPageNo;
}
public void setStartPageNo(int startPageNo) {
_startPageNo = startPageNo;
}
public void drawSelection(RenderingContext c, InlineText inlineText) {
throw new UnsupportedOperationException();
}
public boolean isSupportsSelection() {
return false;
}
public boolean isSupportsCMYKColors() {
return true;
}
public List findPagePositionsByID(CssContext c, Pattern pattern) {
Map idMap = _sharedContext.getIdMap();
if (idMap == null) {
return Collections.EMPTY_LIST;
}
List result = new ArrayList();
for (Iterator i = idMap.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Entry) i.next();
String id = (String) entry.getKey();
if (pattern.matcher(id).find()) {
Box box = (Box) entry.getValue();
PagePosition pos = calcPDFPagePosition(c, id, box);
if (pos != null) {
result.add(pos);
}
}
}
Collections.sort(result, new Comparator() {
public int compare(Object arg0, Object arg1) {
PagePosition p1 = (PagePosition) arg0;
PagePosition p2 = (PagePosition) arg1;
return p1.getPageNo() - p2.getPageNo();
}
});
return result;
}
private PagePosition calcPDFPagePosition(CssContext c, String id, Box box) {
PageBox page = _root.getLayer().getLastPage(c, box);
if (page == null) {
return null;
}
float x = box.getAbsX() + page.getMarginBorderPadding(c, CalculatedStyle.LEFT);
float y = (page.getBottom() - (box.getAbsY() + box.getHeight())) + page.getMarginBorderPadding(c, CalculatedStyle.BOTTOM);
x /= _dotsPerPoint;
y /= _dotsPerPoint;
PagePosition result = new PagePosition();
result.setId(id);
result.setPageNo(page.getPageNo());
result.setX(x);
result.setY(y);
result.setWidth(box.getEffectiveWidth() / _dotsPerPoint);
result.setHeight(box.getHeight() / _dotsPerPoint);
return result;
}
public void setRenderingContext(RenderingContext result) {
_renderingContext = result;
}
public void setBidiReorderer(BidiReorderer reorderer) {
_reorderer = reorderer;
}
@Override
public void popTransforms(List inverse) {
Collections.reverse(inverse);
for (AffineTransform transform : inverse) {
transformStack.pop();
_cp.setPdfMatrix(transform);
}
}
@Override
public List pushTransforms(List transforms) {
List inverse = new ArrayList(transforms.size());
try {
for (AffineTransform transform : transforms) {
double[] mx = new double[6];
transform.getMatrix(mx);
mx[4] /= _dotsPerPoint;
mx[5] /= _dotsPerPoint;
mx[5] = -mx[5];
AffineTransform normalized = new AffineTransform(mx);
inverse.add(normalized.createInverse());
transformStack.push(normalized);
_cp.setPdfMatrix(normalized);
}
} catch (NoninvertibleTransformException e) {
XRLog.render(Level.WARNING, "Tried to set a non-invertible CSS transform. Ignored.");
}
return inverse;
}
// FIXME: Not sure if this is ever needed.
private void reapplyTransforms() {
int idx = 0;
for (Iterator iter = transformStack.descendingIterator(); iter.hasNext(); ) {
AffineTransform transform = iter.next();
if (idx >= clipTransformIndex) {
_cp.setPdfMatrix(transform);
}
idx++;
}
}
@Override
public float getAbsoluteTransformOriginX() {
return _absoluteTransformOriginX;
}
@Override
public float getAbsoluteTransformOriginY() {
return _absoluteTransformOriginY;
}
// The below methods are for the experimental SVG code and should not be used for other uses.
@Override
@Deprecated
public void saveState() {
_cp.saveGraphics();
}
@Override
@Deprecated
public void restoreState() {
_cp.restoreGraphics();
}
@Override
public void setPaint(Paint paint) {
if (paint instanceof Color) {
Color c = (Color) paint;
this.setColor(new FSRGBColor(c.getRed(), c.getGreen(), c.getBlue()));
} else {
XRLog.render(Level.WARNING, "Unknown paint: " + paint.getClass().getCanonicalName());
}
}
@Override
public void setAlpha(int alpha) {
}
@Override
@Deprecated
public void setRawClip(Shape s) {
_clip = new Area(s);
followPath(s, CLIP);
}
@Override
@Deprecated
public void rawClip(Shape s) {
if (_clip == null)
_clip = new Area(s);
else
_clip.intersect(new Area(s));
followPath(s, CLIP);
}
@Override
@Deprecated
public Shape getRawClip() {
return _clip;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy