Sunday, January 18, 2009

FOP Positive Descender Underline Bug

Well here is my cowardly effort at giving back to the open source community. But first! What is the open source community?

Nerds:
It's all about nerds. I'm one and so are you if you are reading this after doing a google search for a solution to the positive descender underline bug in Apache FOP. But if not I'll give a quick run down. In the world of computer programming there is a movement which holds dear the concept that ideas and information are free. Free as in unshackled, free as in free to be exchanged. Sometimes you have to pay money for the info and ideas, but if / when you do you should have access to the whole idea. The same way when you buy a car you are allowed to open the bonnet and tinker with the guts. So programmers often allow others to see their work and we are all enriched by it. For a more detailed discussion: open source [wikipedia]

The Bug:
The bug I have solved manifests itself in a bit of software called Apache FOP. This software allows me to take some stuff drawn on the screen in Adobe Flash and turn it into pdf documents or png images. The problem happens when you try to underline text in an embedded font which has a positive descender. A positive descender means that part of the text of the font hangs below the baseline. For example in Lucida Sans the p, y, j and g all have little squiggly bits hanging below the base of the other characters. In this instance FOP fails to draw an underline because it throws an error that says it can't draw a border with a negative height.

Suspect Math:
The cause is a bit of suspect math in the underline
public abstract class AbstractPathOrientedRenderer extends PrintRenderer {
...
protected void renderTextDecoration(FontMetrics fm, int fontsize, InlineArea inline,
int baseline, int startx) {
boolean hasTextDeco = inline.hasUnderline()
|| inline.hasOverline()
|| inline.hasLineThrough();
if (hasTextDeco) {
endTextObject();
float descender = fm.getDescender(fontsize) / 1000f;
float capHeight = fm.getCapHeight(fontsize) / 1000f;
float halfLineWidth = descender / -8f / 2f;
float endx = (startx + inline.getIPD()) / 1000f;
if (inline.hasUnderline()) {
Color ct = (Color) inline.getTrait(Trait.UNDERLINE_COLOR);
float y = baseline - descender / 2f;
drawBorderLine(startx / 1000f, (y - halfLineWidth) / 1000f,
endx, (y + halfLineWidth) / 1000f,
true, true, Constants.EN_SOLID, ct);
}...
So you can see the bolded parts involve some calculation of a y position based on the descender value of the font. If the descender value is negative it all works nicely, if it is positive we end of with an inverted set of values in the call to drawBorderLine() method.

My Solution:
Here you see what I changed:
protected void renderTextDecoration(FontMetrics fm, int fontsize, InlineArea inline,
int baseline, int startx) {
boolean hasTextDeco = inline.hasUnderline()
|| inline.hasOverline()
|| inline.hasLineThrough();
if (hasTextDeco) {
endTextObject();
float descender = Math.abs(fm.getDescender(fontsize) / 1000f);
float capHeight = fm.getCapHeight(fontsize) / 1000f;
float halfLineWidth = descender / 16f;
float endx = (startx + inline.getIPD()) / 1000f;
if (inline.hasUnderline()) {
Color ct = (Color) inline.getTrait(Trait.UNDERLINE_COLOR);
float y = baseline + descender / 2f;
drawBorderLine(startx / 1000f, (y - halfLineWidth) / 1000f,
endx, (y + halfLineWidth) / 1000f,
true, true, Constants.EN_SOLID, ct);
}...
Now the bolded bits firstly show I get the absolute value of the descender, I divide it by 16 (rather than by 8 then by 2) and finally I add half the descender value to the baseline to get the y position.
The short of it is that the code now works for both negative and positive value descenders. The world rejoice!

Did I submit this back to the opensource project? No, because I've already hacked up my copy of FOP beyond recognition to reduce the on disk output size of pdfs using large svg images (by reducing the number of decimal points of precision from 8 to 3) and using a file backed caching system to step around the memory issues associated with printing said large pdfs. Feel free to ask me for more details about those fixes, if I don't get around to writing about them here.

2 comments:

  1. Thank you, it works. Could you also give a hint about using file system caching? What classes/methods have you patched to achieve that result?

    ReplyDelete
  2. It is the StringWriter.

    I created:
    /**
    *
    */
    package org.apache.fop.util;

    import java.io.BufferedInputStream;
    import java.io.BufferedOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.io.StringWriter;

    ;

    /**
    * @author surrey
    *
    * Acts as a StringWriter until the length of the StringBuffer reaches a
    * threshold at which point the current buffer and all new writing goes to a
    * file.
    *
    */
    public class FileBackedStringWriter extends StringWriter {

    private boolean toFile = false;

    private File bufFile;

    private OutputStream fos;

    private static int threshold = 10240000;

    public FileBackedStringWriter() {

    }

    @Override
    public StringBuffer getBuffer() {
    StringBuffer b = new StringBuffer(toString());
    return b;
    }

    @Override
    public String toString() {
    return new String(getBytes());
    }

    @Override
    public void write(String str) {
    if (!toFile) {
    if ((super.getBuffer().length() + str.length()) < threshold) {
    super.write(str);
    } else {
    toFile = true;
    prepareOutputStream();
    try {
    fos.write(super.getBuffer().toString().getBytes());
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    if (toFile) {
    prepareOutputStream();
    try {
    fos.write(str.getBytes());
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    @Override
    protected void finalize() throws Throwable {
    try {
    if (fos != null) {
    fos.flush();
    fos.close();
    fos = null;
    }

    if (bufFile != null && bufFile.exists()) {
    bufFile.delete();
    }
    } finally {
    super.finalize();
    }

    }

    private void prepareOutputStream() {
    if (fos == null) {
    try {
    bufFile = File.createTempFile(
    "org.apache.fop.util.FileBackedStringWriter-", ".temp");
    bufFile.deleteOnExit();
    fos = new BufferedOutputStream(new FileOutputStream(bufFile));
    } catch (FileNotFoundException e) {
    throw new IllegalArgumentException("tempDir must be set");
    } catch (IOException e) {
    throw new IllegalArgumentException("tempDir must be set");
    }
    }
    }

    public static void setThreshold(int size) {
    threshold = size;
    }

    public byte[] getBytes() {
    if (toFile) {
    try {
    if (fos != null) {
    fos.flush();
    fos.close();
    fos = null;
    }
    InputStream in = new BufferedInputStream(new FileInputStream(
    bufFile));
    byte[] buffer = new byte[new Long(bufFile.length()).intValue()];
    in.read(buffer);
    in.close();
    return buffer;
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    return super.toString().getBytes();
    }
    }

    So look for instances of the StringWriter and replace it.

    ReplyDelete