mirror of https://github.com/SixLabors/ImageSharp
9 changed files with 603 additions and 179 deletions
@ -0,0 +1,151 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
|
|||
// TODO: This could be useful elsewhere.
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components |
|||
{ |
|||
/// <summary>
|
|||
/// A stream reader that add a secondary level buffer in addition to native stream buffered reading
|
|||
/// to reduce the overhead of small incremental reads.
|
|||
/// </summary>
|
|||
internal class DoubleBufferedStreamReader |
|||
{ |
|||
/// <summary>
|
|||
/// The length, in bytes, of the chunk
|
|||
/// </summary>
|
|||
public const int ChunkLength = 4096; |
|||
|
|||
private readonly Stream stream; |
|||
|
|||
private readonly byte[] chunk; |
|||
|
|||
private int bytesRead; |
|||
|
|||
private long position; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DoubleBufferedStreamReader"/> class.
|
|||
/// </summary>
|
|||
/// <param name="stream">The input stream.</param>
|
|||
public DoubleBufferedStreamReader(Stream stream) |
|||
{ |
|||
this.stream = stream; |
|||
this.Length = stream.Length; |
|||
|
|||
// TODO: Consider pooling this.
|
|||
this.chunk = new byte[ChunkLength]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the length, in bytes, of the stream
|
|||
/// </summary>
|
|||
public long Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the current position within the stream
|
|||
/// </summary>
|
|||
public long Position |
|||
{ |
|||
get |
|||
{ |
|||
return this.position; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
// Reset everything. It's easier than tracking.
|
|||
this.position = value; |
|||
this.stream.Seek(this.position, SeekOrigin.Begin); |
|||
this.bytesRead = ChunkLength; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a byte from the stream and advances the position within the stream by one
|
|||
/// byte, or returns -1 if at the end of the stream.
|
|||
/// </summary>
|
|||
/// <returns>The unsigned byte cast to an <see cref="int"/>, or -1 if at the end of the stream.</returns>
|
|||
public int ReadByte() |
|||
{ |
|||
if (this.position >= this.Length) |
|||
{ |
|||
return -1; |
|||
} |
|||
|
|||
if (this.position == 0 || this.bytesRead >= ChunkLength) |
|||
{ |
|||
this.stream.Seek(this.position, SeekOrigin.Begin); |
|||
this.stream.Read(this.chunk, 0, ChunkLength); |
|||
this.bytesRead = 0; |
|||
} |
|||
|
|||
this.position++; |
|||
return this.chunk[this.bytesRead++]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Skips the number of bytes in the stream
|
|||
/// </summary>
|
|||
/// <param name="count">The number of bytes to skip</param>
|
|||
public void Skip(int count) |
|||
{ |
|||
this.position += count; |
|||
this.bytesRead += count; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a sequence of bytes from the current stream and advances the position within the stream
|
|||
/// by the number of bytes read.
|
|||
/// </summary>
|
|||
/// <param name="buffer">
|
|||
/// An array of bytes. When this method returns, the buffer contains the specified
|
|||
/// byte array with the values between offset and (offset + count - 1) replaced by
|
|||
/// the bytes read from the current source.
|
|||
/// </param>
|
|||
/// <param name="offset">
|
|||
/// The zero-based byte offset in buffer at which to begin storing the data read
|
|||
/// from the current stream.
|
|||
/// </param>
|
|||
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
|
|||
/// <returns>
|
|||
/// The total number of bytes read into the buffer. This can be less than the number
|
|||
/// of bytes requested if that many bytes are not currently available, or zero (0)
|
|||
/// if the end of the stream has been reached.
|
|||
/// </returns>
|
|||
public int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
int n = 0; |
|||
if (buffer.Length <= ChunkLength) |
|||
{ |
|||
if (this.position == 0 || count + this.bytesRead > ChunkLength) |
|||
{ |
|||
// Refill our buffer then copy.
|
|||
this.stream.Seek(this.position, SeekOrigin.Begin); |
|||
this.stream.Read(this.chunk, 0, ChunkLength); |
|||
this.bytesRead = 0; |
|||
} |
|||
|
|||
Buffer.BlockCopy(this.chunk, this.bytesRead, buffer, offset, count); |
|||
this.position += count; |
|||
this.bytesRead += count; |
|||
|
|||
n = Math.Min(count, (int)(this.Length - this.position)); |
|||
} |
|||
else |
|||
{ |
|||
// Read to target but don't copy to our chunk.
|
|||
this.stream.Seek(this.position, SeekOrigin.Begin); |
|||
n = this.stream.Read(buffer, offset, count); |
|||
|
|||
// Ensure next read fills the chunk
|
|||
this.bytesRead = ChunkLength; |
|||
this.position += count; |
|||
} |
|||
|
|||
return Math.Max(n, 0); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.IO; |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.GolangPort; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort; |
|||
using SixLabors.ImageSharp.Tests; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg |
|||
{ |
|||
[Config(typeof(Config.ShortClr))] |
|||
public class Identify |
|||
{ |
|||
private byte[] jpegBytes; |
|||
|
|||
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); |
|||
|
|||
[Params(TestImages.Jpeg.Baseline.Jpeg420Exif, TestImages.Jpeg.Baseline.Calliphora)] |
|||
public string TestImage { get; set; } |
|||
|
|||
[GlobalSetup] |
|||
public void ReadImages() |
|||
{ |
|||
if (this.jpegBytes == null) |
|||
{ |
|||
this.jpegBytes = File.ReadAllBytes(this.TestImageFullPath); |
|||
} |
|||
} |
|||
|
|||
[Benchmark] |
|||
public IImageInfo IdentifyGolang() |
|||
{ |
|||
using (var memoryStream = new MemoryStream(this.jpegBytes)) |
|||
{ |
|||
var decoder = new OrigJpegDecoder(); |
|||
|
|||
return decoder.Identify(Configuration.Default, memoryStream); |
|||
} |
|||
} |
|||
|
|||
[Benchmark] |
|||
public IImageInfo IdentifyPdfJs() |
|||
{ |
|||
using (var memoryStream = new MemoryStream(this.jpegBytes)) |
|||
{ |
|||
var decoder = new PdfJsJpegDecoder(); |
|||
return decoder.Identify(Configuration.Default, memoryStream); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
using System; |
|||
using System.IO; |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.General |
|||
{ |
|||
[Config(typeof(Config.ShortClr))] |
|||
public class DoubleBufferedStreams |
|||
{ |
|||
private byte[] buffer = CreateTestBytes(); |
|||
|
|||
[Benchmark] |
|||
public int StandardStream() |
|||
{ |
|||
int r = 0; |
|||
using (var stream = new MemoryStream(this.buffer)) |
|||
{ |
|||
for (int i = 0; i < stream.Length; i++) |
|||
{ |
|||
r += stream.ReadByte(); |
|||
} |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int ChunkedStream() |
|||
{ |
|||
int r = 0; |
|||
using (var stream = new MemoryStream(this.buffer)) |
|||
{ |
|||
var reader = new DoubleBufferedStreamReader(stream); |
|||
for (int i = 0; i < reader.Length; i++) |
|||
{ |
|||
r += reader.ReadByte(); |
|||
} |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
private static byte[] CreateTestBytes() |
|||
{ |
|||
byte[] buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3]; |
|||
var random = new Random(); |
|||
random.NextBytes(buffer); |
|||
|
|||
return buffer; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Jpg |
|||
{ |
|||
public class DoubleBufferedStreamReaderTests |
|||
{ |
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanReadSingleByteFromOrigin() |
|||
{ |
|||
using (MemoryStream stream = CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
var reader = new DoubleBufferedStreamReader(stream); |
|||
|
|||
Assert.Equal(expected[0], reader.ReadByte()); |
|||
|
|||
// We've read a whole chunk but increment by 1 in our reader.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); |
|||
Assert.Equal(1, reader.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanReadSubsequentSingleByteCorrectly() |
|||
{ |
|||
using (MemoryStream stream = CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
var reader = new DoubleBufferedStreamReader(stream); |
|||
|
|||
for (int i = 0; i < expected.Length; i++) |
|||
{ |
|||
Assert.Equal(expected[i], reader.ReadByte()); |
|||
Assert.Equal(i + 1, reader.Position); |
|||
|
|||
if (i < DoubleBufferedStreamReader.ChunkLength) |
|||
{ |
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); |
|||
} |
|||
else if (i >= DoubleBufferedStreamReader.ChunkLength && i < DoubleBufferedStreamReader.ChunkLength * 2) |
|||
{ |
|||
// We should have advanced to the second chunk now.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2); |
|||
} |
|||
else |
|||
{ |
|||
// We should have advanced to the third chunk now.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanReadMultipleBytesFromOrigin() |
|||
{ |
|||
using (MemoryStream stream = CreateTestStream()) |
|||
{ |
|||
byte[] buffer = new byte[2]; |
|||
byte[] expected = stream.ToArray(); |
|||
var reader = new DoubleBufferedStreamReader(stream); |
|||
|
|||
Assert.Equal(2, reader.Read(buffer, 0, 2)); |
|||
Assert.Equal(expected[0], buffer[0]); |
|||
Assert.Equal(expected[1], buffer[1]); |
|||
|
|||
// We've read a whole chunk but increment by the buffer length in our reader.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); |
|||
Assert.Equal(buffer.Length, reader.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanReadSubsequentMultipleByteCorrectly() |
|||
{ |
|||
using (MemoryStream stream = CreateTestStream()) |
|||
{ |
|||
byte[] buffer = new byte[2]; |
|||
byte[] expected = stream.ToArray(); |
|||
var reader = new DoubleBufferedStreamReader(stream); |
|||
|
|||
for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) |
|||
{ |
|||
if (o + 2 == expected.Length) |
|||
{ |
|||
// We've reached the end of the stream
|
|||
Assert.Equal(0, reader.Read(buffer, 0, 2)); |
|||
} |
|||
else |
|||
{ |
|||
Assert.Equal(2, reader.Read(buffer, 0, 2)); |
|||
} |
|||
|
|||
Assert.Equal(expected[o], buffer[0]); |
|||
Assert.Equal(expected[o + 1], buffer[1]); |
|||
Assert.Equal(o + 2, reader.Position); |
|||
|
|||
int offset = i * 2; |
|||
if (offset < DoubleBufferedStreamReader.ChunkLength) |
|||
{ |
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); |
|||
} |
|||
else if (offset >= DoubleBufferedStreamReader.ChunkLength && offset < DoubleBufferedStreamReader.ChunkLength * 2) |
|||
{ |
|||
// We should have advanced to the second chunk now.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2); |
|||
} |
|||
else |
|||
{ |
|||
// We should have advanced to the third chunk now.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private MemoryStream CreateTestStream() |
|||
{ |
|||
byte[] buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3]; |
|||
var random = new Random(); |
|||
random.NextBytes(buffer); |
|||
|
|||
return new MemoryStream(buffer); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue