blob: 668734bdebc570f87e0f82e85f4115ddc93ed718 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id$ */
package org.apache.fop.render.pdf;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.fop.pdf.PDFColorHandler;
import org.apache.fop.pdf.PDFDocument;
import org.apache.fop.pdf.PDFFilterList;
import org.apache.fop.pdf.PDFLinearization;
import org.apache.fop.pdf.PDFNumber;
import org.apache.fop.pdf.PDFPaintingState;
import org.apache.fop.pdf.PDFReference;
import org.apache.fop.pdf.PDFResourceContext;
import org.apache.fop.pdf.PDFStream;
import org.apache.fop.pdf.PDFText;
import org.apache.fop.pdf.PDFTextUtil;
import org.apache.fop.pdf.PDFXObject;
import org.apache.fop.render.intermediate.IFContext;
/**
* Generator class encapsulating all object references and state necessary to generate a
* PDF content stream.
*/
public class PDFContentGenerator {
/** Controls whether comments are written to the PDF stream. */
protected static final boolean WRITE_COMMENTS = true;
private PDFDocument document;
private OutputStream outputStream;
private PDFResourceContext resourceContext;
/** the current stream to add PDF commands to */
private PDFStream currentStream;
private PDFColorHandler colorHandler;
/** drawing state */
protected PDFPaintingState currentState;
/** Text generation utility holding the current font status */
protected PDFTextUtil textutil;
private boolean inMarkedContentSequence;
private boolean inArtifactMode;
private AffineTransform transform;
private IFContext context;
/**
* Main constructor. Creates a new PDF stream and additional helper classes for text painting
* and state management.
* @param document the PDF document
* @param out the output stream the PDF document is generated to
* @param resourceContext the resource context
*/
public PDFContentGenerator(PDFDocument document, OutputStream out,
PDFResourceContext resourceContext) {
this(document, out, resourceContext, null);
}
public PDFContentGenerator(PDFDocument document, OutputStream out,
PDFResourceContext resourceContext, IFContext context) {
this.document = document;
this.outputStream = out;
this.resourceContext = resourceContext;
this.currentStream = document.getFactory()
.makeStream(PDFFilterList.CONTENT_FILTER, false);
this.textutil = new PDFTextUtil() {
protected void write(String code) {
currentStream.add(code);
}
protected void write(StringBuffer code) {
currentStream.add(code);
}
};
this.currentState = new PDFPaintingState();
this.colorHandler = new PDFColorHandler(document.getResources(), resourceContext);
this.context = context;
}
public AffineTransform getAffineTransform() {
return transform;
}
/**
* Returns the applicable resource context for the generator.
* @return the resource context
*/
public PDFDocument getDocument() {
return this.document;
}
/**
* Returns the output stream the PDF document is written to.
* @return the output stream
*/
public OutputStream getOutputStream() {
return this.outputStream;
}
/**
* Returns the applicable resource context for the generator.
* @return the resource context
*/
public PDFResourceContext getResourceContext() {
return this.resourceContext;
}
/**
* Returns the {@link PDFStream} associated with this instance.
* @return the PDF stream
*/
public PDFStream getStream() {
return this.currentStream;
}
/**
* Returns the {@link PDFPaintingState} associated with this instance.
* @return the PDF state
*/
public PDFPaintingState getState() {
return this.currentState;
}
/**
* Returns the {@link PDFTextUtil} associated with this instance.
* @return the text utility
*/
public PDFTextUtil getTextUtil() {
return this.textutil;
}
/**
* Flushes all queued PDF objects ready to be written to the output stream.
* @throws IOException if an error occurs while flushing the PDF objects
*/
public void flushPDFDoc() throws IOException {
if (document.isLinearizationEnabled()) {
new PDFLinearization(document).outputPages(outputStream);
}
this.document.output(this.outputStream);
}
/**
* Writes out a comment.
* @param text text for the comment
*/
protected void comment(String text) {
if (WRITE_COMMENTS) {
getStream().add("% " + text + "\n");
}
}
/** Save graphics state. */
protected void saveGraphicsState() {
endTextObject();
getState().save();
getStream().add("q\n");
}
/** Save graphics state with optional layer. */
protected void saveGraphicsState(String layer) {
endTextObject();
getState().save();
maybeBeginLayer(layer);
getStream().add("q\n");
}
/**
* Save graphics state.
* @param structElemType an element type
* @param sequenceNum a sequence number
*/
protected void saveGraphicsState(String structElemType, int sequenceNum) {
endTextObject();
getState().save();
beginMarkedContentSequence(structElemType, sequenceNum);
getStream().add("q\n");
}
/**
* Begins a new marked content sequence (BDC or BMC). If {@code structElemType} is
* null, a BMC operator with an "Artifact" tag is generated. Otherwise, a BDC operator
* with {@code structElemType} as a tag is generated, and the given mcid stored in its
* property list.
*
* @param structElemType the type of the associated structure element
* @param mcid the marked content identifier
*/
protected void beginMarkedContentSequence(String structElemType, int mcid) {
beginMarkedContentSequence(structElemType, mcid, null);
}
/**
* Begins a new marked content sequence (BDC or BMC). If {@code structElemType} is
* null, a BMC operator with an "Artifact" tag is generated. Otherwise, a BDC operator
* with {@code structElemType} as a tag is generated, and the given mcid and actual
* text are stored in its property list.
*
* @param structElemType the type of the associated structure element
* @param mcid the marked content identifier
* @param actualText the replacement text for the marked content
*/
protected void beginMarkedContentSequence(String structElemType, int mcid, String actualText) {
assert !this.inMarkedContentSequence;
assert !this.inArtifactMode;
if (structElemType != null) {
String actualTextProperty = actualText == null ? ""
: " /ActualText " + PDFText.escapeText(actualText);
getStream().add(structElemType + " <</MCID " + String.valueOf(mcid)
+ actualTextProperty + ">>\n"
+ "BDC\n");
} else {
if (context != null && context.getRegionType() != null) {
getStream().add("/Artifact\n<</Type /Pagination\n/Subtype /" + context.getRegionType() + ">>\nBDC\n");
} else {
getStream().add("/Artifact\nBMC\n");
}
this.inArtifactMode = true;
}
this.inMarkedContentSequence = true;
}
void endMarkedContentSequence() {
getStream().add("EMC\n");
this.inMarkedContentSequence = false;
this.inArtifactMode = false;
}
/**
* Restored the graphics state valid before the previous {@link #saveGraphicsState()}.
* @param popState true if the state should also be popped, false if only the PDF command
* should be issued
*/
protected void restoreGraphicsState(boolean popState) {
endTextObject();
getStream().add("Q\n");
maybeEndLayer();
if (popState) {
getState().restore();
}
}
/**
* Same as {@link #restoreGraphicsState(boolean)}, with <code>true</code> as
* a parameter.
*/
protected void restoreGraphicsState() {
restoreGraphicsState(true);
}
/**
* Same as {@link #restoreGraphicsState()}, additionally ending the current
* marked content sequence if any.
*/
protected void restoreGraphicsStateAccess() {
endTextObject();
getStream().add("Q\n");
if (this.inMarkedContentSequence) {
endMarkedContentSequence();
}
getState().restore();
}
private void maybeBeginLayer(String layer) {
if ((layer != null) && (layer.length() > 0)) {
getState().setLayer(layer);
beginOptionalContent(layer);
}
}
private void maybeEndLayer() {
if (getState().getLayerChanged()) {
endOptionalContent();
}
}
private int ocNameIndex;
private void beginOptionalContent(String layerId) {
String name;
PDFReference layer = document.resolveExtensionReference(layerId);
if (layer != null) {
name = "oc" + ++ocNameIndex;
document.getResources().addProperty(name, layer);
} else {
name = "unknown";
}
getStream().add("/OC /" + name + " BDC\n");
}
private void endOptionalContent() {
getStream().add("EMC\n");
}
/** Indicates the beginning of a text object. */
protected void beginTextObject() {
if (!textutil.isInTextObject()) {
textutil.beginTextObject();
}
}
/**
* Indicates the beginning of a marked-content text object.
*
* @param structElemType structure element type
* @param mcid sequence number
* @see #beginTextObject()
* @see #beginMarkedContentSequence(String, int)
*/
protected void beginTextObject(String structElemType, int mcid) {
beginTextObject(structElemType, mcid, null);
}
/**
* Indicates the beginning of a marked-content text object.
*
* @param structElemType structure element type
* @param mcid sequence number
* @param actualText the replacement text for the marked content
* @see #beginTextObject()
* @see #beginMarkedContentSequence
*/
protected void beginTextObject(String structElemType, int mcid, String actualText) {
if (!textutil.isInTextObject()) {
beginMarkedContentSequence(structElemType, mcid, actualText);
textutil.beginTextObject();
}
}
/** Indicates the end of a text object. */
protected void endTextObject() {
if (textutil.isInTextObject()) {
textutil.endTextObject();
if (this.inMarkedContentSequence) {
endMarkedContentSequence();
}
}
}
/**
* Concatenates the given transformation matrix with the current one.
* @param transform the transformation matrix (in points)
*/
public void concatenate(AffineTransform transform) {
this.transform = transform;
if (!transform.isIdentity()) {
getState().concatenate(transform);
getStream().add(CTMHelper.toPDFString(transform, false) + " cm\n");
}
}
/**
* Intersects the current clip region with the given rectangle.
* @param rect the clip rectangle
*/
public void clipRect(Rectangle rect) {
StringBuffer sb = new StringBuffer();
sb.append(format(rect.x / 1000f)).append(' ');
sb.append(format(rect.y / 1000f)).append(' ');
sb.append(format(rect.width / 1000f)).append(' ');
sb.append(format(rect.height / 1000f)).append(" re W n\n");
add(sb.toString());
}
/**
* Adds content to the stream.
* @param content the PDF content
*/
public void add(String content) {
getStream().add(content);
}
/**
* Formats a float value (normally coordinates in points) as Strings.
* @param value the value
* @return the formatted value
*/
public static final String format(float value) {
return PDFNumber.doubleOut(value);
}
/**
* Sets the current line width in points.
* @param width line width in points
*/
public void updateLineWidth(float width) {
if (getState().setLineWidth(width)) {
//Only write if value has changed WRT the current line width
getStream().add(format(width) + " w\n");
}
}
/**
* Sets the current character spacing (Tc) value.
* @param value the Tc value (in unscaled text units)
*/
public void updateCharacterSpacing(float value) {
if (getState().setCharacterSpacing(value)) {
getStream().add(format(value) + " Tc\n");
}
}
/**
* Establishes a new foreground or fill color.
* @param col the color to apply
* @param fill true to set the fill color, false for the foreground color
* @param stream the PDFStream to write the PDF code to
*/
public void setColor(Color col, boolean fill, PDFStream stream) {
assert stream != null;
StringBuffer sb = new StringBuffer();
setColor(col, fill, sb);
stream.add(sb.toString());
}
/**
* Establishes a new foreground or fill color.
* @param col the color to apply
* @param fill true to set the fill color, false for the foreground color
*/
public void setColor(Color col, boolean fill) {
setColor(col, fill, getStream());
}
/**
* Establishes a new foreground or fill color. In contrast to updateColor
* this method does not check the PDFState for optimization possibilities.
* @param col the color to apply
* @param fill true to set the fill color, false for the foreground color
* @param pdf StringBuffer to write the PDF code to, if null, the code is
* written to the current stream.
*/
protected void setColor(Color col, boolean fill, StringBuffer pdf) {
if (pdf != null) {
colorHandler.establishColor(pdf, col, fill, true);
} else {
setColor(col, fill, getStream());
}
}
/**
* Establishes a new foreground or fill color.
* @param col the color to apply (null skips this operation)
* @param fill true to set the fill color, false for the foreground color
* @param pdf StringBuffer to write the PDF code to, if null, the code is
* written to the current stream.
*/
public void updateColor(Color col, boolean fill, StringBuffer pdf) {
if (col == null) {
return;
}
boolean update = false;
if (fill) {
update = getState().setBackColor(col);
} else {
update = getState().setColor(col);
}
if (update) {
setColor(col, fill, pdf);
}
}
/**
* Places a previously registered image at a certain place on the page.
* @param x X coordinate
* @param y Y coordinate
* @param w width for image
* @param h height for image
* @param xobj the image XObject
*/
public void placeImage(float x, float y, float w, float h, PDFXObject xobj) {
saveGraphicsState();
add(format(w) + " 0 0 "
+ format(-h) + " "
+ format(x) + " "
+ format(y + h)
+ " cm\n" + xobj.getName() + " Do\n");
restoreGraphicsState();
}
public void placeImage(AffineTransform at, String stream) {
saveGraphicsState();
concatenate(at);
add(stream);
restoreGraphicsState();
}
/**
* Places a previously registered image at a certain place on the page,
* bracketing it as a marked-content sequence.
*
* @param x X coordinate
* @param y Y coordinate
* @param w width for image
* @param h height for image
* @param xobj the image XObject
* @param structElemType structure element type
* @param mcid sequence number
* @see #beginMarkedContentSequence(String, int)
*/
public void placeImage(float x, float y, float w, float h, PDFXObject xobj,
String structElemType, int mcid) {
saveGraphicsState(structElemType, mcid);
add(format(w) + " 0 0 "
+ format(-h) + " "
+ format(x) + " "
+ format(y + h)
+ " cm\n" + xobj.getName() + " Do\n");
restoreGraphicsStateAccess();
}
}