| /* |
| * 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(); |
| } |
| |
| } |