You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by ja...@apache.org on 2014/08/31 04:24:19 UTC

svn commit: r1621554 - in /pdfbox/trunk: fontbox/src/main/java/org/apache/fontbox/ttf/ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/ pdfbox/src/main/java/org/apache/pdfbox/rendering/font/ pdfbox/src/test/java/org/apache/pdfbox/rendering/ preflig...

Author: jahewson
Date: Sun Aug 31 02:24:19 2014
New Revision: 1621554

URL: http://svn.apache.org/r1621554
Log:
PDFBOX-2303: Lazy loading of glyphs in TrueType fonts

Added:
    pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestRegressions.java
Modified:
    pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyfCompositeDescript.java
    pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java
    pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDTrueTypeFont.java
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/font/TTFGlyph2D.java
    pdfbox/trunk/preflight/src/main/java/org/apache/pdfbox/preflight/font/container/CIDType2Container.java

Modified: pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyfCompositeDescript.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyfCompositeDescript.java?rev=1621554&r1=1621553&r2=1621554&view=diff
==============================================================================
--- pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyfCompositeDescript.java (original)
+++ pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyfCompositeDescript.java Sun Aug 31 02:24:19 2014
@@ -41,7 +41,7 @@ public class GlyfCompositeDescript exten
     private static final Log LOG = LogFactory.getLog(GlyfCompositeDescript.class);
 
     private final List<GlyfCompositeComp> components = new ArrayList<GlyfCompositeComp>();
-    private GlyphData[] glyphs = null;
+    private GlyphTable glyphTable = null;
     private boolean beingResolved = false;
     private boolean resolved = false;
 
@@ -56,7 +56,7 @@ public class GlyfCompositeDescript exten
     {
         super((short) -1, bais);
 
-        glyphs = glyphTable.getGlyphs();
+        this.glyphTable = glyphTable;
 
         // Get all of the composite components
         GlyfCompositeComp comp;
@@ -265,14 +265,19 @@ public class GlyfCompositeDescript exten
 
     private GlyphDescription getGlypDescription(int index)
     {
-        if (glyphs != null && index < glyphs.length)
+        try
         {
-            GlyphData glyph = glyphs[index];
+            GlyphData glyph = glyphTable.getGlyph(index);
             if (glyph != null)
             {
                 return glyph.getDescription();
             }
+            return null;
+        }
+        catch (IOException e)
+        {
+            LOG.error(e);
+            return null;
         }
-        return null;
     }
 }

Modified: pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java?rev=1621554&r1=1621553&r2=1621554&view=diff
==============================================================================
--- pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java (original)
+++ pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java Sun Aug 31 02:24:19 2014
@@ -17,12 +17,13 @@
 package org.apache.fontbox.ttf;
 
 import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * A table in a true type font.
  * 
