com.openhtmltopdf.pdfboxout.PdfBoxFastOutputDevice 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.
The newest version!
/*
* {{{ 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.IdentValue;
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.css.style.derived.FSLinearGradient;
import com.openhtmltopdf.css.value.FontSpecification;
import com.openhtmltopdf.extend.FSImage;
import com.openhtmltopdf.extend.OutputDevice;
import com.openhtmltopdf.extend.OutputDeviceGraphicsDrawer;
import com.openhtmltopdf.extend.StructureType;
import com.openhtmltopdf.extend.TextRenderer;
import com.openhtmltopdf.layout.SharedContext;
import com.openhtmltopdf.outputdevice.helper.FontResolverHelper;
import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver.FontDescription;
import com.openhtmltopdf.pdfboxout.PdfBoxUtil.FontRun;
import com.openhtmltopdf.pdfboxout.PdfBoxUtil.Metadata;
import com.openhtmltopdf.pdfboxout.fontstore.FontNotFoundException;
import com.openhtmltopdf.render.*;
import com.openhtmltopdf.simple.extend.ReplacedElementScaleHelper;
import com.openhtmltopdf.util.ArrayUtil;
import com.openhtmltopdf.util.LogMessageId;
import com.openhtmltopdf.util.OpenUtil;
import com.openhtmltopdf.util.XRLog;
import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2D;
import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2DFontTextDrawer;
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.font.PDFont;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.awt.*;
import java.awt.RenderingHints.Key;
import java.awt.geom.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.List;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class PdfBoxFastOutputDevice extends AbstractOutputDevice implements OutputDevice, PdfBoxOutputDevice {
//
// 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 enum GraphicsOperation {
FILL,
STROKE,
CLIP;
}
private static class PageState {
// 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;
private PageState copy() {
PageState ret = new PageState();
ret.fillColor = this.fillColor;
ret.strokeColor = this.strokeColor;
return ret;
}
}
private static final AffineTransform IDENTITY = new AffineTransform();
private static final BasicStroke STROKE_ONE = new BasicStroke(1);
private static final boolean 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();
// The desired colors as set by setColor.
// To make sure this color is set on the PDF graphics stream call ensureFillColor or ensureStrokeColor.
private final PageState _desiredPageState = new PageState();
// The page state stack
private final Deque _pageStateStack = new ArrayDeque<>();
// 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;
// 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;
// Manages bookmarks for the current document.
private PdfBoxBookmarkManager _bmManager;
// Contains a list of metadata items for the document.
private final List _metadata = new ArrayList<>();
// Contains all the state needed to manage form controls
private final PdfBoxPerDocumentFormState _formState = new PdfBoxPerDocumentFormState();
// 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 PdfBoxFastLinkManager _linkManager;
// Not used currently.
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();
// Font Mapping for the Graphics2D output
private PdfBoxGraphics2DFontTextDrawer _fontTextDrawer;
// If we are attempting to be PDF/UA compliant (ie tagged pdf), a helper, otherwise null.
private PdfBoxAccessibilityHelper _pdfUa;
private final boolean _pdfUaConform;
private final boolean _pdfAConform;
public PdfBoxFastOutputDevice(float dotsPerPoint, boolean testMode, boolean pdfUaConform, boolean pdfAConform) {
_dotsPerPoint = dotsPerPoint;
_testMode = testMode;
_pdfUaConform = pdfUaConform;
_pdfAConform = pdfAConform;
}
@Override
public void setWriter(PDDocument writer) {
_writer = writer;
}
@Override
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.
*/
@Override
public void initializePage(PDPageContentStream currentPage, PDPage page, float height) {
_cp = new PdfContentStreamAdapter(currentPage);
_page = page;
_pageHeight = height;
_desiredPageState.fillColor = null;
_desiredPageState.strokeColor = null;
pushState(new PageState());
_transform = new AffineTransform();
_transform.scale(1.0d / _dotsPerPoint, 1.0d / _dotsPerPoint);
_stroke = transformStroke(STROKE_ONE);
_originalStroke = _stroke;
_oldStroke = _stroke;
setStrokeDiff(_stroke, null);
if (_pdfUa != null) {
_pdfUa.startPage(_page, _cp, _renderingContext, _pageHeight, _transform);
}
}
private PageState currentState() {
return _pageStateStack.peekFirst();
}
private void pushState(PageState state) {
_pageStateStack.addFirst(state);
}
private PageState popState() {
return _pageStateStack.removeFirst();
}
@Override
public void finishPage() {
_cp.closeContent();
popState();
if (_pdfUa != null) {
_pdfUa.endPage();
}
}
@Override
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.
*/
@Override
public void paintBackground(RenderingContext c, Box box) {
super.paintBackground(c, box);
// processLinkLater will take care of making sure it is actually a link.
_linkManager.processLinkLater(c, box, _page, _pageHeight, _transform);
if (box.getElement() != null && box.getElement().getNodeName().equals("form")) {
_formState.addFormIfRequired(box, this);
} else if (box.getElement() != null &&
ArrayUtil.isOneOf(box.getElement().getNodeName(), "input", "textarea", "button", "select", "openhtmltopdf-combo")) {
// Add controls to list to process later. We do this in case we paint a control background
// before its associated form.
_formState.addControlIfRequired(box, _page, _transform, c, _pageHeight);
}
}
private void processControls() {
_formState.processControls(_sharedContext, _writer, _root);
}
/**
* Given a value in dots units, converts to PDF points.
*/
@Override
public float getDeviceLength(float length) {
return length / _dotsPerPoint;
}
@Override
public void drawBorderLine(Shape bounds, int side, int lineWidth, boolean solid) {
draw(bounds);
}
@Override
public void setColor(FSColor color) {
if (color instanceof FSRGBColor) {
this._desiredPageState.fillColor = color;
this._desiredPageState.strokeColor = color;
} else if (color instanceof FSCMYKColor) {
this._desiredPageState.fillColor = color;
this._desiredPageState.strokeColor = color;
} else {
assert(color instanceof FSRGBColor || color instanceof FSCMYKColor);
}
}
@Override
public void draw(Shape s) {
followPath(s, GraphicsOperation.STROKE);
}
@Override
protected void drawLine(int x1, int y1, int x2, int y2) {
Line2D line = new Line2D.Double(x1, y1, x2, y2);
draw(line);
}
@Override
public void drawRect(int x, int y, int width, int height) {
draw(new Rectangle(x, y, width, height));
}
@Override
public void drawOval(int x, int y, int width, int height) {
Ellipse2D oval = new Ellipse2D.Float(x, y, width, height);
draw(oval);
}
@Override
public void fill(Shape s) {
followPath(s, GraphicsOperation.FILL);
}
@Override
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));
}
}
@Override
public void fillOval(int x, int y, int width, int height) {
Ellipse2D oval = new Ellipse2D.Float(x, y, width, height);
fill(oval);
}
@Override
public void translate(double tx, double ty) {
_transform.translate(tx, ty);
}
@Override
public Object getRenderingHint(Key key) {
return null;
}
@Override
public void setRenderingHint(Key key, Object value) {
}
@Override
public void setFont(FSFont font) {
_font = ((PdfBoxFSFont) font);
if (_font.getFontDescription().isEmpty()) {
throw new FontNotFoundException(this.getFontSpecification());
}
}
/**
* 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;
}
@Override
public void drawString(String s, float x, float y, JustificationInfo info) {
PDFont firstFont = _font.getFontDescription().get(0).getFont();
String effectiveString = TextRenderer.getEffectivePrintableString(s);
// First check if the string contains printable characters only and
// will print with the current font entirely.
try {
firstFont.getStringWidth(effectiveString);
// We got here, so all is good.
drawStringFast(effectiveString, 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, effectiveString, _reorderer);
float xOffset = 0f;
for (FontRun run : fontRuns) {
drawStringFast(run.str, x + xOffset, y, info, run.des, _font.getSize2D());
try {
if (info == null) {
xOffset += ((run.des.getFont().getStringWidth(run.str) / 1000f) * _font.getSize2D());
} else {
xOffset += ((run.des.getFont().getStringWidth(run.str) / 1000f) * _font.getSize2D()) +
(run.spaceCharacterCount * info.getSpaceAdjust()) +
(run.otherCharacterCount * info.getNonSpaceAdjust());
}
} catch (Exception e) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId0Param.RENDER_BUG_FONT_DIDNT_CONTAIN_EXPECTED_CHARACTER, e);
}
}
}
@Override
public void drawStringFast(String s, float x, float y, JustificationInfo info, FontDescription desc, float fontSize) {
if (s.length() == 0)
return;
ensureFillColor();
AffineTransform at = new AffineTransform(getTransform());
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;
boolean resetMode = false;
FontSpecification fontSpec = getFontSpecification();
if (fontSpec != null) {
int need = FontResolverHelper.convertWeightToInt(fontSpec.fontWeight);
int have = desc.getWeight();
if (need > have) {
_cp.setRenderingMode(RenderingMode.FILL_STROKE);
float lineWidth = fontSize * 0.04f; // 4% of font size
_cp.setLineWidth(lineWidth);
resetMode = true;
ensureStrokeColor();
}
if ((fontSpec.fontStyle == IdentValue.ITALIC) && (desc.getStyle() != IdentValue.ITALIC)) {
b = 0f;
c = 0.21256f;
}
}
_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 ) {
// Note: Justification info is also used
// to implement letter-spacing CSS property.
// Justification must be done through TJ rendering
// because Tw param does not work for UNICODE fonts
Object[] array = makeJustificationArray(s, info);
_cp.drawStringWithPositioning(array);
} else {
_cp.drawString(s);
}
_cp.endText();
if (resetMode) {
_cp.setRenderingMode(RenderingMode.FILL);
_cp.setLineWidth(1);
}
}
private Object[] makeJustificationArray(String s, JustificationInfo info) {
List