| /* |
| * 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.fontbox.cff; |
| |
| import java.awt.geom.AffineTransform; |
| import java.awt.geom.GeneralPath; |
| import java.awt.geom.Point2D; |
| import java.awt.geom.Rectangle2D; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.apache.logging.log4j.Logger; |
| import org.apache.logging.log4j.LogManager; |
| |
| import org.apache.fontbox.cff.CharStringCommand.Type1KeyWord; |
| import org.apache.fontbox.encoding.StandardEncoding; |
| import org.apache.fontbox.type1.Type1CharStringReader; |
| |
| /** |
| * This class represents and renders a Type 1 CharString. |
| * |
| * @author Villu Ruusmann |
| * @author John Hewson |
| */ |
| public class Type1CharString |
| { |
| private static final Logger LOG = LogManager.getLogger(Type1CharString.class); |
| |
| private final Type1CharStringReader font; |
| private final String fontName; |
| private final String glyphName; |
| private GeneralPath path = null; |
| private int width = 0; |
| private Point2D.Float leftSideBearing = null; |
| private Point2D.Float current = null; |
| private boolean isFlex = false; |
| private final List<Point2D.Float> flexPoints = new ArrayList<>(); |
| private final List<Object> type1Sequence = new ArrayList<>(); |
| private int commandCount = 0; |
| |
| /** |
| * Constructs a new Type1CharString object. |
| * |
| * @param font Parent Type 1 CharString font. |
| * @param fontName Name of the font. |
| * @param glyphName Name of the glyph. |
| * @param sequence Type 1 char string sequence |
| */ |
| public Type1CharString(Type1CharStringReader font, String fontName, String glyphName, |
| List<Object> sequence) |
| { |
| this(font, fontName, glyphName); |
| type1Sequence.addAll(sequence); |
| } |
| |
| /** |
| * Constructor for use in subclasses. |
| * |
| * @param font Parent Type 1 CharString font. |
| * @param fontName Name of the font. |
| * @param glyphName Name of the glyph. |
| */ |
| protected Type1CharString(Type1CharStringReader font, String fontName, String glyphName) |
| { |
| this.font = font; |
| this.fontName = fontName; |
| this.glyphName = glyphName; |
| this.current = new Point2D.Float(0, 0); |
| } |
| |
| // todo: NEW name (or CID as hex) |
| public String getName() |
| { |
| return glyphName; |
| } |
| |
| /** |
| * Returns the bounds of the renderer path. |
| * @return the bounds as Rectangle2D |
| */ |
| public Rectangle2D getBounds() |
| { |
| synchronized(LOG) |
| { |
| if (path == null) |
| { |
| render(); |
| } |
| } |
| return path.getBounds2D(); |
| } |
| |
| /** |
| * Returns the advance width of the glyph. |
| * @return the width |
| */ |
| public int getWidth() |
| { |
| synchronized(LOG) |
| { |
| if (path == null) |
| { |
| render(); |
| } |
| } |
| return width; |
| } |
| |
| /** |
| * Returns the path of the character. |
| * @return the path |
| */ |
| public GeneralPath getPath() |
| { |
| synchronized(LOG) |
| { |
| if (path == null) |
| { |
| render(); |
| } |
| } |
| return path; |
| } |
| |
| /** |
| * Renders the Type 1 char string sequence to a GeneralPath. |
| */ |
| private void render() |
| { |
| path = new GeneralPath(); |
| leftSideBearing = new Point2D.Float(0, 0); |
| width = 0; |
| List<Number> numbers = new ArrayList<>(); |
| type1Sequence.forEach(obj -> { |
| if (obj instanceof CharStringCommand) |
| { |
| handleType1Command(numbers, (CharStringCommand) obj); |
| } |
| else |
| { |
| numbers.add((Number) obj); |
| } |
| }); |
| } |
| |
| private void handleType1Command(List<Number> numbers, CharStringCommand command) |
| { |
| commandCount++; |
| Type1KeyWord type1KeyWord = command.getType1KeyWord(); |
| if (type1KeyWord == null) |
| { |
| // indicates an invalid charstring |
| LOG.warn("Unknown charstring command in glyph {} of font {}", glyphName, fontName); |
| numbers.clear(); |
| return; |
| } |
| switch(type1KeyWord) |
| { |
| case RMOVETO: |
| if (numbers.size() >= 2) |
| { |
| if (isFlex) |
| { |
| flexPoints.add(new Point2D.Float(numbers.get(0).floatValue(), numbers.get(1).floatValue())); |
| } |
| else |
| { |
| rmoveTo(numbers.get(0), numbers.get(1)); |
| } |
| } |
| break; |
| case VMOVETO: |
| if (!numbers.isEmpty()) |
| { |
| if (isFlex) |
| { |
| // not in the Type 1 spec, but exists in some fonts |
| flexPoints.add(new Point2D.Float(0f, numbers.get(0).floatValue())); |
| } |
| else |
| { |
| rmoveTo(0, numbers.get(0)); |
| } |
| } |
| break; |
| case HMOVETO: |
| if (!numbers.isEmpty()) |
| { |
| if (isFlex) |
| { |
| // not in the Type 1 spec, but exists in some fonts |
| flexPoints.add(new Point2D.Float(numbers.get(0).floatValue(), 0f)); |
| } |
| else |
| { |
| rmoveTo(numbers.get(0), 0); |
| } |
| } |
| break; |
| case RLINETO: |
| if (numbers.size() >= 2) |
| { |
| rlineTo(numbers.get(0), numbers.get(1)); |
| } |
| break; |
| case HLINETO: |
| if (!numbers.isEmpty()) |
| { |
| rlineTo(numbers.get(0), 0); |
| } |
| break; |
| case VLINETO: |
| if (!numbers.isEmpty()) |
| { |
| rlineTo(0, numbers.get(0)); |
| } |
| break; |
| case RRCURVETO: |
| if (numbers.size() >= 6) |
| { |
| rrcurveTo(numbers.get(0), numbers.get(1), numbers.get(2), |
| numbers.get(3), numbers.get(4), numbers.get(5)); |
| } |
| break; |
| case CLOSEPATH: |
| closeCharString1Path(); |
| break; |
| case SBW: |
| if (numbers.size() >= 3) |
| { |
| leftSideBearing = new Point2D.Float(numbers.get(0).floatValue(), numbers.get(1).floatValue()); |
| width = numbers.get(2).intValue(); |
| current.setLocation(leftSideBearing); |
| } |
| break; |
| case HSBW: |
| if (numbers.size() >= 2) |
| { |
| leftSideBearing = new Point2D.Float(numbers.get(0).floatValue(), 0); |
| width = numbers.get(1).intValue(); |
| current.setLocation(leftSideBearing); |
| } |
| break; |
| case VHCURVETO: |
| if (numbers.size() >= 4) |
| { |
| rrcurveTo(0, numbers.get(0), numbers.get(1), |
| numbers.get(2), numbers.get(3), 0); |
| } |
| break; |
| case HVCURVETO: |
| if (numbers.size() >= 4) |
| { |
| rrcurveTo(numbers.get(0), 0, numbers.get(1), |
| numbers.get(2), 0, numbers.get(3)); |
| } |
| break; |
| case SEAC: |
| if (numbers.size() >= 5) |
| { |
| seac(numbers.get(0), numbers.get(1), numbers.get(2), numbers.get(3), numbers.get(4)); |
| } |
| break; |
| case SETCURRENTPOINT: |
| if (numbers.size() >= 2) |
| { |
| setcurrentpoint(numbers.get(0), numbers.get(1)); |
| } |
| break; |
| case CALLOTHERSUBR: |
| if (!numbers.isEmpty()) |
| { |
| callothersubr(numbers.get(0).intValue()); |
| } |
| break; |
| case DIV: |
| if (numbers.size() >= 2) |
| { |
| float b = numbers.get(numbers.size() - 1).floatValue(); |
| float a = numbers.get(numbers.size() - 2).floatValue(); |
| |
| float result = a / b; |
| |
| numbers.remove(numbers.size() - 1); |
| numbers.remove(numbers.size() - 1); |
| numbers.add(result); |
| return; |
| } |
| break; |
| case HSTEM: |
| case VSTEM: |
| case HSTEM3: |
| case VSTEM3: |
| case DOTSECTION: |
| // ignore hints |
| break; |
| case ENDCHAR: |
| // end |
| break; |
| case RET: |
| case CALLSUBR: |
| // indicates an invalid charstring |
| LOG.warn("Unexpected charstring command: {} in glyph {} of font {}", type1KeyWord, |
| glyphName, fontName); |
| break; |
| default: |
| // indicates a PDFBox bug |
| throw new IllegalArgumentException("Unhandled command: " + type1KeyWord); |
| } |
| numbers.clear(); |
| } |
| |
| /** |
| * Sets the current absolute point without performing a moveto. |
| * Used only with results from callothersubr |
| */ |
| private void setcurrentpoint(Number x, Number y) |
| { |
| current.setLocation(x.floatValue(), y.floatValue()); |
| } |
| |
| /** |
| * Flex (via OtherSubrs) |
| * @param num OtherSubrs entry number |
| */ |
| private void callothersubr(int num) |
| { |
| if (num == 0) |
| { |
| // end flex |
| isFlex = false; |
| |
| if (flexPoints.size() < 7) |
| { |
| LOG.warn("flex without moveTo in font {}, glyph {}, command {}", fontName, |
| glyphName, commandCount); |
| return; |
| } |
| |
| // reference point is relative to start point |
| Point2D.Float reference = flexPoints.get(0); |
| reference.setLocation(current.getX() + reference.getX(), |
| current.getY() + reference.getY()); |
| |
| // first point is relative to reference point |
| Point2D.Float first = flexPoints.get(1); |
| first.setLocation(reference.getX() + first.getX(), reference.getY() + first.getY()); |
| |
| // make the first point relative to the start point |
| first.setLocation(first.getX() - current.getX(), first.getY() - current.getY()); |
| |
| Point2D.Float p1 = flexPoints.get(1); |
| Point2D.Float p2 = flexPoints.get(2); |
| Point2D.Float p3 = flexPoints.get(3); |
| rrcurveTo(p1.getX(), p1.getY(), p2.getX(), p2.getY(), p3.getX(), p3.getY()); |
| |
| Point2D.Float p4 = flexPoints.get(4); |
| Point2D.Float p5 = flexPoints.get(5); |
| Point2D.Float p6 = flexPoints.get(6); |
| rrcurveTo(p4.getX(), p4.getY(), p5.getX(), p5.getY(), p6.getX(), p6.getY()); |
| |
| flexPoints.clear(); |
| } |
| else if (num == 1) |
| { |
| // begin flex |
| isFlex = true; |
| } |
| else |
| { |
| LOG.warn("Invalid callothersubr parameter: {}", num); |
| } |
| } |
| |
| /** |
| * Relative moveto. |
| */ |
| private void rmoveTo(Number dx, Number dy) |
| { |
| float x = (float)current.getX() + dx.floatValue(); |
| float y = (float)current.getY() + dy.floatValue(); |
| path.moveTo(x, y); |
| current.setLocation(x, y); |
| } |
| |
| /** |
| * Relative lineto. |
| */ |
| private void rlineTo(Number dx, Number dy) |
| { |
| float x = (float)current.getX() + dx.floatValue(); |
| float y = (float)current.getY() + dy.floatValue(); |
| if (path.getCurrentPoint() == null) |
| { |
| LOG.warn("rlineTo without initial moveTo in font {}, glyph {}", fontName, glyphName); |
| path.moveTo(x, y); |
| } |
| else |
| { |
| path.lineTo(x, y); |
| } |
| current.setLocation(x, y); |
| } |
| |
| /** |
| * Relative curveto. |
| */ |
| private void rrcurveTo(Number dx1, Number dy1, Number dx2, Number dy2, |
| Number dx3, Number dy3) |
| { |
| float x1 = (float) current.getX() + dx1.floatValue(); |
| float y1 = (float) current.getY() + dy1.floatValue(); |
| float x2 = x1 + dx2.floatValue(); |
| float y2 = y1 + dy2.floatValue(); |
| float x3 = x2 + dx3.floatValue(); |
| float y3 = y2 + dy3.floatValue(); |
| if (path.getCurrentPoint() == null) |
| { |
| LOG.warn("rrcurveTo without initial moveTo in font {}, glyph {}", fontName, glyphName); |
| path.moveTo(x3, y3); |
| } |
| else |
| { |
| path.curveTo(x1, y1, x2, y2, x3, y3); |
| } |
| current.setLocation(x3, y3); |
| } |
| |
| /** |
| * Close path. |
| */ |
| private void closeCharString1Path() |
| { |
| if (path.getCurrentPoint() == null) |
| { |
| LOG.warn("closepath without initial moveTo in font {}, glyph {}", fontName, glyphName); |
| } |
| else |
| { |
| path.closePath(); |
| } |
| path.moveTo(current.getX(), current.getY()); |
| } |
| |
| /** |
| * Standard Encoding Accented Character |
| * |
| * Makes an accented character from two other characters. |
| * @param asb |
| */ |
| private void seac(Number asb, Number adx, Number ady, Number bchar, Number achar) |
| { |
| // base character |
| String baseName = StandardEncoding.INSTANCE.getName(bchar.intValue()); |
| try |
| { |
| Type1CharString base = font.getType1CharString(baseName); |
| path.append(base.getPath().getPathIterator(null), false); |
| } |
| catch (IOException e) |
| { |
| LOG.warn("invalid seac character in glyph {} of font {}", glyphName, fontName, e); |
| } |
| // accent character |
| String accentName = StandardEncoding.INSTANCE.getName(achar.intValue()); |
| try |
| { |
| Type1CharString accent = font.getType1CharString(accentName); |
| GeneralPath accentPath = accent.getPath(); |
| if (path == accentPath) |
| { |
| // PDFBOX-5339: avoid ArrayIndexOutOfBoundsException |
| // reproducable with poc file crash-4698e0dc7833a3f959d06707e01d03cda52a83f4 |
| LOG.warn("Path for {} and for accent {} are same, ignored", baseName, accentName); |
| return; |
| } |
| AffineTransform at = AffineTransform.getTranslateInstance( |
| leftSideBearing.getX() + adx.floatValue() - asb.floatValue(), |
| leftSideBearing.getY() + ady.floatValue()); |
| path.append(accentPath.getPathIterator(at), false); |
| } |
| catch (IOException e) |
| { |
| LOG.warn("invalid seac character in glyph {} of font {}", glyphName, fontName, e); |
| } |
| } |
| |
| /** |
| * Add a command to the type1 sequence. |
| * |
| * @param numbers the parameters of the command to be added |
| * @param command the command to be added |
| */ |
| protected void addCommand(List<Number> numbers, CharStringCommand command) |
| { |
| type1Sequence.addAll(numbers); |
| type1Sequence.add(command); |
| } |
| |
| /** |
| * Indicates if the underlying type1 sequence is empty. |
| * |
| * @return true if the sequence is empty |
| */ |
| protected boolean isSequenceEmpty() |
| { |
| return type1Sequence.isEmpty(); |
| } |
| |
| /** |
| * Returns the last entry of the underlying type1 sequence. |
| * |
| * @return the last entry of the type 1 sequence or null if empty |
| */ |
| protected Object getLastSequenceEntry() |
| { |
| if (!type1Sequence.isEmpty()) |
| { |
| return type1Sequence.get(type1Sequence.size() - 1); |
| } |
| return null; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return type1Sequence.toString().replace("|","\n").replace(",", " "); |
| } |
| } |