You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by le...@apache.org on 2015/06/06 18:39:48 UTC

svn commit: r1683929 - in /pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox: cos/COSDocument.java cos/COSStream.java io/ScratchFile.java io/ScratchFileBuffer.java

Author: lehmi
Date: Sat Jun  6 16:39:48 2015
New Revision: 1683929

URL: http://svn.apache.org/r1683929
Log:
PDFBOX-2301: use one scratch file instead of many as proposed by Jesse Long

Added:
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFile.java   (with props)
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFileBuffer.java   (with props)
Modified:
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDocument.java
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSStream.java

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDocument.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDocument.java?rev=1683929&r1=1683928&r2=1683929&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDocument.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDocument.java Sat Jun  6 16:39:48 2015
@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Map;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.io.ScratchFile;
 import org.apache.pdfbox.pdfparser.PDFObjectStreamParser;
 
 /**
@@ -74,10 +75,8 @@ public class COSDocument extends COSBase
     private boolean closed = false;
 
     private boolean isXRefStream;
-    
-    private final File scratchDirectory;
-    
-    private final boolean useScratchFile;
+
+    private ScratchFile scratchFile;
 
     /**
      * Constructor.
@@ -102,8 +101,17 @@ public class COSDocument extends COSBase
      */
     public COSDocument(File scratchDir, boolean useScratchFiles)
     {
-        scratchDirectory = scratchDir;
-        useScratchFile = useScratchFiles;
+        if (useScratchFiles)
+        {
+            try 
+            {
+                scratchFile = new ScratchFile(scratchDir);
+            }
+            catch (IOException e)
+            {
+                LOG.error("Can't create temp file, using memory buffer instead", e);
+            }
+        }
     }
 
     /**
@@ -121,7 +129,7 @@ public class COSDocument extends COSBase
      */
     public COSStream createCOSStream()
     {
-        return new COSStream( useScratchFile, scratchDirectory);
+        return new COSStream(scratchFile);
     }
 
     /**
@@ -133,7 +141,7 @@ public class COSDocument extends COSBase
      */
     public COSStream createCOSStream(COSDictionary dictionary)
     {
-        return new COSStream( dictionary, useScratchFile, scratchDirectory );
+        return new COSStream( dictionary, scratchFile );
     }
 
     /**
@@ -424,6 +432,11 @@ public class COSDocument extends COSBase
                     }
                 }
             }
+
+            if (scratchFile != null)
+            {
+                scratchFile.close();
+            }
             closed = true;
         }
     }

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSStream.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSStream.java?rev=1683929&r1=1683928&r2=1683929&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSStream.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSStream.java Sat Jun  6 16:39:48 2015
@@ -21,22 +21,19 @@ import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
 import org.apache.pdfbox.filter.DecodeResult;
 import org.apache.pdfbox.filter.Filter;
 import org.apache.pdfbox.filter.FilterFactory;
 import org.apache.pdfbox.io.IOUtils;
 import org.apache.pdfbox.io.RandomAccess;
 import org.apache.pdfbox.io.RandomAccessBuffer;
-import org.apache.pdfbox.io.RandomAccessFile;
 import org.apache.pdfbox.io.RandomAccessFileInputStream;
 import org.apache.pdfbox.io.RandomAccessFileOutputStream;
+import org.apache.pdfbox.io.ScratchFile;
 
 /**
  * This class represents a stream object in a PDF document.
@@ -45,11 +42,6 @@ import org.apache.pdfbox.io.RandomAccess
  */
 public class COSStream extends COSDictionary implements Closeable
 {
-    /**
-     * Log instance.
-     */
-    private static final Log LOG = LogFactory.getLog(COSStream.class);
-
     private static final int BUFFER_SIZE=16384;
 
     /**
@@ -70,11 +62,7 @@ public class COSStream extends COSDictio
     private RandomAccessFileOutputStream unFilteredStream;
     private DecodeResult decodeResult;
 
-    private File scratchFileFiltered;
-    private File scratchFileUnfiltered;
-
-    private final boolean scratchFiles;
-    private final File scratchFileDirectory;
+    private final ScratchFile scratchFile;
 
     /**
      * Constructor.  Creates a new stream with an empty dictionary.
@@ -82,7 +70,7 @@ public class COSStream extends COSDictio
      */
     public COSStream( )
     {
-        this(false, null);
+        this((ScratchFile)null);
     }
 
     /**
@@ -93,43 +81,39 @@ public class COSStream extends COSDictio
      */
     public COSStream( COSDictionary dictionary )
     {
-        this(dictionary, false, null);
+        this(dictionary, null);
     }
 
     /**
      * Constructor.  Creates a new stream with an empty dictionary.
      *
-     * @param useScratchFiles enables the usage of a scratch file if set to true
-     * @param scratchDirectory directory to be used to create the scratch file. If null java.io.temp is used instead.
+     * @param scratchFile scratch file to use.
      *
      */
-    public COSStream( boolean useScratchFiles, File scratchDirectory )
+    public COSStream( ScratchFile scratchFile )
     {
         super();
-        scratchFiles= useScratchFiles;
-        scratchFileDirectory = scratchDirectory;
+        this.scratchFile = scratchFile;
     }
 
     /**
      * Constructor.
      *
      * @param dictionary The dictionary that is associated with this stream.
-     * @param useScratchFiles enables the usage of a scratch file if set to true
-     * @param scratchDirectory directory to be used to create the scratch file. If null java.io.temp is used instead.
+     * @param scratchFile The scratch file to use.
      *
      */
-    public COSStream( COSDictionary dictionary, boolean useScratchFiles, File scratchDirectory  )
+    public COSStream( COSDictionary dictionary, ScratchFile scratchFile )
     {
         super( dictionary );
-        scratchFiles= useScratchFiles;
-        scratchFileDirectory = scratchDirectory;
+        this.scratchFile = scratchFile;
     }
 
-    private RandomAccess createBuffer(boolean filtered) throws IOException
+    private RandomAccess createBuffer() throws IOException
     {
-        if (scratchFiles)
+        if (scratchFile != null)
         {
-            return createScratchFile(filtered);
+            return scratchFile.createBuffer();
         }
         else
         {
@@ -138,33 +122,6 @@ public class COSStream extends COSDictio
     }
 
     /**
-     * Create a scratch file to be used as buffer.
-     */
-    private RandomAccess createScratchFile(boolean filtered)
-    {
-        try
-        {
-            if (filtered)
-            {
-                deleteFile(scratchFileFiltered);
-                scratchFileFiltered = File.createTempFile("PDFBox_streamf_", null, scratchFileDirectory);
-                return new RandomAccessFile(scratchFileFiltered, "rw");
-            }
-            else
-            {
-                deleteFile(scratchFileUnfiltered);
-                scratchFileUnfiltered = File.createTempFile("PDFBox_streamu_", null, scratchFileDirectory);
-                return new RandomAccessFile(scratchFileUnfiltered, "rw");
-            }
-        }
-        catch (IOException exception)
-        {
-            LOG.error("Can't create temp file, using memory buffer instead", exception);
-            return new RandomAccessBuffer();
-        }
-    }
-
-    /**
      * This will get the stream with all of the filters applied.
      *
      * @return the bytes of the physical (encoded) stream
@@ -374,7 +331,7 @@ public class COSStream extends COSDictio
         {
             if (result == null)
             {
-                result = createBuffer(false);
+                result = createBuffer();
             }
         }
         else
@@ -396,7 +353,7 @@ public class COSStream extends COSDictio
             IOUtils.closeQuietly(unFilteredStream);
             if (destBuffer == null)
             {
-                result = createBuffer(false);
+                result = createBuffer();
             }
             else
             {
@@ -468,7 +425,7 @@ public class COSStream extends COSDictio
         IOUtils.closeQuietly(filteredStream);
         if (destBuffer == null)
         {
-            result = createBuffer(true);
+            result = createBuffer();
         }
         else
         {
@@ -599,7 +556,7 @@ public class COSStream extends COSDictio
     {
         if (filteredBuffer == null)
         {
-            filteredBuffer = createBuffer(true);
+            filteredBuffer = createBuffer();
         }
         else if (clear)
         {
@@ -617,7 +574,7 @@ public class COSStream extends COSDictio
     {
         if (unfilteredBuffer == null)
         {
-            unfilteredBuffer = createBuffer(false);
+            unfilteredBuffer = createBuffer();
         }
         else if (clear)
         {
@@ -647,18 +604,5 @@ public class COSStream extends COSDictio
         {
             filteredBuffer.close();
         }
-        deleteFile(scratchFileFiltered);
-        deleteFile(scratchFileUnfiltered);
-    }
-
-    private void deleteFile(File file) throws IOException
-    {
-        if (file != null && file.exists())
-        {
-            if (!file.delete())
-            {
-                throw new IOException("Can't delete the temporary scratch file "+file.getAbsolutePath());
-            }
-        }
     }
 }

Added: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFile.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFile.java?rev=1683929&view=auto
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFile.java (added)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFile.java Sat Jun  6 16:39:48 2015
@@ -0,0 +1,130 @@
+/*
+ * 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.io;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.cos.COSStream;
+
+/**
+ * A temporary file which can hold multiple buffers of temporary data. A new temporary file is created for each new
+ * {@link ScratchFile} instance, and is deleted when the {@link ScratchFile} is closed.
+ * <p>
+ * Multiple buffers can be creating by calling the {@link #createBuffer()} method.
+ * <p>
+ * The file is split into pages, each page containing a pointer to the previous and next pages. This allows for
+ * multiple, separate streams in the same file.
+ *
+ * @author Jesse Long
+ */
+public class ScratchFile implements Closeable
+{
+    private static final Log LOG = LogFactory.getLog(COSStream.class);
+    private File file;
+    private java.io.RandomAccessFile raf;
+
+    /**
+     * Creates a new scratch file. If a {code scratchFileDirectory} is supplied, then the scratch file is created in
+     * that directory.
+     * 
+     * @param scratchFileDirectory The directory in which to create the scratch file, or {code null} if the scratch
+     * should be created in the default temporary directory.
+     * @throws IOException If there was a problem creating a temporary file.
+     */
+    public ScratchFile(File scratchFileDirectory) throws IOException
+    {
+        file = File.createTempFile("PDFBox", ".tmp", scratchFileDirectory);
+        try
+        {
+            raf = new java.io.RandomAccessFile(file, "rw");
+        }
+        catch (IOException e)
+        {
+            if (!file.delete())
+            {
+                LOG.warn("Error deleting scratch file: " + file.getAbsolutePath());
+            }
+            throw e;
+        }
+    }
+
+    /**
+     * Returns the underlying {@link java.io.RandomAccessFile}.
+     * 
+     * @return The underlying {@link java.io.RandomAccessFile}.
+     */
+    java.io.RandomAccessFile getRandomAccessFile()
+    {
+        return raf;
+    }
+
+    /**
+     * Checks if this scratch file has already been closed. If the file has been closed, an {@link IOException} is
+     * thrown.
+     * 
+     * @throws IOException If the file has already been closed.
+     */
+    void checkClosed() throws IOException
+    {
+        if (raf == null)
+        {
+            throw new IOException("Scratch file already closed");
+        }
+    }
+
+    /**
+     * Creates a new buffer in the scratch file.
+     * 
+     * @return A new buffer.
+     * @throws IOException If an error occurred.
+     */
+    public RandomAccess createBuffer() throws IOException
+    {
+        return new ScratchFileBuffer(this);
+    }
+
+    /**
+     * Closes and deletes the temporary file. No further interaction with the scratch file or associated buffers can
+     * happen after this method is called.
+     * 
+     * @throws IOException If there was a problem closing or deleting the temporary file.
+     */
+    @Override
+    public void close() throws IOException
+    {
+        if (raf != null)
+        {
+            raf.close();
+            raf = null;
+        }
+
+        if (file != null)
+        {
+            if (file.delete())
+            {
+                file = null;
+            }
+            else
+            {
+                throw new IOException("Error deleting scratch file: " + file.getAbsolutePath());
+            }
+        }
+    }
+}

Propchange: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFile.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFileBuffer.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFileBuffer.java?rev=1683929&view=auto
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFileBuffer.java (added)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFileBuffer.java Sat Jun  6 16:39:48 2015
@@ -0,0 +1,515 @@
+/*
+ * 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.io;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * A {@link RandomAccess} implemented as a doubly linked list over multiple pages in a {@link java.io.RandomAccessFile}.
+ * <p>
+ * Each page is {@link #PAGE_SIZE} bytes, with the first 8 bytes being a pointer to page index (
+ * {@code pageOffset / PAGE_SIZE}) of the previous page in the buffer, and the last 8 bytes being a pointer to the page
+ * index of the next page in the buffer.
+ * 
+ * @author Jesse Long
+ */
+class ScratchFileBuffer implements RandomAccess
+{
+    /**
+     * The size of each page.
+     */
+    private static final int PAGE_SIZE = 4096;
+    /**
+     * The underlying scratch file.
+     */
+    private ScratchFile scratchFile;
+    /**
+     * The random access file of the scratch file.
+     */
+    private RandomAccessFile raFile;
+    /**
+     * The first page in this buffer.
+     */
+    private final long firstPage;
+    /**
+     * The number of bytes of content in this buffer.
+     */
+    private long length = 0;
+    /**
+     * The index of the page in which the current position of this buffer is in.
+     */
+    private long currentPage;
+    /**
+     * The current position of the buffer as an offset in the current page.
+     */
+    private int positionInPage;
+    /**
+     * The current position in the space of the whole buffer.
+     */
+    private long positionInBuffer;
+
+    /**
+     * Creates a new buffer in the provided {@link ScratchFile}.
+     * 
+     * @param scratchFile The {@link ScratchFile} in which to create the new buffer.
+     * @throws IOException If there was an error writing to the file.
+     */
+    ScratchFileBuffer(ScratchFile scratchFile) throws IOException
+    {
+        scratchFile.checkClosed();
+
+        this.scratchFile = scratchFile;
+
+        raFile = scratchFile.getRandomAccessFile();
+
+        /*
+         * We must allocate a new first page for each new buffer, in case multiple buffers are created at the same time,
+         * and use the same space.
+         */
+        firstPage = createNewPage();
+
+        /*
+         * Mark the first page back pointer to -1 to indicate start of buffer.
+         */
+        raFile.seek(firstPage * PAGE_SIZE);
+        raFile.writeLong(-1L);
+
+        /*
+         * Reset variables to beginning of empty buffer.
+         */
+        clear();
+    }
+
+    /**
+     * Checks if this buffer, or the underlying {@link ScratchFile} have been closed, throwing {@link IOException} if
+     * so.
+     * 
+     * @throws IOException If either this buffer, or the underlying {@link ScratchFile} have been closed.
+     */
+    private void checkClosed() throws IOException
+    {
+        if (scratchFile == null)
+        {
+            throw new IOException("Scratch file buffer already closed");
+        }
+        scratchFile.checkClosed();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long length() throws IOException
+    {
+        checkClosed();
+        return length;
+    }
+
+    /**
+     * Allocates a new page, and links the current and the new page.
+     * 
+     * @throws IOException If there was an error writing to the file.
+     */
+    private void growToNewPage() throws IOException
+    {
+        long newPage = createNewPage();
+
+        /*
+         * We should only grow to a new page when previous pages are full. If not, links won't work.
+         */
+        if (positionInPage != PAGE_SIZE - 8)
+        {
+            throw new IOException("Corruption detected in scratch file");
+        }
+        seekToCurrentPositionInFile();
+        raFile.writeLong(newPage);
+        
+        long previousPage = currentPage;
+        currentPage = newPage;
+        positionInPage = 0;
+        /*
+         * write back link to previous page.
+         */
+        seekToCurrentPositionInFile();
+        raFile.writeLong(previousPage);
+        positionInPage = 8;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void write(int b) throws IOException
+    {
+        checkClosed();
+        seekToCurrentPositionInFile();
+        if (positionInPage == PAGE_SIZE - 8)
+        {
+            growToNewPage();
+        }
+
+        raFile.write(b);
+
+        positionInPage++;
+        positionInBuffer++;
+        if (positionInBuffer > length)
+        {
+            length = positionInBuffer;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException
+    {
+        checkClosed();
+
+        seekToCurrentPositionInFile();
+
+        while (len > 0)
+        {
+            if (positionInPage == PAGE_SIZE - 8)
+            {
+                growToNewPage();
+            }
+
+            int availableSpaceInCurrentPage = (PAGE_SIZE - 8) - positionInPage;
+
+            int bytesToWrite = Math.min(len, availableSpaceInCurrentPage);
+
+            raFile.write(b, off, bytesToWrite);
+
+            off += bytesToWrite;
+            len -= bytesToWrite;
+            positionInPage += bytesToWrite;
+            positionInBuffer += bytesToWrite;
+            if (positionInBuffer > length)
+            {
+                length = positionInBuffer;
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final void clear() throws IOException
+    {
+        checkClosed();
+        length = 0;
+        currentPage = firstPage;
+        positionInBuffer = 0;
+        positionInPage = 8;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long getPosition() throws IOException
+    {
+        checkClosed();
+        return positionInBuffer;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void seek(long seekToPosition) throws IOException
+    {
+        checkClosed();
+
+        /*
+         * Can't seek past end of file. If you want to change implementation, seek to end of file, write zero bytes for
+         * remaining seek distance.
+         */
+        if (seekToPosition > length)
+        {
+            throw new EOFException();
+        }
+
+        if (seekToPosition < positionInBuffer)
+        {
+            if (currentPage != firstPage && seekToPosition < (positionInBuffer / 2))
+            {
+                /*
+                 * If we are seeking backwards, and the seek to position is closer to the beginning of the buffer than
+                 * our current position, just go to the start of the buffer and seek forward from there. Recurse exactly
+                 * once.
+                 */
+                currentPage = firstPage;
+                positionInPage = 8;
+                positionInBuffer = 0;
+                seek(seekToPosition);
+            }
+            else
+            {
+                while (positionInBuffer - seekToPosition > positionInPage - 8)
+                {
+                    raFile.seek(currentPage * PAGE_SIZE);
+                    long previousPage = raFile.readLong();
+                    currentPage = previousPage;
+                    positionInBuffer -= (positionInPage - 8);
+                    positionInPage = PAGE_SIZE - 8;
+                }
+
+                positionInPage -= (positionInBuffer - seekToPosition);
+                positionInBuffer = seekToPosition;
+            }
+        }
+        else
+        {
+            while (seekToPosition - positionInBuffer > (PAGE_SIZE - 8) - positionInPage)
+            {
+                // seek to 8 bytes from end of current page, to read next page pointer.
+                raFile.seek(((currentPage + 1) * PAGE_SIZE) - 8);
+                long nextPage = raFile.readLong();
+                positionInBuffer += (PAGE_SIZE - 8) - positionInPage;
+                currentPage = nextPage;
+                positionInPage = 8;
+            }
+
+            positionInPage += seekToPosition - positionInBuffer;
+            positionInBuffer = seekToPosition;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isClosed()
+    {
+        return scratchFile == null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int peek() throws IOException
+    {
+        int result = read();
+        if (result != -1)
+        {
+            rewind(1);
+        }
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void rewind(int bytes) throws IOException
+    {
+        seek(positionInBuffer - bytes);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public byte[] readFully(int len) throws IOException
+    {
+        byte[] b = new byte[len];
+
+        int n = 0;
+        do
+        {
+            int count = read(b, n, len - n);
+            if (count < 0)
+            {
+                throw new EOFException();
+            }
+            n += count;
+        } while (n < len);
+
+        return b;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isEOF() throws IOException
+    {
+        checkClosed();
+        return positionInBuffer == length;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int available() throws IOException
+    {
+        checkClosed();
+        return (int) Math.min(length - positionInBuffer, Integer.MAX_VALUE);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int read() throws IOException
+    {
+        checkClosed();
+
+        if (positionInBuffer >= length)
+        {
+            return -1;
+        }
+
+        seekToCurrentPositionInFile();
+
+        if (positionInPage == PAGE_SIZE - 8)
+        {
+            currentPage = raFile.readLong();
+            positionInPage = 8;
+            seekToCurrentPositionInFile();
+        }
+
+        int retv = raFile.read();
+
+        if (retv >= 0)
+        {
+            positionInPage++;
+            positionInBuffer++;
+        }
+
+        return retv;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int read(byte[] b) throws IOException
+    {
+        return read(b, 0, b.length);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException
+    {
+        checkClosed();
+
+        if (positionInBuffer >= length)
+        {
+            return -1;
+        }
+
+        seekToCurrentPositionInFile();
+
+        if (positionInPage == PAGE_SIZE - 8)
+        {
+            currentPage = raFile.readLong();
+            positionInPage = 8;
+            seekToCurrentPositionInFile();
+        }
+
+        len = (int) Math.min(len, length - positionInBuffer);
+
+        int totalBytesRead = 0;
+
+        while (len > 0)
+        {
+            int availableInThisPage = (PAGE_SIZE - 8) - positionInPage;
+
+            int rdbytes = raFile.read(b, off, Math.min(len, availableInThisPage));
+
+            if (rdbytes < 0)
+            {
+                throw new IOException("EOF reached before end of scratch file stream");
+            }
+
+            if (rdbytes == availableInThisPage)
+            {
+                currentPage = raFile.readLong();
+                positionInPage = 8;
+                seekToCurrentPositionInFile();
+            }
+            else
+            {
+                positionInPage += rdbytes;
+            }
+
+            totalBytesRead += rdbytes;
+            positionInBuffer += rdbytes;
+            off += rdbytes;
+            len -= rdbytes;
+        }
+
+        return totalBytesRead;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void close() throws IOException
+    {
+        scratchFile = null;
+        raFile = null;
+    }
+
+    /**
+     * Positions the underlying {@link java.io.RandomAccessFile} to the correct position for use by this buffer.
+     * 
+     * @throws IOException If there was a problem seeking in the {@link java.io.RandomAccessFile}.
+     */
+    private void seekToCurrentPositionInFile() throws IOException
+    {
+        long positionInFile = (currentPage * PAGE_SIZE) + positionInPage;
+        if (raFile.getFilePointer() != positionInFile)
+        {
+            raFile.seek(positionInFile);
+        }
+    }
+
+    /**
+     * Allocates a new page in the temporary file by growing the file, returning the page index of the new page.
+     * 
+     * @return The index of the new page.
+     * @throws IOException If there was an error growing the file.
+     */
+    private long createNewPage() throws IOException
+    {
+        long fileLen = raFile.length();
+
+        fileLen += PAGE_SIZE;
+
+        if (fileLen % PAGE_SIZE > 0)
+        {
+            fileLen += PAGE_SIZE - (fileLen % PAGE_SIZE);
+        }
+
+        raFile.setLength(fileLen);
+
+        return (fileLen / PAGE_SIZE) - 1;
+    }
+}

Propchange: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/ScratchFileBuffer.java
------------------------------------------------------------------------------
    svn:eol-style = native