Modernise PngPredictor and refactor LzwFilter and FlateFilter to reduce memory allocation

This commit is contained in:
BobLd
2025-05-29 18:45:52 +01:00
parent f84f2aceec
commit 4bdb85d1ff
4 changed files with 381 additions and 804 deletions

View File

@@ -40,18 +40,10 @@
try
{
var decompressed = Decompress(input);
if (predictor == -1)
{
return decompressed;
}
var colors = Math.Min(parameters.GetIntOrDefault(NameToken.Colors, DefaultColors), 32);
var bitsPerComponent = parameters.GetIntOrDefault(NameToken.BitsPerComponent, DefaultBitsPerComponent);
var columns = parameters.GetIntOrDefault(NameToken.Columns, DefaultColumns);
return PngPredictor.Decode(decompressed, predictor, colors, bitsPerComponent, columns);
return Decompress(input, predictor, colors, bitsPerComponent, columns);
}
catch
{
@@ -61,10 +53,9 @@
return input;
}
private static byte[] Decompress(Memory<byte> input)
private static Memory<byte> Decompress(Memory<byte> input, int predictor, int colors, int bitsPerComponent, int columns)
{
using (var memoryStream = MemoryHelper.AsReadOnlyMemoryStream(input))
using (var output = new MemoryStream())
{
// The first 2 bytes are the header which DeflateStream does not support.
memoryStream.ReadByte();
@@ -73,8 +64,17 @@
try
{
using (var deflate = new DeflateStream(memoryStream, CompressionMode.Decompress))
using (var output = new MemoryStream((int)(input.Length * 1.5)))
using (var f = PngPredictor.WrapPredictor(output, predictor, colors, bitsPerComponent, columns))
{
deflate.CopyTo(output);
deflate.CopyTo(f);
f.Flush();
if (output.TryGetBuffer(out var segment))
{
return segment.AsMemory();
}
return output.ToArray();
}
}

View File

@@ -5,6 +5,7 @@ namespace UglyToad.PdfPig.Filters
using System;
using System.Collections.Generic;
using Lzw;
using System.IO;
using Tokens;
using Util;
@@ -37,57 +38,63 @@ namespace UglyToad.PdfPig.Filters
var earlyChange = parameters.GetIntOrDefault(NameToken.EarlyChange, 1);
if (predictor > 1)
{
var decompressed = Decode(input.Span, earlyChange == 1);
var colors = Math.Min(parameters.GetIntOrDefault(NameToken.Colors, DefaultColors), 32);
var bitsPerComponent = parameters.GetIntOrDefault(NameToken.BitsPerComponent, DefaultBitsPerComponent);
var columns = parameters.GetIntOrDefault(NameToken.Columns, DefaultColumns);
var colors = Math.Min(parameters.GetIntOrDefault(NameToken.Colors, DefaultColors), 32);
var bitsPerComponent = parameters.GetIntOrDefault(NameToken.BitsPerComponent, DefaultBitsPerComponent);
var columns = parameters.GetIntOrDefault(NameToken.Columns, DefaultColumns);
return PngPredictor.Decode(decompressed, predictor, colors, bitsPerComponent, columns);
}
return Decode(input.Span, earlyChange == 1);
return Decode(input.Span, earlyChange == 1, predictor, colors, bitsPerComponent, columns);
}
private static byte[] Decode(ReadOnlySpan<byte> input, bool isEarlyChange)
private static Memory<byte> Decode(ReadOnlySpan<byte> input, bool isEarlyChange, int predictor, int colors, int bitsPerComponent, int columns)
{
// A guess.
var result = new List<byte>((int)(input.Length * 1.5));
var table = GetDefaultTable();
var codeBits = 9;
var data = new BitStream(input);
var codeOffset = isEarlyChange ? 0 : 1;
var previous = -1;
while (true)
using (var output = new MemoryStream((int)(input.Length * 1.5))) // A guess.
using (var result = PngPredictor.WrapPredictor(output, predictor, colors, bitsPerComponent, columns))
{
var next = data.Get(codeBits);
var table = GetDefaultTable();
if (next == EodMarker)
{
break;
}
if (next == ClearTable)
{
table = GetDefaultTable();
previous = -1;
codeBits = 9;
continue;
}
var codeBits = 9;
if (table.TryGetValue(next, out var b))
{
result.AddRange(b);
var data = new BitStream(input);
if (previous >= 0)
var codeOffset = isEarlyChange ? 0 : 1;
var previous = -1;
while (true)
{
var next = data.Get(codeBits);
if (next == EodMarker)
{
break;
}
if (next == ClearTable)
{
table = GetDefaultTable();
previous = -1;
codeBits = 9;
continue;
}
if (table.TryGetValue(next, out var b))
{
result.Write(b,0, b.Length);
if (previous >= 0)
{
var lastSequence = table[previous];
var newSequence = new byte[lastSequence.Length + 1];
Array.Copy(lastSequence, newSequence, lastSequence.Length);
newSequence[lastSequence.Length] = b[0];
table[table.Count] = newSequence;
}
}
else
{
var lastSequence = table[previous];
@@ -95,47 +102,42 @@ namespace UglyToad.PdfPig.Filters
Array.Copy(lastSequence, newSequence, lastSequence.Length);
newSequence[lastSequence.Length] = b[0];
newSequence[lastSequence.Length] = lastSequence[0];
result.Write(newSequence, 0, newSequence.Length);
table[table.Count] = newSequence;
}
previous = next;
if (table.Count >= ElevenBitBoundary + codeOffset)
{
codeBits = 12;
}
else if (table.Count >= TenBitBoundary + codeOffset)
{
codeBits = 11;
}
else if (table.Count >= NineBitBoundary + codeOffset)
{
codeBits = 10;
}
else
{
codeBits = 9;
}
}
else
result.Flush();
if (output.TryGetBuffer(out var segment))
{
var lastSequence = table[previous];
var newSequence = new byte[lastSequence.Length + 1];
Array.Copy(lastSequence, newSequence, lastSequence.Length);
newSequence[lastSequence.Length] = lastSequence[0];
result.AddRange(newSequence);
table[table.Count] = newSequence;
return segment.AsMemory();
}
previous = next;
if (table.Count >= ElevenBitBoundary + codeOffset)
{
codeBits = 12;
}
else if (table.Count >= TenBitBoundary + codeOffset)
{
codeBits = 11;
}
else if (table.Count >= NineBitBoundary + codeOffset)
{
codeBits = 10;
}
else
{
codeBits = 9;
}
return output.ToArray();
}
return result.ToArray();
}
private static Dictionary<int, byte[]> GetDefaultTable()

View File

@@ -1,225 +1,198 @@
namespace UglyToad.PdfPig.Filters
{
using System;
using System.Collections.Generic;
using System.IO;
using IO;
// https://github.com/apache/pdfbox/blob/a53a70db16ea3133994120bcf1e216b9e760c05b/pdfbox/src/main/java/org/apache/pdfbox/filter/Predictor.java#L30
/// <summary>
/// Helper class to contain predictor decoding used by Flate and LZW filter.
/// </summary>
internal static class PngPredictor
{
public static byte[] Decode(byte[] inputBytes, int predictor, int colors, int bitsPerComponent, int columns)
/// <summary>
/// Decodes a single line of data in-place.
/// </summary>
/// <param name="predictor">Predictor value for the current line.</param>
/// <param name="colors">Number of color components, from decode parameters.</param>
/// <param name="bitsPerComponent">Number of bits per components, from decode parameters.</param>
/// <param name="columns">Number samples in a row, from decode parameters.</param>
/// <param name="actline">Current (active) line to decode. Data will be decoded in-place, i.e. - the contents of this buffer will be modified.</param>
/// <param name="lastline">The previous decoded line. When decoding the first line, this parameter should be an empty byte array of the same length as <c>actline</c>.</param>
public static void DecodePredictorRow(int predictor, int colors, int bitsPerComponent, int columns, byte[] actline, byte[] lastline)
{
if (inputBytes is null)
{
throw new ArgumentNullException(nameof(inputBytes));
}
if (predictor == 1)
{
return inputBytes;
// no prediction
return;
}
int bitsPerPixel = colors * bitsPerComponent;
int bytesPerPixel = (bitsPerPixel + 7) / 8;
int rowlength = (columns * bitsPerPixel + 7) / 8;
var actline = new byte[rowlength];
var lastline = new byte[rowlength];
int rowLength = actline.Length;
int linepredictor = predictor;
var result = new List<byte>();
using (var memoryStream = new MemoryStream())
using (var output = new BinaryWriter(memoryStream))
using (var input = new RandomAccessBuffer(inputBytes))
switch (predictor)
{
while (input.Available() > 0)
{
// test for PNG predictor; each value >= 10 (not only 15) indicates usage of PNG predictor
if (predictor >= 10)
case 2:
// PRED TIFF SUB
if (bitsPerComponent == 8)
{
// PNG predictor; each row starts with predictor type (0, 1, 2, 3, 4)
// read per line predictor
linepredictor = input.Read();
if (linepredictor == -1)
// for 8 bits per component it is the same algorithm as PRED SUB of PNG format
for (int p = bytesPerPixel; p < rowLength; p++)
{
return result.ToArray();
int sub = actline[p] & 0xff;
int left = actline[p - bytesPerPixel] & 0xff;
actline[p] = (byte)(sub + left);
}
// add 10 to tread value 0 as 10, 1 as 11, ...
linepredictor += 10;
}
// read line
int i;
int offset = 0;
while (offset < rowlength && ((i = input.Read(actline, offset, rowlength - offset)) != -1))
else if (bitsPerComponent == 16)
{
if (i == 0)
for (int p = bytesPerPixel; p < rowLength - 1; p += 2)
{
// TODO: #291, this indicates a bug in reading logic.
// This only avoids the infinite loop it does not fix the logic bug.
break;
int sub = ((actline[p] & 0xff) << 8) + (actline[p + 1] & 0xff);
int left = ((actline[p - bytesPerPixel] & 0xff) << 8) + (actline[p - bytesPerPixel + 1] & 0xff);
int sum = sub + left;
actline[p] = (byte)((sum >> 8) & 0xff);
actline[p + 1] = (byte)(sum & 0xff);
}
offset += i;
}
// do prediction as specified input PNG-Specification 1.2
switch (linepredictor)
else if (bitsPerComponent == 1 && colors == 1)
{
case 2:
// PRED TIFF SUB
if (bitsPerComponent == 8)
{
// for 8 bits per component it is the same algorithm as PRED SUB of PNG format
for (int p = bytesPerPixel; p < rowlength; p++)
{
int sub = actline[p] & 0xff;
int left = actline[p - bytesPerPixel] & 0xff;
actline[p] = (byte)(sub + left);
}
break;
}
if (bitsPerComponent == 16)
{
for (int p = bytesPerPixel; p < rowlength; p += 2)
{
int sub = ((actline[p] & 0xff) << 8) + (actline[p + 1] & 0xff);
int left = (((actline[p - bytesPerPixel] & 0xff) << 8)
+ (actline[p - bytesPerPixel + 1] & 0xff));
actline[p] = (byte)(((sub + left) >> 8) & 0xff);
actline[p + 1] = (byte)((sub + left) & 0xff);
}
break;
}
if (bitsPerComponent == 1 && colors == 1)
{
// bytesPerPixel cannot be used:
// "A row shall occupy a whole number of bytes, rounded up if necessary.
// Samples and their components shall be packed into bytes
// from high-order to low-order bits."
for (int p = 0; p < rowlength; p++)
{
for (int bit = 7; bit >= 0; --bit)
{
int sub = (actline[p] >> bit) & 1;
if (p == 0 && bit == 7)
{
continue;
}
int left;
if (bit == 7)
{
// use bit #0 from previous byte
left = actline[p - 1] & 1;
}
else
{
// use "previous" bit
left = (actline[p] >> (bit + 1)) & 1;
}
if (((sub + left) & 1) == 0)
{
// reset bit
actline[p] = (byte)(actline[p] & ~(1 << bit));
}
else
{
// set bit
actline[p] = (byte)(actline[p] | (1 << bit));
}
}
}
break;
}
// everything else, i.e. bpc 2 and 4, but has been tested for bpc 1 and 8 too
int elements = columns * colors;
for (int p = colors; p < elements; ++p)
{
int bytePosSub = p * bitsPerComponent / 8;
int bitPosSub = 8 - p * bitsPerComponent % 8 - bitsPerComponent;
int bytePosLeft = (p - colors) * bitsPerComponent / 8;
int bitPosLeft = 8 - (p - colors) * bitsPerComponent % 8 - bitsPerComponent;
// bytesPerPixel cannot be used:
// "A row shall occupy a whole number of bytes, rounded up if necessary.
// Samples and their components shall be packed into bytes
// from high-order to low-order bits."
int sub = GetBitSeq(actline[bytePosSub], bitPosSub, bitsPerComponent);
int left = GetBitSeq(actline[bytePosLeft], bitPosLeft, bitsPerComponent);
actline[bytePosSub] = (byte)CalcSetBitSeq(actline[bytePosSub], bitPosSub,
bitsPerComponent,
sub + left);
}
break;
case 10:
// PRED NONE
// do nothing
break;
case 11:
// PRED SUB
for (int p = bytesPerPixel; p < rowlength; p++)
for (int p = 0; p < rowLength; p++)
{
for (int bit = 7; bit >= 0; --bit)
{
int sub = actline[p];
int left = actline[p - bytesPerPixel];
actline[p] = (byte)(sub + left);
}
break;
case 12:
// PRED UP
for (int p = 0; p < rowlength; p++)
{
int up = actline[p] & 0xff;
int prior = lastline[p] & 0xff;
actline[p] = (byte)((up + prior) & 0xff);
}
break;
case 13:
// PRED AVG
for (int p = 0; p < rowlength; p++)
{
int avg = actline[p] & 0xff;
int left = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0;
int up = lastline[p] & 0xff;
actline[p] = (byte)((avg + (left + up) / 2) & 0xff);
}
break;
case 14:
// PRED PAETH
for (int p = 0; p < rowlength; p++)
{
int paeth = actline[p] & 0xff;
int a = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0; // left
int b = lastline[p] & 0xff; // upper
int c = p - bytesPerPixel >= 0 ? lastline[p - bytesPerPixel] & 0xff : 0; // upperleft
int value = a + b - c;
int absa = Math.Abs(value - a);
int absb = Math.Abs(value - b);
int absc = Math.Abs(value - c);
if (absa <= absb && absa <= absc)
int sub = (actline[p] >> bit) & 1;
if (p == 0 && bit == 7)
{
actline[p] = (byte)((paeth + a) & 0xff);
continue;
}
else if (absb <= absc)
int left;
if (bit == 7)
{
actline[p] = (byte)((paeth + b) & 0xff);
// use bit #0 from previous byte
left = actline[p - 1] & 1;
}
else
{
actline[p] = (byte)((paeth + c) & 0xff);
// use "previous" bit
left = (actline[p] >> (bit + 1)) & 1;
}
if (((sub + left) & 1) == 0)
{
// reset bit
actline[p] &= (byte)~(1 << bit);
}
else
{
// set bit
actline[p] |= (byte)(1 << bit);
}
}
break;
}
}
Array.Copy(actline, 0, lastline, 0, rowlength);
output.Write(actline);
}
else
{
// everything else, i.e. bpc 2 and 4, but has been tested for bpc 1 and 8 too
int elements = columns * colors;
for (int p = colors; p < elements; ++p)
{
int bytePosSub = p * bitsPerComponent / 8;
int bitPosSub = 8 - p * bitsPerComponent % 8 - bitsPerComponent;
int bytePosLeft = (p - colors) * bitsPerComponent / 8;
int bitPosLeft = 8 - (p - colors) * bitsPerComponent % 8 - bitsPerComponent;
int sub = GetBitSeq(actline[bytePosSub], bitPosSub, bitsPerComponent);
int left = GetBitSeq(actline[bytePosLeft], bitPosLeft, bitsPerComponent);
actline[bytePosSub] = (byte)CalcSetBitSeq(actline[bytePosSub], bitPosSub, bitsPerComponent, sub + left);
}
}
break;
return memoryStream.ToArray();
case 10:
// PRED NONE
// do nothing
break;
case 11:
// PRED SUB
for (int p = bytesPerPixel; p < rowLength; p++)
{
int sub = actline[p];
int left = actline[p - bytesPerPixel];
actline[p] = (byte)(sub + left);
}
break;
case 12:
// PRED UP
for (int p = 0; p < rowLength; p++)
{
int up = actline[p] & 0xff;
int prior = lastline[p] & 0xff;
actline[p] = (byte)((up + prior) & 0xff);
}
break;
case 13:
// PRED AVG
for (int p = 0; p < rowLength; p++)
{
int avg = actline[p] & 0xff;
int left = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0;
int up = lastline[p] & 0xff;
actline[p] = (byte)((avg + (left + up) / 2) & 0xff);
}
break;
case 14:
// PRED PAETH
for (int p = 0; p < rowLength; p++)
{
int paeth = actline[p] & 0xff;
int a = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0;
int b = lastline[p] & 0xff;
int c = p - bytesPerPixel >= 0 ? lastline[p - bytesPerPixel] & 0xff : 0;
int value = a + b - c;
int absa = Math.Abs(value - a);
int absb = Math.Abs(value - b);
int absc = Math.Abs(value - c);
if (absa <= absb && absa <= absc)
{
actline[p] = (byte)((paeth + a) & 0xff);
}
else if (absb <= absc)
{
actline[p] = (byte)((paeth + b) & 0xff);
}
else
{
actline[p] = (byte)((paeth + c) & 0xff);
}
}
break;
default:
break;
}
}
public static int CalculateRowLength(int colors, int bitsPerComponent, int columns)
{
int bitsPerPixel = colors * bitsPerComponent;
return (columns * bitsPerPixel + 7) / 8;
}
/// <summary>
/// GetLongOrDefault value from bit interval from a byte
/// Get value from bit interval from a byte.
/// </summary>
private static int GetBitSeq(int by, int startBit, int bitSize)
{
@@ -228,8 +201,13 @@
}
/// <summary>
/// Set value input a bit interval and return that value
/// Set value in a bit interval and return that value.
/// </summary>
/// <param name="by"></param>
/// <param name="startBit"></param>
/// <param name="bitSize"></param>
/// <param name="val"></param>
/// <returns></returns>
private static int CalcSetBitSeq(int by, int startBit, int bitSize, int val)
{
int mask = ((1 << bitSize) - 1);
@@ -237,5 +215,136 @@
mask = ~(mask << startBit);
return (by & mask) | (truncatedVal << startBit);
}
/// <summary>
/// Wraps a <see cref="Stream"/> in a predictor decoding stream as necessary.
/// <para>If no predictor is specified by the parameters, the original stream is returned as is.</para>
/// </summary>
/// <param name="outStream">The stream to which decoded data should be written.</param>
/// <param name="predictor"></param>
/// <param name="colors"></param>
/// <param name="bitsPerComponent"></param>
/// <param name="columns"></param>
/// <returns>A <see cref="Stream"/> is returned, which will write decoded data
/// into the given stream. If no predictor is specified, the original stream is returned.</returns>
public static Stream WrapPredictor(Stream outStream, int predictor, int colors, int bitsPerComponent, int columns)
{
if (predictor > 1)
{
return new PredictorOutputStream(outStream, predictor, colors, bitsPerComponent, columns);
}
return outStream;
}
/**
* Output stream that implements predictor decoding. Data is buffered until a complete
* row is available, which is then decoded and written to the underlying stream.
* The previous row is retained for decoding the next row.
*/
private sealed class PredictorOutputStream : Stream
{
private readonly Stream _baseStream;
private int _predictor;
private readonly int _colors;
private readonly int _bitsPerComponent;
private readonly int _columns;
private readonly int _rowLength;
private readonly bool _predictorPerRow;
private byte[] _currentRow;
private byte[] _lastRow;
private int _currentRowData = 0;
private bool _predictorRead = false;
public PredictorOutputStream(Stream baseStream, int predictor, int colors, int bitsPerComponent, int columns)
{
_baseStream = baseStream;
_predictor = predictor;
_colors = colors;
_bitsPerComponent = bitsPerComponent;
_columns = columns;
_rowLength = CalculateRowLength(colors, bitsPerComponent, columns);
_predictorPerRow = predictor >= 10;
_currentRow = new byte[_rowLength];
_lastRow = new byte[_rowLength];
}
public override void Write(byte[] buffer, int offset, int count)
{
int currentOffset = offset;
int maxOffset = currentOffset + count;
while (currentOffset < maxOffset)
{
if (_predictorPerRow && _currentRowData == 0 && !_predictorRead)
{
// PNG predictor; each row starts with predictor type (0, 1, 2, 3, 4)
// read per line predictor, add 10 to tread value 0 as 10, 1 as 11, ...
_predictor = buffer[currentOffset] + 10;
currentOffset++;
_predictorRead = true;
}
else
{
int toRead = Math.Min(_rowLength - _currentRowData, maxOffset - currentOffset);
Array.Copy(buffer, currentOffset, _currentRow, _currentRowData, toRead);
_currentRowData += toRead;
currentOffset += toRead;
// current row is filled, decode it, write it to underlying stream,
// and reset the state.
if (_currentRowData == _currentRow.Length)
{
DecodeAndWriteRow();
}
}
}
}
private void DecodeAndWriteRow()
{
DecodePredictorRow(_predictor, _colors, _bitsPerComponent, _columns, _currentRow, _lastRow);
_baseStream.Write(_currentRow, 0, _currentRow.Length);
FlipRows();
}
/**
* Flips the row buffers (to avoid copying), and resets the current-row index
* and predictorRead flag
*/
private void FlipRows()
{
(_lastRow, _currentRow) = (_currentRow, _lastRow);
_currentRowData = 0;
_predictorRead = false;
}
public override void Flush()
{
// The last row is allowed to be incomplete, and should be completed with zeros.
if (_currentRowData > 0)
{
// public static void fill(int[] a, int fromIndex, int toIndex, int val)
// Arrays.fill(currentRow, currentRowData, rowLength, (byte)0);
_currentRow.AsSpan(_currentRowData, _rowLength - _currentRowData).Fill(byte.MinValue);
DecodeAndWriteRow();
}
_baseStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException("Read not supported");
}
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void WriteByte(byte value) => throw new NotSupportedException("Not supported");
}
}
}

View File

@@ -1,534 +0,0 @@
#nullable disable
namespace UglyToad.PdfPig.IO
{
using System;
using System.Collections.Generic;
using System.IO;
internal class RandomAccessBuffer : IDisposable
{
// default chunk size is 1kb
private static readonly int DefaultChunkSize = 1024;
// use the default chunk size
private int chunkSize = DefaultChunkSize;
// list containing all chunks
private List<byte[]> bufferList;
// current chunk
private byte[] currentBuffer;
// current pointer to the whole buffer
private long pointer;
// current pointer for the current chunk
private int currentBufferPointer;
// size of the whole buffer
private long size;
// current chunk list index
private int bufferListIndex;
// maximum chunk list index
private int bufferListMaxIndex;
/**
* Default constructor.
*/
public RandomAccessBuffer() : this(DefaultChunkSize)
{
}
/**
* Default constructor.
*/
private RandomAccessBuffer(int definedChunkSize)
{
// starting with one chunk
bufferList = new List<byte[]>();
chunkSize = definedChunkSize;
currentBuffer = new byte[chunkSize];
bufferList.Add(currentBuffer);
pointer = 0;
currentBufferPointer = 0;
size = 0;
bufferListIndex = 0;
bufferListMaxIndex = 0;
}
/**
* Create a random access buffer using the given byte array.
*
* @param input the byte array to be read
*/
public RandomAccessBuffer(byte[] input)
{
// this is a special case. The given byte array is used as the one
// and only chunk.
bufferList = new List<byte[]>(1);
chunkSize = input.Length;
currentBuffer = input;
bufferList.Add(currentBuffer);
pointer = 0;
currentBufferPointer = 0;
size = chunkSize;
bufferListIndex = 0;
bufferListMaxIndex = 0;
}
/**
* Create a random access buffer of the given input stream by copying the data.
*
* @param input the input stream to be read
* @throws IOException if something went wrong while copying the data
*/
public RandomAccessBuffer(BinaryReader input)
{
//this();
byte[]
byteBuffer = new byte[8192];
int bytesRead = 0;
while ((bytesRead = input.Read(byteBuffer, 0, 8192)) > -1)
{
write(byteBuffer, 0, bytesRead);
}
Seek(0);
}
public RandomAccessBuffer Clone()
{
RandomAccessBuffer copy = new RandomAccessBuffer(chunkSize)
{
bufferList = new List<byte[]>(bufferList.Count)
};
foreach (byte[] buffer in bufferList)
{
byte[] newBuffer = new byte[buffer.Length];
Array.Copy(buffer, 0, newBuffer, 0, buffer.Length);
copy.bufferList.Add(newBuffer);
}
if (currentBuffer != null)
{
copy.currentBuffer = copy.bufferList[copy.bufferList.Count - 1];
}
else
{
copy.currentBuffer = null;
}
copy.pointer = pointer;
copy.currentBufferPointer = currentBufferPointer;
copy.size = size;
copy.bufferListIndex = bufferListIndex;
copy.bufferListMaxIndex = bufferListMaxIndex;
return copy;
}
/**
* {@inheritDoc}
*/
public void Dispose()
{
currentBuffer = null;
bufferList.Clear();
pointer = 0;
currentBufferPointer = 0;
size = 0;
bufferListIndex = 0;
}
/**
* {@inheritDoc}
*/
public void clear()
{
bufferList.Clear();
currentBuffer = new byte[chunkSize];
bufferList.Add(currentBuffer);
pointer = 0;
currentBufferPointer = 0;
size = 0;
bufferListIndex = 0;
bufferListMaxIndex = 0;
}
/**
* {@inheritDoc}
*/
public void Seek(long position)
{
CheckClosed();
if (position < 0)
{
throw new InvalidOperationException($"Invalid position {position}");
}
pointer = position;
if (pointer < size)
{
// calculate the chunk list index
bufferListIndex = (int) (pointer / chunkSize);
currentBufferPointer = (int) (pointer % chunkSize);
currentBuffer = bufferList[bufferListIndex];
}
else
{
// it is allowed to jump beyond the end of the file
// jump to the end of the buffer
bufferListIndex = bufferListMaxIndex;
currentBuffer = bufferList[bufferListIndex];
currentBufferPointer = (int) (size % chunkSize);
}
}
/**
* {@inheritDoc}
*/
public long GetPosition()
{
CheckClosed();
return pointer;
}
/**
* {@inheritDoc}
*/
public int Read()
{
CheckClosed();
if (pointer >= this.size)
{
return -1;
}
if (currentBufferPointer >= chunkSize)
{
if (bufferListIndex >= bufferListMaxIndex)
{
return -1;
}
else
{
currentBuffer = bufferList[++bufferListIndex];
currentBufferPointer = 0;
}
}
pointer++;
return currentBuffer[currentBufferPointer++] & 0xff;
}
/**
* {@inheritDoc}
*/
public int Read(byte[] b, int offset, int length)
{
CheckClosed();
if (pointer >= size)
{
return 0;
}
int bytesRead = ReadRemainingBytes(b, offset, length);
while (bytesRead < length && Available() > 0)
{
bytesRead += ReadRemainingBytes(b, offset + bytesRead, length - bytesRead);
if (currentBufferPointer == chunkSize)
{
NextBuffer();
}
}
return bytesRead;
}
private int ReadRemainingBytes(byte[] b, int offset, int length)
{
if (pointer >= size)
{
return 0;
}
int maxLength = (int) Math.Min(length, size - pointer);
int remainingBytes = chunkSize - currentBufferPointer;
// no more bytes left
if (remainingBytes == 0)
{
return 0;
}
if (maxLength >= remainingBytes)
{
// copy the remaining bytes from the current buffer
Array.Copy(currentBuffer, currentBufferPointer, b, offset, remainingBytes);
// end of file reached
currentBufferPointer += remainingBytes;
pointer += remainingBytes;
return remainingBytes;
}
else
{
// copy the remaining bytes from the whole buffer
Array.Copy(currentBuffer, currentBufferPointer, b, offset, maxLength);
// end of file reached
currentBufferPointer += maxLength;
pointer += maxLength;
return maxLength;
}
}
/**
* {@inheritDoc}
*/
public long Length()
{
CheckClosed();
return size;
}
/**
* {@inheritDoc}
*/
public void write(int b)
{
CheckClosed();
// end of buffer reached?
if (currentBufferPointer >= chunkSize)
{
if (pointer + chunkSize >= int.MaxValue)
{
throw new OutOfMemoryException("RandomAccessBuffer overflow");
}
ExpandBuffer();
}
currentBuffer[currentBufferPointer++] = (byte) b;
pointer++;
if (pointer > this.size)
{
this.size = pointer;
}
// end of buffer reached now?
if (currentBufferPointer >= chunkSize)
{
if (pointer + chunkSize >= int.MaxValue)
{
throw new OutOfMemoryException("RandomAccessBuffer overflow");
}
ExpandBuffer();
}
}
/**
* {@inheritDoc}
*/
public void write(byte[] b)
{
write(b, 0, b.Length);
}
/**
* {@inheritDoc}
*/
public void write(byte[] b, int offset, int length)
{
CheckClosed();
long newSize = pointer + length;
int remainingBytes = chunkSize - currentBufferPointer;
if (length >= remainingBytes)
{
if (newSize > int.MaxValue)
{
throw new OutOfMemoryException("RandomAccessBuffer overflow");
}
// copy the first bytes to the current buffer
Array.Copy(b, offset, currentBuffer, currentBufferPointer, remainingBytes);
int newOffset = offset + remainingBytes;
long remainingBytes2Write = length - remainingBytes;
// determine how many buffers are needed for the remaining bytes
int numberOfNewArrays = (int) remainingBytes2Write / chunkSize;
for (int i = 0; i < numberOfNewArrays; i++)
{
ExpandBuffer();
Array.Copy(b, newOffset, currentBuffer, currentBufferPointer, chunkSize);
newOffset += chunkSize;
}
// are there still some bytes to be written?
remainingBytes2Write -= numberOfNewArrays * (long) chunkSize;
if (remainingBytes2Write >= 0)
{
ExpandBuffer();
if (remainingBytes2Write > 0)
{
Array.Copy(b, newOffset, currentBuffer, currentBufferPointer, (int) remainingBytes2Write);
}
currentBufferPointer = (int) remainingBytes2Write;
}
}
else
{
Array.Copy(b, offset, currentBuffer, currentBufferPointer, length);
currentBufferPointer += length;
}
pointer += length;
if (pointer > this.size)
{
this.size = pointer;
}
}
/**
* create a new buffer chunk and adjust all pointers and indices.
*/
private void ExpandBuffer()
{
if (bufferListMaxIndex > bufferListIndex)
{
// there is already an existing chunk
NextBuffer();
}
else
{
// create a new chunk and add it to the buffer
currentBuffer = new byte[chunkSize];
bufferList.Add(currentBuffer);
currentBufferPointer = 0;
bufferListMaxIndex++;
bufferListIndex++;
}
}
/**
* switch to the next buffer chunk and reset the buffer pointer.
*/
private void NextBuffer()
{
if (bufferListIndex == bufferListMaxIndex)
{
throw new InvalidOperationException("No more chunks available, end of buffer reached");
}
currentBufferPointer = 0;
currentBuffer = bufferList[++bufferListIndex];
}
/**
* Ensure that the RandomAccessBuffer is not closed
* @throws IOException
*/
private void CheckClosed()
{
if (currentBuffer is null)
{
// consider that the rab is closed if there is no current buffer
throw new ObjectDisposedException("RandomAccessBuffer already closed");
}
}
/**
* {@inheritDoc}
*/
public bool IsClosed()
{
return currentBuffer is null;
}
/**
* {@inheritDoc}
*/
public bool IsEof()
{
CheckClosed();
return pointer >= size;
}
/**
* {@inheritDoc}
*/
public int Available()
{
return (int) Math.Min(Length() - GetPosition(), int.MaxValue);
}
public void ReturnToBeginning()
{
Seek(0);
}
/**
* {@inheritDoc}
*/
public int Peek()
{
int result = Read();
if (result != -1)
{
Rewind(1);
}
return result;
}
/**
* {@inheritDoc}
*/
public void Rewind(int bytes)
{
CheckClosed();
Seek(GetPosition() - bytes);
}
/**
* {@inheritDoc}
*/
public byte[] ReadFully(int length)
{
byte[]
b = new byte[length];
int bytesRead = Read(b);
while (bytesRead < length)
{
bytesRead += Read(b, bytesRead, length - bytesRead);
}
return b;
}
/**
* {@inheritDoc}
*/
public int Read(byte[] b)
{
return Read(b, 0, b.Length);
}
public void Unread(int b)
{
Rewind(1);
}
public void Unread(byte[] bytes)
{
Rewind(bytes.Length);
}
public void Unread(byte[] bytes, int start, int len)
{
Rewind(len - start);
}
}
}