PDAbstractContentStream.java
/*
* 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.
*/
package org.apache.pdfbox.pdmodel;
import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fontbox.ttf.CmapLookup;
import org.apache.fontbox.ttf.gsub.CompoundCharacterTokenizer;
import org.apache.fontbox.ttf.gsub.GsubWorker;
import org.apache.fontbox.ttf.gsub.GsubWorkerFactory;
import org.apache.fontbox.ttf.model.GsubData;
import org.apache.pdfbox.contentstream.operator.OperatorName;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSNumber;
import org.apache.pdfbox.pdfwriter.COSWriter;
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceCMYK;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceN;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased;
import org.apache.pdfbox.pdmodel.graphics.color.PDPattern;
import org.apache.pdfbox.pdmodel.graphics.color.PDSeparation;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDInlineImage;
import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
import org.apache.pdfbox.util.Matrix;
import org.apache.pdfbox.util.NumberFormatUtil;
/**
* Provides the ability to write to a content stream.
*
* @author Ben Litchfield
*/
abstract class PDAbstractContentStream implements Closeable
{
private static final Log LOG = LogFactory.getLog(PDAbstractContentStream.class);
protected final PDDocument document; // may be null
protected final OutputStream outputStream;
protected final PDResources resources;
protected boolean inTextMode = false;
protected final Deque<PDFont> fontStack = new ArrayDeque<>();
protected final Deque<PDColorSpace> nonStrokingColorSpaceStack = new ArrayDeque<>();
protected final Deque<PDColorSpace> strokingColorSpaceStack = new ArrayDeque<>();
// number format
private final NumberFormat formatDecimal = NumberFormat.getNumberInstance(Locale.US);
private final byte[] formatBuffer = new byte[32];
private final Map<PDType0Font, GsubWorker> gsubWorkers = new HashMap<>();
private final GsubWorkerFactory gsubWorkerFactory = new GsubWorkerFactory();
/**
* Create a new appearance stream.
*
* @param document may be null
* @param outputStream The appearances output stream to write to.
* @param resources The resources to use
*/
PDAbstractContentStream(PDDocument document, OutputStream outputStream, PDResources resources)
{
this.document = document;
this.outputStream = outputStream;
this.resources = resources;
formatDecimal.setMaximumFractionDigits(4);
formatDecimal.setGroupingUsed(false);
}
/**
* Sets the maximum number of digits allowed for fractional numbers.
*
* @see NumberFormat#setMaximumFractionDigits(int)
* @param fractionDigitsNumber
*/
protected void setMaximumFractionDigits(int fractionDigitsNumber)
{
formatDecimal.setMaximumFractionDigits(fractionDigitsNumber);
}
/**
* Begin some text operations.
*
* @throws IOException If there is an error writing to the stream or if you attempt to
* nest beginText calls.
* @throws IllegalStateException If the method was not allowed to be called at this time.
*/
public void beginText() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: Nested beginText() calls are not allowed.");
}
writeOperator(OperatorName.BEGIN_TEXT);
inTextMode = true;
}
/**
* End some text operations.
*
* @throws IOException If there is an error writing to the stream or if you attempt to
* nest endText calls.
* @throws IllegalStateException If the method was not allowed to be called at this time.
*/
public void endText() throws IOException
{
if (!inTextMode)
{
throw new IllegalStateException("Error: You must call beginText() before calling endText.");
}
writeOperator(OperatorName.END_TEXT);
inTextMode = false;
}
/**
* Set the font and font size to draw text with.
*
* @param font The font to use.
* @param fontSize The font size to draw the text.
* @throws IOException If there is an error writing the font information.
*/
public void setFont(PDFont font, float fontSize) throws IOException
{
if (fontStack.isEmpty())
{
fontStack.add(font);
}
else
{
fontStack.pop();
fontStack.push(font);
}
// keep track of fonts which are configured for subsetting
if (font.willBeSubset())
{
if (document != null)
{
document.getFontsToSubset().add(font);
}
else
{
LOG.warn("Using the subsetted font '" + font.getName() +
"' without a PDDocument context; call subset() before saving");
}
}
else if (!font.isEmbedded() && !font.isStandard14())
{
LOG.warn("attempting to use font '" + font.getName() + "' that isn't embedded");
}
// complex text layout
if (font instanceof PDType0Font)
{
PDType0Font pdType0Font = (PDType0Font) font;
GsubData gsubData = pdType0Font.getGsubData();
if (gsubData != GsubData.NO_DATA_FOUND)
{
GsubWorker gsubWorker = gsubWorkerFactory.getGsubWorker(pdType0Font.getCmapLookup(),
gsubData);
gsubWorkers.put((PDType0Font) font, gsubWorker);
}
}
writeOperand(resources.add(font));
writeOperand(fontSize);
writeOperator(OperatorName.SET_FONT_AND_SIZE);
}
/**
* Shows the given text at the location specified by the current text matrix with the given
* interspersed positioning. This allows the user to efficiently position each glyph or sequence
* of glyphs.
*
* @param textWithPositioningArray An array consisting of String and Float types. Each String is
* output to the page using the current text matrix. Using the default coordinate system, each
* interspersed number adjusts the current text matrix by translating to the left or down for
* horizontal and vertical text respectively. The number is expressed in thousands of a text
* space unit, and may be negative.
*
* @throws IOException if an io exception occurs.
*/
public void showTextWithPositioning(Object[] textWithPositioningArray) throws IOException
{
write("[");
for (Object obj : textWithPositioningArray)
{
if (obj instanceof String)
{
showTextInternal((String) obj);
}
else if (obj instanceof Float)
{
writeOperand((Float) obj);
}
else
{
throw new IllegalArgumentException("Argument must consist of array of Float and String types");
}
}
write("] ");
writeOperator(OperatorName.SHOW_TEXT_ADJUSTED);
}
/**
* Shows the given text at the location specified by the current text matrix.
*
* @param text The Unicode text to show.
* @throws IOException If an io exception occurs.
* @throws IllegalArgumentException if a character isn't supported by the current font
*/
public void showText(String text) throws IOException
{
showTextInternal(text);
write(" ");
writeOperator(OperatorName.SHOW_TEXT);
}
/**
* Outputs a string using the correct encoding and subsetting as required.
*
* @param text The Unicode text to show.
*
* @throws IOException If an io exception occurs.
*/
protected void showTextInternal(String text) throws IOException
{
if (!inTextMode)
{
throw new IllegalStateException("Must call beginText() before showText()");
}
if (fontStack.isEmpty())
{
throw new IllegalStateException("Must call setFont() before showText()");
}
PDFont font = fontStack.peek();
// complex text layout
byte[] encodedText = null;
if (font instanceof PDType0Font)
{
GsubWorker gsubWorker = gsubWorkers.get(font);
if (gsubWorker != null)
{
PDType0Font pdType0Font = (PDType0Font) font;
Set<Integer> glyphIds = new HashSet<>();
encodedText = encodeForGsub(gsubWorker, glyphIds, pdType0Font, text);
if (pdType0Font.willBeSubset())
{
pdType0Font.addGlyphsToSubset(glyphIds);
}
}
}
if (encodedText == null)
{
encodedText = font.encode(text);
}
// Unicode code points to keep when subsetting
if (font.willBeSubset())
{
int offset = 0;
while (offset < text.length())
{
int codePoint = text.codePointAt(offset);
font.addToSubset(codePoint);
offset += Character.charCount(codePoint);
}
}
COSWriter.writeString(encodedText, outputStream);
}
/**
* Sets the text leading.
*
* @param leading The leading in unscaled text units.
* @throws IOException If there is an error writing to the stream.
*/
public void setLeading(float leading) throws IOException
{
writeOperand(leading);
writeOperator(OperatorName.SET_TEXT_LEADING);
}
/**
* Move to the start of the next line of text. Requires the leading (see {@link #setLeading})
* to have been set.
*
* @throws IOException If there is an error writing to the stream.
*/
public void newLine() throws IOException
{
if (!inTextMode)
{
throw new IllegalStateException("Must call beginText() before newLine()");
}
writeOperator(OperatorName.NEXT_LINE);
}
/**
* The Td operator.
* Move to the start of the next line, offset from the start of the current line by (tx, ty).
*
* @param tx The x translation.
* @param ty The y translation.
* @throws IOException If there is an error writing to the stream.
* @throws IllegalStateException If the method was not allowed to be called at this time.
*/
public void newLineAtOffset(float tx, float ty) throws IOException
{
if (!inTextMode)
{
throw new IllegalStateException("Error: must call beginText() before newLineAtOffset()");
}
writeOperand(tx);
writeOperand(ty);
writeOperator(OperatorName.MOVE_TEXT);
}
/**
* The Tm operator. Sets the text matrix to the given values.
* A current text matrix will be replaced with the new one.
*
* @param matrix the transformation matrix
* @throws IOException If there is an error writing to the stream.
* @throws IllegalStateException If the method was not allowed to be called at this time.
*/
public void setTextMatrix(Matrix matrix) throws IOException
{
if (!inTextMode)
{
throw new IllegalStateException("Error: must call beginText() before setTextMatrix");
}
writeAffineTransform(matrix.createAffineTransform());
writeOperator(OperatorName.SET_MATRIX);
}
/**
* Draw an image at the x,y coordinates, with the default size of the image.
*
* @param image The image to draw.
* @param x The x-coordinate to draw the image.
* @param y The y-coordinate to draw the image.
*
* @throws IOException If there is an error writing to the stream.
*/
public void drawImage(PDImageXObject image, float x, float y) throws IOException
{
drawImage(image, x, y, image.getWidth(), image.getHeight());
}
/**
* Draw an image at the x,y coordinates, with the given size.
*
* @param image The image to draw.
* @param x The x-coordinate to draw the image.
* @param y The y-coordinate to draw the image.
* @param width The width to draw the image.
* @param height The height to draw the image.
*
* @throws IOException If there is an error writing to the stream.
* @throws IllegalStateException If the method was called within a text block.
*/
public void drawImage(PDImageXObject image, float x, float y, float width, float height) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: drawImage is not allowed within a text block.");
}
saveGraphicsState();
AffineTransform transform = new AffineTransform(width, 0, 0, height, x, y);
transform(new Matrix(transform));
writeOperand(resources.add(image));
writeOperator(OperatorName.DRAW_OBJECT);
restoreGraphicsState();
}
/**
* Draw an image at the origin with the given transformation matrix.
*
* @param image The image to draw.
* @param matrix The transformation matrix to apply to the image.
*
* @throws IOException If there is an error writing to the stream.
* @throws IllegalStateException If the method was called within a text block.
*/
public void drawImage(PDImageXObject image, Matrix matrix) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: drawImage is not allowed within a text block.");
}
saveGraphicsState();
AffineTransform transform = matrix.createAffineTransform();
transform(new Matrix(transform));
writeOperand(resources.add(image));
writeOperator(OperatorName.DRAW_OBJECT);
restoreGraphicsState();
}
/**
* Draw an inline image at the x,y coordinates, with the default size of the image.
*
* @param inlineImage The inline image to draw.
* @param x The x-coordinate to draw the inline image.
* @param y The y-coordinate to draw the inline image.
*
* @throws IOException If there is an error writing to the stream.
*/
public void drawImage(PDInlineImage inlineImage, float x, float y) throws IOException
{
drawImage(inlineImage, x, y, inlineImage.getWidth(), inlineImage.getHeight());
}
/**
* Draw an inline image at the x,y coordinates and a certain width and height.
*
* @param inlineImage The inline image to draw.
* @param x The x-coordinate to draw the inline image.
* @param y The y-coordinate to draw the inline image.
* @param width The width of the inline image to draw.
* @param height The height of the inline image to draw.
*
* @throws IOException If there is an error writing to the stream.
* @throws IllegalStateException If the method was called within a text block.
*/
public void drawImage(PDInlineImage inlineImage, float x, float y, float width, float height) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: drawImage is not allowed within a text block.");
}
saveGraphicsState();
transform(new Matrix(width, 0, 0, height, x, y));
// create the image dictionary
StringBuilder sb = new StringBuilder();
sb.append(OperatorName.BEGIN_INLINE_IMAGE);
sb.append("\n /W ");
sb.append(inlineImage.getWidth());
sb.append("\n /H ");
sb.append(inlineImage.getHeight());
sb.append("\n /CS ");
sb.append("/");
sb.append(inlineImage.getColorSpace().getName());
COSArray decodeArray = inlineImage.getDecode();
if (decodeArray != null && decodeArray.size() > 0)
{
sb.append("\n /D ");
sb.append("[");
for (COSBase base : decodeArray)
{
sb.append(((COSNumber) base).intValue());
sb.append(" ");
}
sb.append("]");
}
if (inlineImage.isStencil())
{
sb.append("\n /IM true");
}
sb.append("\n /BPC ");
sb.append(inlineImage.getBitsPerComponent());
// image dictionary
write(sb.toString());
writeLine();
// binary data
writeOperator(OperatorName.BEGIN_INLINE_IMAGE_DATA);
writeBytes(inlineImage.getData());
writeLine();
writeOperator(OperatorName.END_INLINE_IMAGE);
restoreGraphicsState();
}
/**
* Draws the given Form XObject at the current location.
*
* @param form Form XObject
* @throws IOException if the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void drawForm(PDFormXObject form) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: drawForm is not allowed within a text block.");
}
writeOperand(resources.add(form));
writeOperator(OperatorName.DRAW_OBJECT);
}
/**
* The cm operator. Concatenates the given matrix with the current transformation matrix (CTM),
* which maps user space coordinates used within a PDF content stream into output device
* coordinates. More details on coordinates can be found in the PDF 32000 specification, 8.3.2
* Coordinate Spaces.
*
* @param matrix the transformation matrix
* @throws IOException If there is an error writing to the stream.
*/
public void transform(Matrix matrix) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: Modifying the current transformation matrix is not allowed within text objects.");
}
writeAffineTransform(matrix.createAffineTransform());
writeOperator(OperatorName.CONCAT);
}
/**
* q operator. Saves the current graphics state.
* @throws IOException If an error occurs while writing to the stream.
*/
public void saveGraphicsState() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: Saving the graphics state is not allowed within text objects.");
}
if (!fontStack.isEmpty())
{
fontStack.push(fontStack.peek());
}
if (!strokingColorSpaceStack.isEmpty())
{
strokingColorSpaceStack.push(strokingColorSpaceStack.peek());
}
if (!nonStrokingColorSpaceStack.isEmpty())
{
nonStrokingColorSpaceStack.push(nonStrokingColorSpaceStack.peek());
}
writeOperator(OperatorName.SAVE);
}
/**
* Q operator. Restores the current graphics state.
* @throws IOException If an error occurs while writing to the stream.
*/
public void restoreGraphicsState() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: Restoring the graphics state is not allowed within text objects.");
}
if (!fontStack.isEmpty())
{
fontStack.pop();
}
if (!strokingColorSpaceStack.isEmpty())
{
strokingColorSpaceStack.pop();
}
if (!nonStrokingColorSpaceStack.isEmpty())
{
nonStrokingColorSpaceStack.pop();
}
writeOperator(OperatorName.RESTORE);
}
protected COSName getName(PDColorSpace colorSpace)
{
if (colorSpace instanceof PDDeviceGray ||
colorSpace instanceof PDDeviceRGB ||
colorSpace instanceof PDDeviceCMYK)
{
return COSName.getPDFName(colorSpace.getName());
}
else
{
return resources.add(colorSpace);
}
}
/**
* Sets the stroking color and, if necessary, the stroking color space.
*
* @param color Color in a specific color space.
* @throws IOException If an IO error occurs while writing to the stream.
*/
public void setStrokingColor(PDColor color) throws IOException
{
if (strokingColorSpaceStack.isEmpty() ||
strokingColorSpaceStack.peek() != color.getColorSpace())
{
writeOperand(getName(color.getColorSpace()));
writeOperator(OperatorName.STROKING_COLORSPACE);
setStrokingColorSpaceStack(color.getColorSpace());
}
for (float value : color.getComponents())
{
writeOperand(value);
}
if (color.getColorSpace() instanceof PDPattern)
{
writeOperand(color.getPatternName());
}
if (color.getColorSpace() instanceof PDPattern ||
color.getColorSpace() instanceof PDSeparation ||
color.getColorSpace() instanceof PDDeviceN ||
color.getColorSpace() instanceof PDICCBased)
{
writeOperator(OperatorName.STROKING_COLOR_N);
}
else
{
writeOperator(OperatorName.STROKING_COLOR);
}
}
/**
* Set the stroking color using an AWT color. Conversion uses the default sRGB color space.
*
* @param color The color to set.
* @throws IOException If an IO error occurs while writing to the stream.
*/
public void setStrokingColor(Color color) throws IOException
{
float[] components = new float[] {
color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f };
PDColor pdColor = new PDColor(components, PDDeviceRGB.INSTANCE);
setStrokingColor(pdColor);
}
/**
* Set the stroking color in the DeviceRGB color space. Range is 0..1.
*
* @param r The red value
* @param g The green value.
* @param b The blue value.
* @throws IOException If an IO error occurs while writing to the stream.
* @throws IllegalArgumentException If the parameters are invalid.
*/
public void setStrokingColor(float r, float g, float b) throws IOException
{
if (isOutsideOneInterval(r) || isOutsideOneInterval(g) || isOutsideOneInterval(b))
{
throw new IllegalArgumentException("Parameters must be within 0..1, but are "
+ String.format("(%.2f,%.2f,%.2f)", r, g, b));
}
writeOperand(r);
writeOperand(g);
writeOperand(b);
writeOperator(OperatorName.STROKING_COLOR_RGB);
setStrokingColorSpaceStack(PDDeviceRGB.INSTANCE);
}
/**
* Set the stroking color in the DeviceCMYK color space. Range is 0..1
*
* @param c The cyan value.
* @param m The magenta value.
* @param y The yellow value.
* @param k The black value.
* @throws IOException If an IO error occurs while writing to the stream.
* @throws IllegalArgumentException If the parameters are invalid.
*/
public void setStrokingColor(float c, float m, float y, float k) throws IOException
{
if (isOutsideOneInterval(c) || isOutsideOneInterval(m) || isOutsideOneInterval(y) || isOutsideOneInterval(k))
{
throw new IllegalArgumentException("Parameters must be within 0..1, but are "
+ String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k));
}
writeOperand(c);
writeOperand(m);
writeOperand(y);
writeOperand(k);
writeOperator(OperatorName.STROKING_COLOR_CMYK);
setStrokingColorSpaceStack(PDDeviceCMYK.INSTANCE);
}
/**
* Set the stroking color in the DeviceGray color space. Range is 0..1.
*
* @param g The gray value.
* @throws IOException If an IO error occurs while writing to the stream.
* @throws IllegalArgumentException If the parameter is invalid.
*/
public void setStrokingColor(float g) throws IOException
{
if (isOutsideOneInterval(g))
{
throw new IllegalArgumentException("Parameter must be within 0..1, but is " + g);
}
writeOperand(g);
writeOperator(OperatorName.STROKING_COLOR_GRAY);
setStrokingColorSpaceStack(PDDeviceGray.INSTANCE);
}
/**
* Sets the non-stroking color and, if necessary, the non-stroking color space.
*
* @param color Color in a specific color space.
* @throws IOException If an IO error occurs while writing to the stream.
*/
public void setNonStrokingColor(PDColor color) throws IOException
{
if (nonStrokingColorSpaceStack.isEmpty() ||
nonStrokingColorSpaceStack.peek() != color.getColorSpace())
{
writeOperand(getName(color.getColorSpace()));
writeOperator(OperatorName.NON_STROKING_COLORSPACE);
setNonStrokingColorSpaceStack(color.getColorSpace());
}
for (float value : color.getComponents())
{
writeOperand(value);
}
if (color.getColorSpace() instanceof PDPattern)
{
writeOperand(color.getPatternName());
}
if (color.getColorSpace() instanceof PDPattern ||
color.getColorSpace() instanceof PDSeparation ||
color.getColorSpace() instanceof PDDeviceN ||
color.getColorSpace() instanceof PDICCBased)
{
writeOperator(OperatorName.NON_STROKING_COLOR_N);
}
else
{
writeOperator(OperatorName.NON_STROKING_COLOR);
}
}
/**
* Set the non-stroking color using an AWT color. Conversion uses the default sRGB color space.
*
* @param color The color to set.
* @throws IOException If an IO error occurs while writing to the stream.
*/
public void setNonStrokingColor(Color color) throws IOException
{
float[] components = new float[] {
color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f };
PDColor pdColor = new PDColor(components, PDDeviceRGB.INSTANCE);
setNonStrokingColor(pdColor);
}
/**
* Set the non-stroking color in the DeviceRGB color space. Range is 0..1.
*
* @param r The red value.
* @param g The green value.
* @param b The blue value.
* @throws IOException If an IO error occurs while writing to the stream.
* @throws IllegalArgumentException If the parameters are invalid.
*/
public void setNonStrokingColor(float r, float g, float b) throws IOException
{
if (isOutsideOneInterval(r) || isOutsideOneInterval(g) || isOutsideOneInterval(b))
{
throw new IllegalArgumentException("Parameters must be within 0..1, but are "
+ String.format("(%.2f,%.2f,%.2f)", r, g, b));
}
writeOperand(r);
writeOperand(g);
writeOperand(b);
writeOperator(OperatorName.NON_STROKING_RGB);
setNonStrokingColorSpaceStack(PDDeviceRGB.INSTANCE);
}
/**
* Set the non-stroking color in the DeviceCMYK color space. Range is 0..1.
*
* @param c The cyan value.
* @param m The magenta value.
* @param y The yellow value.
* @param k The black value.
* @throws IOException If an IO error occurs while writing to the stream.
*/
public void setNonStrokingColor(float c, float m, float y, float k) throws IOException
{
if (isOutsideOneInterval(c) || isOutsideOneInterval(m) || isOutsideOneInterval(y) || isOutsideOneInterval(k))
{
throw new IllegalArgumentException("Parameters must be within 0..1, but are "
+ String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k));
}
writeOperand(c);
writeOperand(m);
writeOperand(y);
writeOperand(k);
writeOperator(OperatorName.NON_STROKING_CMYK);
setNonStrokingColorSpaceStack(PDDeviceCMYK.INSTANCE);
}
/**
* Set the non-stroking color in the DeviceGray color space. Range is 0..1.
*
* @param g The gray value.
* @throws IOException If an IO error occurs while writing to the stream.
* @throws IllegalArgumentException If the parameter is invalid.
*/
public void setNonStrokingColor(float g) throws IOException
{
if (isOutsideOneInterval(g))
{
throw new IllegalArgumentException("Parameter must be within 0..1, but is " + g);
}
writeOperand(g);
writeOperator(OperatorName.NON_STROKING_GRAY);
setNonStrokingColorSpaceStack(PDDeviceGray.INSTANCE);
}
/**
* Add a rectangle to the current path.
*
* @param x The lower left x coordinate.
* @param y The lower left y coordinate.
* @param width The width of the rectangle.
* @param height The height of the rectangle.
* @throws IOException If the content stream could not be written.
* @throws IllegalStateException If the method was called within a text block.
*/
public void addRect(float x, float y, float width, float height) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: addRect is not allowed within a text block.");
}
writeOperand(x);
writeOperand(y);
writeOperand(width);
writeOperand(height);
writeOperator(OperatorName.APPEND_RECT);
}
/**
* Append a cubic Bézier curve to the current path. The curve extends from the current point to
* the point (x3, y3), using (x1, y1) and (x2, y2) as the Bézier control points.
*
* @param x1 x coordinate of the point 1
* @param y1 y coordinate of the point 1
* @param x2 x coordinate of the point 2
* @param y2 y coordinate of the point 2
* @param x3 x coordinate of the point 3
* @param y3 y coordinate of the point 3
* @throws IOException If the content stream could not be written.
* @throws IllegalStateException If the method was called within a text block.
*/
public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: curveTo is not allowed within a text block.");
}
writeOperand(x1);
writeOperand(y1);
writeOperand(x2);
writeOperand(y2);
writeOperand(x3);
writeOperand(y3);
writeOperator(OperatorName.CURVE_TO);
}
/**
* Append a cubic Bézier curve to the current path. The curve extends from the current point to
* the point (x3, y3), using the current point and (x2, y2) as the Bézier control points.
*
* @param x2 x coordinate of the point 2
* @param y2 y coordinate of the point 2
* @param x3 x coordinate of the point 3
* @param y3 y coordinate of the point 3
* @throws IllegalStateException If the method was called within a text block.
* @throws IOException If the content stream could not be written.
*/
public void curveTo2(float x2, float y2, float x3, float y3) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: curveTo2 is not allowed within a text block.");
}
writeOperand(x2);
writeOperand(y2);
writeOperand(x3);
writeOperand(y3);
writeOperator(OperatorName.CURVE_TO_REPLICATE_INITIAL_POINT);
}
/**
* Append a cubic Bézier curve to the current path. The curve extends from the current point to
* the point (x3, y3), using (x1, y1) and (x3, y3) as the Bézier control points.
*
* @param x1 x coordinate of the point 1
* @param y1 y coordinate of the point 1
* @param x3 x coordinate of the point 3
* @param y3 y coordinate of the point 3
* @throws IOException If the content stream could not be written.
* @throws IllegalStateException If the method was called within a text block.
*/
public void curveTo1(float x1, float y1, float x3, float y3) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: curveTo1 is not allowed within a text block.");
}
writeOperand(x1);
writeOperand(y1);
writeOperand(x3);
writeOperand(y3);
writeOperator(OperatorName.CURVE_TO_REPLICATE_FINAL_POINT);
}
/**
* Move the current position to the given coordinates.
*
* @param x The x coordinate.
* @param y The y coordinate.
* @throws IOException If the content stream could not be written.
* @throws IllegalStateException If the method was called within a text block.
*/
public void moveTo(float x, float y) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: moveTo is not allowed within a text block.");
}
writeOperand(x);
writeOperand(y);
writeOperator(OperatorName.MOVE_TO);
}
/**
* Draw a line from the current position to the given coordinates.
*
* @param x The x coordinate.
* @param y The y coordinate.
* @throws IOException If the content stream could not be written.
* @throws IllegalStateException If the method was called within a text block.
*/
public void lineTo(float x, float y) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: lineTo is not allowed within a text block.");
}
writeOperand(x);
writeOperand(y);
writeOperator(OperatorName.LINE_TO);
}
/**
* Stroke the path.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void stroke() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: stroke is not allowed within a text block.");
}
writeOperator(OperatorName.STROKE_PATH);
}
/**
* Close and stroke the path.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void closeAndStroke() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: closeAndStroke is not allowed within a text block.");
}
writeOperator(OperatorName.CLOSE_AND_STROKE);
}
/**
* Fills the path using the nonzero winding number rule.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void fill() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: fill is not allowed within a text block.");
}
writeOperator(OperatorName.FILL_NON_ZERO);
}
/**
* Fills the path using the even-odd winding rule.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void fillEvenOdd() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: fillEvenOdd is not allowed within a text block.");
}
writeOperator(OperatorName.FILL_EVEN_ODD);
}
/**
* Fill and then stroke the path, using the nonzero winding number rule to determine the region
* to fill. This shall produce the same result as constructing two identical path objects,
* painting the first with {@link #fill() } and the second with {@link #stroke() }.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void fillAndStroke() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: fillAndStroke is not allowed within a text block.");
}
writeOperator(OperatorName.FILL_NON_ZERO_AND_STROKE);
}
/**
* Fill and then stroke the path, using the even-odd rule to determine the region to
* fill. This shall produce the same result as constructing two identical path objects, painting
* the first with {@link #fillEvenOdd() } and the second with {@link #stroke() }.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void fillAndStrokeEvenOdd() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: fillAndStrokeEvenOdd is not allowed within a text block.");
}
writeOperator(OperatorName.FILL_EVEN_ODD_AND_STROKE);
}
/**
* Close, fill, and then stroke the path, using the nonzero winding number rule to determine the
* region to fill. This shall have the same effect as the sequence {@link #closePath() }
* and then {@link #fillAndStroke() }.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void closeAndFillAndStroke() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: closeAndFillAndStroke is not allowed within a text block.");
}
writeOperator(OperatorName.CLOSE_FILL_NON_ZERO_AND_STROKE);
}
/**
* Close, fill, and then stroke the path, using the even-odd rule to determine the region to
* fill. This shall have the same effect as the sequence {@link #closePath() }
* and then {@link #fillAndStrokeEvenOdd() }.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void closeAndFillAndStrokeEvenOdd() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: closeAndFillAndStrokeEvenOdd is not allowed within a text block.");
}
writeOperator(OperatorName.CLOSE_FILL_EVEN_ODD_AND_STROKE);
}
/**
* Fills the clipping area with the given shading.
*
* @param shading Shading resource
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void shadingFill(PDShading shading) throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: shadingFill is not allowed within a text block.");
}
writeOperand(resources.add(shading));
writeOperator(OperatorName.SHADING_FILL);
}
/**
* Closes the current subpath.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void closePath() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: closePath is not allowed within a text block.");
}
writeOperator(OperatorName.CLOSE_PATH);
}
/**
* Intersects the current clipping path with the current path, using the nonzero rule.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void clip() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: clip is not allowed within a text block.");
}
writeOperator(OperatorName.CLIP_NON_ZERO);
// end path without filling or stroking
writeOperator(OperatorName.ENDPATH);
}
/**
* Intersects the current clipping path with the current path, using the even-odd rule.
*
* @throws IOException If the content stream could not be written
* @throws IllegalStateException If the method was called within a text block.
*/
public void clipEvenOdd() throws IOException
{
if (inTextMode)
{
throw new IllegalStateException("Error: clipEvenOdd is not allowed within a text block.");
}
writeOperator(OperatorName.CLIP_EVEN_ODD);
// end path without filling or stroking
writeOperator(OperatorName.ENDPATH);
}
/**
* Set line width to the given value.
*
* @param lineWidth The width which is used for drawing.
* @throws IOException If the content stream could not be written
*/
public void setLineWidth(float lineWidth) throws IOException
{
writeOperand(lineWidth);
writeOperator(OperatorName.SET_LINE_WIDTH);
}
/**
* Set the line join style.
*
* @param lineJoinStyle 0 for miter join, 1 for round join, and 2 for bevel join.
* @throws IOException If the content stream could not be written.
* @throws IllegalArgumentException If the parameter is not a valid line join style.
*/
public void setLineJoinStyle(int lineJoinStyle) throws IOException
{
if (lineJoinStyle >= 0 && lineJoinStyle <= 2)
{
writeOperand(lineJoinStyle);
writeOperator(OperatorName.SET_LINE_JOINSTYLE);
}
else
{
throw new IllegalArgumentException("Error: unknown value for line join style");
}
}
/**
* Set the line cap style.
*
* @param lineCapStyle 0 for butt cap, 1 for round cap, and 2 for projecting square cap.
* @throws IOException If the content stream could not be written.
* @throws IllegalArgumentException If the parameter is not a valid line cap style.
*/
public void setLineCapStyle(int lineCapStyle) throws IOException
{
if (lineCapStyle >= 0 && lineCapStyle <= 2)
{
writeOperand(lineCapStyle);
writeOperator(OperatorName.SET_LINE_CAPSTYLE);
}
else
{
throw new IllegalArgumentException("Error: unknown value for line cap style");
}
}
/**
* Set the line dash pattern.
*
* @param pattern The pattern array
* @param phase The phase of the pattern
* @throws IOException If the content stream could not be written.
*/
public void setLineDashPattern(float[] pattern, float phase) throws IOException
{
write("[");
for (float value : pattern)
{
writeOperand(value);
}
write("] ");
writeOperand(phase);
writeOperator(OperatorName.SET_LINE_DASHPATTERN);
}
/**
* Set the miter limit.
*
* @param miterLimit the new miter limit.
* @throws IOException If the content stream could not be written.
* @throws IllegalArgumentException If the parameter is \u2264 0.
*/
public void setMiterLimit(float miterLimit) throws IOException
{
if (miterLimit <= 0.0)
{
throw new IllegalArgumentException("A miter limit <= 0 is invalid and will not render in Acrobat Reader");
}
writeOperand(miterLimit);
writeOperator(OperatorName.SET_LINE_MITERLIMIT);
}
/**
* Begin a marked content sequence.
*
* @param tag the tag
* @throws IOException If the content stream could not be written
*/
public void beginMarkedContent(COSName tag) throws IOException
{
writeOperand(tag);
writeOperator(OperatorName.BEGIN_MARKED_CONTENT);
}
/**
* Begin a marked content sequence with a reference to an entry in the page resources'
* Properties dictionary.
*
* @param tag the tag
* @param propertyList property list
* @throws IOException If the content stream could not be written
*/
public void beginMarkedContent(COSName tag, PDPropertyList propertyList) throws IOException
{
writeOperand(tag);
writeOperand(resources.add(propertyList));
writeOperator(OperatorName.BEGIN_MARKED_CONTENT_SEQ);
}
/**
* End a marked content sequence.
*
* @throws IOException If the content stream could not be written
*/
public void endMarkedContent() throws IOException
{
writeOperator(OperatorName.END_MARKED_CONTENT);
}
/**
* Set an extended graphics state.
*
* @param state The extended graphics state.
* @throws IOException If the content stream could not be written.
*/
public void setGraphicsStateParameters(PDExtendedGraphicsState state) throws IOException
{
writeOperand(resources.add(state));
writeOperator(OperatorName.SET_GRAPHICS_STATE_PARAMS);
}
/**
* Write a comment line.
*
* @param comment
* @throws IOException If the content stream could not be written.
* @throws IllegalArgumentException If the comment contains a newline. This is not allowed,
* because the next line could be ordinary PDF content.
*/
public void addComment(String comment) throws IOException
{
if (comment.indexOf('\n') >= 0 || comment.indexOf('\r') >= 0)
{
throw new IllegalArgumentException("comment should not include a newline");
}
outputStream.write('%');
outputStream.write(comment.getBytes(StandardCharsets.US_ASCII));
outputStream.write('\n');
}
/**
* Writes a real number to the content stream.
* @param real
* @throws java.io.IOException
* @throws IllegalArgumentException if the parameter is not a finite number
*/
protected void writeOperand(float real) throws IOException
{
if (!Float.isFinite(real))
{
throw new IllegalArgumentException(real + " is not a finite number");
}
int byteCount = NumberFormatUtil.formatFloatFast(real, formatDecimal.getMaximumFractionDigits(), formatBuffer);
if (byteCount == -1)
{
//Fast formatting failed
write(formatDecimal.format(real));
}
else
{
outputStream.write(formatBuffer, 0, byteCount);
}
outputStream.write(' ');
}
/**
* Writes an integer number to the content stream.
* @param integer
* @throws java.io.IOException
*/
protected void writeOperand(int integer) throws IOException
{
write(formatDecimal.format(integer));
outputStream.write(' ');
}
/**
* Writes a COSName to the content stream.
* @param name
* @throws java.io.IOException
*/
protected void writeOperand(COSName name) throws IOException
{
name.writePDF(outputStream);
outputStream.write(' ');
}
/**
* Writes a string to the content stream as ASCII.
* @param text
* @throws java.io.IOException
*/
protected void writeOperator(String text) throws IOException
{
outputStream.write(text.getBytes(StandardCharsets.US_ASCII));
outputStream.write('\n');
}
/**
* Writes a string to the content stream as ASCII.
* @param text
* @throws java.io.IOException
*/
protected void write(String text) throws IOException
{
outputStream.write(text.getBytes(StandardCharsets.US_ASCII));
}
/**
* Writes a byte[] to the content stream.
* @param data
* @throws java.io.IOException
*/
protected void write(byte[] data) throws IOException
{
outputStream.write(data);
}
/**
* Writes a newline to the content stream as ASCII.
* @throws java.io.IOException
*/
protected void writeLine() throws IOException
{
outputStream.write('\n');
}
/**
* Writes binary data to the content stream.
* @param data
* @throws java.io.IOException
*/
protected void writeBytes(byte[] data) throws IOException
{
outputStream.write(data);
}
/**
* Writes an AffineTransform to the content stream as an array.
*/
private void writeAffineTransform(AffineTransform transform) throws IOException
{
double[] values = new double[6];
transform.getMatrix(values);
for (double v : values)
{
writeOperand((float) v);
}
}
/**
* Close the content stream. This must be called when you are done with this object.
*
* @throws IOException If the underlying stream has a problem being written to.
*/
@Override
public void close() throws IOException
{
if (inTextMode)
{
LOG.warn("You did not call endText(), some viewers won't display your text");
}
outputStream.close();
}
protected boolean isOutside255Interval(int val)
{
return val < 0 || val > 255;
}
private boolean isOutsideOneInterval(double val)
{
return val < 0 || val > 1;
}
protected void setStrokingColorSpaceStack(PDColorSpace colorSpace)
{
if (strokingColorSpaceStack.isEmpty())
{
strokingColorSpaceStack.add(colorSpace);
}
else
{
strokingColorSpaceStack.pop();
strokingColorSpaceStack.push(colorSpace);
}
}
protected void setNonStrokingColorSpaceStack(PDColorSpace colorSpace)
{
if (nonStrokingColorSpaceStack.isEmpty())
{
nonStrokingColorSpaceStack.add(colorSpace);
}
else
{
nonStrokingColorSpaceStack.pop();
nonStrokingColorSpaceStack.push(colorSpace);
}
}
/**
* Set the character spacing. The value shall be added to the horizontal or vertical component
* of the glyph's displacement, depending on the writing mode.
*
* @param spacing character spacing
* @throws IOException If the content stream could not be written.
*/
public void setCharacterSpacing(float spacing) throws IOException
{
writeOperand(spacing);
writeOperator(OperatorName.SET_CHAR_SPACING);
}
/**
* Set the word spacing. The value shall be added to the horizontal or vertical component of the
* ASCII SPACE character, depending on the writing mode.
* <p>
* This will have an effect only with Type1 and TrueType fonts, not with Type0 fonts. The PDF
* specification tells why: "Word spacing shall be applied to every occurrence of the
* single-byte character code 32 in a string when using a simple font or a composite font that
* defines code 32 as a single-byte code. It shall not apply to occurrences of the byte value 32
* in multiple-byte codes."
*
* @param spacing word spacing
* @throws IOException If the content stream could not be written.
*/
public void setWordSpacing(float spacing) throws IOException
{
writeOperand(spacing);
writeOperator(OperatorName.SET_WORD_SPACING);
}
/**
* Set the horizontal scaling to scale / 100.
*
* @param scale number specifying the percentage of the normal width. Default value: 100 (normal
* width).
* @throws IOException If the content stream could not be written.
*/
public void setHorizontalScaling(float scale) throws IOException
{
writeOperand(scale);
writeOperator(OperatorName.SET_TEXT_HORIZONTAL_SCALING);
}
/**
* Set the text rendering mode. This determines whether showing text shall cause glyph outlines
* to be stroked, filled, used as a clipping boundary, or some combination of the three.
*
* @param rm The text rendering mode.
* @throws IOException If the content stream could not be written.
*/
public void setRenderingMode(RenderingMode rm) throws IOException
{
writeOperand(rm.intValue());
writeOperator(OperatorName.SET_TEXT_RENDERINGMODE);
}
/**
* Set the text rise value, i.e. move the baseline up or down. This is useful for drawing
* superscripts or subscripts.
*
* @param rise Specifies the distance, in unscaled text space units, to move the baseline up or
* down from its default location. 0 restores the default location.
* @throws IOException
*/
public void setTextRise(float rise) throws IOException
{
writeOperand(rise);
writeOperator(OperatorName.SET_TEXT_RISE);
}
private byte[] encodeForGsub(GsubWorker gsubWorker,
Set<Integer> glyphIds, PDType0Font font, String text) throws IOException
{
Pattern spaceRegex = Pattern.compile("\\s");
// break the entire chunk of text into words by splitting it with space
List<String> words = new CompoundCharacterTokenizer("\\s").tokenize(text);
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (String word : words)
{
if (spaceRegex.matcher(word).matches())
{
out.write(font.encode(word));
}
else
{
glyphIds.addAll(applyGSUBRules(gsubWorker, out, font, word));
}
}
return out.toByteArray();
}
private List<Integer> applyGSUBRules(GsubWorker gsubWorker, ByteArrayOutputStream out, PDType0Font font, String word) throws IOException
{
List<Integer> originalGlyphIds = new ArrayList<>();
CmapLookup cmapLookup = font.getCmapLookup();
// convert characters into glyphIds
for (char unicodeChar : word.toCharArray())
{
int glyphId = cmapLookup.getGlyphId(unicodeChar);
if (glyphId <= 0)
{
throw new IllegalStateException(
"could not find the glyphId for the character: " + unicodeChar);
}
originalGlyphIds.add(glyphId);
}
List<Integer> glyphIdsAfterGsub = gsubWorker.applyTransforms(originalGlyphIds);
for (Integer glyphId : glyphIdsAfterGsub)
{
out.write(font.encodeGlyphId(glyphId));
}
return glyphIdsAfterGsub;
}
}