/*
* 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 com.tom_roush.fontbox.cff;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.Log;
import com.tom_roush.fontbox.encoding.StandardEncoding;
import com.tom_roush.fontbox.type1.Type1CharStringReader;
import com.tom_roush.pdfbox.util.awt.AffineTransform;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* This class represents and renders a Type 1 CharString.
*
* @author Villu Ruusmann
* @author John Hewson
*/
public class Type1CharString
{
private Type1CharStringReader font;
private String fontName, glyphName;
private Path path = null;
private int width = 0;
private PointF leftSideBearing = null;
private PointF current = null;
private boolean isFlex = false;
private List<PointF> flexPoints = new ArrayList<PointF>();
protected List<Object> type1Sequence;
protected int commandCount;
/**
* Constructs a new Type1CharString object.
* @param font Parent Type 1 CharString font
* @param sequence Type 1 char string sequence
*/
public Type1CharString(Type1CharStringReader font, String fontName, String glyphName,
List<Object> sequence)
{
this(font, fontName, glyphName);
type1Sequence = sequence;
}
/**
* Constructor for use in subclasses.
* @param font Parent Type 1 CharString font
*/
protected Type1CharString(Type1CharStringReader font, String fontName, String glyphName)
{
this.font = font;
this.fontName = fontName;
this.glyphName = glyphName;
this.current = new PointF(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 RectF getBounds()
{
if (path == null)
{
render();
}
RectF retval = null;
path.computeBounds(retval, true);
return retval;
}
/**
* Returns the advance width of the glyph.
* @return the width
*/
public int getWidth()
{
if (path == null)
{
render();
}
return width;
}
/**
* Returns the path of the character.
* @return the path
*/
public Path getPath()
{
if (path == null)
{
render();
}
return path;
}
/**
* Returns the Type 1 char string sequence.
* @return the Type 1 sequence
*/
public List<Object> getType1Sequence()
{
return type1Sequence;
}
/**
* Renders the Type 1 char string sequence to a GeneralPath.
*/
private void render()
{
path = new Path();
leftSideBearing = new PointF(0, 0);
width = 0;
CharStringHandler handler = new CharStringHandler() {
@Override
public List<Integer> handleCommand(List<Integer> numbers, CharStringCommand command)
{
return Type1CharString.this.handleCommand(numbers, command);
}
};
handler.handleSequence(type1Sequence);
}
private List<Integer> handleCommand(List<Integer> numbers, CharStringCommand command)
{
commandCount++;
String name = CharStringCommand.TYPE1_VOCABULARY.get(command.getKey());
if ("rmoveto".equals(name))
{
if (numbers.size() >= 2)
{
if (isFlex)
{
flexPoints.add(new PointF(numbers.get(0), numbers.get(1)));
}
else
{
rmoveTo(numbers.get(0), numbers.get(1));
}
}
}
else if ("vmoveto".equals(name))
{
if (numbers.size() >= 1)
{
if (isFlex)
{
// not in the Type 1 spec, but exists in some fonts
flexPoints.add(new PointF(0, numbers.get(0)));
}
else
{
rmoveTo(0, numbers.get(0));
}
}
}
else if ("hmoveto".equals(name))
{
if (numbers.size() >= 1)
{
if (isFlex)
{
// not in the Type 1 spec, but exists in some fonts
flexPoints.add(new PointF(numbers.get(0), 0));
}
else
{
rmoveTo(numbers.get(0), 0);
}
}
}
else if ("rlineto".equals(name))
{
if (numbers.size() >= 2)
{
rlineTo(numbers.get(0), numbers.get(1));
}
}
else if ("hlineto".equals(name))
{
if (numbers.size() >= 1)
{
rlineTo(numbers.get(0), 0);
}
}
else if ("vlineto".equals(name))
{
if (numbers.size() >= 1)
{
rlineTo(0, numbers.get(0));
}
}
else if ("rrcurveto".equals(name))
{
if (numbers.size() >= 6)
{
rrcurveTo(numbers.get(0), numbers.get(1), numbers.get(2), numbers.get(3),
numbers.get(4), numbers.get(5));
}
}
else if ("closepath".equals(name))
{
closepath();
}
else if ("sbw".equals(name))
{
if (numbers.size() >= 3)
{
leftSideBearing = new PointF(numbers.get(0), numbers.get(1));
width = numbers.get(2);
current.set(leftSideBearing);
}
}
else if ("hsbw".equals(name))
{
if (numbers.size() >= 2)
{
leftSideBearing = new PointF(numbers.get(0), 0);
width = numbers.get(1);
current.set(leftSideBearing);
}
}
else if ("vhcurveto".equals(name))
{
if (numbers.size() >= 4)
{
rrcurveTo(0, numbers.get(0), numbers.get(1), numbers.get(2), numbers.get(3), 0);
}
}
else if ("hvcurveto".equals(name))
{
if (numbers.size() >= 4)
{
rrcurveTo(numbers.get(0), 0, numbers.get(1), numbers.get(2), 0, numbers.get(3));
}
}
else if ("seac".equals(name))
{
if (numbers.size() >= 6)
{
seac(numbers.get(0), numbers.get(1), numbers.get(2), numbers.get(3),
numbers.get(4));
}
}
else if ("setcurrentpoint".equals(name))
{
if (numbers.size() >= 2)
{
setcurrentpoint(numbers.get(0), numbers.get(1));
}
}
else if ("callothersubr".equals(name))
{
if (numbers.size() >= 1)
{
callothersubr(numbers.get(0));
}
}
else if ("div".equals(name))
{
int b = numbers.get(numbers.size() -1);
int a = numbers.get(numbers.size() -2);
int result = a / b; // TODO loss of precision, should be float
List<Integer> list = new ArrayList<Integer>(numbers);
list.remove(list.size() - 1);
list.remove(list.size() - 1);
list.add(result);
return list;
}
else if ("hstem".equals(name) || "vstem".equals(name) ||
"hstem3".equals(name) || "vstem3".equals(name) || "dotsection".equals(name))
{
// ignore hints
}
else if ("endchar".equals(name))
{
// end
}
else if ("return".equals(name))
{
// indicates an invalid charstring
Log.w("PdfBox-Android", "Unexpected charstring command: " + command.getKey() +
" in glyph " + glyphName + " of font " + fontName);
}
else if (name != null)
{
// indicates a PDFBox bug
throw new IllegalArgumentException("Unhandled command: " + name);
}
else
{
// indicates an invalid charstring
Log.w("PdfBox-Android", "Unknown charstring command: " + command.getKey() + " in glyph "
+ glyphName + " of font " + fontName);
}
return null;
}
/**
* Sets the current absolute point without performing a moveto.
* Used only with results from callothersubr
*/
private void setcurrentpoint(int x, int y)
{
current.set(x, y);
}
/**
* 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.w("PdfBox-Android", "flex without moveTo in font " + fontName + ", glyph " + glyphName +
", command " + commandCount);
return;
}
// reference point is relative to start point
PointF reference = flexPoints.get(0);
reference.set(current.x + reference.x,
current.y + reference.y);
// first point is relative to reference point
PointF first = flexPoints.get(1);
first.set(reference.x + first.x, reference.y + first.y);
// make the first point relative to the start point
first.set(first.x - current.x, first.y - current.y);
rrcurveTo(flexPoints.get(1).x, flexPoints.get(1).y,
flexPoints.get(2).x, flexPoints.get(2).y,
flexPoints.get(3).x, flexPoints.get(3).y);
rrcurveTo(flexPoints.get(4).x, flexPoints.get(4).y,
flexPoints.get(5).x, flexPoints.get(5).y,
flexPoints.get(6).x, flexPoints.get(6).y);
flexPoints.clear();
}
else if (num == 1)
{
// begin flex
isFlex = true;
}
else
{
// indicates a PDFBox bug
throw new IllegalArgumentException("Unexpected other subroutine: " + num);
}
}
/**
* Relative moveto.
*/
private void rmoveTo(Number dx, Number dy)
{
float x = current.x + dx.floatValue();
float y = current.y + dy.floatValue();
path.moveTo(x, y);
current.set(x, y);
}
/**
* Relative lineto.
*/
private void rlineTo(Number dx, Number dy)
{
float x = current.x + dx.floatValue();
float y = current.y + dy.floatValue();
// if (path.getCurrentPoint() == null) TODO: Patch for now
if(path.isEmpty())
{
Log.w("PdfBox-Android", "rlineTo without initial moveTo in font " + fontName + ", glyph " + glyphName);
path.moveTo(x, y);
}
else
{
path.lineTo(x, y);
}
current.set(x, y);
}
/**
* Relative curveto.
*/
private void rrcurveTo(Number dx1, Number dy1, Number dx2, Number dy2,
Number dx3, Number dy3)
{
float x1 = current.x + dx1.floatValue();
float y1 = current.y + 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) TODO: Patch for now
if(path.isEmpty())
{
Log.w("PdfBox-Android", "rrcurveTo without initial moveTo in font " + fontName + ", glyph " + glyphName);
path.moveTo(x3, y3);
}
else
{
path.cubicTo(x1, y1, x2, y2, x3, y3); // TODO: Should this be relative?
}
current.set(x3, y3);
}
/**
* Close path.
*/
private void closepath()
{
// if (path.getCurrentPoint() == null) TODO: Patch for now
if(path.isEmpty())
{
Log.w("PdfBox-Android", "closepath without initial moveTo in font " + fontName + ", glyph " + glyphName);
}
else
{
path.close();
}
path.moveTo(current.x, current.y);
}
/**
* 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());
if (baseName != null)
{
try
{
Type1CharString base = font.getType1CharString(baseName);
// path.append(base.getPath().getPathIterator(null), false); TODO: check this
path.op(base.getPath(), Path.Op.UNION);
}
catch (IOException e)
{
Log.w("PdfBox-Android", "invalid seac character in glyph " + glyphName + " of font " + fontName);
}
}
// accent character
String accentName = StandardEncoding.INSTANCE.getName(achar.intValue());
if (accentName != null)
{
try
{
Type1CharString accent = font.getType1CharString(accentName);
AffineTransform at = AffineTransform.getTranslateInstance(
leftSideBearing.x + adx.floatValue(),
leftSideBearing.y + ady.floatValue());
// path.append(accent.getPath().getPathIterator(at), false); TODO: Check this
path.op(accent.getPath(), Path.Op.UNION);
}
catch (IOException e)
{
Log.w("PdfBox-Android", "invalid seac character in glyph " + glyphName + " of font " + fontName);
}
}
}
@Override
public String toString()
{
return type1Sequence.toString().replace("|","\n").replace(",", " ");
}
}