- * @author Ben Litchfield (ben@benlitchfield.com)
- * 
+ * @author Ben Litchfield
  */
 public class GlyphTable extends TTFTable
 {
@@ -33,6 +34,12 @@ public class GlyphTable extends TTFTable
 
     private GlyphData[] glyphs;
 
+    // lazy table reading
+    private TTFDataStream data;
+    private IndexToLocationTable loca;
+    private int numGlyphs;
+    protected Map<Integer, GlyphData> cache = new ConcurrentHashMap<Integer, GlyphData>();
+
     /**
      * This will read the required data from the stream.
      * 
@@ -42,23 +49,34 @@ public class GlyphTable extends TTFTable
      */
     public void read(TrueTypeFont ttf, TTFDataStream data) throws IOException
     {
-        IndexToLocationTable loc = ttf.getIndexToLocation();
+        loca = ttf.getIndexToLocation();
+        numGlyphs = ttf.getNumberOfGlyphs();
+
+        // we don't actually read the table yet because it can contain tens of thousands of glyphs
+        this.data = data;
+        initialized = true;
+    }
+
+    /**
+     * Reads all glyphs from the font. Can be very slow.
+     */
+    private void readAll() throws IOException
+    {
         // the glyph offsets
-        long[] offsets = loc.getOffsets();
-        // number of glyphs
-        int numGlyphs = ttf.getNumberOfGlyphs();
+        long[] offsets = loca.getOffsets();
+
         // the end of the glyph table
         // should not be 0, but sometimes is, see PDFBOX-2044
         // structure of this table: see
         // https://developer.apple.com/fonts/TTRefMan/RM06/Chap6loca.html
-        long endOfGlyphs = offsets[numGlyphs]; 
+        long endOfGlyphs = offsets[numGlyphs];
         long offset = getOffset();
         glyphs = new GlyphData[numGlyphs];
         for (int i = 0; i < numGlyphs; i++)
         {
             // end of glyphs reached?
             if (endOfGlyphs != 0 &&
-                    endOfGlyphs == offsets[i]) 
+                    endOfGlyphs == offsets[i])
             {
                 break;
             }
@@ -85,10 +103,14 @@ public class GlyphTable extends TTFTable
     }
 
     /**
-     * @return Returns the glyphs.
+     * Returns all glyphs. This method can be very slow.
      */
-    public GlyphData[] getGlyphs()
+    public synchronized GlyphData[] getGlyphs() throws IOException
     {
+        if (glyphs == null)
+        {
+            readAll();
+        }
         return glyphs;
     }
 
@@ -99,4 +121,55 @@ public class GlyphTable extends TTFTable
     {
         glyphs = glyphsValue;
     }
+
+    /**
+     * Returns the data for the glyph with the given GID.
+     *
+     * @param gid GID
+     * @throws IOException if the font cannot be read
+     */
+    public GlyphData getGlyph(int gid) throws IOException
+    {
+        if (gid < 0 || gid >= numGlyphs)
+        {
+            return null;
+        }
+
+        if (cache.containsKey(gid))
+        {
+            return cache.get(gid);
+        }
+
+        synchronized (this)
+        {
+            // save
+            long currentPosition = data.getCurrentPosition();
+
+            // read a single glyph
+            long[] offsets = loca.getOffsets();
+
+            GlyphData glyph;
+            if (offsets[gid] == offsets[gid + 1])
+            {
+                // no outline
+                glyph = null;
+            }
+            else
+            {
+                data.seek(getOffset() + offsets[gid]);
+                glyph = new GlyphData();
+                glyph.initData(this, data);
+
+                // resolve composite glyph
+                if (glyph.getDescription().isComposite())
+                {
+                    glyph.getDescription().resolve();
+                }
+            }
+
+            // restore
+            data.seek(currentPosition);
+            return glyph;
+        }
+    }
 }

