/* * Copyright 2006-2017 ICEsoft Technologies Canada Corp. * * Licensed 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.icepdf.core.util.content; import org.icepdf.core.pobjects.*; import org.icepdf.core.pobjects.fonts.FontFile; import org.icepdf.core.pobjects.fonts.FontManager; import org.icepdf.core.pobjects.graphics.*; import org.icepdf.core.pobjects.graphics.commands.*; import org.icepdf.core.pobjects.graphics.text.GlyphText; import org.icepdf.core.pobjects.graphics.text.PageText; import org.icepdf.core.util.Defs; import org.icepdf.core.util.Library; import java.awt.*; import java.awt.geom.*; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.LinkedList; import java.util.Stack; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; /** * AbstractContentParser contains all base operand implementations for the * Post Script operand set. * * @since 5.0 */ public abstract class AbstractContentParser implements ContentParser { private static final Logger logger = Logger.getLogger(AbstractContentParser.class.toString()); private static boolean disableTransparencyGroups; private static boolean enabledOverPrint; private static boolean enabledFontFallback; static { // decide if large images will be scaled disableTransparencyGroups = Defs.sysPropertyBoolean("org.icepdf.core.disableTransparencyGroup", false); // decide if basic over print support will be enabled. enabledOverPrint = Defs.sysPropertyBoolean("org.icepdf.core.enabledOverPrint", true); enabledFontFallback = Defs.sysPropertyBoolean("org.icepdf.core.enabledFontFallback", false); } public static final float OVERPAINT_ALPHA = 0.4f; private static ClipDrawCmd clipDrawCmd = new ClipDrawCmd(); private static NoClipDrawCmd noClipDrawCmd = new NoClipDrawCmd(); protected GraphicsState graphicState; protected Library library; protected Resources resources; protected Shapes shapes; // keep track of embedded marked content protected LinkedList<OptionalContents> oCGs; // represents a geometric path constructed from straight lines, and // quadratic and cubic (Beauctezier) curves. It can contain // multiple sub paths. protected GeneralPath geometricPath; // flag to handle none text based coordinate operand "cm" inside of a text block protected boolean inTextBlock; // TextBlock affine transform can be altered by the "cm" operand an thus // the text base affine transform must be accessible outside the parsTtext method protected AffineTransform textBlockBase; // when parsing a type3 font we need to keep track of the the scale factor // of the device space ctm. protected float glyph2UserSpaceScale = 1.0f; // xObject image count; protected AtomicInteger imageIndex = new AtomicInteger(1); // stack to help with the parse protected Stack<Object> stack = new Stack<Object>(); /** * @param l PDF library master object. * @param r resources */ public AbstractContentParser(Library l, Resources r) { library = l; resources = r; } /** * Returns the Shapes that have accumulated turing multiple calls to * parse(). * * @return resultant shapes object of all processed content streams. */ public Shapes getShapes() { shapes.contract(); return shapes; } /** * Returns the stack of object used to parse content streams. If parse * was successful the stack should be empty. * * @return stack of objects accumulated during a cotent stream parse. */ public Stack<Object> getStack() { return stack; } /** * Returns the current graphics state object being used by this content * stream. * * @return current graphics context of content stream. May be null if * parse method has not been previously called. */ public GraphicsState getGraphicsState() { return graphicState; } /** * Sets the graphics state object which will be used for the current content * parsing. This method must be called before the parse method is called * otherwise it will not have an effect on the state of the draw operands. * * @param graphicState graphics state of this content stream */ public void setGraphicsState(GraphicsState graphicState) { this.graphicState = graphicState; } /** * Parse a pages content stream. * * @param streamBytes byte stream containing page content * @return a Shapes Object containing all the pages text and images shapes. * @throws InterruptedException if current parse thread is interrupted. * @throws java.io.IOException unexpected end of content stream. */ public abstract ContentParser parse(byte[][] streamBytes, Page page) throws InterruptedException, IOException; /** * Specialized method for extracting text from documents. * * @param source content stream source. * @return vector where each entry is the text extracted from a text block. */ public abstract Shapes parseTextBlocks(byte[][] source) throws UnsupportedEncodingException, InterruptedException; protected static void consume_G(GraphicsState graphicState, Stack stack, Library library) { float gray = ((Number) stack.pop()).floatValue(); // Stroke Color Gray graphicState.setStrokeColorSpace( PColorSpace.getColorSpace(library, DeviceGray.DEVICEGRAY_KEY)); graphicState.setStrokeColor(new Color(gray, gray, gray)); } protected static void consume_g(GraphicsState graphicState, Stack stack, Library library) { float gray = Math.abs(((Number) stack.pop()).floatValue()); // Fill Color Gray graphicState.setFillColorSpace( PColorSpace.getColorSpace(library, DeviceGray.DEVICEGRAY_KEY)); graphicState.setFillColor(new Color(gray, gray, gray)); } protected static void consume_RG(GraphicsState graphicState, Stack stack, Library library) { float b = ((Number) stack.pop()).floatValue(); float gg = ((Number) stack.pop()).floatValue(); float r = ((Number) stack.pop()).floatValue(); b = Math.max(0.0f, Math.min(1.0f, b)); gg = Math.max(0.0f, Math.min(1.0f, gg)); r = Math.max(0.0f, Math.min(1.0f, r)); // set stoke colour graphicState.setStrokeColorSpace( PColorSpace.getColorSpace(library, DeviceRGB.DEVICERGB_KEY)); graphicState.setStrokeColor(new Color(r, gg, b)); } protected static void consume_rg(GraphicsState graphicState, Stack stack, Library library) { if (stack.size() >= 3) { float b = ((Number) stack.pop()).floatValue(); float gg = ((Number) stack.pop()).floatValue(); float r = ((Number) stack.pop()).floatValue(); b = Math.max(0.0f, Math.min(1.0f, b)); gg = Math.max(0.0f, Math.min(1.0f, gg)); r = Math.max(0.0f, Math.min(1.0f, r)); // set fill colour graphicState.setFillColorSpace( PColorSpace.getColorSpace(library, DeviceRGB.DEVICERGB_KEY)); graphicState.setFillColor(new Color(r, gg, b)); } } protected static void consume_K(GraphicsState graphicState, Stack stack, Library library) { if (stack.size() >= 4) { float k = ((Number) stack.pop()).floatValue(); float y = ((Number) stack.pop()).floatValue(); float m = ((Number) stack.pop()).floatValue(); float c = ((Number) stack.pop()).floatValue(); PColorSpace pColorSpace = PColorSpace.getColorSpace(library, DeviceCMYK.DEVICECMYK_KEY); // set stroke colour graphicState.setStrokeColorSpace(pColorSpace); graphicState.setStrokeColor(pColorSpace.getColor( new float[]{k, y, m, c}, true)); } } protected static void consume_k(GraphicsState graphicState, Stack stack, Library library) { float k = ((Number) stack.pop()).floatValue(); float y = ((Number) stack.pop()).floatValue(); float m = ((Number) stack.pop()).floatValue(); float c = ((Number) stack.pop()).floatValue(); // build a colour space. PColorSpace pColorSpace = PColorSpace.getColorSpace(library, DeviceCMYK.DEVICECMYK_KEY); // set fill colour graphicState.setFillColorSpace(pColorSpace); graphicState.setFillColor(pColorSpace.getColor( new float[]{k, y, m, c}, true)); } protected static void consume_CS(GraphicsState graphicState, Stack stack, Resources resources) { Name n = (Name) stack.pop(); // Fill Color ColorSpace, resources call uses factory call to PColorSpace.getColorSpace // which returns an colour space including a pattern graphicState.setStrokeColorSpace(resources.getColorSpace(n)); } protected static void consume_cs(GraphicsState graphicState, Stack stack, Resources resources) { Name n = (Name) stack.pop(); // Fill Color ColorSpace, resources call uses factory call to PColorSpace.getColorSpace // which returns an colour space including a pattern graphicState.setFillColorSpace(resources.getColorSpace(n)); } protected static void consume_ri(Stack stack) { stack.pop(); } protected static void consume_SC(GraphicsState graphicState, Stack stack, Library library, Resources resources, boolean isTint) { Object o = stack.peek(); // if a name then we are dealing with a pattern if (o instanceof Name) { Name patternName = (Name) stack.pop(); Pattern pattern = resources.getPattern(patternName); // Create or update the current PatternColorSpace with an instance // of the current pattern. These object will be used later during // fill, show text and Do with image masks. if (graphicState.getStrokeColorSpace() instanceof PatternColor) { PatternColor pc = (PatternColor) graphicState.getStrokeColorSpace(); pc.setPattern(pattern); } else { PatternColor pc = new PatternColor(null, null); pc.setPattern(pattern); graphicState.setStrokeColorSpace(pc); } // two cases to take into account: // for none coloured tiling patterns we must parse the component // values that specify colour. otherwise we just use the name // for all other pattern types. if (pattern instanceof TilingPattern) { TilingPattern tilingPattern = (TilingPattern) pattern; if (tilingPattern.getPaintType() == TilingPattern.PAINTING_TYPE_UNCOLORED_TILING_PATTERN) { // parsing is of the form 'C1...Cn name scn' // first find out colour space specified by name int compLength = graphicState.getStrokeColorSpace().getNumComponents(); // peek and then pop until a none Float is found int nCount = 0; // next calculate the colour based ont he space and c1..Cn float colour[] = new float[compLength]; // peek and pop all of the colour floats while (!stack.isEmpty() && stack.peek() instanceof Number && nCount < compLength) { colour[nCount] = ((Number) stack.pop()).floatValue(); nCount++; } Color color = graphicState.getStrokeColorSpace().getColor(colour, isTint); graphicState.setStrokeColor(color); tilingPattern.setUnColored(color); } } } else if (o instanceof Number) { // some pdfs encoding do not explicitly change the default colour // space from the default DeviceGrey. The following code checks // how many n values are available and if different then current // graphicState.strokeColorSpace it is changed as needed // first get assumed number of components int colorSpaceN = graphicState.getStrokeColorSpace().getNumComponents(); // peek and then pop until a none Float is found int nCount = 0; // set colour to max of 4 which is cymk, int compLength = 4; float colour[] = new float[compLength]; // peek and pop all of the colour floats while (!stack.isEmpty() && stack.peek() instanceof Number && nCount < compLength) { colour[nCount] = ((Number) stack.pop()).floatValue(); nCount++; } // check to see if nCount and colorSpaceN are the same if (nCount != colorSpaceN) { // change the colour state to nCount equivalent graphicState.setStrokeColorSpace( PColorSpace.getColorSpace(library, nCount)); } // shrink the array to the correct length float[] f = new float[nCount]; System.arraycopy(colour, 0, f, 0, nCount); graphicState.setStrokeColor(graphicState.getStrokeColorSpace().getColor(f, isTint)); } } protected static void consume_sc(GraphicsState graphicState, Stack stack, Library library, Resources resources, boolean isTint) { Object o = null; if (!stack.isEmpty()) { o = stack.peek(); } // if a name then we are dealing with a pattern. if (o instanceof Name) { Name patternName = (Name) stack.pop(); Pattern pattern = resources.getPattern(patternName); // Create or update the current PatternColorSpace with an instance // of the current pattern. These object will be used later during // fill, show text and Do with image masks. if (graphicState.getFillColorSpace() instanceof PatternColor) { PatternColor pc = (PatternColor) graphicState.getFillColorSpace(); pc.setPattern(pattern); } else { PatternColor pc = new PatternColor(library, null); pc.setPattern(pattern); graphicState.setFillColorSpace(pc); } // two cases to take into account: // for none coloured tiling patterns we must parse the component // values that specify colour. otherwise we just use the name // for all other pattern types. if (pattern instanceof TilingPattern) { TilingPattern tilingPattern = (TilingPattern) pattern; if (tilingPattern.getPaintType() == TilingPattern.PAINTING_TYPE_UNCOLORED_TILING_PATTERN) { // parsing is of the form 'C1...Cn name scn' // first find out colour space specified by name int compLength = graphicState.getFillColorSpace().getNumComponents(); // peek and then pop until a none Float is found int nCount = 0; // next calculate the colour based ont he space and c1..Cn float colour[] = new float[compLength]; // peek and pop all of the colour floats while (!stack.isEmpty() && stack.peek() instanceof Number && nCount < compLength) { colour[nCount] = ((Number) stack.pop()).floatValue(); nCount++; } // fill colour to be used when painting. Color color = graphicState.getFillColorSpace().getColor(colour, isTint); graphicState.setFillColor(color); tilingPattern.setUnColored(color); } } } else if (o instanceof Number) { // some PDFs encoding do not explicitly change the default colour // space from the default DeviceGrey. The following code checks // how many n values are available and if different then current // graphicState.fillColorSpace it is changed as needed // first get assumed number of components int colorSpaceN = graphicState.getFillColorSpace().getNumComponents(); // peek and then pop until a none Float is found int nCount = 0; // set colour to max of 4 which is cymk, // we have a corner case where 5 components are defined and once // pushed throw the function produce a valid color. int compLength = 5; float colour[] = new float[compLength]; // peek and pop all of the colour floats while (!stack.isEmpty() && stack.peek() instanceof Number && nCount < compLength) { colour[nCount] = ((Number) stack.pop()).floatValue(); nCount++; } // check to see if nCount and colorSpaceN are the same if (nCount != colorSpaceN) { // change the colour state to nCount equivalent graphicState.setFillColorSpace( PColorSpace.getColorSpace(library, nCount)); } // shrink the array to the correct length float[] f = new float[nCount]; System.arraycopy(colour, 0, f, 0, nCount); graphicState.setFillColor(graphicState.getFillColorSpace().getColor(f, true)); } } protected static GraphicsState consume_q(GraphicsState graphicState) { return graphicState.save(); } protected GraphicsState consume_Q(GraphicsState graphicState, Shapes shapes) { GraphicsState gs1 = graphicState.restore(); // point returned stack if (gs1 != null) { graphicState = gs1; } // otherwise start a new stack else { graphicState = new GraphicsState(shapes); graphicState.set(new AffineTransform()); shapes.add(noClipDrawCmd); } return graphicState; } protected static void consume_cm(GraphicsState graphicState, Stack stack, boolean inTextBlock, AffineTransform textBlockBase) { float f = ((Number) stack.pop()).floatValue(); float e = ((Number) stack.pop()).floatValue(); float d = ((Number) stack.pop()).floatValue(); float c = ((Number) stack.pop()).floatValue(); float b = ((Number) stack.pop()).floatValue(); float a = ((Number) stack.pop()).floatValue(); // get the current CTM AffineTransform af = new AffineTransform(graphicState.getCTM()); // do the matrix concatenation math af.concatenate(new AffineTransform(a, b, c, d, e, f)); // add the transformation to the graphics state graphicState.set(af); // update the clip, translate by this CM graphicState.updateClipCM(new AffineTransform(a, b, c, d, e, f)); // apply the cm just as we would a tm if (inTextBlock) { // update the textBlockBase with the cm matrix af = new AffineTransform(textBlockBase); // apply the transform // corner case of a negative ShearX causing layout issue, only one case in a text block if (c < 0) c = Math.abs(c); graphicState.getTextState().tmatrix = new AffineTransform(a, b, c, d, e, f); af.concatenate(graphicState.getTextState().tmatrix); graphicState.set(af); // update the textBlockBase as the tm was specified in the BT block // and we still need to keep the offset. textBlockBase.setTransform(new AffineTransform(graphicState.getCTM())); } } protected static void consume_i(Stack stack) { if (stack.size() >= 1) { stack.pop(); } } protected static void consume_J(GraphicsState graphicState, Stack stack, Shapes shapes) { // collectTokenFrequency(PdfOps.J_TOKEN); // get the value from the stack graphicState.setLineCap((int) (((Number) stack.pop()).floatValue())); // Butt cap, stroke is squared off at the endpoint of the path // there is no projection beyond the end of the path if (graphicState.getLineCap() == 0) { graphicState.setLineCap(BasicStroke.CAP_BUTT); } // Round cap, a semicircular arc with a diameter equal to the line // width is drawn around the endpoint and filled in else if (graphicState.getLineCap() == 1) { graphicState.setLineCap(BasicStroke.CAP_ROUND); } // Projecting square cap. The stroke continues beyond the endpoint // of the path for a distance equal to half the line width and is // then squared off. else if (graphicState.getLineCap() == 2) { graphicState.setLineCap(BasicStroke.CAP_SQUARE); } // Mark the stroke as being changed and store state in the // shapes object setStroke(shapes, graphicState); } /** * Process the xObject content. * * @param graphicState graphic state to appent * @param stack stack of object being parsed. * @param shapes shapes object. * @param resources associated resources. * @param viewParse true indicates parsing is for a normal view. If false * the consumption of Do will skip Image based xObjects for performance. */ protected static GraphicsState consume_Do(GraphicsState graphicState, Stack stack, Shapes shapes, Resources resources, boolean viewParse, // events AtomicInteger imageIndex, Page page) { Name xobjectName = (Name) stack.pop(); if (resources == null) return graphicState; // Form XObject Object xObject = resources.getXObject(xobjectName); if (xObject instanceof Form) { // Do operator steps: // 1.)save the graphics context graphicState = graphicState.save(); // Try and find the named reference 'xobjectName', pass in a copy // of the current graphics state for the new content stream Form formXObject = (Form) xObject; if (formXObject != null) { // check if the form is an optional content group. Object oc = formXObject.getObject(OptionalContent.OC_KEY); if (oc != null) { OptionalContent optionalContent = resources.getLibrary().getCatalog().getOptionalContent(); optionalContent.init(); if (!optionalContent.isVisible(oc)) { return graphicState; } } // init form XObject with current gs state but we need to keep the original state for blending GraphicsState xformGraphicsState = new GraphicsState(graphicState); formXObject.setGraphicsState(xformGraphicsState); if (formXObject.isTransparencyGroup()) { // assign the state to the graphic state for later // processing during the paint xformGraphicsState.setTransparencyGroup(formXObject.isTransparencyGroup()); xformGraphicsState.setIsolated(formXObject.isIsolated()); xformGraphicsState.setKnockOut(formXObject.isKnockOut()); } // according to spec the formXObject might not have // resources reference as a result we pass in the current // one in the hope that any resources can be found. formXObject.setParentResources(resources); formXObject.init(); // 2.) concatenate matrix entry with the current CTM AffineTransform af = new AffineTransform(graphicState.getCTM()); af.concatenate(formXObject.getMatrix()); shapes.add(new TransformDrawCmd(af)); // 3.) Clip according to the form BBox entry if (graphicState.getClip() != null) { AffineTransform matrix = formXObject.getMatrix(); Area bbox = new Area(formXObject.getBBox()); Area clip = graphicState.getClip(); // create inverse of matrix so we can transform // the clip to form space. try { matrix = matrix.createInverse(); } catch (NoninvertibleTransformException e) { logger.warning("Error create xObject matrix inverse"); } // apply the new clip now that they are in the // same space. Shape shape = matrix.createTransformedShape(clip); bbox.intersect(new Area(shape)); shapes.add(new ShapeDrawCmd(bbox)); } else { shapes.add(new ShapeDrawCmd(formXObject.getBBox())); } shapes.add(clipDrawCmd); // 4.) Paint the graphics objects in font stream. // still some work to do do here with regards to BM vs. alpha comp. if ((formXObject.getExtGState() != null && (formXObject.getExtGState().getBlendingMode() == null || formXObject.getExtGState().getBlendingMode().equals(BlendComposite.NORMAL_VALUE)))) { setAlpha(formXObject.getShapes(), graphicState, graphicState.getAlphaRule(), graphicState.getFillAlpha()); setAlpha(shapes, graphicState, graphicState.getAlphaRule(), graphicState.getFillAlpha()); } // If we have a transparency group we paint it // slightly different then a regular xObject as we // need to capture the alpha which is only possible // by paint the xObject to an image. if (!disableTransparencyGroups && ((formXObject.getBBox().getWidth() < FormDrawCmd.MAX_IMAGE_SIZE && formXObject.getBBox().getWidth() > 1) && (formXObject.getBBox().getHeight() < FormDrawCmd.MAX_IMAGE_SIZE && formXObject.getBBox().getHeight() > 1) && (formXObject.getExtGState() != null && (formXObject.getExtGState().getSMask() != null || formXObject.getExtGState().getBlendingMode() != null || (formXObject.getExtGState().getNonStrokingAlphConstant() < 1 && formXObject.getExtGState().getNonStrokingAlphConstant() > 0))) )) { // add the hold form for further processing. shapes.add(new FormDrawCmd(formXObject)); } // the down side of painting to an image is that we // lose quality if there is a affine transform, so // if it isn't a group transparency we paint old way // by just adding the objects to the shapes stack. else { shapes.add(new ShapesDrawCmd(formXObject.getShapes())); } // update text sprites with geometric path state if (formXObject.getShapes() != null && formXObject.getShapes().getPageText() != null) { // normalize each sprite. AffineTransform pageSpace = graphicState.getCTM(); pageSpace.concatenate(formXObject.getMatrix()); formXObject.getShapes().getPageText() .applyXObjectTransform(pageSpace); // add the text to the current shapes for extraction and // selection purposes. PageText pageText = formXObject.getShapes().getPageText(); if (pageText != null && pageText.getPageLines() != null) { shapes.getPageText().addPageLines( pageText.getPageLines()); } } shapes.add(new NoClipDrawCmd()); } // 5.) Restore the saved graphics state graphicState = graphicState.restore(); } // Image XObject else if (viewParse) { ImageStream imageStream = (ImageStream) xObject; if (imageStream != null) { Object oc = imageStream.getObject(OptionalContent.OC_KEY); if (oc != null) { OptionalContent optionalContent = resources.getLibrary().getCatalog().getOptionalContent(); optionalContent.init(); // avoid loading the image if oc is not visible // may have to add this logic to the stack for dynamic content // if we get an example. if (!optionalContent.isEmptyDefinition() && !optionalContent.isVisible(oc)) { return graphicState; } } // create an ImageReference for future decoding ImageReference imageReference = ImageReferenceFactory.getImageReference( imageStream, resources, graphicState, imageIndex.get(), page); imageIndex.incrementAndGet(); if (imageReference != null) { AffineTransform af = new AffineTransform(graphicState.getCTM()); graphicState.scale(1, -1); graphicState.translate(0, -1); // add the image shapes.add(new ImageDrawCmd(imageReference)); graphicState.set(af); } } } return graphicState; } protected static void consume_d(GraphicsState graphicState, Stack stack, Shapes shapes) { float dashPhase; float[] dashArray; try { // pop dashPhase off the stack dashPhase = Math.abs(((Number) stack.pop()).floatValue()); // pop the dashVector of the stack java.util.List dashVector = (java.util.List) stack.pop(); // if the dash vector size is zero we have a default none dashed // line and thus we skip out if (!dashVector.isEmpty() && dashVector.get(0) != null) { // convert dash vector to a array of floats final int sz = dashVector.size(); dashArray = new float[sz]; Object tmp; boolean nullArray = false; for (int i = 0; i < sz; i++) { tmp = dashVector.get(i); float dash; if (tmp != null && tmp instanceof Number) { dash = Math.abs(((Number) dashVector.get(i)).floatValue()); // java has a hard time with painting dash array with values < 0.05. // null the dash array as we can't pain it PDF-966. if (dash < 0.05f) nullArray = true; dashArray[i] = dash; } } // corner case check to see if the dash array contains a first element // that is very different then second which is likely the result of // a itext/MS office bug where a dash element of the array isn't scaled to // user space. if (dashArray.length > 1 && dashArray[0] != 0) { boolean isOffice = false; int spread = 10000; for (int i = 0, max = dashArray.length - 1; i < max; i++) { float diff = dashArray[i] - dashArray[i + 1]; if (diff > spread || diff < -spread) { isOffice = true; break; } } if (isOffice) { for (int i = 0, max = dashArray.length; i < max; i++) { if (dashArray[i] < 10) { // scale to PDF space. dashArray[i] = dashArray[i] * 1000; } } } } // null the dash array if one of the dash values was less then 0.05. if (nullArray) { dashArray = null; } } // default to standard black line else { dashPhase = 0; dashArray = null; } // assign state now that everything is assumed good // from a class cast exception point of view. graphicState.setDashArray(dashArray); graphicState.setDashPhase(dashPhase); } catch (ClassCastException e) { logger.log(Level.FINE, "Dash pattern syntax error: ", e); } // update stroke state with possibly new dash data. setStroke(shapes, graphicState); } protected static void consume_j(GraphicsState graphicState, Stack stack, Shapes shapes) { // grab the value graphicState.setLineJoin((int) (((Number) stack.pop()).floatValue())); // Miter Join - the outer edges of the strokes for the two // segments are extended until they meet at an angle, like a picture // frame if (graphicState.getLineJoin() == 0) { graphicState.setLineJoin(BasicStroke.JOIN_MITER); } // Round join - an arc of a circle with a diameter equal to the line // width is drawn around the point where the two segments meet, // connecting the outer edges of the strokes for the two segments else if (graphicState.getLineJoin() == 1) { graphicState.setLineJoin(BasicStroke.JOIN_ROUND); } // Bevel join - The two segments are finished with butt caps and the // ends of the segments is filled with a triangle else if (graphicState.getLineJoin() == 2) { graphicState.setLineJoin(BasicStroke.JOIN_BEVEL); } // updates shapes with with the new stroke type setStroke(shapes, graphicState); } protected static void consume_w(GraphicsState graphicState, Stack stack, Shapes shapes, float glyph2UserSpaceScale) { // apply any type3 font scalling which is set via the glyph2User space affine transform. if (!stack.isEmpty()) { float scale = ((Number) stack.pop()).floatValue() * glyph2UserSpaceScale; graphicState.setLineWidth(scale); setStroke(shapes, graphicState); } } protected static void consume_M(GraphicsState graphicState, Stack stack, Shapes shapes) { graphicState.setMiterLimit(((Number) stack.pop()).floatValue()); setStroke(shapes, graphicState); } protected static void consume_gs(GraphicsState graphicState, Stack stack, Resources resources, Shapes shapes) { Object gs = stack.pop(); if (gs instanceof Name && resources != null) { // Get ExtGState and merge it with ExtGState extGState = resources.getExtGState((Name) gs); if (extGState != null) { graphicState.concatenate(extGState); } float alpha = graphicState.getFillAlpha(); if (graphicState.getExtGState() != null && graphicState.getExtGState().getBlendingMode() != null // && graphicState.getExtGState().getOverprintMode() == 1 ) { // BlendComposite is still having trouble with alpha values < 1.0 and if we apply a blend to the top of // the stack, the src pixels aren't the intended value. if (shapes.getShapes().size() > 0) shapes.add(new BlendCompositeDrawCmd(graphicState.getExtGState().getBlendingMode(), alpha)); } // apply the alpha as it's own composite if (alpha > 0 && alpha < 1.0) setAlpha(shapes, graphicState, graphicState.getAlphaRule(), graphicState.getFillAlpha()); } } protected static void consume_Tf(GraphicsState graphicState, Stack stack, Resources resources) { float size = ((Number) stack.pop()).floatValue(); Name name2 = (Name) stack.pop(); // build the new font and initialize it. graphicState.getTextState().tsize = size; graphicState.getTextState().fontName = name2; graphicState.getTextState().font = resources.getFont(name2); // in the rare case that the font can't be found then we try and build // one so the document can be rendered in some shape or form. if (graphicState.getTextState().font == null || graphicState.getTextState().font.getFont() == null) { // turn on the old awt font engine, as we have a null font // FontFactory fontFactory = FontFactory.getInstance(); // boolean awtState = fontFactory.isAwtFontSubstitution(); // fontFactory.setAwtFontSubstitution(true); try { // this should almost never happen but of course we have a few // corner cases: // get the first pages resources, no need to lock the page, already locked. Page page = resources.getLibrary().getCatalog().getPageTree().getPage(0); page.initPageResources(); Resources res = page.getResources(); // try and get a font off the first page. Object pageFonts = res.getEntries().get(Resources.FONT_KEY); // check for an indirect reference if (pageFonts instanceof Reference) { pageFonts = resources.getLibrary().getObject((Reference) pageFonts); } if (pageFonts instanceof HashMap) { // get first font Reference fontRef = (Reference) ((HashMap) pageFonts).get(name2); if (fontRef != null) { graphicState.getTextState().font = (org.icepdf.core.pobjects.fonts.Font) resources.getLibrary() .getObject(fontRef); graphicState.getTextState().font.init(); } } } catch (Throwable throwable) { // keep block protected as we don't want to accidentally turn off // the font engine. logger.warning("Warning could not find font by named resource " + name2); } // return factory to original state. // fontFactory.setAwtFontSubstitution(awtState); // if no fonts found then we just bail and accept the null pointer } if (graphicState.getTextState().font != null) { graphicState.getTextState().currentfont = graphicState.getTextState().font.getFont().deriveFont(size); } else { // not font found which is a problem, so we need to check for interactive form dictionary graphicState.getTextState().font = resources.getLibrary().getInteractiveFormFont(name2.getName()); if (graphicState.getTextState().font != null) { graphicState.getTextState().currentfont = graphicState.getTextState().font.getFont(); graphicState.getTextState().currentfont = graphicState.getTextState().font.getFont().deriveFont(size); } } } protected static void consume_Tc(GraphicsState graphicState, Stack stack) { graphicState.getTextState().cspace = ((Number) stack.pop()).floatValue(); } protected static void consume_tm(GraphicsState graphicState, Stack stack, TextMetrics textMetrics, PageText pageText, double previousBTStart, AffineTransform textBlockBase, LinkedList<OptionalContents> oCGs) { textMetrics.setShift(0); textMetrics.setPreviousAdvance(0); textMetrics.getAdvance().setLocation(0, 0); // pop carefully, as there are few corner cases where // the af is split up with a BT or other token Object next; // initialize an identity matrix, add parse out the // numbers we have working from f6 down to f1. float[] tm = new float[]{1f, 0, 0, 1f, 0, 0}; for (int i = 0, hits = 5, max = stack.size(); hits != -1 && i < max; i++) { next = stack.pop(); if (next instanceof Number) { tm[hits] = ((Number) next).floatValue(); hits--; } } AffineTransform af = new AffineTransform(textBlockBase); // grab old values. // double oldTransY = graphicState.getCTM().getTranslateY(); // double oldScaleY = graphicState.getCTM().getScaleY(); // apply the transform graphicState.getTextState().tmatrix = new AffineTransform(tm); af.concatenate(graphicState.getTextState().tmatrix); graphicState.set(af); graphicState.scale(1, -1); // text extraction logic // capture x coord of BT y offset, tm, Td, TD. if (textMetrics.isYstart()) { textMetrics.setyBTStart(tm[5]); textMetrics.setYstart(false); } // update the extract text pageText.setTextTransform(new AffineTransform(tm)); } protected static void consume_T_star(GraphicsState graphicState, TextMetrics textMetrics, PageText pageText, LinkedList<OptionalContents> oCGs) { graphicState.translate(-textMetrics.getShift(), 0); textMetrics.setShift(0); textMetrics.setPreviousAdvance(0); textMetrics.getAdvance().setLocation(0, 0); graphicState.translate(0, graphicState.getTextState().leading); // always indicates a new line pageText.newLine(oCGs); } protected static void consume_TD(GraphicsState graphicState, Stack stack, TextMetrics textMetrics, PageText pageText, LinkedList<OptionalContents> oCGs) { float y = ((Number) stack.pop()).floatValue(); float x = ((Number) stack.pop()).floatValue(); graphicState.translate(-textMetrics.getShift(), 0); textMetrics.setShift(0); textMetrics.setPreviousAdvance(0); textMetrics.getAdvance().setLocation(0, 0); graphicState.translate(x, -y); graphicState.getTextState().leading = -y; // capture x coord of BT y offset, tm, Td, TD. if (textMetrics.isYstart()) { textMetrics.setyBTStart(y); textMetrics.setYstart(false); } } protected static void consume_double_quote(GraphicsState graphicState, Stack stack, Shapes shapes, TextMetrics textMetrics, GlyphOutlineClip glyphOutlineClip, LinkedList<OptionalContents> oCGs) { StringObject stringObject = (StringObject) stack.pop(); graphicState.getTextState().cspace = ((Number) stack.pop()).floatValue(); graphicState.getTextState().wspace = ((Number) stack.pop()).floatValue(); graphicState.translate(-textMetrics.getShift(), graphicState.getTextState().leading); // apply transparency setAlpha(shapes, graphicState, graphicState.getAlphaRule(), graphicState.getFillAlpha()); textMetrics.setShift(0); textMetrics.setPreviousAdvance(0); textMetrics.getAdvance().setLocation(0, 0); TextState textState = graphicState.getTextState(); AffineTransform tmp = applyTextScaling(graphicState); drawString(stringObject.getLiteralStringBuffer( textState.font.getSubTypeFormat(), textState.font.getFont()), textMetrics, graphicState.getTextState(), shapes, glyphOutlineClip, graphicState, oCGs); graphicState.set(tmp); graphicState.translate(textMetrics.getAdvance().x, 0); float shift = textMetrics.getShift(); shift += textMetrics.getAdvance().x; textMetrics.setShift(shift); } protected static void consume_single_quote(GraphicsState graphicState, Stack stack, Shapes shapes, TextMetrics textMetrics, GlyphOutlineClip glyphOutlineClip, LinkedList<OptionalContents> oCGs) { // ' = T* + Tj, who knew? consume_T_star(graphicState, textMetrics, shapes.getPageText(), oCGs); consume_Tj(graphicState, stack, shapes, textMetrics, glyphOutlineClip, oCGs); } protected static void consume_Td(GraphicsState graphicState, Stack stack, TextMetrics textMetrics, PageText pageText, double previousBTStart, LinkedList<OptionalContents> oCGs) { float y = ((Number) stack.pop()).floatValue(); float x = ((Number) stack.pop()).floatValue(); graphicState.translate(-textMetrics.getShift(), 0); textMetrics.setShift(0); textMetrics.setPreviousAdvance(0); textMetrics.getAdvance().setLocation(0, 0); // x,y are expressed in unscaled but we don't scale until // a text showing operator is called. graphicState.translate(x, -y); // capture x coord of BT y offset, tm, Td, TD. if (textMetrics.isYstart()) { float newY = (float) graphicState.getCTM().getTranslateY(); textMetrics.setyBTStart(newY); textMetrics.setYstart(false); } } protected static void consume_Tz(GraphicsState graphicState, Stack stack) { Object ob = stack.pop(); if (ob instanceof Number) { float hScaling = ((Number) ob).floatValue(); // store the scaled value, but not apply the state operator at this time graphicState.getTextState().hScalling = hScaling / 100.0f; } } protected static void consume_Tw(GraphicsState graphicState, Stack stack) { graphicState.getTextState().wspace = ((Number) stack.pop()).floatValue(); } protected static void consume_Tr(GraphicsState graphicState, Stack stack) { graphicState.getTextState().rmode = (int) ((Number) stack.pop()).floatValue(); } protected static void consume_TL(GraphicsState graphicState, Stack stack) { graphicState.getTextState().leading = ((Number) stack.pop()).floatValue(); } protected static void consume_Ts(GraphicsState graphicState, Stack stack) { graphicState.getTextState().trise = ((Number) stack.pop()).floatValue(); } protected static GeneralPath consume_L(Stack stack, GeneralPath geometricPath) { float y = ((Number) stack.pop()).floatValue(); float x = ((Number) stack.pop()).floatValue(); if (geometricPath == null) { geometricPath = new GeneralPath(); } geometricPath.lineTo(x, y); return geometricPath; } protected static GeneralPath consume_m(Stack stack, GeneralPath geometricPath) { if (geometricPath == null) { geometricPath = new GeneralPath(); } if (stack.size() >= 2) { float y = ((Number) stack.pop()).floatValue(); float x = ((Number) stack.pop()).floatValue(); geometricPath.moveTo(x, y); } return geometricPath; } protected static GeneralPath consume_c(Stack stack, GeneralPath geometricPath) { if (!stack.isEmpty()) { float y3 = ((Number) stack.pop()).floatValue(); float x3 = ((Number) stack.pop()).floatValue(); float y2 = ((Number) stack.pop()).floatValue(); float x2 = ((Number) stack.pop()).floatValue(); float y1 = ((Number) stack.pop()).floatValue(); float x1 = ((Number) stack.pop()).floatValue(); if (geometricPath == null) { geometricPath = new GeneralPath(); } geometricPath.curveTo(x1, y1, x2, y2, x3, y3); } return geometricPath; } protected static GeneralPath consume_S(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws InterruptedException { if (geometricPath != null) { commonStroke(graphicState, shapes, geometricPath); geometricPath = null; } return geometricPath; } protected static GeneralPath consume_F(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws NoninvertibleTransformException, InterruptedException { if (geometricPath != null) { geometricPath.setWindingRule(GeneralPath.WIND_NON_ZERO); commonFill(shapes, graphicState, geometricPath); } geometricPath = null; return geometricPath; } protected static GeneralPath consume_f(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws NoninvertibleTransformException, InterruptedException { if (geometricPath != null) { geometricPath.setWindingRule(GeneralPath.WIND_NON_ZERO); commonFill(shapes, graphicState, geometricPath); } geometricPath = null; return geometricPath; } protected static GeneralPath consume_re(Stack stack, GeneralPath geometricPath) { if (geometricPath == null) { geometricPath = new GeneralPath(); } float h = ((Number) stack.pop()).floatValue(); float w = ((Number) stack.pop()).floatValue(); float y = ((Number) stack.pop()).floatValue(); float x = ((Number) stack.pop()).floatValue(); geometricPath.moveTo(x, y); geometricPath.lineTo(x + w, y); geometricPath.lineTo(x + w, y + h); geometricPath.lineTo(x, y + h); geometricPath.lineTo(x, y); return geometricPath; } protected static void consume_h(GeneralPath geometricPath) { if (geometricPath != null) { geometricPath.closePath(); } } protected static void consume_BDC(Stack stack, Shapes shapes, LinkedList<OptionalContents> oCGs, Resources resources) throws InterruptedException { Object properties = stack.pop();// properties Name tag = (Name) stack.pop();// tag OptionalContents optionalContents = null; // try and process the Optional content. if (tag.equals(OptionalContent.OC_KEY)) { if (properties instanceof Name) { optionalContents = resources.getPropertyEntry((Name) properties); // make sure the reference is valid, no point // jumping through all the hopes if we don't have too. if (optionalContents != null) { optionalContents.init(); // valid OC, add a marker command to the stack. shapes.add(new OCGStartDrawCmd(optionalContents)); } } } if (optionalContents == null) { // create a temporary optional object. Name tmp = OptionalContent.NONE_OC_FLAG; if (properties instanceof Name) { tmp = (Name) properties; } optionalContents = new OptionalContentGroup(tmp.getName(), true); } if (oCGs != null) { oCGs.add(optionalContents); } } protected static void consume_EMC(Shapes shapes, LinkedList<OptionalContents> oCGs) { // add the new draw command to the stack. // restore the main stack. if (oCGs != null && !oCGs.isEmpty()) { OptionalContents optionalContents = oCGs.removeLast(); // mark the end of an OCG. if (optionalContents.isOCG()) { // push the OC end command on the shapes shapes.add(new OCGEndDrawCmd()); } } } protected static void consume_BMC(Stack stack, Shapes shapes, LinkedList<OptionalContents> oCGs, Resources resources) throws InterruptedException { Object properties = stack.pop();// properties // try and process the Optional content. if (properties instanceof Name && resources != null) { OptionalContents optionalContents = resources.getPropertyEntry((Name) properties); // make sure the reference is valid, no point // jumping through all the hopes if we don't have too. if (optionalContents != null) { optionalContents.init(); shapes.add(new OCGStartDrawCmd(optionalContents)); } else { Name tmp = (Name) properties; optionalContents = new OptionalContentGroup(tmp.getName(), true); } if (oCGs != null) { oCGs.add(optionalContents); } } } protected static GeneralPath consume_f_star(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws NoninvertibleTransformException, InterruptedException { if (geometricPath != null) { // need to apply pattern.. geometricPath.setWindingRule(GeneralPath.WIND_EVEN_ODD); commonFill(shapes, graphicState, geometricPath); } geometricPath = null; return geometricPath; } protected static GeneralPath consume_b(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws NoninvertibleTransformException, InterruptedException { if (geometricPath != null) { geometricPath.setWindingRule(GeneralPath.WIND_NON_ZERO); geometricPath.closePath(); commonFill(shapes, graphicState, geometricPath); commonStroke(graphicState, shapes, geometricPath); } geometricPath = null; return geometricPath; } protected static GeneralPath consume_n(GeneralPath geometricPath) throws NoninvertibleTransformException { geometricPath = null; return geometricPath; } protected static void consume_W(GraphicsState graphicState, GeneralPath geometricPath) throws NoninvertibleTransformException { if (geometricPath != null) { geometricPath.setWindingRule(GeneralPath.WIND_NON_ZERO); geometricPath.closePath(); graphicState.setClip(geometricPath); } } protected static void consume_v(Stack stack, GeneralPath geometricPath) { float y3 = ((Number) stack.pop()).floatValue(); float x3 = ((Number) stack.pop()).floatValue(); float y2 = ((Number) stack.pop()).floatValue(); float x2 = ((Number) stack.pop()).floatValue(); geometricPath.curveTo( (float) geometricPath.getCurrentPoint().getX(), (float) geometricPath.getCurrentPoint().getY(), x2, y2, x3, y3); } protected static void consume_y(Stack stack, GeneralPath geometricPath) { float y3 = ((Number) stack.pop()).floatValue(); float x3 = ((Number) stack.pop()).floatValue(); float y1 = ((Number) stack.pop()).floatValue(); float x1 = ((Number) stack.pop()).floatValue(); geometricPath.curveTo(x1, y1, x3, y3, x3, y3); } protected static GeneralPath consume_B(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws NoninvertibleTransformException, InterruptedException { if (geometricPath != null) { geometricPath.setWindingRule(GeneralPath.WIND_NON_ZERO); commonFill(shapes, graphicState, geometricPath); commonStroke(graphicState, shapes, geometricPath); } geometricPath = null; return geometricPath; } protected static GraphicsState consume_d0(GraphicsState graphicState, Stack stack) { // save the stack graphicState = graphicState.save(); // need two pops to get Wx and Wy data float y = ((Number) stack.pop()).floatValue(); float x = ((Number) stack.pop()).floatValue(); TextState textState = graphicState.getTextState(); textState.setType3HorizontalDisplacement(new Point.Float(x, y)); return graphicState; } protected static GeneralPath consume_s(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws InterruptedException { if (geometricPath != null) { geometricPath.closePath(); commonStroke(graphicState, shapes, geometricPath); geometricPath = null; } return geometricPath; } protected static GeneralPath consume_b_star(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws NoninvertibleTransformException, InterruptedException { if (geometricPath != null) { geometricPath.setWindingRule(GeneralPath.WIND_EVEN_ODD); geometricPath.closePath(); commonStroke(graphicState, shapes, geometricPath); commonFill(shapes, graphicState, geometricPath); } geometricPath = null; return geometricPath; } protected static GraphicsState consume_d1(GraphicsState graphicState, Stack stack) { // save the stack graphicState = graphicState.save(); // need two pops to get Wx and Wy data float x2 = ((Number) stack.pop()).floatValue(); float y2 = ((Number) stack.pop()).floatValue(); float x1 = ((Number) stack.pop()).floatValue(); float y1 = ((Number) stack.pop()).floatValue(); float y = ((Number) stack.pop()).floatValue(); float x = ((Number) stack.pop()).floatValue(); TextState textState = graphicState.getTextState(); textState.setType3HorizontalDisplacement( new Point2D.Float(x, y)); textState.setType3BBox(new PRectangle( new Point2D.Float(x1, y1), new Point2D.Float(x2, y2))); return graphicState; } protected static GeneralPath consume_B_star(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws NoninvertibleTransformException, InterruptedException { if (geometricPath != null) { geometricPath.setWindingRule(GeneralPath.WIND_EVEN_ODD); commonStroke(graphicState, shapes, geometricPath); commonFill(shapes, graphicState, geometricPath); } geometricPath = null; return geometricPath; } public static void consume_W_star(GraphicsState graphicState, GeneralPath geometricPath) { if (geometricPath != null) { geometricPath.setWindingRule(GeneralPath.WIND_EVEN_ODD); geometricPath.closePath(); graphicState.setClip(geometricPath); } } public static void consume_DP(Stack stack) { stack.pop(); // properties stack.pop(); // name } public static void consume_MP(Stack stack) { stack.pop(); } public static void consume_sh(GraphicsState graphicState, Stack stack, Shapes shapes, Resources resources) throws InterruptedException { Object o = stack.peek(); // if a name then we are dealing with a pattern. if (o instanceof Name) { Name patternName = (Name) stack.pop(); Pattern pattern = resources.getShading(patternName); if (pattern != null) { pattern.init(graphicState); // we paint the shape and color shading as defined // by the pattern dictionary and respect the current clip // TODO further work is needed here to build out the pattern fill. if (graphicState.getExtGState() != null && graphicState.getExtGState().getSMask() != null) { setAlpha(shapes, graphicState, graphicState.getAlphaRule(), 0.50f); } else { setAlpha(shapes, graphicState, graphicState.getAlphaRule(), graphicState.getFillAlpha()); } shapes.add(new PaintDrawCmd(pattern.getPaint())); shapes.add(new ShapeDrawCmd(graphicState.getClip())); shapes.add(new FillDrawCmd()); } else { // apply the current fill color along ith a little alpha // to at least try to paint a colour for an unsupported mesh // type pattern. setAlpha(shapes, graphicState, graphicState.getAlphaRule(), 0.50f); shapes.add(new PaintDrawCmd(graphicState.getFillColor())); shapes.add(new ShapeDrawCmd(graphicState.getClip())); shapes.add(new FillDrawCmd()); } } } protected static void consume_TJ(GraphicsState graphicState, Stack stack, Shapes shapes, TextMetrics textMetrics, GlyphOutlineClip glyphOutlineClip, LinkedList<OptionalContents> oCGs) { // apply scaling AffineTransform tmp = applyTextScaling(graphicState); // apply transparency setAlpha(shapes, graphicState, graphicState.getAlphaRule(), graphicState.getFillAlpha()); java.util.List v = (java.util.List) stack.pop(); Number f; StringObject stringObject; TextState textState; for (Object currentObject : v) { if (currentObject instanceof StringObject) { stringObject = (StringObject) currentObject; textState = graphicState.getTextState(); // draw string takes care of PageText extraction drawString(stringObject.getLiteralStringBuffer( textState.font.getSubTypeFormat(), textState.font.getFont()), textMetrics, graphicState.getTextState(), shapes, glyphOutlineClip, graphicState, oCGs); } else if (currentObject instanceof Number) { f = (Number) currentObject; textMetrics.getAdvance().x -= (f.floatValue() / 1000f) * graphicState.getTextState().currentfont.getSize(); } textMetrics.setPreviousAdvance(textMetrics.getAdvance().x); } graphicState.set(tmp); } protected static void consume_Tj(GraphicsState graphicState, Stack stack, Shapes shapes, TextMetrics textMetrics, GlyphOutlineClip glyphOutlineClip, LinkedList<OptionalContents> oCGs) { if (stack.size() != 0) { Object tjValue = stack.pop(); StringObject stringObject; TextState textState; if (tjValue instanceof StringObject) { stringObject = (StringObject) tjValue; textState = graphicState.getTextState(); // apply scaling AffineTransform tmp = applyTextScaling(graphicState); // apply transparency setAlpha(shapes, graphicState, graphicState.getAlphaRule(), graphicState.getFillAlpha()); // draw string will take care of text pageText construction drawString(stringObject.getLiteralStringBuffer( textState.font.getSubTypeFormat(), textState.font.getFont()), textMetrics, graphicState.getTextState(), shapes, glyphOutlineClip, graphicState, oCGs); graphicState.set(tmp); } } } /** * Utility method for calculating the advanceX need for the * <code>displayText</code> given the strings parsed textState. Each of * <code>displayText</code> glyphs and respective, text state is added to * the shapes collection. * * @param displayText text that will be drawn to the screen * @param textMetrics current advanceX of last drawn string, * last advance of where the string should be drawn * @param textState formating properties associated with displayText * @param shapes collection of all shapes for page content being parsed. */ protected static void drawString( StringBuilder displayText, TextMetrics textMetrics, TextState textState, Shapes shapes, GlyphOutlineClip glyphOutlineClip, GraphicsState graphicState, LinkedList<OptionalContents> oCGs) { float advanceX = textMetrics.getAdvance().x; float advanceY = textMetrics.getAdvance().y; if (displayText.length() == 0) { textMetrics.getAdvance().setLocation(textMetrics.getPreviousAdvance(), 0f); return; } // Postion of previous Glyph, all relative to text block float lastx = 0, lasty = 0; // Make sure that the previous advanceX is greater then then where we // are going to place the next glyph, see not 57 in 1.6 spec for more // information. char currentChar = displayText.charAt(0); // Position of the specified glyph relative to the origin of glyphVector float firstCharWidth = (float) textState.currentfont.echarAdvance(currentChar).getX(); if ((advanceX + firstCharWidth) < textMetrics.getPreviousAdvance()) { advanceX = textMetrics.getPreviousAdvance(); } // Data need on font FontFile currentFont = textState.currentfont; boolean isVerticalWriting = textState.font.isVerticalWriting(); // int spaceCharacter = currentFont.getSpaceEchar(); // font metrics data float textRise = textState.trise; float characterSpace = textState.cspace * textState.hScalling; float whiteSpace = textState.wspace * textState.hScalling; int textLength = displayText.length(); // create a new sprite to hold the text objects TextSprite textSprites = new TextSprite(currentFont, textLength, new AffineTransform(graphicState.getCTM()), new AffineTransform(textState.tmatrix)); // glyph placement params float currentX, currentY; float newAdvanceX, newAdvanceY; // Iterate through displayText to calculate the the new advanceX value for (int i = 0; i < textLength; i++) { currentChar = displayText.charAt(i); if (enabledFontFallback) { boolean display = currentFont.canDisplayEchar(currentChar); // slow display test, but allows us to fall back on a different font if needed. if (!display) { FontFile fontFile = FontManager.getInstance().getInstance(currentFont.getName(), 0); textSprites.setFont(fontFile); } } // Position of the specified glyph relative to the origin of glyphVector // advance is handled by the particular font implementation. newAdvanceX = (float) currentFont.echarAdvance(currentChar).getX(); newAdvanceY = newAdvanceX; if (!isVerticalWriting) { // add fonts rise to the to glyph position (sup,sub scripts) currentX = advanceX + lastx; currentY = lasty - textRise; lastx += newAdvanceX; // store the pre Tc and Tw dimension. textMetrics.setPreviousAdvance(lastx); lastx += characterSpace; // lastly add space widths, no funny corner case yet for this one. if (displayText.charAt(i) == 32) { // currently to unreliable currentFont.getSpaceEchar() lastx += whiteSpace; } } else { // add fonts rise to the to glyph position (sup,sub scripts) lasty += (newAdvanceY - textRise); currentX = advanceX - (newAdvanceX / 2.0f); currentY = advanceY + lasty; } // get normalized from from text sprite GlyphText glyphText = textSprites.addText( String.valueOf(currentChar), // cid textState.currentfont.toUnicode(currentChar), // unicode value currentX, currentY, newAdvanceX); shapes.getPageText().addGlyph(glyphText, oCGs); } // append the finally offset of the with of the character advanceX += lastx; advanceY += lasty; /** * The text rendering mode, Tmode, determines whether showing text * causes glyph outlines to be stroked, filled, used as a clipping * boundary, or some combination of the three. * * No Support for 4, 5, 6 and 7. * * 0 - Fill text * 1 - Stroke text * 2 - fill, then stroke text * 3 - Neither fill nor stroke text (invisible) * 4 - Fill text and add to path for clipping * 5 - Stroke text and add to path for clipping. * 6 - Fill, then stroke text and add to path for clipping. * 7 - Add text to path for clipping. */ int rmode = textState.rmode; switch (rmode) { // fill text: 0 case TextState.MODE_FILL: drawModeFill(graphicState, textSprites, shapes, rmode); break; // Stroke text: 1 case TextState.MODE_STROKE: drawModeStroke(graphicState, textSprites, textState, shapes, rmode); break; // Fill, then stroke text: 2 case TextState.MODE_FILL_STROKE: drawModeFillStroke(graphicState, textSprites, textState, shapes, rmode); break; // Neither fill nor stroke text (invisible): 3 case TextState.MODE_INVISIBLE: // do nothing break; // Fill text and add to path for clipping: 4 case TextState.MODE_FILL_ADD: drawModeFill(graphicState, textSprites, shapes, rmode); glyphOutlineClip.addTextSprite(textSprites); break; // Stroke Text and add to path for clipping: 5 case TextState.MODE_STROKE_ADD: drawModeStroke(graphicState, textSprites, textState, shapes, rmode); glyphOutlineClip.addTextSprite(textSprites); break; // Fill, then stroke text adn add to path for clipping: 6 case TextState.MODE_FILL_STROKE_ADD: drawModeFillStroke(graphicState, textSprites, textState, shapes, rmode); glyphOutlineClip.addTextSprite(textSprites); break; // Add text to path for clipping: 7 case TextState.MODE_ADD: glyphOutlineClip.addTextSprite(textSprites); break; } textMetrics.getAdvance().setLocation(advanceX, advanceY); } /** * Utility Method for adding a text sprites to the Shapes stack, given the * specified rmode. * * @param textSprites text to add to shapes stack * @param shapes shapes stack * @param rmode write mode */ protected static void drawModeFill(GraphicsState graphicState, TextSprite textSprites, Shapes shapes, int rmode) { textSprites.setRMode(rmode); textSprites.setStrokeColor(graphicState.getFillColor()); shapes.add(new ColorDrawCmd(graphicState.getFillColor())); shapes.add(new TextSpriteDrawCmd(textSprites)); } /** * Utility Method for adding a text sprites to the Shapes stack, given the * specifed rmode. * * @param textSprites text to add to shapes stack * @param shapes shapes stack * @param textState text state used to build new stroke * @param rmode write mode */ protected static void drawModeStroke(GraphicsState graphicState, TextSprite textSprites, TextState textState, Shapes shapes, int rmode) { // setup textSprite with a strokeColor and the correct rmode textSprites.setRMode(rmode); textSprites.setStrokeColor(graphicState.getStrokeColor()); // save the old line width float old = graphicState.getLineWidth(); // set the line width for the glyph float lineWidth = graphicState.getLineWidth(); double scale = textState.tmatrix.getScaleX(); // double check for a near zero value as it will really mess up the division result, zero is just fine. if (scale > 0.001 || scale == 0) { lineWidth /= scale; graphicState.setLineWidth(lineWidth); } else { // corner case stroke adjustment, still can't find anything in spec about this. lineWidth *= scale * 100; graphicState.setLineWidth(lineWidth); } // update the stroke and add the text to shapes setStroke(shapes, graphicState); shapes.add(new ColorDrawCmd(graphicState.getStrokeColor())); shapes.add(new TextSpriteDrawCmd(textSprites)); // restore graphics state graphicState.setLineWidth(old); setStroke(shapes, graphicState); } /** * Utility Method for adding a text sprites to the Shapes stack, given the * specifed rmode. * * @param textSprites text to add to shapes stack * @param textState text state used to build new stroke * @param shapes shapes stack * @param rmode write mode */ protected static void drawModeFillStroke(GraphicsState graphicState, TextSprite textSprites, TextState textState, Shapes shapes, int rmode) { // setup textSprite with a strokeColor and the correct rmode textSprites.setRMode(rmode); textSprites.setStrokeColor(graphicState.getStrokeColor()); // save the old line width float old = graphicState.getLineWidth(); // set the line width for the glyph float lineWidth = graphicState.getLineWidth(); double scale = textState.tmatrix.getScaleX(); // double check for a near zero value as it will really mess up the division result, zero is just fine. if (scale > 0.0001 || scale == 0) { lineWidth /= scale; graphicState.setLineWidth(lineWidth); } // update the stroke and add the text to shapes setStroke(shapes, graphicState); shapes.add(new ColorDrawCmd(graphicState.getFillColor())); shapes.add(new TextSpriteDrawCmd(textSprites)); // restore graphics state graphicState.setLineWidth(old); setStroke(shapes, graphicState); } /** * Common stroke operations used by S and s. Takes into * account patternColour and regular old fill colour. * * @param shapes current shapes stack * @param geometricPath current path. */ protected static void commonStroke(GraphicsState graphicState, Shapes shapes, GeneralPath geometricPath) throws InterruptedException { // get current fill alpha and concatenate with overprinting if present if (graphicState.isOverprintStroking()) { setAlpha(shapes, graphicState, graphicState.getAlphaRule(), commonOverPrintAlpha(graphicState.getStrokeAlpha(), graphicState.getStrokeColorSpace())); } // The knockout effect can only be achieved by changing the alpha // composite to source. I don't have a test case for this for stroke // but what we do for stroke is usually what we do for fill... else if (graphicState.isKnockOut()) { setAlpha(shapes, graphicState, AlphaComposite.SRC, graphicState.getStrokeAlpha()); } // found a PatternColor if (graphicState.getStrokeColorSpace() instanceof PatternColor) { // Create a pointer to the pattern colour PatternColor patternColor = (PatternColor) graphicState.getStrokeColorSpace(); // grab the pattern from the colour Pattern pattern = patternColor.getPattern(); // Start processing tiling pattern if (pattern != null && pattern.getPatternType() == Pattern.PATTERN_TYPE_TILING) { // currently not doing any special handling for colour or uncoloured // paint, as it done when the scn or sc tokens are parsed. TilingPattern tilingPattern = (TilingPattern) pattern; // 1.)save the graphics context graphicState = graphicState.save(); // 2.) install the graphic state tilingPattern.setParentGraphicState(graphicState); tilingPattern.init(graphicState); // 4.) Restore the saved graphics state graphicState = graphicState.restore(); // 1x1 tiles don't seem to paint so we'll resort to using the // first pattern colour or the uncolour. if ((tilingPattern.getbBoxMod() != null && (tilingPattern.getbBoxMod().getWidth() > 1 || tilingPattern.getbBoxMod().getHeight() > 1))) { shapes.add(new TilingPatternDrawCmd(tilingPattern)); } else { // draw partial fill colour if (tilingPattern.getPaintType() == TilingPattern.PAINTING_TYPE_UNCOLORED_TILING_PATTERN) { shapes.add(new ColorDrawCmd(tilingPattern.getUnColored())); } else { shapes.add(new ColorDrawCmd(tilingPattern.getFirstColor())); } } shapes.add(new ShapeDrawCmd(geometricPath)); shapes.add(new DrawDrawCmd()); } else if (pattern != null && pattern.getPatternType() == Pattern.PATTERN_TYPE_SHADING) { pattern.init(graphicState); shapes.add(new PaintDrawCmd(pattern.getPaint())); shapes.add(new ShapeDrawCmd(geometricPath)); shapes.add(new DrawDrawCmd()); } } else { setAlpha(shapes, graphicState, graphicState.getAlphaRule(), graphicState.getStrokeAlpha()); shapes.add(new ColorDrawCmd(graphicState.getStrokeColor())); shapes.add(new ShapeDrawCmd(geometricPath)); shapes.add(new DrawDrawCmd()); } // set alpha back to original value. // if (graphicState.isOverprintStroking()) { // setAlpha(shapes, graphicState, AlphaComposite.SRC_OVER, graphicState.getFillAlpha()); // } } /** * Utility method for fudging overprinting calculation for screen * representation. This feature is optional an off by default. * <p> * Can be enable with -Dorg.icepdf.core.enabledOverPrint=true * * @param alpha alph constant * @return tweaked over printing alpha */ protected static float commonOverPrintAlpha(float alpha, PColorSpace colorSpace) { if (!enabledOverPrint) { return alpha; } if (colorSpace instanceof DeviceN) {// || colorSpace instanceof Separation) { // if alpha is already present we reduce it and we minimize // it if it is already lower then our over paint. This an approximation // only for improved screen representation. if (alpha != 1.0f && alpha > OVERPAINT_ALPHA) { alpha -= OVERPAINT_ALPHA; } else if (alpha < OVERPAINT_ALPHA) { // alpha = 0.1f; } else { alpha = OVERPAINT_ALPHA; } return alpha; } return alpha; } /** * Common fill operations used by f, F, F*, b, b*, B, B*. Takes into * account patternColour and regular old fill colour. * * @param shapes current shapes stack * @param graphicState current graphics state. * @param geometricPath current path. */ protected static void commonFill(Shapes shapes, GraphicsState graphicState, GeneralPath geometricPath) throws NoninvertibleTransformException, InterruptedException { // get current fill alpha and concatenate with overprinting if present if (graphicState.isOverprintOther()) { setAlpha(shapes, graphicState, graphicState.getAlphaRule(), commonOverPrintAlpha(graphicState.getFillAlpha(), graphicState.getFillColorSpace())); } // avoid doing fill, as we likely have blending mode that will obfuscate the underlying // content. if (graphicState.getExtGState() != null && graphicState.getExtGState().getSMask() != null) { return; } // The knockout effect can only be achieved by changing the alpha // composite to source. else if (graphicState.isKnockOut()) { setAlpha(shapes, graphicState, AlphaComposite.SRC, graphicState.getFillAlpha()); } else if (graphicState.getExtGState() == null || graphicState.getExtGState().getBlendingMode() == null) { setAlpha(shapes, graphicState, graphicState.getAlphaRule(), graphicState.getFillAlpha()); } // found a PatternColor if (graphicState.getFillColorSpace() instanceof PatternColor) { // Create a pointer to the pattern colour PatternColor patternColor = (PatternColor) graphicState.getFillColorSpace(); // grab the pattern from the colour Pattern pattern = patternColor.getPattern(); // Start processing tiling pattern if (pattern != null && pattern.getPatternType() == Pattern.PATTERN_TYPE_TILING) { // currently not doing any special handling for colour or uncoloured // paint, as it done when the scn or sc tokens are parsed. TilingPattern tilingPattern = (TilingPattern) pattern; // 1.)save the graphics context graphicState = graphicState.save(); // 2.) install the graphic state tilingPattern.setParentGraphicState(graphicState); tilingPattern.init(graphicState); // 4.) Restore the saved graphics state graphicState = graphicState.restore(); // tiles nee to be 1x1 or larger to paint so we'll resort to using the // first pattern colour or the uncolour. if (tilingPattern.getbBoxMod() != null && (tilingPattern.getbBoxMod().getWidth() >= 0.5 || tilingPattern.getbBoxMod().getHeight() >= 0.5)) { shapes.add(new TilingPatternDrawCmd(tilingPattern)); } else { // draw partial fill colour if (tilingPattern.getPaintType() == TilingPattern.PAINTING_TYPE_UNCOLORED_TILING_PATTERN) { shapes.add(new ColorDrawCmd(tilingPattern.getUnColored())); } else { shapes.add(new ColorDrawCmd(tilingPattern.getFirstColor())); } } shapes.add(new ShapeDrawCmd(geometricPath)); shapes.add(new FillDrawCmd()); } else if (pattern != null && pattern.getPatternType() == Pattern.PATTERN_TYPE_SHADING) { pattern.init(graphicState); shapes.add(new PaintDrawCmd(pattern.getPaint())); shapes.add(new ShapeDrawCmd(geometricPath)); shapes.add(new FillDrawCmd()); } } else { // if (graphicState.getExtGState() != null // && graphicState.getExtGState().getBlendingMode() != null // && graphicState.getExtGState().getOverprintMode() == 1 ) { // shapes.add(new BlendCompositeDrawCmd(graphicState.getExtGState().getBlendingMode(), // graphicState.getFillAlpha())); // } shapes.add(new ColorDrawCmd(graphicState.getFillColor())); shapes.add(new ShapeDrawCmd(geometricPath)); shapes.add(new FillDrawCmd()); } // add old alpha back to stack // if (graphicState.isOverprintOther()) { // setAlpha(shapes, graphicState, graphicState.getAlphaRule(), graphicState.getFillAlpha()); // } } /** * Sets the state of the BasicStrok with the latest values from the * graphicSate instance value: * graphicState.lineWidth - line width * graphicState.lineCap - line cap type * graphicState.lineJoin - line join type * graphicState.miterLimit - miter limit * * @param shapes current Shapes object for the page being parsed * @param graphicState graphic state used to build this stroke instance. */ protected static void setStroke(Shapes shapes, GraphicsState graphicState) { shapes.add(new StrokeDrawCmd(new BasicStroke(graphicState.getLineWidth(), graphicState.getLineCap(), graphicState.getLineJoin(), graphicState.getMiterLimit(), graphicState.getDashArray(), graphicState.getDashPhase()))); } /** * Text scaling must be applied to the main graphic state. It can not * be applied to the Text Matrix. We only have two test cases for its * use but it appears that the scaling has to bee applied before a text * write operand occurs, otherwise a call to Tm seems to break text * positioning. * <p> * Scaling is special as it can be negative and thus apply a horizontal * flip on the graphic state. * * @param graphicState current graphics state. */ protected static AffineTransform applyTextScaling(GraphicsState graphicState) { // get the current CTM AffineTransform af = new AffineTransform(graphicState.getCTM()); // the mystery continues, it appears that only the negative or positive // value of tz is actually used. If the original non 1 number is used the // layout will be messed up. AffineTransform oldHScaling = new AffineTransform(graphicState.getCTM()); float hScaling = graphicState.getTextState().hScalling; AffineTransform horizontalScalingTransform = new AffineTransform( af.getScaleX() * hScaling, af.getShearY(), af.getShearX(), af.getScaleY(), af.getTranslateX(), af.getTranslateY()); // add the transformation to the graphics state graphicState.set(horizontalScalingTransform); return oldHScaling; } /** * Adds a new Alpha Composite object ot the shapes stack. * * @param shapes - current shapes vector to add Alpha Composite to * @param rule - rule to apply to the alphaComposite. * @param alpha - alpha value, opaque = 1.0f. */ protected static void setAlpha(Shapes shapes, GraphicsState graphicsState, int rule, float alpha) { // Build the alpha composite object and add it to the shapes but only // if it hash changed. if (shapes != null && (shapes.getAlpha() != alpha || shapes.getRule() != rule)) { AlphaComposite alphaComposite = AlphaComposite.getInstance(rule, alpha); shapes.add(new AlphaDrawCmd(alphaComposite)); shapes.setAlpha(alpha); shapes.setRule(rule); } } public void setGlyph2UserSpaceScale(float scale) { glyph2UserSpaceScale = scale; } }