Modified: pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java?rev=1621554&r1=1621553&r2=1621554&view=diff
==============================================================================
--- pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java (original)
+++ pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java Sun Aug 31 02:24:19 2014
@@ -406,9 +406,8 @@ public class TrueTypeFont implements Typ
     {
         readPostScriptNames();
 
-        GlyphData[] glyphs = getGlyph().getGlyphs();
         Integer gid = postScriptNames.get(name);
-        if (gid == null || gid < 0 || gid >= glyphs.length)
+        if (gid == null || gid < 0 || gid >= getMaximumProfile().getNumGlyphs())
         {
             return 0;
         }
@@ -420,21 +419,21 @@ public class TrueTypeFont implements Typ
     {
         readPostScriptNames();
 
-        GlyphData[] glyphs = getGlyph().getGlyphs();
         Integer gid = postScriptNames.get(name);
-        if (gid == null || gid < 0 || gid >= glyphs.length)
+        if (gid == null || gid < 0 || gid >= getMaximumProfile().getNumGlyphs())
         {
             gid = 0;
         }
 
         // some glyphs have no outlines (e.g. space, table, newline)
-        if (glyphs[gid] == null)
+        GlyphData glyph = getGlyph().getGlyph(gid);
+        if (glyph == null)
         {
             return new GeneralPath();
         }
         else
         {
-            GeneralPath path = glyphs[gid].getPath();
+            GeneralPath path = glyph.getPath();
 
             // scale to 1000upem, per PostScript convention
             float scale = 1000f / getUnitsPerEm();
@@ -466,8 +465,7 @@ public class TrueTypeFont implements Typ
         readPostScriptNames();
 
         Integer gid = postScriptNames.get(name);
-        GlyphData[] glyphs = getGlyph().getGlyphs();
-        return !(gid == null || gid < 0 || gid >= glyphs.length);
+        return !(gid == null || gid < 0 || gid >= getMaximumProfile().getNumGlyphs());
     }
 
     @Override

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDTrueTypeFont.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDTrueTypeFont.java?rev=1621554&r1=1621553&r2=1621554&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDTrueTypeFont.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDTrueTypeFont.java Sun Aug 31 02:24:19 2014
@@ -190,7 +190,7 @@ public class PDTrueTypeFont extends PDSi
     public float getHeight(int code) throws IOException
     {
         int gid = codeToGID(code);
-        GlyphData glyph = ttf.getGlyph().getGlyphs()[gid];
+        GlyphData glyph = ttf.getGlyph().getGlyph(gid);
         if (glyph != null)
         {
             return glyph.getBoundingBox().getHeight();

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/font/TTFGlyph2D.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/font/TTFGlyph2D.java?rev=1621554&r1=1621553&r2=1621554&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/font/TTFGlyph2D.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/font/TTFGlyph2D.java Sun Aug 31 02:24:19 2014
@@ -139,14 +139,14 @@ public class TTFGlyph2D implements Glyph
         }
         else
         {
-            GlyphData[] glyphData = ttf.getGlyph().getGlyphs();
-            if (gid >= glyphData.length)
+            GlyphData glyph = ttf.getGlyph().getGlyph(gid);
+            if (gid >= ttf.getMaximumProfile().getNumGlyphs())
             {
                 LOG.warn(font.getName() + ": Glyph not found: " + gid);
                 glyphPath = new GeneralPath();
                 glyphs.put(gid, glyphPath);
             }
-            else if (glyphData[gid] == null)
+            else if (glyph == null)
             {
                 // empty glyph (e.g. space, newline)
                 glyphPath = new GeneralPath();
@@ -154,7 +154,6 @@ public class TTFGlyph2D implements Glyph
             }
             else
             {
-                GlyphData glyph = glyphData[gid];
                 glyphPath = glyph.getPath();
                 if (hasScaling)
                 {

Added: pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestRegressions.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestRegressions.java?rev=1621554&view=auto
==============================================================================
--- pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestRegressions.java (added)
+++ pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestRegressions.java Sun Aug 31 02:24:19 2014
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pdfbox.rendering;
+
+import org.apache.pdfbox.ParallelParameterized;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameters;
+
+import javax.imageio.ImageIO;
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.color.ColorSpace;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorConvertOp;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * todo: JavaDoc
+ *
+ * @author John Hewson
+ */
+@RunWith(ParallelParameterized.class)  // todo: disable this test by default? (for now?)
+//@Ignore
+public class TestRegressions
+{
+    private static final String PDF_DIR = "../../regression/pdf";
+    private static final String PNG_DIR = "../../regression/png";
+    private static final String OUT_DIR = "../../regression/out";
+    private static final String DIFF_DIR = "../../regression/diff";
+
+    @Parameters(name = "{0}")
+    public static Collection<Object[]> data()
+    {
+        // todo: sanity check for compatible JDK?
+
+        // populate the input parameters
+        File[] files = new File(PDF_DIR).listFiles();
+        List<Object[]> params = new ArrayList<Object[]>();
+        if (files != null)
+        {
+            for (File file : files)
+            {
+                if (file.getName().endsWith(".pdf"))
+                {
+                    params.add(new Object[] { file.getName() });
+                }
+            }
+        }
+        return params;
+    }
+
+    private final String filename;
+    private File inDir, outDir, diffDir;
+
+    public TestRegressions(String fileName)
+    {
+        this.filename = fileName;
+    }
+
+    @Before
+    public void init()
+    {
+        File file = new File(PDF_DIR, filename);
+
+        // create output dir
+        String dirName =  file.getName().substring(0,  file.getName().lastIndexOf('.'));
+        outDir = new File(OUT_DIR, dirName);
+        outDir.mkdirs();
+
+        // input png dirs
+        inDir = new File(PNG_DIR, dirName);
+
+        // diff dir (if any)
+        diffDir = new File(DIFF_DIR, dirName);
+        if (diffDir.exists())
+        {
+            // clean the diff dir
+            for(File png: diffDir.listFiles())
+            {
+                png.delete();
+            }
+            diffDir.delete();
+        }
+
+        // clean the output dir
+        for(File png: outDir.listFiles())
+        {
+            png.delete();
+        }
+    }
+
+    @Test
+    public void render() throws IOException
+    {
+        File file = new File(PDF_DIR, filename);
+        PDDocument document = PDDocument.load(file);
+        try
+        {
+            boolean noPNG = false;
+            boolean isDifferent = false;
+
+            PDFRenderer renderer = new PDFRenderer(document);
+            for (int i = 0, size = document.getNumberOfPages(); i < size; i++)
+            {
+                BufferedImage imageTest = renderer.renderImageWithDPI(i, 72);
+
+                // write to output file
+                File outFile = new File(outDir, (i+1) + ".png");
+                ImageIO.write(imageTest, "PNG", outFile);
+
+                // load expected png
+                File inFile = new File(inDir, (i+1) + ".png");
+                if (inFile.exists())
+                {
+                    BufferedImage imageGood = ImageIO.read(inFile);
+
+                    // compare
+                    compare: for (int y = 0; y < imageGood.getHeight(); y++)
+                    {
+                        for (int x = 0; x < imageGood.getWidth(); x++)
+                        {
+                            if (imageGood.getRGB(x, y) != imageTest.getRGB(x, y))
+                            {
+                                // save diff to file
+                                BufferedImage imageDiff = diff(imageGood, imageTest);
+                                diffDir.mkdirs();
+                                File diffFile = new File(diffDir, (i+1) + ".png");
+                                ImageIO.write(imageDiff, "PNG", diffFile);
+
+                                // keep rendering all pages
+                                isDifferent = true;
+                                break compare;
+                            }
+                        }
+                    }
+                }
+                else
+                {
+                    // no expected png, probably the first time this file has been used, so we
+                    // continue to render all pages, but we'll fail the test at the end
+                    noPNG = true;
+                }
+            }
+
+            if (noPNG)
+            {
+                // if this is the first time that this PDF file has been rendered then the test
+                // will fail, but the "out" directory will contain the result. If it is good then
+                // it can be manually added to the "png" directory and committed to SVN
+                Assert.fail("No PNG found for '" + file.getName() +
+                            "', perhaps this is a new file?");
+            }
+            else if (isDifferent)
+            {
+                // fail after all pages have been rendered
+                Assert.fail("Rendering differs in '" + file.getName() + "'");
+            }
+        }
+        finally
+        {
+            document.close();
+        }
+    }
+
+    private BufferedImage diff(BufferedImage imageGood, BufferedImage imageTest)
+    {
+        BufferedImage diff = new BufferedImage(imageGood.getWidth(), imageGood.getHeight(),
+                BufferedImage.TYPE_INT_RGB);
+
+        // convert good image to grayscale
+        ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
+        ColorConvertOp op = new ColorConvertOp(cs, null);
+        BufferedImage imageGray = op.filter(imageGood, null);
+
+        // draw good image as grayscale background
+        Graphics2D graphics = diff.createGraphics();
+        graphics.setBackground(Color.WHITE);
+        graphics.clearRect(0, 0, imageGood.getWidth(), imageGood.getHeight());
+        graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f));
+        graphics.drawImage(imageGray, null, null);
+        graphics.dispose();
+
+        // draw differing pixels in red
+        for (int y = 0; y < imageGood.getHeight(); y++)
+        {
+            for (int x = 0; x < imageGood.getWidth(); x++)
+            {
+                if (imageGood.getRGB(x, y) != imageTest.getRGB(x, y))
+                {
+                    int rgb = new Color(255, 0, 0).getRGB();
+                    diff.setRGB(x, y, rgb);
+                }
+            }
+        }
+        return diff;
+    }
+}

Modified: pdfbox/trunk/preflight/src/main/java/org/apache/pdfbox/preflight/font/container/CIDType2Container.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/preflight/src/main/java/org/apache/pdfbox/preflight/font/container/CIDType2Container.java?rev=1621554&r1=1621553&r2=1621554&view=diff
==============================================================================
--- pdfbox/trunk/preflight/src/main/java/org/apache/pdfbox/preflight/font/container/CIDType2Container.java (original)
+++ pdfbox/trunk/preflight/src/main/java/org/apache/pdfbox/preflight/font/container/CIDType2Container.java Sun Aug 31 02:24:19 2014
@@ -49,7 +49,7 @@ public class CIDType2Container extends F
         try
         {
             // if glyph exists we can check the width
-            if (this.ttf != null && this.ttf.getGlyph().getGlyphs().length > glyphIndex)
+            if (this.ttf != null && this.ttf.getMaximumProfile().getNumGlyphs() > glyphIndex)
             {
             /*
              * In a Mono space font program, the length of the AdvanceWidth array must be one. According to the TrueType