mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
143 changed files with 6118 additions and 901 deletions
@ -0,0 +1,53 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp |
|||
{ |
|||
/// <summary>
|
|||
/// Extensions methods fpor the <see cref="GraphicsOptions"/> class.
|
|||
/// </summary>
|
|||
internal static class GraphicsOptionsExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Evaluates if a given SOURCE color can completely replace a BACKDROP color given the current blending and composition settings.
|
|||
/// </summary>
|
|||
/// <param name="options">The graphics options.</param>
|
|||
/// <param name="color">The source color.</param>
|
|||
/// <returns>true if the color can be considered opaque</returns>
|
|||
/// <remarks>
|
|||
/// Blending and composition is an expensive operation, in some cases, like
|
|||
/// filling with a solid color, the blending can be avoided by a plain color replacement.
|
|||
/// This method can be useful for such processors to select the fast path.
|
|||
/// </remarks>
|
|||
public static bool IsOpaqueColorWithoutBlending(this GraphicsOptions options, Color color) |
|||
{ |
|||
if (options.ColorBlendingMode != PixelColorBlendingMode.Normal) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (options.AlphaCompositionMode != PixelAlphaCompositionMode.SrcOver |
|||
&& options.AlphaCompositionMode != PixelAlphaCompositionMode.Src) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
const float Opaque = 1F; |
|||
|
|||
if (options.BlendPercentage != Opaque) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (((Vector4)color).W != Opaque) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp |
|||
{ |
|||
/// <summary>
|
|||
/// Utility methods for numeric primitives.
|
|||
/// </summary>
|
|||
internal static class NumberUtils |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static float ClampFloat(float value, float min, float max) |
|||
{ |
|||
if (value >= max) |
|||
{ |
|||
return max; |
|||
} |
|||
|
|||
if (value <= min) |
|||
{ |
|||
return min; |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Png |
|||
{ |
|||
/// <summary>
|
|||
/// Cold path optimizations for throwing png format based exceptions.
|
|||
/// </summary>
|
|||
internal static class PngThrowHelper |
|||
{ |
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowNoHeader() => throw new ImageFormatException("PNG Image does not contain a header chunk"); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowNoData() => throw new ImageFormatException("PNG Image does not contain a data chunk"); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowInvalidChunkType() => throw new ImageFormatException("Invalid PNG data."); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new ImageFormatException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowNotSupportedColor() => new NotSupportedException("Unsupported PNG color type"); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowUnknownFilter() => throw new ImageFormatException("Unknown filter type."); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Png.Zlib |
|||
{ |
|||
internal static class DeflateThrowHelper |
|||
{ |
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowAlreadyFinished() => throw new InvalidOperationException("Finish() already called."); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowAlreadyClosed() => throw new InvalidOperationException("Deflator already closed."); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowUnknownCompression() => throw new InvalidOperationException("Unknown compression function."); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowNotProcessed() => throw new InvalidOperationException("Old input was not completely processed."); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowNull(string name) => throw new ArgumentNullException(name); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated."); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
public static void ThrowNoDeflate() => throw new ImageFormatException("Cannot deflate all input."); |
|||
} |
|||
} |
|||
@ -0,0 +1,295 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Png.Zlib |
|||
{ |
|||
/// <summary>
|
|||
/// This class compresses input with the deflate algorithm described in RFC 1951.
|
|||
/// It has several compression levels and three different strategies described below.
|
|||
/// </summary>
|
|||
internal sealed class Deflater : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// The best and slowest compression level. This tries to find very
|
|||
/// long and distant string repetitions.
|
|||
/// </summary>
|
|||
public const int BestCompression = 9; |
|||
|
|||
/// <summary>
|
|||
/// The worst but fastest compression level.
|
|||
/// </summary>
|
|||
public const int BestSpeed = 1; |
|||
|
|||
/// <summary>
|
|||
/// The default compression level.
|
|||
/// </summary>
|
|||
public const int DefaultCompression = -1; |
|||
|
|||
/// <summary>
|
|||
/// This level won't compress at all but output uncompressed blocks.
|
|||
/// </summary>
|
|||
public const int NoCompression = 0; |
|||
|
|||
/// <summary>
|
|||
/// The compression method. This is the only method supported so far.
|
|||
/// There is no need to use this constant at all.
|
|||
/// </summary>
|
|||
public const int Deflated = 8; |
|||
|
|||
/// <summary>
|
|||
/// Compression level.
|
|||
/// </summary>
|
|||
private int level; |
|||
|
|||
/// <summary>
|
|||
/// The current state.
|
|||
/// </summary>
|
|||
private int state; |
|||
|
|||
private DeflaterEngine engine; |
|||
private bool isDisposed; |
|||
|
|||
private const int IsFlushing = 0x04; |
|||
private const int IsFinishing = 0x08; |
|||
private const int BusyState = 0x10; |
|||
private const int FlushingState = 0x14; |
|||
private const int FinishingState = 0x1c; |
|||
private const int FinishedState = 0x1e; |
|||
private const int ClosedState = 0x7f; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Deflater"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
/// <param name="level">The compression level, a value between NoCompression and BestCompression.
|
|||
/// </param>
|
|||
/// <exception cref="ArgumentOutOfRangeException">if level is out of range.</exception>
|
|||
public Deflater(MemoryAllocator memoryAllocator, int level) |
|||
{ |
|||
if (level == DefaultCompression) |
|||
{ |
|||
level = 6; |
|||
} |
|||
else if (level < NoCompression || level > BestCompression) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(level)); |
|||
} |
|||
|
|||
// TODO: Possibly provide DeflateStrategy as an option.
|
|||
this.engine = new DeflaterEngine(memoryAllocator, DeflateStrategy.Default); |
|||
|
|||
this.SetLevel(level); |
|||
this.Reset(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compression Level as an enum for safer use
|
|||
/// </summary>
|
|||
public enum CompressionLevel |
|||
{ |
|||
/// <summary>
|
|||
/// The best and slowest compression level. This tries to find very
|
|||
/// long and distant string repetitions.
|
|||
/// </summary>
|
|||
BestCompression = Deflater.BestCompression, |
|||
|
|||
/// <summary>
|
|||
/// The worst but fastest compression level.
|
|||
/// </summary>
|
|||
BestSpeed = Deflater.BestSpeed, |
|||
|
|||
/// <summary>
|
|||
/// The default compression level.
|
|||
/// </summary>
|
|||
DefaultCompression = Deflater.DefaultCompression, |
|||
|
|||
/// <summary>
|
|||
/// This level won't compress at all but output uncompressed blocks.
|
|||
/// </summary>
|
|||
NoCompression = Deflater.NoCompression, |
|||
|
|||
/// <summary>
|
|||
/// The compression method. This is the only method supported so far.
|
|||
/// There is no need to use this constant at all.
|
|||
/// </summary>
|
|||
Deflated = Deflater.Deflated |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whetherthe stream was finished and no more output bytes
|
|||
/// are available.
|
|||
/// </summary>
|
|||
public bool IsFinished => (this.state == FinishedState) && this.engine.Pending.IsFlushed; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the input buffer is empty.
|
|||
/// You should then call setInput().
|
|||
/// NOTE: This method can also return true when the stream
|
|||
/// was finished.
|
|||
/// </summary>
|
|||
public bool IsNeedingInput => this.engine.NeedsInput(); |
|||
|
|||
/// <summary>
|
|||
/// Resets the deflater. The deflater acts afterwards as if it was
|
|||
/// just created with the same compression level and strategy as it
|
|||
/// had before.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() |
|||
{ |
|||
this.state = BusyState; |
|||
this.engine.Pending.Reset(); |
|||
this.engine.Reset(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Flushes the current input block. Further calls to Deflate() will
|
|||
/// produce enough output to inflate everything in the current input
|
|||
/// block. It is used by DeflaterOutputStream to implement Flush().
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Flush() => this.state |= IsFlushing; |
|||
|
|||
/// <summary>
|
|||
/// Finishes the deflater with the current input block. It is an error
|
|||
/// to give more input after this method was called. This method must
|
|||
/// be called to force all bytes to be flushed.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Finish() => this.state |= IsFlushing | IsFinishing; |
|||
|
|||
/// <summary>
|
|||
/// Sets the data which should be compressed next. This should be
|
|||
/// only called when needsInput indicates that more input is needed.
|
|||
/// The given byte array should not be changed, before needsInput() returns
|
|||
/// true again.
|
|||
/// </summary>
|
|||
/// <param name="input">The buffer containing the input data.</param>
|
|||
/// <param name="offset">The start of the data.</param>
|
|||
/// <param name="count">The number of data bytes of input.</param>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// if the buffer was finished or if previous input is still pending.
|
|||
/// </exception>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void SetInput(byte[] input, int offset, int count) |
|||
{ |
|||
if ((this.state & IsFinishing) != 0) |
|||
{ |
|||
DeflateThrowHelper.ThrowAlreadyFinished(); |
|||
} |
|||
|
|||
this.engine.SetInput(input, offset, count); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the compression level. There is no guarantee of the exact
|
|||
/// position of the change, but if you call this when needsInput is
|
|||
/// true the change of compression level will occur somewhere near
|
|||
/// before the end of the so far given input.
|
|||
/// </summary>
|
|||
/// <param name="level">
|
|||
/// the new compression level.
|
|||
/// </param>
|
|||
public void SetLevel(int level) |
|||
{ |
|||
if (level == DefaultCompression) |
|||
{ |
|||
level = 6; |
|||
} |
|||
else if (level < NoCompression || level > BestCompression) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(level)); |
|||
} |
|||
|
|||
if (this.level != level) |
|||
{ |
|||
this.level = level; |
|||
this.engine.SetLevel(level); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Deflates the current input block to the given array.
|
|||
/// </summary>
|
|||
/// <param name="output">Buffer to store the compressed data.</param>
|
|||
/// <param name="offset">Offset into the output array.</param>
|
|||
/// <param name="length">The maximum number of bytes that may be stored.</param>
|
|||
/// <returns>
|
|||
/// The number of compressed bytes added to the output, or 0 if either
|
|||
/// <see cref="IsNeedingInput"/> or <see cref="IsFinished"/> returns true or length is zero.
|
|||
/// </returns>
|
|||
public int Deflate(byte[] output, int offset, int length) |
|||
{ |
|||
int origLength = length; |
|||
|
|||
if (this.state == ClosedState) |
|||
{ |
|||
DeflateThrowHelper.ThrowAlreadyClosed(); |
|||
} |
|||
|
|||
while (true) |
|||
{ |
|||
int count = this.engine.Pending.Flush(output, offset, length); |
|||
offset += count; |
|||
length -= count; |
|||
|
|||
if (length == 0 || this.state == FinishedState) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
if (!this.engine.Deflate((this.state & IsFlushing) != 0, (this.state & IsFinishing) != 0)) |
|||
{ |
|||
switch (this.state) |
|||
{ |
|||
case BusyState: |
|||
// We need more input now
|
|||
return origLength - length; |
|||
|
|||
case FlushingState: |
|||
if (this.level != NoCompression) |
|||
{ |
|||
// We have to supply some lookahead. 8 bit lookahead
|
|||
// is needed by the zlib inflater, and we must fill
|
|||
// the next byte, so that all bits are flushed.
|
|||
int neededbits = 8 + ((-this.engine.Pending.BitCount) & 7); |
|||
while (neededbits > 0) |
|||
{ |
|||
// Write a static tree block consisting solely of an EOF:
|
|||
this.engine.Pending.WriteBits(2, 10); |
|||
neededbits -= 10; |
|||
} |
|||
} |
|||
|
|||
this.state = BusyState; |
|||
break; |
|||
|
|||
case FinishingState: |
|||
this.engine.Pending.AlignToByte(); |
|||
this.state = FinishedState; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return origLength - length; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.engine.Dispose(); |
|||
this.engine = null; |
|||
this.isDisposed = true; |
|||
} |
|||
|
|||
GC.SuppressFinalize(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
// <auto-generated/>
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Png.Zlib |
|||
{ |
|||
/// <summary>
|
|||
/// This class contains constants used for deflation.
|
|||
/// </summary>
|
|||
public static class DeflaterConstants |
|||
{ |
|||
/// <summary>
|
|||
/// Set to true to enable debugging
|
|||
/// </summary>
|
|||
public const bool DEBUGGING = false; |
|||
|
|||
/// <summary>
|
|||
/// Written to Zip file to identify a stored block
|
|||
/// </summary>
|
|||
public const int STORED_BLOCK = 0; |
|||
|
|||
/// <summary>
|
|||
/// Identifies static tree in Zip file
|
|||
/// </summary>
|
|||
public const int STATIC_TREES = 1; |
|||
|
|||
/// <summary>
|
|||
/// Identifies dynamic tree in Zip file
|
|||
/// </summary>
|
|||
public const int DYN_TREES = 2; |
|||
|
|||
/// <summary>
|
|||
/// Header flag indicating a preset dictionary for deflation
|
|||
/// </summary>
|
|||
public const int PRESET_DICT = 0x20; |
|||
|
|||
/// <summary>
|
|||
/// Sets internal buffer sizes for Huffman encoding
|
|||
/// </summary>
|
|||
public const int DEFAULT_MEM_LEVEL = 8; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MAX_MATCH = 258; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MIN_MATCH = 3; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MAX_WBITS = 15; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int WSIZE = 1 << MAX_WBITS; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int WMASK = WSIZE - 1; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int HASH_SIZE = 1 << HASH_BITS; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int HASH_MASK = HASH_SIZE - 1; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8); |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5); |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int DEFLATE_STORED = 0; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int DEFLATE_FAST = 1; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int DEFLATE_SLOW = 2; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 }; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 }; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 }; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 }; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 }; |
|||
} |
|||
} |
|||
@ -0,0 +1,859 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Png.Zlib |
|||
{ |
|||
/// <summary>
|
|||
/// Strategies for deflater
|
|||
/// </summary>
|
|||
public enum DeflateStrategy |
|||
{ |
|||
/// <summary>
|
|||
/// The default strategy
|
|||
/// </summary>
|
|||
Default = 0, |
|||
|
|||
/// <summary>
|
|||
/// This strategy will only allow longer string repetitions. It is
|
|||
/// useful for random data with a small character set.
|
|||
/// </summary>
|
|||
Filtered = 1, |
|||
|
|||
/// <summary>
|
|||
/// This strategy will not look for string repetitions at all. It
|
|||
/// only encodes with Huffman trees (which means, that more common
|
|||
/// characters get a smaller encoding.
|
|||
/// </summary>
|
|||
HuffmanOnly = 2 |
|||
} |
|||
|
|||
// DEFLATE ALGORITHM:
|
|||
//
|
|||
// The uncompressed stream is inserted into the window array. When
|
|||
// the window array is full the first half is thrown away and the
|
|||
// second half is copied to the beginning.
|
|||
//
|
|||
// The head array is a hash table. Three characters build a hash value
|
|||
// and they the value points to the corresponding index in window of
|
|||
// the last string with this hash. The prev array implements a
|
|||
// linked list of matches with the same hash: prev[index & WMASK] points
|
|||
// to the previous index with the same hash.
|
|||
//
|
|||
|
|||
/// <summary>
|
|||
/// Low level compression engine for deflate algorithm which uses a 32K sliding window
|
|||
/// with secondary compression from Huffman/Shannon-Fano codes.
|
|||
/// </summary>
|
|||
internal sealed unsafe class DeflaterEngine : IDisposable |
|||
{ |
|||
private const int TooFar = 4096; |
|||
|
|||
// Hash index of string to be inserted
|
|||
private int insertHashIndex; |
|||
|
|||
private int matchStart; |
|||
|
|||
// Length of best match
|
|||
private int matchLen; |
|||
|
|||
// Set if previous match exists
|
|||
private bool prevAvailable; |
|||
|
|||
private int blockStart; |
|||
|
|||
/// <summary>
|
|||
/// Points to the current character in the window.
|
|||
/// </summary>
|
|||
private int strstart; |
|||
|
|||
/// <summary>
|
|||
/// lookahead is the number of characters starting at strstart in
|
|||
/// window that are valid.
|
|||
/// So window[strstart] until window[strstart+lookahead-1] are valid
|
|||
/// characters.
|
|||
/// </summary>
|
|||
private int lookahead; |
|||
|
|||
/// <summary>
|
|||
/// The current compression function.
|
|||
/// </summary>
|
|||
private int compressionFunction; |
|||
|
|||
/// <summary>
|
|||
/// The input data for compression.
|
|||
/// </summary>
|
|||
private byte[] inputBuf; |
|||
|
|||
/// <summary>
|
|||
/// The offset into inputBuf, where input data starts.
|
|||
/// </summary>
|
|||
private int inputOff; |
|||
|
|||
/// <summary>
|
|||
/// The end offset of the input data.
|
|||
/// </summary>
|
|||
private int inputEnd; |
|||
|
|||
private readonly DeflateStrategy strategy; |
|||
private DeflaterHuffman huffman; |
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Hashtable, hashing three characters to an index for window, so
|
|||
/// that window[index]..window[index+2] have this hash code.
|
|||
/// Note that the array should really be unsigned short, so you need
|
|||
/// to and the values with 0xFFFF.
|
|||
/// </summary>
|
|||
private IMemoryOwner<short> headMemoryOwner; |
|||
private MemoryHandle headMemoryHandle; |
|||
private readonly Memory<short> head; |
|||
private readonly short* pinnedHeadPointer; |
|||
|
|||
/// <summary>
|
|||
/// <code>prev[index & WMASK]</code> points to the previous index that has the
|
|||
/// same hash code as the string starting at index. This way
|
|||
/// entries with the same hash code are in a linked list.
|
|||
/// Note that the array should really be unsigned short, so you need
|
|||
/// to and the values with 0xFFFF.
|
|||
/// </summary>
|
|||
private IMemoryOwner<short> prevMemoryOwner; |
|||
private MemoryHandle prevMemoryHandle; |
|||
private readonly Memory<short> prev; |
|||
private readonly short* pinnedPrevPointer; |
|||
|
|||
/// <summary>
|
|||
/// This array contains the part of the uncompressed stream that
|
|||
/// is of relevance. The current character is indexed by strstart.
|
|||
/// </summary>
|
|||
private IManagedByteBuffer windowMemoryOwner; |
|||
private MemoryHandle windowMemoryHandle; |
|||
private readonly byte[] window; |
|||
private readonly byte* pinnedWindowPointer; |
|||
|
|||
private int maxChain; |
|||
private int maxLazy; |
|||
private int niceLength; |
|||
private int goodLength; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DeflaterEngine"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
/// <param name="strategy">The deflate strategy to use.</param>
|
|||
public DeflaterEngine(MemoryAllocator memoryAllocator, DeflateStrategy strategy) |
|||
{ |
|||
this.huffman = new DeflaterHuffman(memoryAllocator); |
|||
this.Pending = this.huffman.Pending; |
|||
this.strategy = strategy; |
|||
|
|||
// Create pinned pointers to the various buffers to allow indexing
|
|||
// without bounds checks.
|
|||
this.windowMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE); |
|||
this.window = this.windowMemoryOwner.Array; |
|||
this.windowMemoryHandle = this.windowMemoryOwner.Memory.Pin(); |
|||
this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer; |
|||
|
|||
this.headMemoryOwner = memoryAllocator.Allocate<short>(DeflaterConstants.HASH_SIZE); |
|||
this.head = this.headMemoryOwner.Memory; |
|||
this.headMemoryHandle = this.headMemoryOwner.Memory.Pin(); |
|||
this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer; |
|||
|
|||
this.prevMemoryOwner = memoryAllocator.Allocate<short>(DeflaterConstants.WSIZE); |
|||
this.prev = this.prevMemoryOwner.Memory; |
|||
this.prevMemoryHandle = this.prevMemoryOwner.Memory.Pin(); |
|||
this.pinnedPrevPointer = (short*)this.prevMemoryHandle.Pointer; |
|||
|
|||
// We start at index 1, to avoid an implementation deficiency, that
|
|||
// we cannot build a repeat pattern at index 0.
|
|||
this.blockStart = this.strstart = 1; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the pending buffer to use.
|
|||
/// </summary>
|
|||
public DeflaterPendingBuffer Pending { get; } |
|||
|
|||
/// <summary>
|
|||
/// Deflate drives actual compression of data
|
|||
/// </summary>
|
|||
/// <param name="flush">True to flush input buffers</param>
|
|||
/// <param name="finish">Finish deflation with the current input.</param>
|
|||
/// <returns>Returns true if progress has been made.</returns>
|
|||
public bool Deflate(bool flush, bool finish) |
|||
{ |
|||
bool progress = false; |
|||
do |
|||
{ |
|||
this.FillWindow(); |
|||
bool canFlush = flush && (this.inputOff == this.inputEnd); |
|||
|
|||
switch (this.compressionFunction) |
|||
{ |
|||
case DeflaterConstants.DEFLATE_STORED: |
|||
progress = this.DeflateStored(canFlush, finish); |
|||
break; |
|||
|
|||
case DeflaterConstants.DEFLATE_FAST: |
|||
progress = this.DeflateFast(canFlush, finish); |
|||
break; |
|||
|
|||
case DeflaterConstants.DEFLATE_SLOW: |
|||
progress = this.DeflateSlow(canFlush, finish); |
|||
break; |
|||
|
|||
default: |
|||
DeflateThrowHelper.ThrowUnknownCompression(); |
|||
break; |
|||
} |
|||
} |
|||
while (this.Pending.IsFlushed && progress); // repeat while we have no pending output and progress was made
|
|||
return progress; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets input data to be deflated. Should only be called when <see cref="NeedsInput"/>
|
|||
/// returns true
|
|||
/// </summary>
|
|||
/// <param name="buffer">The buffer containing input data.</param>
|
|||
/// <param name="offset">The offset of the first byte of data.</param>
|
|||
/// <param name="count">The number of bytes of data to use as input.</param>
|
|||
public void SetInput(byte[] buffer, int offset, int count) |
|||
{ |
|||
if (buffer is null) |
|||
{ |
|||
DeflateThrowHelper.ThrowNull(nameof(buffer)); |
|||
} |
|||
|
|||
if (offset < 0) |
|||
{ |
|||
DeflateThrowHelper.ThrowOutOfRange(nameof(offset)); |
|||
} |
|||
|
|||
if (count < 0) |
|||
{ |
|||
DeflateThrowHelper.ThrowOutOfRange(nameof(count)); |
|||
} |
|||
|
|||
if (this.inputOff < this.inputEnd) |
|||
{ |
|||
DeflateThrowHelper.ThrowNotProcessed(); |
|||
} |
|||
|
|||
int end = offset + count; |
|||
|
|||
// We want to throw an ArgumentOutOfRangeException early.
|
|||
// The check is very tricky: it also handles integer wrap around.
|
|||
if ((offset > end) || (end > buffer.Length)) |
|||
{ |
|||
DeflateThrowHelper.ThrowOutOfRange(nameof(count)); |
|||
} |
|||
|
|||
this.inputBuf = buffer; |
|||
this.inputOff = offset; |
|||
this.inputEnd = end; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines if more <see cref="SetInput">input</see> is needed.
|
|||
/// </summary>
|
|||
/// <returns>Return true if input is needed via <see cref="SetInput">SetInput</see></returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool NeedsInput() => this.inputEnd == this.inputOff; |
|||
|
|||
/// <summary>
|
|||
/// Reset internal state
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() |
|||
{ |
|||
this.huffman.Reset(); |
|||
this.blockStart = this.strstart = 1; |
|||
this.lookahead = 0; |
|||
this.prevAvailable = false; |
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
this.head.Span.Slice(0, DeflaterConstants.HASH_SIZE).Clear(); |
|||
this.prev.Span.Slice(0, DeflaterConstants.WSIZE).Clear(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Set the deflate level (0-9)
|
|||
/// </summary>
|
|||
/// <param name="level">The value to set the level to.</param>
|
|||
public void SetLevel(int level) |
|||
{ |
|||
if ((level < 0) || (level > 9)) |
|||
{ |
|||
DeflateThrowHelper.ThrowOutOfRange(nameof(level)); |
|||
} |
|||
|
|||
this.goodLength = DeflaterConstants.GOOD_LENGTH[level]; |
|||
this.maxLazy = DeflaterConstants.MAX_LAZY[level]; |
|||
this.niceLength = DeflaterConstants.NICE_LENGTH[level]; |
|||
this.maxChain = DeflaterConstants.MAX_CHAIN[level]; |
|||
|
|||
if (DeflaterConstants.COMPR_FUNC[level] != this.compressionFunction) |
|||
{ |
|||
switch (this.compressionFunction) |
|||
{ |
|||
case DeflaterConstants.DEFLATE_STORED: |
|||
if (this.strstart > this.blockStart) |
|||
{ |
|||
this.huffman.FlushStoredBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); |
|||
this.blockStart = this.strstart; |
|||
} |
|||
|
|||
this.UpdateHash(); |
|||
break; |
|||
|
|||
case DeflaterConstants.DEFLATE_FAST: |
|||
if (this.strstart > this.blockStart) |
|||
{ |
|||
this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); |
|||
this.blockStart = this.strstart; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case DeflaterConstants.DEFLATE_SLOW: |
|||
if (this.prevAvailable) |
|||
{ |
|||
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xFF); |
|||
} |
|||
|
|||
if (this.strstart > this.blockStart) |
|||
{ |
|||
this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); |
|||
this.blockStart = this.strstart; |
|||
} |
|||
|
|||
this.prevAvailable = false; |
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
break; |
|||
} |
|||
|
|||
this.compressionFunction = DeflaterConstants.COMPR_FUNC[level]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Fill the window
|
|||
/// </summary>
|
|||
public void FillWindow() |
|||
{ |
|||
// If the window is almost full and there is insufficient lookahead,
|
|||
// move the upper half to the lower one to make room in the upper half.
|
|||
if (this.strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) |
|||
{ |
|||
this.SlideWindow(); |
|||
} |
|||
|
|||
// If there is not enough lookahead, but still some input left, read in the input.
|
|||
if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && this.inputOff < this.inputEnd) |
|||
{ |
|||
int more = (2 * DeflaterConstants.WSIZE) - this.lookahead - this.strstart; |
|||
|
|||
if (more > this.inputEnd - this.inputOff) |
|||
{ |
|||
more = this.inputEnd - this.inputOff; |
|||
} |
|||
|
|||
Array.Copy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more); |
|||
|
|||
this.inputOff += more; |
|||
this.lookahead += more; |
|||
} |
|||
|
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH) |
|||
{ |
|||
this.UpdateHash(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.huffman.Dispose(); |
|||
|
|||
this.windowMemoryHandle.Dispose(); |
|||
this.windowMemoryOwner.Dispose(); |
|||
|
|||
this.headMemoryHandle.Dispose(); |
|||
this.headMemoryOwner.Dispose(); |
|||
|
|||
this.prevMemoryHandle.Dispose(); |
|||
this.prevMemoryOwner.Dispose(); |
|||
|
|||
this.windowMemoryOwner = null; |
|||
this.headMemoryOwner = null; |
|||
this.prevMemoryOwner = null; |
|||
this.huffman = null; |
|||
|
|||
this.isDisposed = true; |
|||
} |
|||
|
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private void UpdateHash() |
|||
{ |
|||
byte* pinned = this.pinnedWindowPointer; |
|||
this.insertHashIndex = (pinned[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ pinned[this.strstart + 1]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Inserts the current string in the head hash and returns the previous
|
|||
/// value for this hash.
|
|||
/// </summary>
|
|||
/// <returns>The previous hash value</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private int InsertString() |
|||
{ |
|||
short match; |
|||
int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.pinnedWindowPointer[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; |
|||
|
|||
short* pinnedHead = this.pinnedHeadPointer; |
|||
this.pinnedPrevPointer[this.strstart & DeflaterConstants.WMASK] = match = pinnedHead[hash]; |
|||
pinnedHead[hash] = unchecked((short)this.strstart); |
|||
this.insertHashIndex = hash; |
|||
return match & 0xFFFF; |
|||
} |
|||
|
|||
private void SlideWindow() |
|||
{ |
|||
Unsafe.CopyBlockUnaligned(ref this.window[0], ref this.window[DeflaterConstants.WSIZE], DeflaterConstants.WSIZE); |
|||
this.matchStart -= DeflaterConstants.WSIZE; |
|||
this.strstart -= DeflaterConstants.WSIZE; |
|||
this.blockStart -= DeflaterConstants.WSIZE; |
|||
|
|||
// Slide the hash table (could be avoided with 32 bit values
|
|||
// at the expense of memory usage).
|
|||
short* pinnedHead = this.pinnedHeadPointer; |
|||
for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i) |
|||
{ |
|||
int m = pinnedHead[i] & 0xFFFF; |
|||
pinnedHead[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); |
|||
} |
|||
|
|||
// Slide the prev table.
|
|||
short* pinnedPrev = this.pinnedPrevPointer; |
|||
for (int i = 0; i < DeflaterConstants.WSIZE; i++) |
|||
{ |
|||
int m = pinnedPrev[i] & 0xFFFF; |
|||
pinnedPrev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <para>
|
|||
/// Find the best (longest) string in the window matching the
|
|||
/// string starting at strstart.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// Preconditions:
|
|||
/// <code>
|
|||
/// strstart + DeflaterConstants.MAX_MATCH <= window.length.</code>
|
|||
/// </para>
|
|||
/// </summary>
|
|||
/// <param name="curMatch">The current match.</param>
|
|||
/// <returns>True if a match greater than the minimum length is found</returns>
|
|||
private bool FindLongestMatch(int curMatch) |
|||
{ |
|||
int match; |
|||
int scan = this.strstart; |
|||
|
|||
// scanMax is the highest position that we can look at
|
|||
int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1; |
|||
int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); |
|||
|
|||
int chainLength = this.maxChain; |
|||
int niceLength = Math.Min(this.niceLength, this.lookahead); |
|||
|
|||
int matchStrt = this.matchStart; |
|||
this.matchLen = Math.Max(this.matchLen, DeflaterConstants.MIN_MATCH - 1); |
|||
int matchLength = this.matchLen; |
|||
|
|||
if (scan + matchLength > scanMax) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
byte* pinnedWindow = this.pinnedWindowPointer; |
|||
int scanStart = this.strstart; |
|||
byte scanEnd1 = pinnedWindow[scan + matchLength - 1]; |
|||
byte scanEnd = pinnedWindow[scan + matchLength]; |
|||
|
|||
// Do not waste too much time if we already have a good match:
|
|||
if (matchLength >= this.goodLength) |
|||
{ |
|||
chainLength >>= 2; |
|||
} |
|||
|
|||
short* pinnedPrev = this.pinnedPrevPointer; |
|||
do |
|||
{ |
|||
match = curMatch; |
|||
scan = scanStart; |
|||
|
|||
if (pinnedWindow[match + matchLength] != scanEnd |
|||
|| pinnedWindow[match + matchLength - 1] != scanEnd1 |
|||
|| pinnedWindow[match] != pinnedWindow[scan] |
|||
|| pinnedWindow[++match] != pinnedWindow[++scan]) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// scan is set to strstart+1 and the comparison passed, so
|
|||
// scanMax - scan is the maximum number of bytes we can compare.
|
|||
// below we compare 8 bytes at a time, so first we compare
|
|||
// (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8
|
|||
// n & (8 - 1) == n % 8.
|
|||
switch ((scanMax - scan) & 7) |
|||
{ |
|||
case 1: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 2: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 3: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 4: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 5: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 6: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 7: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
if (pinnedWindow[scan] == pinnedWindow[match]) |
|||
{ |
|||
// We check for insufficient lookahead only every 8th comparison;
|
|||
// the 256th check will be made at strstart + 258 unless lookahead is
|
|||
// exhausted first.
|
|||
do |
|||
{ |
|||
if (scan == scanMax) |
|||
{ |
|||
++scan; // advance to first position not matched
|
|||
++match; |
|||
|
|||
break; |
|||
} |
|||
} |
|||
while (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]); |
|||
} |
|||
|
|||
if (scan - scanStart > matchLength) |
|||
{ |
|||
matchStrt = curMatch; |
|||
matchLength = scan - scanStart; |
|||
|
|||
if (matchLength >= niceLength) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
scanEnd1 = pinnedWindow[scan - 1]; |
|||
scanEnd = pinnedWindow[scan]; |
|||
} |
|||
} |
|||
while ((curMatch = pinnedPrev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); |
|||
|
|||
this.matchStart = matchStrt; |
|||
this.matchLen = matchLength; |
|||
return matchLength >= DeflaterConstants.MIN_MATCH; |
|||
} |
|||
|
|||
private bool DeflateStored(bool flush, bool finish) |
|||
{ |
|||
if (!flush && (this.lookahead == 0)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
this.strstart += this.lookahead; |
|||
this.lookahead = 0; |
|||
|
|||
int storedLength = this.strstart - this.blockStart; |
|||
|
|||
if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full
|
|||
(this.blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window
|
|||
flush) |
|||
{ |
|||
bool lastBlock = finish; |
|||
if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE) |
|||
{ |
|||
storedLength = DeflaterConstants.MAX_BLOCK_SIZE; |
|||
lastBlock = false; |
|||
} |
|||
|
|||
this.huffman.FlushStoredBlock(this.window, this.blockStart, storedLength, lastBlock); |
|||
this.blockStart += storedLength; |
|||
return !(lastBlock || storedLength == 0); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private bool DeflateFast(bool flush, bool finish) |
|||
{ |
|||
if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) |
|||
{ |
|||
if (this.lookahead == 0) |
|||
{ |
|||
// We are flushing everything
|
|||
this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish); |
|||
this.blockStart = this.strstart; |
|||
return false; |
|||
} |
|||
|
|||
if (this.strstart > (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD) |
|||
{ |
|||
// slide window, as FindLongestMatch needs this.
|
|||
// This should only happen when flushing and the window
|
|||
// is almost full.
|
|||
this.SlideWindow(); |
|||
} |
|||
|
|||
int hashHead; |
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH && |
|||
(hashHead = this.InsertString()) != 0 && |
|||
this.strategy != DeflateStrategy.HuffmanOnly && |
|||
this.strstart - hashHead <= DeflaterConstants.MAX_DIST && |
|||
this.FindLongestMatch(hashHead)) |
|||
{ |
|||
// longestMatch sets matchStart and matchLen
|
|||
bool full = this.huffman.TallyDist(this.strstart - this.matchStart, this.matchLen); |
|||
|
|||
this.lookahead -= this.matchLen; |
|||
if (this.matchLen <= this.maxLazy && this.lookahead >= DeflaterConstants.MIN_MATCH) |
|||
{ |
|||
while (--this.matchLen > 0) |
|||
{ |
|||
++this.strstart; |
|||
this.InsertString(); |
|||
} |
|||
|
|||
++this.strstart; |
|||
} |
|||
else |
|||
{ |
|||
this.strstart += this.matchLen; |
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH - 1) |
|||
{ |
|||
this.UpdateHash(); |
|||
} |
|||
} |
|||
|
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
if (!full) |
|||
{ |
|||
continue; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// No match found
|
|||
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart] & 0xff); |
|||
++this.strstart; |
|||
--this.lookahead; |
|||
} |
|||
|
|||
if (this.huffman.IsFull()) |
|||
{ |
|||
bool lastBlock = finish && (this.lookahead == 0); |
|||
this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, lastBlock); |
|||
this.blockStart = this.strstart; |
|||
return !lastBlock; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private bool DeflateSlow(bool flush, bool finish) |
|||
{ |
|||
if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) |
|||
{ |
|||
if (this.lookahead == 0) |
|||
{ |
|||
if (this.prevAvailable) |
|||
{ |
|||
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); |
|||
} |
|||
|
|||
this.prevAvailable = false; |
|||
|
|||
// We are flushing everything
|
|||
this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish); |
|||
this.blockStart = this.strstart; |
|||
return false; |
|||
} |
|||
|
|||
if (this.strstart >= (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD) |
|||
{ |
|||
// slide window, as FindLongestMatch needs this.
|
|||
// This should only happen when flushing and the window
|
|||
// is almost full.
|
|||
this.SlideWindow(); |
|||
} |
|||
|
|||
int prevMatch = this.matchStart; |
|||
int prevLen = this.matchLen; |
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH) |
|||
{ |
|||
int hashHead = this.InsertString(); |
|||
|
|||
if (this.strategy != DeflateStrategy.HuffmanOnly && |
|||
hashHead != 0 && |
|||
this.strstart - hashHead <= DeflaterConstants.MAX_DIST && |
|||
this.FindLongestMatch(hashHead)) |
|||
{ |
|||
// longestMatch sets matchStart and matchLen
|
|||
// Discard match if too small and too far away
|
|||
if (this.matchLen <= 5 && (this.strategy == DeflateStrategy.Filtered || (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > TooFar))) |
|||
{ |
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// previous match was better
|
|||
if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen)) |
|||
{ |
|||
this.huffman.TallyDist(this.strstart - 1 - prevMatch, prevLen); |
|||
prevLen -= 2; |
|||
do |
|||
{ |
|||
this.strstart++; |
|||
this.lookahead--; |
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH) |
|||
{ |
|||
this.InsertString(); |
|||
} |
|||
} |
|||
while (--prevLen > 0); |
|||
|
|||
this.strstart++; |
|||
this.lookahead--; |
|||
this.prevAvailable = false; |
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
} |
|||
else |
|||
{ |
|||
if (this.prevAvailable) |
|||
{ |
|||
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); |
|||
} |
|||
|
|||
this.prevAvailable = true; |
|||
this.strstart++; |
|||
this.lookahead--; |
|||
} |
|||
|
|||
if (this.huffman.IsFull()) |
|||
{ |
|||
int len = this.strstart - this.blockStart; |
|||
if (this.prevAvailable) |
|||
{ |
|||
len--; |
|||
} |
|||
|
|||
bool lastBlock = finish && (this.lookahead == 0) && !this.prevAvailable; |
|||
this.huffman.FlushBlock(this.window, this.blockStart, len, lastBlock); |
|||
this.blockStart += len; |
|||
return !lastBlock; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,949 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Png.Zlib |
|||
{ |
|||
/// <summary>
|
|||
/// Performs Deflate Huffman encoding.
|
|||
/// </summary>
|
|||
internal sealed unsafe class DeflaterHuffman : IDisposable |
|||
{ |
|||
private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); |
|||
|
|||
// The number of literal codes.
|
|||
private const int LiteralNumber = 286; |
|||
|
|||
// Number of distance codes
|
|||
private const int DistanceNumber = 30; |
|||
|
|||
// Number of codes used to transfer bit lengths
|
|||
private const int BitLengthNumber = 19; |
|||
|
|||
// Repeat previous bit length 3-6 times (2 bits of repeat count)
|
|||
private const int Repeat3To6 = 16; |
|||
|
|||
// Repeat a zero length 3-10 times (3 bits of repeat count)
|
|||
private const int Repeat3To10 = 17; |
|||
|
|||
// Repeat a zero length 11-138 times (7 bits of repeat count)
|
|||
private const int Repeat11To138 = 18; |
|||
|
|||
private const int EofSymbol = 256; |
|||
|
|||
// The lengths of the bit length codes are sent in order of decreasing
|
|||
// probability, to avoid transmitting the lengths for unused bit length codes.
|
|||
private static readonly int[] BitLengthOrder = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; |
|||
|
|||
private static readonly byte[] Bit4Reverse = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 }; |
|||
|
|||
private static readonly short[] StaticLCodes; |
|||
private static readonly byte[] StaticLLength; |
|||
private static readonly short[] StaticDCodes; |
|||
private static readonly byte[] StaticDLength; |
|||
|
|||
private Tree literalTree; |
|||
private Tree distTree; |
|||
private Tree blTree; |
|||
|
|||
// Buffer for distances
|
|||
private readonly IMemoryOwner<short> distanceManagedBuffer; |
|||
private readonly short* pinnedDistanceBuffer; |
|||
private MemoryHandle distanceBufferHandle; |
|||
|
|||
private readonly IMemoryOwner<short> literalManagedBuffer; |
|||
private readonly short* pinnedLiteralBuffer; |
|||
private MemoryHandle literalBufferHandle; |
|||
|
|||
private int lastLiteral; |
|||
private int extraBits; |
|||
private bool isDisposed; |
|||
|
|||
// TODO: These should be pre-generated array/readonlyspans.
|
|||
static DeflaterHuffman() |
|||
{ |
|||
// See RFC 1951 3.2.6
|
|||
// Literal codes
|
|||
StaticLCodes = new short[LiteralNumber]; |
|||
StaticLLength = new byte[LiteralNumber]; |
|||
|
|||
int i = 0; |
|||
while (i < 144) |
|||
{ |
|||
StaticLCodes[i] = BitReverse((0x030 + i) << 8); |
|||
StaticLLength[i++] = 8; |
|||
} |
|||
|
|||
while (i < 256) |
|||
{ |
|||
StaticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); |
|||
StaticLLength[i++] = 9; |
|||
} |
|||
|
|||
while (i < 280) |
|||
{ |
|||
StaticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); |
|||
StaticLLength[i++] = 7; |
|||
} |
|||
|
|||
while (i < LiteralNumber) |
|||
{ |
|||
StaticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); |
|||
StaticLLength[i++] = 8; |
|||
} |
|||
|
|||
// Distance codes
|
|||
StaticDCodes = new short[DistanceNumber]; |
|||
StaticDLength = new byte[DistanceNumber]; |
|||
for (i = 0; i < DistanceNumber; i++) |
|||
{ |
|||
StaticDCodes[i] = BitReverse(i << 11); |
|||
StaticDLength[i] = 5; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DeflaterHuffman"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
public DeflaterHuffman(MemoryAllocator memoryAllocator) |
|||
{ |
|||
this.Pending = new DeflaterPendingBuffer(memoryAllocator); |
|||
|
|||
this.literalTree = new Tree(memoryAllocator, LiteralNumber, 257, 15); |
|||
this.distTree = new Tree(memoryAllocator, DistanceNumber, 1, 15); |
|||
this.blTree = new Tree(memoryAllocator, BitLengthNumber, 4, 7); |
|||
|
|||
this.distanceManagedBuffer = memoryAllocator.Allocate<short>(BufferSize); |
|||
this.distanceBufferHandle = this.distanceManagedBuffer.Memory.Pin(); |
|||
this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer; |
|||
|
|||
this.literalManagedBuffer = memoryAllocator.Allocate<short>(BufferSize); |
|||
this.literalBufferHandle = this.literalManagedBuffer.Memory.Pin(); |
|||
this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the pending buffer to use.
|
|||
/// </summary>
|
|||
public DeflaterPendingBuffer Pending { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Reset internal state
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() |
|||
{ |
|||
this.lastLiteral = 0; |
|||
this.extraBits = 0; |
|||
this.literalTree.Reset(); |
|||
this.distTree.Reset(); |
|||
this.blTree.Reset(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write all trees to pending buffer
|
|||
/// </summary>
|
|||
/// <param name="blTreeCodes">The number/rank of treecodes to send.</param>
|
|||
public void SendAllTrees(int blTreeCodes) |
|||
{ |
|||
this.blTree.BuildCodes(); |
|||
this.literalTree.BuildCodes(); |
|||
this.distTree.BuildCodes(); |
|||
this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5); |
|||
this.Pending.WriteBits(this.distTree.NumCodes - 1, 5); |
|||
this.Pending.WriteBits(blTreeCodes - 4, 4); |
|||
for (int rank = 0; rank < blTreeCodes; rank++) |
|||
{ |
|||
this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3); |
|||
} |
|||
|
|||
this.literalTree.WriteTree(this.Pending, this.blTree); |
|||
this.distTree.WriteTree(this.Pending, this.blTree); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compress current buffer writing data to pending buffer
|
|||
/// </summary>
|
|||
public void CompressBlock() |
|||
{ |
|||
DeflaterPendingBuffer pendingBuffer = this.Pending; |
|||
short* pinnedDistance = this.pinnedDistanceBuffer; |
|||
short* pinnedLiteral = this.pinnedLiteralBuffer; |
|||
|
|||
for (int i = 0; i < this.lastLiteral; i++) |
|||
{ |
|||
int litlen = pinnedLiteral[i] & 0xFF; |
|||
int dist = pinnedDistance[i]; |
|||
if (dist-- != 0) |
|||
{ |
|||
int lc = Lcode(litlen); |
|||
this.literalTree.WriteSymbol(pendingBuffer, lc); |
|||
|
|||
int bits = (lc - 261) / 4; |
|||
if (bits > 0 && bits <= 5) |
|||
{ |
|||
this.Pending.WriteBits(litlen & ((1 << bits) - 1), bits); |
|||
} |
|||
|
|||
int dc = Dcode(dist); |
|||
this.distTree.WriteSymbol(pendingBuffer, dc); |
|||
|
|||
bits = (dc >> 1) - 1; |
|||
if (bits > 0) |
|||
{ |
|||
this.Pending.WriteBits(dist & ((1 << bits) - 1), bits); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
this.literalTree.WriteSymbol(pendingBuffer, litlen); |
|||
} |
|||
} |
|||
|
|||
this.literalTree.WriteSymbol(pendingBuffer, EofSymbol); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Flush block to output with no compression
|
|||
/// </summary>
|
|||
/// <param name="stored">Data to write</param>
|
|||
/// <param name="storedOffset">Index of first byte to write</param>
|
|||
/// <param name="storedLength">Count of bytes to write</param>
|
|||
/// <param name="lastBlock">True if this is the last block</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) |
|||
{ |
|||
this.Pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); |
|||
this.Pending.AlignToByte(); |
|||
this.Pending.WriteShort(storedLength); |
|||
this.Pending.WriteShort(~storedLength); |
|||
this.Pending.WriteBlock(stored, storedOffset, storedLength); |
|||
this.Reset(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Flush block to output with compression
|
|||
/// </summary>
|
|||
/// <param name="stored">Data to flush</param>
|
|||
/// <param name="storedOffset">Index of first byte to flush</param>
|
|||
/// <param name="storedLength">Count of bytes to flush</param>
|
|||
/// <param name="lastBlock">True if this is the last block</param>
|
|||
public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) |
|||
{ |
|||
this.literalTree.Frequencies[EofSymbol]++; |
|||
|
|||
// Build trees
|
|||
this.literalTree.BuildTree(); |
|||
this.distTree.BuildTree(); |
|||
|
|||
// Calculate bitlen frequency
|
|||
this.literalTree.CalcBLFreq(this.blTree); |
|||
this.distTree.CalcBLFreq(this.blTree); |
|||
|
|||
// Build bitlen tree
|
|||
this.blTree.BuildTree(); |
|||
|
|||
int blTreeCodes = 4; |
|||
for (int i = 18; i > blTreeCodes; i--) |
|||
{ |
|||
if (this.blTree.Length[BitLengthOrder[i]] > 0) |
|||
{ |
|||
blTreeCodes = i + 1; |
|||
} |
|||
} |
|||
|
|||
int opt_len = 14 + (blTreeCodes * 3) + this.blTree.GetEncodedLength() |
|||
+ this.literalTree.GetEncodedLength() + this.distTree.GetEncodedLength() |
|||
+ this.extraBits; |
|||
|
|||
int static_len = this.extraBits; |
|||
ref byte staticLLengthRef = ref MemoryMarshal.GetReference<byte>(StaticLLength); |
|||
for (int i = 0; i < LiteralNumber; i++) |
|||
{ |
|||
static_len += this.literalTree.Frequencies[i] * Unsafe.Add(ref staticLLengthRef, i); |
|||
} |
|||
|
|||
ref byte staticDLengthRef = ref MemoryMarshal.GetReference<byte>(StaticDLength); |
|||
for (int i = 0; i < DistanceNumber; i++) |
|||
{ |
|||
static_len += this.distTree.Frequencies[i] * Unsafe.Add(ref staticDLengthRef, i); |
|||
} |
|||
|
|||
if (opt_len >= static_len) |
|||
{ |
|||
// Force static trees
|
|||
opt_len = static_len; |
|||
} |
|||
|
|||
if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) |
|||
{ |
|||
// Store Block
|
|||
this.FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); |
|||
} |
|||
else if (opt_len == static_len) |
|||
{ |
|||
// Encode with static tree
|
|||
this.Pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); |
|||
this.literalTree.SetStaticCodes(StaticLCodes, StaticLLength); |
|||
this.distTree.SetStaticCodes(StaticDCodes, StaticDLength); |
|||
this.CompressBlock(); |
|||
this.Reset(); |
|||
} |
|||
else |
|||
{ |
|||
// Encode with dynamic tree
|
|||
this.Pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); |
|||
this.SendAllTrees(blTreeCodes); |
|||
this.CompressBlock(); |
|||
this.Reset(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get value indicating if internal buffer is full
|
|||
/// </summary>
|
|||
/// <returns>true if buffer is full</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool IsFull() => this.lastLiteral >= BufferSize; |
|||
|
|||
/// <summary>
|
|||
/// Add literal to buffer
|
|||
/// </summary>
|
|||
/// <param name="literal">Literal value to add to buffer.</param>
|
|||
/// <returns>Value indicating internal buffer is full</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool TallyLit(int literal) |
|||
{ |
|||
this.pinnedDistanceBuffer[this.lastLiteral] = 0; |
|||
this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)literal; |
|||
this.literalTree.Frequencies[literal]++; |
|||
return this.IsFull(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add distance code and length to literal and distance trees
|
|||
/// </summary>
|
|||
/// <param name="distance">Distance code</param>
|
|||
/// <param name="length">Length</param>
|
|||
/// <returns>Value indicating if internal buffer is full</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool TallyDist(int distance, int length) |
|||
{ |
|||
this.pinnedDistanceBuffer[this.lastLiteral] = (short)distance; |
|||
this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)(length - 3); |
|||
|
|||
int lc = Lcode(length - 3); |
|||
this.literalTree.Frequencies[lc]++; |
|||
if (lc >= 265 && lc < 285) |
|||
{ |
|||
this.extraBits += (lc - 261) / 4; |
|||
} |
|||
|
|||
int dc = Dcode(distance - 1); |
|||
this.distTree.Frequencies[dc]++; |
|||
if (dc >= 4) |
|||
{ |
|||
this.extraBits += (dc >> 1) - 1; |
|||
} |
|||
|
|||
return this.IsFull(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reverse the bits of a 16 bit value.
|
|||
/// </summary>
|
|||
/// <param name="toReverse">Value to reverse bits</param>
|
|||
/// <returns>Value with bits reversed</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static short BitReverse(int toReverse) |
|||
{ |
|||
return (short)(Bit4Reverse[toReverse & 0xF] << 12 |
|||
| Bit4Reverse[(toReverse >> 4) & 0xF] << 8 |
|||
| Bit4Reverse[(toReverse >> 8) & 0xF] << 4 |
|||
| Bit4Reverse[toReverse >> 12]); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.Pending.Dispose(); |
|||
this.distanceBufferHandle.Dispose(); |
|||
this.distanceManagedBuffer.Dispose(); |
|||
this.literalBufferHandle.Dispose(); |
|||
this.literalManagedBuffer.Dispose(); |
|||
|
|||
this.literalTree.Dispose(); |
|||
this.blTree.Dispose(); |
|||
this.distTree.Dispose(); |
|||
|
|||
this.Pending = null; |
|||
this.literalTree = null; |
|||
this.blTree = null; |
|||
this.distTree = null; |
|||
this.isDisposed = true; |
|||
} |
|||
|
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static int Lcode(int length) |
|||
{ |
|||
if (length == 255) |
|||
{ |
|||
return 285; |
|||
} |
|||
|
|||
int code = 257; |
|||
while (length >= 8) |
|||
{ |
|||
code += 4; |
|||
length >>= 1; |
|||
} |
|||
|
|||
return code + length; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static int Dcode(int distance) |
|||
{ |
|||
int code = 0; |
|||
while (distance >= 4) |
|||
{ |
|||
code += 2; |
|||
distance >>= 1; |
|||
} |
|||
|
|||
return code + distance; |
|||
} |
|||
|
|||
private sealed class Tree : IDisposable |
|||
{ |
|||
private readonly int minNumCodes; |
|||
private readonly int[] bitLengthCounts; |
|||
private readonly int maxLength; |
|||
private bool isDisposed; |
|||
|
|||
private readonly int elementCount; |
|||
|
|||
private readonly MemoryAllocator memoryAllocator; |
|||
|
|||
private IMemoryOwner<short> codesMemoryOwner; |
|||
private MemoryHandle codesMemoryHandle; |
|||
private readonly short* codes; |
|||
|
|||
private IMemoryOwner<short> frequenciesMemoryOwner; |
|||
private MemoryHandle frequenciesMemoryHandle; |
|||
|
|||
private IManagedByteBuffer lengthsMemoryOwner; |
|||
private MemoryHandle lengthsMemoryHandle; |
|||
|
|||
public Tree(MemoryAllocator memoryAllocator, int elements, int minCodes, int maxLength) |
|||
{ |
|||
this.memoryAllocator = memoryAllocator; |
|||
this.elementCount = elements; |
|||
this.minNumCodes = minCodes; |
|||
this.maxLength = maxLength; |
|||
|
|||
this.frequenciesMemoryOwner = memoryAllocator.Allocate<short>(elements); |
|||
this.frequenciesMemoryHandle = this.frequenciesMemoryOwner.Memory.Pin(); |
|||
this.Frequencies = (short*)this.frequenciesMemoryHandle.Pointer; |
|||
|
|||
this.lengthsMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(elements); |
|||
this.lengthsMemoryHandle = this.lengthsMemoryOwner.Memory.Pin(); |
|||
this.Length = (byte*)this.lengthsMemoryHandle.Pointer; |
|||
|
|||
this.codesMemoryOwner = memoryAllocator.Allocate<short>(elements); |
|||
this.codesMemoryHandle = this.codesMemoryOwner.Memory.Pin(); |
|||
this.codes = (short*)this.codesMemoryHandle.Pointer; |
|||
|
|||
// Maxes out at 15.
|
|||
this.bitLengthCounts = new int[maxLength]; |
|||
} |
|||
|
|||
public int NumCodes { get; private set; } |
|||
|
|||
public short* Frequencies { get; } |
|||
|
|||
public byte* Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Resets the internal state of the tree
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() |
|||
{ |
|||
this.frequenciesMemoryOwner.Memory.Span.Clear(); |
|||
this.lengthsMemoryOwner.Memory.Span.Clear(); |
|||
this.codesMemoryOwner.Memory.Span.Clear(); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteSymbol(DeflaterPendingBuffer pendingBuffer, int code) |
|||
=> pendingBuffer.WriteBits(this.codes[code] & 0xFFFF, this.Length[code]); |
|||
|
|||
/// <summary>
|
|||
/// Set static codes and length
|
|||
/// </summary>
|
|||
/// <param name="staticCodes">new codes</param>
|
|||
/// <param name="staticLengths">length for new codes</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void SetStaticCodes(ReadOnlySpan<short> staticCodes, ReadOnlySpan<byte> staticLengths) |
|||
{ |
|||
staticCodes.CopyTo(this.codesMemoryOwner.Memory.Span); |
|||
staticLengths.CopyTo(this.lengthsMemoryOwner.Memory.Span); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Build dynamic codes and lengths
|
|||
/// </summary>
|
|||
public void BuildCodes() |
|||
{ |
|||
// Maxes out at 15 * 4
|
|||
Span<int> nextCode = stackalloc int[this.maxLength]; |
|||
ref int nextCodeRef = ref MemoryMarshal.GetReference(nextCode); |
|||
ref int bitLengthCountsRef = ref MemoryMarshal.GetReference<int>(this.bitLengthCounts); |
|||
|
|||
int code = 0; |
|||
for (int bits = 0; bits < this.maxLength; bits++) |
|||
{ |
|||
Unsafe.Add(ref nextCodeRef, bits) = code; |
|||
code += Unsafe.Add(ref bitLengthCountsRef, bits) << (15 - bits); |
|||
} |
|||
|
|||
for (int i = 0; i < this.NumCodes; i++) |
|||
{ |
|||
int bits = this.Length[i]; |
|||
if (bits > 0) |
|||
{ |
|||
this.codes[i] = BitReverse(Unsafe.Add(ref nextCodeRef, bits - 1)); |
|||
Unsafe.Add(ref nextCodeRef, bits - 1) += 1 << (16 - bits); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void BuildTree() |
|||
{ |
|||
int numSymbols = this.elementCount; |
|||
|
|||
// heap is a priority queue, sorted by frequency, least frequent
|
|||
// nodes first. The heap is a binary tree, with the property, that
|
|||
// the parent node is smaller than both child nodes. This assures
|
|||
// that the smallest node is the first parent.
|
|||
//
|
|||
// The binary tree is encoded in an array: 0 is root node and
|
|||
// the nodes 2*n+1, 2*n+2 are the child nodes of node n.
|
|||
// Maxes out at 286 * 4 so too large for the stack.
|
|||
using (IMemoryOwner<int> heapMemoryOwner = this.memoryAllocator.Allocate<int>(numSymbols)) |
|||
{ |
|||
ref int heapRef = ref MemoryMarshal.GetReference(heapMemoryOwner.Memory.Span); |
|||
|
|||
int heapLen = 0; |
|||
int maxCode = 0; |
|||
for (int n = 0; n < numSymbols; n++) |
|||
{ |
|||
int freq = this.Frequencies[n]; |
|||
if (freq != 0) |
|||
{ |
|||
// Insert n into heap
|
|||
int pos = heapLen++; |
|||
int ppos; |
|||
while (pos > 0 && this.Frequencies[Unsafe.Add(ref heapRef, ppos = (pos - 1) >> 1)] > freq) |
|||
{ |
|||
Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, ppos); |
|||
pos = ppos; |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, pos) = n; |
|||
|
|||
maxCode = n; |
|||
} |
|||
} |
|||
|
|||
// We could encode a single literal with 0 bits but then we
|
|||
// don't see the literals. Therefore we force at least two
|
|||
// literals to avoid this case. We don't care about order in
|
|||
// this case, both literals get a 1 bit code.
|
|||
while (heapLen < 2) |
|||
{ |
|||
Unsafe.Add(ref heapRef, heapLen++) = maxCode < 2 ? ++maxCode : 0; |
|||
} |
|||
|
|||
this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes); |
|||
|
|||
int numLeafs = heapLen; |
|||
int childrenLength = (4 * heapLen) - 2; |
|||
using (IMemoryOwner<int> childrenMemoryOwner = this.memoryAllocator.Allocate<int>(childrenLength)) |
|||
using (IMemoryOwner<int> valuesMemoryOwner = this.memoryAllocator.Allocate<int>((2 * heapLen) - 1)) |
|||
{ |
|||
ref int childrenRef = ref MemoryMarshal.GetReference(childrenMemoryOwner.Memory.Span); |
|||
ref int valuesRef = ref MemoryMarshal.GetReference(valuesMemoryOwner.Memory.Span); |
|||
int numNodes = numLeafs; |
|||
|
|||
for (int i = 0; i < heapLen; i++) |
|||
{ |
|||
int node = Unsafe.Add(ref heapRef, i); |
|||
int i2 = 2 * i; |
|||
Unsafe.Add(ref childrenRef, i2) = node; |
|||
Unsafe.Add(ref childrenRef, i2 + 1) = -1; |
|||
Unsafe.Add(ref valuesRef, i) = this.Frequencies[node] << 8; |
|||
Unsafe.Add(ref heapRef, i) = i; |
|||
} |
|||
|
|||
// Construct the Huffman tree by repeatedly combining the least two
|
|||
// frequent nodes.
|
|||
do |
|||
{ |
|||
int first = Unsafe.Add(ref heapRef, 0); |
|||
int last = Unsafe.Add(ref heapRef, --heapLen); |
|||
|
|||
// Propagate the hole to the leafs of the heap
|
|||
int ppos = 0; |
|||
int path = 1; |
|||
|
|||
while (path < heapLen) |
|||
{ |
|||
if (path + 1 < heapLen && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path)) > Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path + 1))) |
|||
{ |
|||
path++; |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); |
|||
ppos = path; |
|||
path = (path * 2) + 1; |
|||
} |
|||
|
|||
// Now propagate the last element down along path. Normally
|
|||
// it shouldn't go too deep.
|
|||
int lastVal = Unsafe.Add(ref valuesRef, last); |
|||
while ((path = ppos) > 0 |
|||
&& Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, ppos = (path - 1) >> 1)) > lastVal) |
|||
{ |
|||
Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, path) = last; |
|||
|
|||
int second = Unsafe.Add(ref heapRef, 0); |
|||
|
|||
// Create a new node father of first and second
|
|||
last = numNodes++; |
|||
Unsafe.Add(ref childrenRef, 2 * last) = first; |
|||
Unsafe.Add(ref childrenRef, (2 * last) + 1) = second; |
|||
int mindepth = Math.Min(Unsafe.Add(ref valuesRef, first) & 0xFF, Unsafe.Add(ref valuesRef, second) & 0xFF); |
|||
Unsafe.Add(ref valuesRef, last) = lastVal = Unsafe.Add(ref valuesRef, first) + Unsafe.Add(ref valuesRef, second) - mindepth + 1; |
|||
|
|||
// Again, propagate the hole to the leafs
|
|||
ppos = 0; |
|||
path = 1; |
|||
|
|||
while (path < heapLen) |
|||
{ |
|||
if (path + 1 < heapLen |
|||
&& Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path)) > Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path + 1))) |
|||
{ |
|||
path++; |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); |
|||
ppos = path; |
|||
path = (ppos * 2) + 1; |
|||
} |
|||
|
|||
// Now propagate the new element down along path
|
|||
while ((path = ppos) > 0 && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, ppos = (path - 1) >> 1)) > lastVal) |
|||
{ |
|||
Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, path) = last; |
|||
} |
|||
while (heapLen > 1); |
|||
|
|||
if (Unsafe.Add(ref heapRef, 0) != (childrenLength >> 1) - 1) |
|||
{ |
|||
DeflateThrowHelper.ThrowHeapViolated(); |
|||
} |
|||
|
|||
this.BuildLength(childrenMemoryOwner.Memory.Span); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get encoded length
|
|||
/// </summary>
|
|||
/// <returns>Encoded length, the sum of frequencies * lengths</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public int GetEncodedLength() |
|||
{ |
|||
int len = 0; |
|||
for (int i = 0; i < this.elementCount; i++) |
|||
{ |
|||
len += this.Frequencies[i] * this.Length[i]; |
|||
} |
|||
|
|||
return len; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scan a literal or distance tree to determine the frequencies of the codes
|
|||
/// in the bit length tree.
|
|||
/// </summary>
|
|||
public void CalcBLFreq(Tree blTree) |
|||
{ |
|||
int maxCount; // max repeat count
|
|||
int minCount; // min repeat count
|
|||
int count; // repeat count of the current code
|
|||
int curLen = -1; // length of current code
|
|||
|
|||
int i = 0; |
|||
while (i < this.NumCodes) |
|||
{ |
|||
count = 1; |
|||
int nextlen = this.Length[i]; |
|||
if (nextlen == 0) |
|||
{ |
|||
maxCount = 138; |
|||
minCount = 3; |
|||
} |
|||
else |
|||
{ |
|||
maxCount = 6; |
|||
minCount = 3; |
|||
if (curLen != nextlen) |
|||
{ |
|||
blTree.Frequencies[nextlen]++; |
|||
count = 0; |
|||
} |
|||
} |
|||
|
|||
curLen = nextlen; |
|||
i++; |
|||
|
|||
while (i < this.NumCodes && curLen == this.Length[i]) |
|||
{ |
|||
i++; |
|||
if (++count >= maxCount) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (count < minCount) |
|||
{ |
|||
blTree.Frequencies[curLen] += (short)count; |
|||
} |
|||
else if (curLen != 0) |
|||
{ |
|||
blTree.Frequencies[Repeat3To6]++; |
|||
} |
|||
else if (count <= 10) |
|||
{ |
|||
blTree.Frequencies[Repeat3To10]++; |
|||
} |
|||
else |
|||
{ |
|||
blTree.Frequencies[Repeat11To138]++; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write the tree values.
|
|||
/// </summary>
|
|||
/// <param name="pendingBuffer">The pending buffer.</param>
|
|||
/// <param name="bitLengthTree">The tree to write.</param>
|
|||
public void WriteTree(DeflaterPendingBuffer pendingBuffer, Tree bitLengthTree) |
|||
{ |
|||
int maxCount; // max repeat count
|
|||
int minCount; // min repeat count
|
|||
int count; // repeat count of the current code
|
|||
int curLen = -1; // length of current code
|
|||
|
|||
int i = 0; |
|||
while (i < this.NumCodes) |
|||
{ |
|||
count = 1; |
|||
int nextlen = this.Length[i]; |
|||
if (nextlen == 0) |
|||
{ |
|||
maxCount = 138; |
|||
minCount = 3; |
|||
} |
|||
else |
|||
{ |
|||
maxCount = 6; |
|||
minCount = 3; |
|||
if (curLen != nextlen) |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, nextlen); |
|||
count = 0; |
|||
} |
|||
} |
|||
|
|||
curLen = nextlen; |
|||
i++; |
|||
|
|||
while (i < this.NumCodes && curLen == this.Length[i]) |
|||
{ |
|||
i++; |
|||
if (++count >= maxCount) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (count < minCount) |
|||
{ |
|||
while (count-- > 0) |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, curLen); |
|||
} |
|||
} |
|||
else if (curLen != 0) |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To6); |
|||
pendingBuffer.WriteBits(count - 3, 2); |
|||
} |
|||
else if (count <= 10) |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To10); |
|||
pendingBuffer.WriteBits(count - 3, 3); |
|||
} |
|||
else |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, Repeat11To138); |
|||
pendingBuffer.WriteBits(count - 11, 7); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void BuildLength(ReadOnlySpan<int> children) |
|||
{ |
|||
byte* lengthPtr = this.Length; |
|||
ref int childrenRef = ref MemoryMarshal.GetReference(children); |
|||
ref int bitLengthCountsRef = ref MemoryMarshal.GetReference<int>(this.bitLengthCounts); |
|||
|
|||
int maxLen = this.maxLength; |
|||
int numNodes = children.Length >> 1; |
|||
int numLeafs = (numNodes + 1) >> 1; |
|||
int overflow = 0; |
|||
|
|||
Array.Clear(this.bitLengthCounts, 0, maxLen); |
|||
|
|||
// First calculate optimal bit lengths
|
|||
using (IMemoryOwner<int> lengthsMemoryOwner = this.memoryAllocator.Allocate<int>(numNodes, AllocationOptions.Clean)) |
|||
{ |
|||
ref int lengthsRef = ref MemoryMarshal.GetReference(lengthsMemoryOwner.Memory.Span); |
|||
|
|||
for (int i = numNodes - 1; i >= 0; i--) |
|||
{ |
|||
if (children[(2 * i) + 1] != -1) |
|||
{ |
|||
int bitLength = Unsafe.Add(ref lengthsRef, i) + 1; |
|||
if (bitLength > maxLen) |
|||
{ |
|||
bitLength = maxLen; |
|||
overflow++; |
|||
} |
|||
|
|||
Unsafe.Add(ref lengthsRef, Unsafe.Add(ref childrenRef, 2 * i)) = Unsafe.Add(ref lengthsRef, Unsafe.Add(ref childrenRef, (2 * i) + 1)) = bitLength; |
|||
} |
|||
else |
|||
{ |
|||
// A leaf node
|
|||
int bitLength = Unsafe.Add(ref lengthsRef, i); |
|||
Unsafe.Add(ref bitLengthCountsRef, bitLength - 1)++; |
|||
lengthPtr[Unsafe.Add(ref childrenRef, 2 * i)] = (byte)Unsafe.Add(ref lengthsRef, i); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (overflow == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
int incrBitLen = maxLen - 1; |
|||
do |
|||
{ |
|||
// Find the first bit length which could increase:
|
|||
while (Unsafe.Add(ref bitLengthCountsRef, --incrBitLen) == 0) |
|||
{ |
|||
} |
|||
|
|||
// Move this node one down and remove a corresponding
|
|||
// number of overflow nodes.
|
|||
do |
|||
{ |
|||
Unsafe.Add(ref bitLengthCountsRef, incrBitLen)--; |
|||
Unsafe.Add(ref bitLengthCountsRef, ++incrBitLen)++; |
|||
overflow -= 1 << (maxLen - 1 - incrBitLen); |
|||
} |
|||
while (overflow > 0 && incrBitLen < maxLen - 1); |
|||
} |
|||
while (overflow > 0); |
|||
|
|||
// We may have overshot above. Move some nodes from maxLength to
|
|||
// maxLength-1 in that case.
|
|||
Unsafe.Add(ref bitLengthCountsRef, maxLen - 1) += overflow; |
|||
Unsafe.Add(ref bitLengthCountsRef, maxLen - 2) -= overflow; |
|||
|
|||
// Now recompute all bit lengths, scanning in increasing
|
|||
// frequency. It is simpler to reconstruct all lengths instead of
|
|||
// fixing only the wrong ones. This idea is taken from 'ar'
|
|||
// written by Haruhiko Okumura.
|
|||
//
|
|||
// The nodes were inserted with decreasing frequency into the childs
|
|||
// array.
|
|||
int nodeIndex = 2 * numLeafs; |
|||
for (int bits = maxLen; bits != 0; bits--) |
|||
{ |
|||
int n = Unsafe.Add(ref bitLengthCountsRef, bits - 1); |
|||
while (n > 0) |
|||
{ |
|||
int childIndex = 2 * Unsafe.Add(ref childrenRef, nodeIndex++); |
|||
if (Unsafe.Add(ref childrenRef, childIndex + 1) == -1) |
|||
{ |
|||
// We found another leaf
|
|||
lengthPtr[Unsafe.Add(ref childrenRef, childIndex)] = (byte)bits; |
|||
n--; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.frequenciesMemoryHandle.Dispose(); |
|||
this.frequenciesMemoryOwner.Dispose(); |
|||
|
|||
this.lengthsMemoryHandle.Dispose(); |
|||
this.lengthsMemoryOwner.Dispose(); |
|||
|
|||
this.codesMemoryHandle.Dispose(); |
|||
this.codesMemoryOwner.Dispose(); |
|||
|
|||
this.frequenciesMemoryOwner = null; |
|||
this.lengthsMemoryOwner = null; |
|||
this.codesMemoryOwner = null; |
|||
|
|||
this.isDisposed = true; |
|||
} |
|||
|
|||
GC.SuppressFinalize(this); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,153 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Png.Zlib |
|||
{ |
|||
/// <summary>
|
|||
/// A special stream deflating or compressing the bytes that are
|
|||
/// written to it. It uses a Deflater to perform actual deflating.
|
|||
/// </summary>
|
|||
internal sealed class DeflaterOutputStream : Stream |
|||
{ |
|||
private const int BufferLength = 512; |
|||
private IManagedByteBuffer memoryOwner; |
|||
private readonly byte[] buffer; |
|||
private Deflater deflater; |
|||
private readonly Stream rawStream; |
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DeflaterOutputStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
/// <param name="rawStream">The output stream where deflated output is written.</param>
|
|||
/// <param name="compressionLevel">The compression level.</param>
|
|||
public DeflaterOutputStream(MemoryAllocator memoryAllocator, Stream rawStream, int compressionLevel) |
|||
{ |
|||
this.rawStream = rawStream; |
|||
this.memoryOwner = memoryAllocator.AllocateManagedByteBuffer(BufferLength); |
|||
this.buffer = this.memoryOwner.Array; |
|||
this.deflater = new Deflater(memoryAllocator, compressionLevel); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite => this.rawStream.CanWrite; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length => this.rawStream.Length; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Position |
|||
{ |
|||
get |
|||
{ |
|||
return this.rawStream.Position; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
throw new NotSupportedException(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void SetLength(long value) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int ReadByte() => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Flush() |
|||
{ |
|||
this.deflater.Flush(); |
|||
this.Deflate(true); |
|||
this.rawStream.Flush(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Write(byte[] buffer, int offset, int count) |
|||
{ |
|||
this.deflater.SetInput(buffer, offset, count); |
|||
this.Deflate(); |
|||
} |
|||
|
|||
private void Deflate() => this.Deflate(false); |
|||
|
|||
private void Deflate(bool flushing) |
|||
{ |
|||
while (flushing || !this.deflater.IsNeedingInput) |
|||
{ |
|||
int deflateCount = this.deflater.Deflate(this.buffer, 0, BufferLength); |
|||
|
|||
if (deflateCount <= 0) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
this.rawStream.Write(this.buffer, 0, deflateCount); |
|||
} |
|||
|
|||
if (!this.deflater.IsNeedingInput) |
|||
{ |
|||
DeflateThrowHelper.ThrowNoDeflate(); |
|||
} |
|||
} |
|||
|
|||
private void Finish() |
|||
{ |
|||
this.deflater.Finish(); |
|||
while (!this.deflater.IsFinished) |
|||
{ |
|||
int len = this.deflater.Deflate(this.buffer, 0, BufferLength); |
|||
if (len <= 0) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
this.rawStream.Write(this.buffer, 0, len); |
|||
} |
|||
|
|||
if (!this.deflater.IsFinished) |
|||
{ |
|||
DeflateThrowHelper.ThrowNoDeflate(); |
|||
} |
|||
|
|||
this.rawStream.Flush(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
if (disposing) |
|||
{ |
|||
this.Finish(); |
|||
this.deflater.Dispose(); |
|||
this.memoryOwner.Dispose(); |
|||
} |
|||
|
|||
this.deflater = null; |
|||
this.memoryOwner = null; |
|||
this.isDisposed = true; |
|||
base.Dispose(disposing); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,179 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Png.Zlib |
|||
{ |
|||
/// <summary>
|
|||
/// Stores pending data for writing data to the Deflater.
|
|||
/// </summary>
|
|||
internal sealed unsafe class DeflaterPendingBuffer : IDisposable |
|||
{ |
|||
private readonly byte[] buffer; |
|||
private readonly byte* pinnedBuffer; |
|||
private IManagedByteBuffer bufferMemoryOwner; |
|||
private MemoryHandle bufferMemoryHandle; |
|||
|
|||
private int start; |
|||
private int end; |
|||
private uint bits; |
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DeflaterPendingBuffer"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
public DeflaterPendingBuffer(MemoryAllocator memoryAllocator) |
|||
{ |
|||
this.bufferMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(DeflaterConstants.PENDING_BUF_SIZE); |
|||
this.buffer = this.bufferMemoryOwner.Array; |
|||
this.bufferMemoryHandle = this.bufferMemoryOwner.Memory.Pin(); |
|||
this.pinnedBuffer = (byte*)this.bufferMemoryHandle.Pointer; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of bits written to the buffer.
|
|||
/// </summary>
|
|||
public int BitCount { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether indicates the buffer has been flushed.
|
|||
/// </summary>
|
|||
public bool IsFlushed => this.end == 0; |
|||
|
|||
/// <summary>
|
|||
/// Clear internal state/buffers.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() => this.start = this.end = this.BitCount = 0; |
|||
|
|||
/// <summary>
|
|||
/// Write a short value to buffer LSB first.
|
|||
/// </summary>
|
|||
/// <param name="value">The value to write.</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteShort(int value) |
|||
{ |
|||
byte* pinned = this.pinnedBuffer; |
|||
pinned[this.end++] = unchecked((byte)value); |
|||
pinned[this.end++] = unchecked((byte)(value >> 8)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write a block of data to the internal buffer.
|
|||
/// </summary>
|
|||
/// <param name="block">The data to write.</param>
|
|||
/// <param name="offset">The offset of first byte to write.</param>
|
|||
/// <param name="length">The number of bytes to write.</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteBlock(byte[] block, int offset, int length) |
|||
{ |
|||
Unsafe.CopyBlockUnaligned(ref this.buffer[this.end], ref block[offset], unchecked((uint)length)); |
|||
this.end += length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Aligns internal buffer on a byte boundary.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void AlignToByte() |
|||
{ |
|||
if (this.BitCount > 0) |
|||
{ |
|||
byte* pinned = this.pinnedBuffer; |
|||
pinned[this.end++] = unchecked((byte)this.bits); |
|||
if (this.BitCount > 8) |
|||
{ |
|||
pinned[this.end++] = unchecked((byte)(this.bits >> 8)); |
|||
} |
|||
} |
|||
|
|||
this.bits = 0; |
|||
this.BitCount = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write bits to internal buffer
|
|||
/// </summary>
|
|||
/// <param name="b">source of bits</param>
|
|||
/// <param name="count">number of bits to write</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteBits(int b, int count) |
|||
{ |
|||
this.bits |= (uint)(b << this.BitCount); |
|||
this.BitCount += count; |
|||
if (this.BitCount >= 16) |
|||
{ |
|||
byte* pinned = this.pinnedBuffer; |
|||
pinned[this.end++] = unchecked((byte)this.bits); |
|||
pinned[this.end++] = unchecked((byte)(this.bits >> 8)); |
|||
this.bits >>= 16; |
|||
this.BitCount -= 16; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write a short value to internal buffer most significant byte first
|
|||
/// </summary>
|
|||
/// <param name="value">The value to write</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteShortMSB(int value) |
|||
{ |
|||
byte* pinned = this.pinnedBuffer; |
|||
pinned[this.end++] = unchecked((byte)(value >> 8)); |
|||
pinned[this.end++] = unchecked((byte)value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Flushes the pending buffer into the given output array.
|
|||
/// If the output array is to small, only a partial flush is done.
|
|||
/// </summary>
|
|||
/// <param name="output">The output array.</param>
|
|||
/// <param name="offset">The offset into output array.</param>
|
|||
/// <param name="length">The maximum number of bytes to store.</param>
|
|||
/// <returns>The number of bytes flushed.</returns>
|
|||
public int Flush(byte[] output, int offset, int length) |
|||
{ |
|||
if (this.BitCount >= 8) |
|||
{ |
|||
this.pinnedBuffer[this.end++] = unchecked((byte)this.bits); |
|||
this.bits >>= 8; |
|||
this.BitCount -= 8; |
|||
} |
|||
|
|||
if (length > this.end - this.start) |
|||
{ |
|||
length = this.end - this.start; |
|||
|
|||
Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length)); |
|||
this.start = 0; |
|||
this.end = 0; |
|||
} |
|||
else |
|||
{ |
|||
Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length)); |
|||
this.start += length; |
|||
} |
|||
|
|||
return length; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.bufferMemoryHandle.Dispose(); |
|||
this.bufferMemoryOwner.Dispose(); |
|||
this.bufferMemoryOwner = null; |
|||
this.isDisposed = true; |
|||
} |
|||
|
|||
GC.SuppressFinalize(this); |
|||
} |
|||
} |
|||
} |
|||
@ -1,2 +1,5 @@ |
|||
Adler32.cs and Crc32.cs have been copied from |
|||
https://github.com/ygrenier/SharpZipLib.Portable |
|||
Deflatestream implementation adapted from |
|||
|
|||
https://github.com/icsharpcode/SharpZipLib |
|||
|
|||
LIcensed under MIT |
|||
|
|||
@ -0,0 +1,6 @@ |
|||
# Encoder/Decoder for true vision targa files |
|||
|
|||
Useful links for reference: |
|||
|
|||
- [FileFront](https://www.fileformat.info/format/tga/egff.htm) |
|||
- [Tga Specification](http://www.dca.fee.unicamp.br/~martino/disciplinas/ea978/tgaffs.pdf) |
|||
@ -0,0 +1,12 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// The options for decoding tga images. Currently empty, but this may change in the future.
|
|||
/// </summary>
|
|||
internal interface ITgaDecoderOptions |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Configuration options for use during tga encoding.
|
|||
/// </summary>
|
|||
internal interface ITgaEncoderOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the number of bits per pixel.
|
|||
/// </summary>
|
|||
TgaBitsPerPixel? BitsPerPixel { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether run length compression should be used.
|
|||
/// </summary>
|
|||
TgaCompression Compression { get; } |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,31 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Enumerates the available bits per pixel the tga encoder supports.
|
|||
/// </summary>
|
|||
public enum TgaBitsPerPixel : byte |
|||
{ |
|||
/// <summary>
|
|||
/// 8 bits per pixel. Each pixel consists of 1 byte.
|
|||
/// </summary>
|
|||
Pixel8 = 8, |
|||
|
|||
/// <summary>
|
|||
/// 16 bits per pixel. Each pixel consists of 2 bytes.
|
|||
/// </summary>
|
|||
Pixel16 = 16, |
|||
|
|||
/// <summary>
|
|||
/// 24 bits per pixel. Each pixel consists of 3 bytes.
|
|||
/// </summary>
|
|||
Pixel24 = 24, |
|||
|
|||
/// <summary>
|
|||
/// 32 bits per pixel. Each pixel consists of 4 bytes.
|
|||
/// </summary>
|
|||
Pixel32 = 32 |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Indicates if compression is used.
|
|||
/// </summary>
|
|||
public enum TgaCompression |
|||
{ |
|||
/// <summary>
|
|||
/// No compression is used.
|
|||
/// </summary>
|
|||
None, |
|||
|
|||
/// <summary>
|
|||
/// Run length encoding is used.
|
|||
/// </summary>
|
|||
RunLength, |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Registers the image encoders, decoders and mime type detectors for the tga format.
|
|||
/// </summary>
|
|||
public sealed class TgaConfigurationModule : IConfigurationModule |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public void Configure(Configuration configuration) |
|||
{ |
|||
configuration.ImageFormatsManager.SetEncoder(TgaFormat.Instance, new TgaEncoder()); |
|||
configuration.ImageFormatsManager.SetDecoder(TgaFormat.Instance, new TgaDecoder()); |
|||
configuration.ImageFormatsManager.AddImageFormatDetector(new TgaImageFormatDetector()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
internal static class TgaConstants |
|||
{ |
|||
/// <summary>
|
|||
/// The list of mimetypes that equate to a targa file.
|
|||
/// </summary>
|
|||
public static readonly IEnumerable<string> MimeTypes = new[] { "image/x-tga", "image/x-targa" }; |
|||
|
|||
/// <summary>
|
|||
/// The list of file extensions that equate to a targa file.
|
|||
/// </summary>
|
|||
public static readonly IEnumerable<string> FileExtensions = new[] { "tga", "vda", "icb", "vst" }; |
|||
|
|||
/// <summary>
|
|||
/// The file header length of a tga image in bytes.
|
|||
/// </summary>
|
|||
public const int FileHeaderLength = 18; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.IO; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Image decoder for Truevision TGA images.
|
|||
/// </summary>
|
|||
public sealed class TgaDecoder : IImageDecoder, ITgaDecoderOptions, IImageInfoDetector |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
return new TgaDecoderCore(configuration, this).Decode<TPixel>(stream); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public Image Decode(Configuration configuration, Stream stream) => this.Decode<Rgba32>(configuration, stream); |
|||
|
|||
/// <inheritdoc/>
|
|||
public IImageInfo Identify(Configuration configuration, Stream stream) |
|||
{ |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
return new TgaDecoderCore(configuration, this).Identify(stream); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,588 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.IO; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
internal sealed class TgaDecoderCore |
|||
{ |
|||
/// <summary>
|
|||
/// The metadata.
|
|||
/// </summary>
|
|||
private ImageMetadata metadata; |
|||
|
|||
/// <summary>
|
|||
/// The tga specific metadata.
|
|||
/// </summary>
|
|||
private TgaMetadata tgaMetadata; |
|||
|
|||
/// <summary>
|
|||
/// The file header containing general information about the image.
|
|||
/// </summary>
|
|||
private TgaFileHeader fileHeader; |
|||
|
|||
/// <summary>
|
|||
/// The global configuration.
|
|||
/// </summary>
|
|||
private readonly Configuration configuration; |
|||
|
|||
/// <summary>
|
|||
/// Used for allocating memory during processing operations.
|
|||
/// </summary>
|
|||
private readonly MemoryAllocator memoryAllocator; |
|||
|
|||
/// <summary>
|
|||
/// The stream to decode from.
|
|||
/// </summary>
|
|||
private Stream currentStream; |
|||
|
|||
/// <summary>
|
|||
/// The bitmap decoder options.
|
|||
/// </summary>
|
|||
private readonly ITgaDecoderOptions options; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TgaDecoderCore"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="options">The options.</param>
|
|||
public TgaDecoderCore(Configuration configuration, ITgaDecoderOptions options) |
|||
{ |
|||
this.configuration = configuration; |
|||
this.memoryAllocator = configuration.MemoryAllocator; |
|||
this.options = options; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Decodes the image from the specified stream.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
|
|||
/// <exception cref="System.ArgumentNullException">
|
|||
/// <para><paramref name="stream"/> is null.</para>
|
|||
/// </exception>
|
|||
/// <returns>The decoded image.</returns>
|
|||
public Image<TPixel> Decode<TPixel>(Stream stream) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
try |
|||
{ |
|||
bool inverted = this.ReadFileHeader(stream); |
|||
this.currentStream.Skip(this.fileHeader.IdLength); |
|||
|
|||
// Parse the color map, if present.
|
|||
if (this.fileHeader.ColorMapType != 0 && this.fileHeader.ColorMapType != 1) |
|||
{ |
|||
TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); |
|||
} |
|||
|
|||
if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0) |
|||
{ |
|||
throw new UnknownImageFormatException("Width or height cannot be 0"); |
|||
} |
|||
|
|||
var image = new Image<TPixel>(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); |
|||
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer(); |
|||
|
|||
if (this.fileHeader.ColorMapType is 1) |
|||
{ |
|||
if (this.fileHeader.CMapLength <= 0) |
|||
{ |
|||
TgaThrowHelper.ThrowImageFormatException("Missing tga color map length"); |
|||
} |
|||
|
|||
if (this.fileHeader.CMapDepth <= 0) |
|||
{ |
|||
TgaThrowHelper.ThrowImageFormatException("Missing tga color map depth"); |
|||
} |
|||
|
|||
int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; |
|||
int colorMapSizeInBytes = this.fileHeader.CMapLength * colorMapPixelSizeInBytes; |
|||
using (IManagedByteBuffer palette = this.memoryAllocator.AllocateManagedByteBuffer(colorMapSizeInBytes, AllocationOptions.Clean)) |
|||
{ |
|||
this.currentStream.Read(palette.Array, this.fileHeader.CMapStart, colorMapSizeInBytes); |
|||
|
|||
if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) |
|||
{ |
|||
this.ReadPalettedRle( |
|||
this.fileHeader.Width, |
|||
this.fileHeader.Height, |
|||
pixels, |
|||
palette.Array, |
|||
colorMapPixelSizeInBytes, |
|||
inverted); |
|||
} |
|||
else |
|||
{ |
|||
this.ReadPaletted( |
|||
this.fileHeader.Width, |
|||
this.fileHeader.Height, |
|||
pixels, |
|||
palette.Array, |
|||
colorMapPixelSizeInBytes, |
|||
inverted); |
|||
} |
|||
} |
|||
|
|||
return image; |
|||
} |
|||
|
|||
// Even if the image type indicates it is not a paletted image, it can still contain a palette. Skip those bytes.
|
|||
if (this.fileHeader.CMapLength > 0) |
|||
{ |
|||
int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; |
|||
this.currentStream.Skip(this.fileHeader.CMapLength * colorMapPixelSizeInBytes); |
|||
} |
|||
|
|||
switch (this.fileHeader.PixelDepth) |
|||
{ |
|||
case 8: |
|||
if (this.fileHeader.ImageType.IsRunLengthEncoded()) |
|||
{ |
|||
this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1, inverted); |
|||
} |
|||
else |
|||
{ |
|||
this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 15: |
|||
case 16: |
|||
if (this.fileHeader.ImageType.IsRunLengthEncoded()) |
|||
{ |
|||
this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2, inverted); |
|||
} |
|||
else |
|||
{ |
|||
this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 24: |
|||
if (this.fileHeader.ImageType.IsRunLengthEncoded()) |
|||
{ |
|||
this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3, inverted); |
|||
} |
|||
else |
|||
{ |
|||
this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 32: |
|||
if (this.fileHeader.ImageType.IsRunLengthEncoded()) |
|||
{ |
|||
this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4, inverted); |
|||
} |
|||
else |
|||
{ |
|||
this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); |
|||
} |
|||
|
|||
break; |
|||
|
|||
default: |
|||
TgaThrowHelper.ThrowNotSupportedException("Does not support this kind of tga files."); |
|||
break; |
|||
} |
|||
|
|||
return image; |
|||
} |
|||
catch (IndexOutOfRangeException e) |
|||
{ |
|||
throw new ImageFormatException("TGA image does not have a valid format.", e); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a uncompressed TGA image with a palette.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <param name="width">The width of the image.</param>
|
|||
/// <param name="height">The height of the image.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|||
/// <param name="palette">The color palette.</param>
|
|||
/// <param name="colorMapPixelSizeInBytes">Color map size of one entry in bytes.</param>
|
|||
/// <param name="inverted">Indicates, if the origin of the image is top left rather the bottom left (the default).</param>
|
|||
private void ReadPaletted<TPixel>(int width, int height, Buffer2D<TPixel> pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(width, AllocationOptions.Clean)) |
|||
{ |
|||
TPixel color = default; |
|||
Span<byte> rowSpan = row.GetSpan(); |
|||
|
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
this.currentStream.Read(row); |
|||
int newY = Invert(y, height, inverted); |
|||
Span<TPixel> pixelRow = pixels.GetRowSpan(newY); |
|||
switch (colorMapPixelSizeInBytes) |
|||
{ |
|||
case 2: |
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
int colorIndex = rowSpan[x]; |
|||
|
|||
// Set alpha value to 1, to treat it as opaque for Bgra5551.
|
|||
Bgra5551 bgra = Unsafe.As<byte, Bgra5551>(ref palette[colorIndex * colorMapPixelSizeInBytes]); |
|||
bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); |
|||
color.FromBgra5551(bgra); |
|||
pixelRow[x] = color; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 3: |
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
int colorIndex = rowSpan[x]; |
|||
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref palette[colorIndex * colorMapPixelSizeInBytes])); |
|||
pixelRow[x] = color; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 4: |
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
int colorIndex = rowSpan[x]; |
|||
color.FromBgra32(Unsafe.As<byte, Bgra32>(ref palette[colorIndex * colorMapPixelSizeInBytes])); |
|||
pixelRow[x] = color; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a run length encoded TGA image with a palette.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <param name="width">The width of the image.</param>
|
|||
/// <param name="height">The height of the image.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|||
/// <param name="palette">The color palette.</param>
|
|||
/// <param name="colorMapPixelSizeInBytes">Color map size of one entry in bytes.</param>
|
|||
/// <param name="inverted">Indicates, if the origin of the image is top left rather the bottom left (the default).</param>
|
|||
private void ReadPalettedRle<TPixel>(int width, int height, Buffer2D<TPixel> pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
int bytesPerPixel = 1; |
|||
using (IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(width * height * bytesPerPixel, AllocationOptions.Clean)) |
|||
{ |
|||
TPixel color = default; |
|||
Span<byte> bufferSpan = buffer.GetSpan(); |
|||
this.UncompressRle(width, height, bufferSpan, bytesPerPixel: 1); |
|||
|
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
int newY = Invert(y, height, inverted); |
|||
Span<TPixel> pixelRow = pixels.GetRowSpan(newY); |
|||
int rowStartIdx = y * width * bytesPerPixel; |
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
int idx = rowStartIdx + x; |
|||
switch (colorMapPixelSizeInBytes) |
|||
{ |
|||
case 1: |
|||
color.FromGray8(Unsafe.As<byte, Gray8>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); |
|||
break; |
|||
case 2: |
|||
// Set alpha value to 1, to treat it as opaque for Bgra5551.
|
|||
Bgra5551 bgra = Unsafe.As<byte, Bgra5551>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes]); |
|||
bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); |
|||
color.FromBgra5551(bgra); |
|||
break; |
|||
case 3: |
|||
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); |
|||
break; |
|||
case 4: |
|||
color.FromBgra32(Unsafe.As<byte, Bgra32>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); |
|||
break; |
|||
} |
|||
|
|||
pixelRow[x] = color; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a uncompressed monochrome TGA image.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <param name="width">The width of the image.</param>
|
|||
/// <param name="height">The height of the image.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|||
/// <param name="inverted">Indicates, if the origin of the image is top left rather the bottom left (the default).</param>
|
|||
private void ReadMonoChrome<TPixel>(int width, int height, Buffer2D<TPixel> pixels, bool inverted) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 1, 0)) |
|||
{ |
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
this.currentStream.Read(row); |
|||
int newY = Invert(y, height, inverted); |
|||
Span<TPixel> pixelSpan = pixels.GetRowSpan(newY); |
|||
PixelOperations<TPixel>.Instance.FromGray8Bytes( |
|||
this.configuration, |
|||
row.GetSpan(), |
|||
pixelSpan, |
|||
width); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a uncompressed TGA image where each pixels has 16 bit.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <param name="width">The width of the image.</param>
|
|||
/// <param name="height">The height of the image.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|||
/// <param name="inverted">Indicates, if the origin of the image is top left rather the bottom left (the default).</param>
|
|||
private void ReadBgra16<TPixel>(int width, int height, Buffer2D<TPixel> pixels, bool inverted) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) |
|||
{ |
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
this.currentStream.Read(row); |
|||
Span<byte> rowSpan = row.GetSpan(); |
|||
|
|||
// We need to set each alpha component value to fully opaque.
|
|||
for (int x = 1; x < rowSpan.Length; x += 2) |
|||
{ |
|||
rowSpan[x] = (byte)(rowSpan[x] | (1 << 7)); |
|||
} |
|||
|
|||
int newY = Invert(y, height, inverted); |
|||
Span<TPixel> pixelSpan = pixels.GetRowSpan(newY); |
|||
PixelOperations<TPixel>.Instance.FromBgra5551Bytes( |
|||
this.configuration, |
|||
rowSpan, |
|||
pixelSpan, |
|||
width); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a uncompressed TGA image where each pixels has 24 bit.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <param name="width">The width of the image.</param>
|
|||
/// <param name="height">The height of the image.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|||
/// <param name="inverted">Indicates, if the origin of the image is top left rather the bottom left (the default).</param>
|
|||
private void ReadBgr24<TPixel>(int width, int height, Buffer2D<TPixel> pixels, bool inverted) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, 0)) |
|||
{ |
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
this.currentStream.Read(row); |
|||
int newY = Invert(y, height, inverted); |
|||
Span<TPixel> pixelSpan = pixels.GetRowSpan(newY); |
|||
PixelOperations<TPixel>.Instance.FromBgr24Bytes( |
|||
this.configuration, |
|||
row.GetSpan(), |
|||
pixelSpan, |
|||
width); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a uncompressed TGA image where each pixels has 32 bit.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <param name="width">The width of the image.</param>
|
|||
/// <param name="height">The height of the image.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|||
/// <param name="inverted">Indicates, if the origin of the image is top left rather the bottom left (the default).</param>
|
|||
private void ReadBgra32<TPixel>(int width, int height, Buffer2D<TPixel> pixels, bool inverted) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) |
|||
{ |
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
this.currentStream.Read(row); |
|||
int newY = Invert(y, height, inverted); |
|||
Span<TPixel> pixelSpan = pixels.GetRowSpan(newY); |
|||
PixelOperations<TPixel>.Instance.FromBgra32Bytes( |
|||
this.configuration, |
|||
row.GetSpan(), |
|||
pixelSpan, |
|||
width); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a run length encoded TGA image.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <param name="width">The width of the image.</param>
|
|||
/// <param name="height">The height of the image.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|||
/// <param name="bytesPerPixel">The bytes per pixel.</param>
|
|||
/// <param name="inverted">Indicates, if the origin of the image is top left rather the bottom left (the default).</param>
|
|||
private void ReadRle<TPixel>(int width, int height, Buffer2D<TPixel> pixels, int bytesPerPixel, bool inverted) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
TPixel color = default; |
|||
using (IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(width * height * bytesPerPixel, AllocationOptions.Clean)) |
|||
{ |
|||
Span<byte> bufferSpan = buffer.GetSpan(); |
|||
this.UncompressRle(width, height, bufferSpan, bytesPerPixel); |
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
int newY = Invert(y, height, inverted); |
|||
Span<TPixel> pixelRow = pixels.GetRowSpan(newY); |
|||
int rowStartIdx = y * width * bytesPerPixel; |
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
int idx = rowStartIdx + (x * bytesPerPixel); |
|||
switch (bytesPerPixel) |
|||
{ |
|||
case 1: |
|||
color.FromGray8(Unsafe.As<byte, Gray8>(ref bufferSpan[idx])); |
|||
break; |
|||
case 2: |
|||
// Set alpha value to 1, to treat it as opaque for Bgra5551.
|
|||
bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128); |
|||
color.FromBgra5551(Unsafe.As<byte, Bgra5551>(ref bufferSpan[idx])); |
|||
break; |
|||
case 3: |
|||
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref bufferSpan[idx])); |
|||
break; |
|||
case 4: |
|||
color.FromBgra32(Unsafe.As<byte, Bgra32>(ref bufferSpan[idx])); |
|||
break; |
|||
} |
|||
|
|||
pixelRow[x] = color; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads the raw image information from the specified stream.
|
|||
/// </summary>
|
|||
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
|
|||
public IImageInfo Identify(Stream stream) |
|||
{ |
|||
this.ReadFileHeader(stream); |
|||
return new ImageInfo( |
|||
new PixelTypeInfo(this.fileHeader.PixelDepth), |
|||
this.fileHeader.Width, |
|||
this.fileHeader.Height, |
|||
this.metadata); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Produce uncompressed tga data from a run length encoded stream.
|
|||
/// </summary>
|
|||
/// <param name="width">The width of the image.</param>
|
|||
/// <param name="height">The height of the image.</param>
|
|||
/// <param name="buffer">Buffer for uncompressed data.</param>
|
|||
/// <param name="bytesPerPixel">The bytes used per pixel.</param>
|
|||
private void UncompressRle(int width, int height, Span<byte> buffer, int bytesPerPixel) |
|||
{ |
|||
int uncompressedPixels = 0; |
|||
var pixel = new byte[bytesPerPixel]; |
|||
int totalPixels = width * height; |
|||
while (uncompressedPixels < totalPixels) |
|||
{ |
|||
byte runLengthByte = (byte)this.currentStream.ReadByte(); |
|||
|
|||
// The high bit of a run length packet is set to 1.
|
|||
int highBit = runLengthByte >> 7; |
|||
if (highBit == 1) |
|||
{ |
|||
int runLength = runLengthByte & 127; |
|||
this.currentStream.Read(pixel, 0, bytesPerPixel); |
|||
int bufferIdx = uncompressedPixels * bytesPerPixel; |
|||
for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) |
|||
{ |
|||
pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); |
|||
bufferIdx += bytesPerPixel; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Non-run-length encoded packet.
|
|||
int runLength = runLengthByte; |
|||
int bufferIdx = uncompressedPixels * bytesPerPixel; |
|||
for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) |
|||
{ |
|||
this.currentStream.Read(pixel, 0, bytesPerPixel); |
|||
pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); |
|||
bufferIdx += bytesPerPixel; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the y- value based on the given height.
|
|||
/// </summary>
|
|||
/// <param name="y">The y- value representing the current row.</param>
|
|||
/// <param name="height">The height of the bitmap.</param>
|
|||
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|||
/// <returns>The <see cref="int"/> representing the inverted value.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y; |
|||
|
|||
/// <summary>
|
|||
/// Reads the tga file header from the stream.
|
|||
/// </summary>
|
|||
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
|
|||
/// <returns>true, if the image origin is top left.</returns>
|
|||
private bool ReadFileHeader(Stream stream) |
|||
{ |
|||
this.currentStream = stream; |
|||
|
|||
#if NETCOREAPP2_1
|
|||
Span<byte> buffer = stackalloc byte[TgaFileHeader.Size]; |
|||
#else
|
|||
var buffer = new byte[TgaFileHeader.Size]; |
|||
#endif
|
|||
this.currentStream.Read(buffer, 0, TgaFileHeader.Size); |
|||
this.fileHeader = TgaFileHeader.Parse(buffer); |
|||
this.metadata = new ImageMetadata(); |
|||
this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); |
|||
this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; |
|||
|
|||
// Bit at position 5 of the descriptor indicates, that the origin is top left instead of bottom right.
|
|||
if ((this.fileHeader.ImageDescriptor & (1 << 5)) != 0) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.IO; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Image encoder for writing an image to a stream as a targa truevision image.
|
|||
/// </summary>
|
|||
public sealed class TgaEncoder : IImageEncoder, ITgaEncoderOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the number of bits per pixel.
|
|||
/// </summary>
|
|||
public TgaBitsPerPixel? BitsPerPixel { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether no compression or run length compression should be used.
|
|||
/// </summary>
|
|||
public TgaCompression Compression { get; set; } = TgaCompression.RunLength; |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Encode<TPixel>(Image<TPixel> image, Stream stream) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); |
|||
encoder.Encode(image, stream); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,348 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using System.IO; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Image encoder for writing an image to a stream as a truevision targa image.
|
|||
/// </summary>
|
|||
internal sealed class TgaEncoderCore |
|||
{ |
|||
/// <summary>
|
|||
/// Used for allocating memory during processing operations.
|
|||
/// </summary>
|
|||
private readonly MemoryAllocator memoryAllocator; |
|||
|
|||
/// <summary>
|
|||
/// The global configuration.
|
|||
/// </summary>
|
|||
private Configuration configuration; |
|||
|
|||
/// <summary>
|
|||
/// Reusable buffer for writing data.
|
|||
/// </summary>
|
|||
private readonly byte[] buffer = new byte[2]; |
|||
|
|||
/// <summary>
|
|||
/// The color depth, in number of bits per pixel.
|
|||
/// </summary>
|
|||
private TgaBitsPerPixel? bitsPerPixel; |
|||
|
|||
/// <summary>
|
|||
/// Indicates if run length compression should be used.
|
|||
/// </summary>
|
|||
private readonly TgaCompression compression; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TgaEncoderCore"/> class.
|
|||
/// </summary>
|
|||
/// <param name="options">The encoder options.</param>
|
|||
/// <param name="memoryAllocator">The memory manager.</param>
|
|||
public TgaEncoderCore(ITgaEncoderOptions options, MemoryAllocator memoryAllocator) |
|||
{ |
|||
this.memoryAllocator = memoryAllocator; |
|||
this.bitsPerPixel = options.BitsPerPixel; |
|||
this.compression = options.Compression; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Encodes the image to the specified stream from the <see cref="ImageFrame{TPixel}"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
|
|||
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
|
|||
public void Encode<TPixel>(Image<TPixel> image, Stream stream) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
Guard.NotNull(image, nameof(image)); |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
this.configuration = image.GetConfiguration(); |
|||
ImageMetadata metadata = image.Metadata; |
|||
TgaMetadata tgaMetadata = metadata.GetFormatMetadata(TgaFormat.Instance); |
|||
this.bitsPerPixel = this.bitsPerPixel ?? tgaMetadata.BitsPerPixel; |
|||
|
|||
TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; |
|||
if (this.bitsPerPixel == TgaBitsPerPixel.Pixel8) |
|||
{ |
|||
imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; |
|||
} |
|||
|
|||
// If compression is used, set bit 5 of the image descriptor to indicate an left top origin.
|
|||
byte imageDescriptor = (byte)(this.compression is TgaCompression.RunLength ? 32 : 0); |
|||
|
|||
var fileHeader = new TgaFileHeader( |
|||
idLength: 0, |
|||
colorMapType: 0, |
|||
imageType: imageType, |
|||
cMapStart: 0, |
|||
cMapLength: 0, |
|||
cMapDepth: 0, |
|||
xOffset: 0, |
|||
yOffset: this.compression is TgaCompression.RunLength ? (short)image.Height : (short)0, // When run length encoding is used, the origin should be top left instead of the default bottom left.
|
|||
width: (short)image.Width, |
|||
height: (short)image.Height, |
|||
pixelDepth: (byte)this.bitsPerPixel.Value, |
|||
imageDescriptor: imageDescriptor); |
|||
|
|||
#if NETCOREAPP2_1
|
|||
Span<byte> buffer = stackalloc byte[TgaFileHeader.Size]; |
|||
#else
|
|||
byte[] buffer = new byte[TgaFileHeader.Size]; |
|||
#endif
|
|||
fileHeader.WriteTo(buffer); |
|||
|
|||
stream.Write(buffer, 0, TgaFileHeader.Size); |
|||
|
|||
if (this.compression is TgaCompression.RunLength) |
|||
{ |
|||
this.WriteRunLengthEndcodedImage(stream, image.Frames.RootFrame); |
|||
} |
|||
else |
|||
{ |
|||
this.WriteImage(stream, image.Frames.RootFrame); |
|||
} |
|||
|
|||
stream.Flush(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the pixel data to the binary stream.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
|
|||
/// <param name="image">
|
|||
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
|
|||
/// </param>
|
|||
private void WriteImage<TPixel>(Stream stream, ImageFrame<TPixel> image) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
Buffer2D<TPixel> pixels = image.PixelBuffer; |
|||
switch (this.bitsPerPixel) |
|||
{ |
|||
case TgaBitsPerPixel.Pixel8: |
|||
this.Write8Bit(stream, pixels); |
|||
break; |
|||
|
|||
case TgaBitsPerPixel.Pixel16: |
|||
this.Write16Bit(stream, pixels); |
|||
break; |
|||
|
|||
case TgaBitsPerPixel.Pixel24: |
|||
this.Write24Bit(stream, pixels); |
|||
break; |
|||
|
|||
case TgaBitsPerPixel.Pixel32: |
|||
this.Write32Bit(stream, pixels); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes a run length encoded tga image to the stream.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <param name="stream">The stream to write the image to.</param>
|
|||
/// <param name="image">The image to encode.</param>
|
|||
private void WriteRunLengthEndcodedImage<TPixel>(Stream stream, ImageFrame<TPixel> image) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
Rgba32 color = default; |
|||
Buffer2D<TPixel> pixels = image.PixelBuffer; |
|||
Span<TPixel> pixelSpan = pixels.GetSpan(); |
|||
int totalPixels = image.Width * image.Height; |
|||
int encodedPixels = 0; |
|||
while (encodedPixels < totalPixels) |
|||
{ |
|||
TPixel currentPixel = pixelSpan[encodedPixels]; |
|||
currentPixel.ToRgba32(ref color); |
|||
byte equalPixelCount = this.FindEqualPixels(pixelSpan.Slice(encodedPixels)); |
|||
|
|||
// Write the number of equal pixels, with the high bit set, indicating ist a compressed pixel run.
|
|||
stream.WriteByte((byte)(equalPixelCount | 128)); |
|||
switch (this.bitsPerPixel) |
|||
{ |
|||
case TgaBitsPerPixel.Pixel8: |
|||
int luminance = GetLuminance(currentPixel); |
|||
stream.WriteByte((byte)luminance); |
|||
break; |
|||
|
|||
case TgaBitsPerPixel.Pixel16: |
|||
var bgra5551 = new Bgra5551(color.ToVector4()); |
|||
BinaryPrimitives.TryWriteInt16LittleEndian(this.buffer, (short)bgra5551.PackedValue); |
|||
stream.WriteByte(this.buffer[0]); |
|||
stream.WriteByte(this.buffer[1]); |
|||
|
|||
break; |
|||
|
|||
case TgaBitsPerPixel.Pixel24: |
|||
stream.WriteByte(color.B); |
|||
stream.WriteByte(color.G); |
|||
stream.WriteByte(color.R); |
|||
break; |
|||
|
|||
case TgaBitsPerPixel.Pixel32: |
|||
stream.WriteByte(color.B); |
|||
stream.WriteByte(color.G); |
|||
stream.WriteByte(color.R); |
|||
stream.WriteByte(color.A); |
|||
break; |
|||
} |
|||
|
|||
encodedPixels += equalPixelCount + 1; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Finds consecutive pixels, which have the same value starting from the pixel span offset 0.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <param name="pixelSpan">The pixel span to search in.</param>
|
|||
/// <returns>The number of equal pixels.</returns>
|
|||
private byte FindEqualPixels<TPixel>(Span<TPixel> pixelSpan) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
int idx = 0; |
|||
byte equalPixelCount = 0; |
|||
while (equalPixelCount < 127 && idx < pixelSpan.Length - 1) |
|||
{ |
|||
TPixel currentPixel = pixelSpan[idx]; |
|||
TPixel nextPixel = pixelSpan[idx + 1]; |
|||
if (currentPixel.Equals(nextPixel)) |
|||
{ |
|||
equalPixelCount++; |
|||
} |
|||
else |
|||
{ |
|||
return equalPixelCount; |
|||
} |
|||
|
|||
idx++; |
|||
} |
|||
|
|||
return equalPixelCount; |
|||
} |
|||
|
|||
private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, 0); |
|||
|
|||
/// <summary>
|
|||
/// Writes the 8bit pixels uncompressed to the stream.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
|
|||
private void Write8Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 1)) |
|||
{ |
|||
for (int y = pixels.Height - 1; y >= 0; y--) |
|||
{ |
|||
Span<TPixel> pixelSpan = pixels.GetRowSpan(y); |
|||
PixelOperations<TPixel>.Instance.ToGray8Bytes( |
|||
this.configuration, |
|||
pixelSpan, |
|||
row.GetSpan(), |
|||
pixelSpan.Length); |
|||
stream.Write(row.Array, 0, row.Length()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the 16bit pixels uncompressed to the stream.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
|
|||
private void Write16Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 2)) |
|||
{ |
|||
for (int y = pixels.Height - 1; y >= 0; y--) |
|||
{ |
|||
Span<TPixel> pixelSpan = pixels.GetRowSpan(y); |
|||
PixelOperations<TPixel>.Instance.ToBgra5551Bytes( |
|||
this.configuration, |
|||
pixelSpan, |
|||
row.GetSpan(), |
|||
pixelSpan.Length); |
|||
stream.Write(row.Array, 0, row.Length()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the 24bit pixels uncompressed to the stream.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
|
|||
private void Write24Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 3)) |
|||
{ |
|||
for (int y = pixels.Height - 1; y >= 0; y--) |
|||
{ |
|||
Span<TPixel> pixelSpan = pixels.GetRowSpan(y); |
|||
PixelOperations<TPixel>.Instance.ToBgr24Bytes( |
|||
this.configuration, |
|||
pixelSpan, |
|||
row.GetSpan(), |
|||
pixelSpan.Length); |
|||
stream.Write(row.Array, 0, row.Length()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the 32bit pixels uncompressed to the stream.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
|
|||
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
|
|||
private void Write32Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 4)) |
|||
{ |
|||
for (int y = pixels.Height - 1; y >= 0; y--) |
|||
{ |
|||
Span<TPixel> pixelSpan = pixels.GetRowSpan(y); |
|||
PixelOperations<TPixel>.Instance.ToBgra32Bytes( |
|||
this.configuration, |
|||
pixelSpan, |
|||
row.GetSpan(), |
|||
pixelSpan.Length); |
|||
stream.Write(row.Array, 0, row.Length()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Convert the pixel values to grayscale using ITU-R Recommendation BT.709.
|
|||
/// </summary>
|
|||
/// <param name="sourcePixel">The pixel to get the luminance from.</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static int GetLuminance<TPixel>(TPixel sourcePixel) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
var vector = sourcePixel.ToVector4(); |
|||
return ImageMaths.GetBT709Luminance(ref vector, 256); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,147 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// This block of bytes tells the application detailed information about the targa image.
|
|||
/// <see href="https://www.fileformat.info/format/tga/egff.htm"/>
|
|||
/// </summary>
|
|||
[StructLayout(LayoutKind.Sequential, Pack = 1)] |
|||
internal readonly struct TgaFileHeader |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the size of the data structure in the targa file.
|
|||
/// </summary>
|
|||
public const int Size = TgaConstants.FileHeaderLength; |
|||
|
|||
public TgaFileHeader( |
|||
byte idLength, |
|||
byte colorMapType, |
|||
TgaImageType imageType, |
|||
short cMapStart, |
|||
short cMapLength, |
|||
byte cMapDepth, |
|||
short xOffset, |
|||
short yOffset, |
|||
short width, |
|||
short height, |
|||
byte pixelDepth, |
|||
byte imageDescriptor) |
|||
{ |
|||
this.IdLength = idLength; |
|||
this.ColorMapType = colorMapType; |
|||
this.ImageType = imageType; |
|||
this.CMapStart = cMapStart; |
|||
this.CMapLength = cMapLength; |
|||
this.CMapDepth = cMapDepth; |
|||
this.XOffset = xOffset; |
|||
this.YOffset = yOffset; |
|||
this.Width = width; |
|||
this.Height = height; |
|||
this.PixelDepth = pixelDepth; |
|||
this.ImageDescriptor = imageDescriptor; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the id length.
|
|||
/// This field identifies the number of bytes contained in Field 6, the Image ID Field. The maximum number
|
|||
/// of characters is 255. A value of zero indicates that no Image ID field is included with the image.
|
|||
/// </summary>
|
|||
public byte IdLength { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the color map type.
|
|||
/// This field indicates the type of color map (if any) included with the image. There are currently 2 defined
|
|||
/// values for this field:
|
|||
/// 0 - indicates that no color-map data is included with this image.
|
|||
/// 1 - indicates that a color-map is included with this image.
|
|||
/// </summary>
|
|||
public byte ColorMapType { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the image type.
|
|||
/// The TGA File Format can be used to store Pseudo-Color, True-Color and Direct-Color images of various
|
|||
/// pixel depths.
|
|||
/// </summary>
|
|||
public TgaImageType ImageType { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the start of the color map.
|
|||
/// This field and its sub-fields describe the color map (if any) used for the image. If the Color Map Type field
|
|||
/// is set to zero, indicating that no color map exists, then these 5 bytes should be set to zero.
|
|||
/// </summary>
|
|||
public short CMapStart { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the total number of color map entries included.
|
|||
/// </summary>
|
|||
public short CMapLength { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of bits per entry. Typically 15, 16, 24 or 32-bit values are used.
|
|||
/// </summary>
|
|||
public byte CMapDepth { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the XOffset.
|
|||
/// These bytes specify the absolute horizontal coordinate for the lower left
|
|||
/// corner of the image as it is positioned on a display device having an
|
|||
/// origin at the lower left of the screen.
|
|||
/// </summary>
|
|||
public short XOffset { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the YOffset.
|
|||
/// These bytes specify the absolute vertical coordinate for the lower left
|
|||
/// corner of the image as it is positioned on a display device having an
|
|||
/// origin at the lower left of the screen.
|
|||
/// </summary>
|
|||
public short YOffset { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the width of the image in pixels.
|
|||
/// </summary>
|
|||
public short Width { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the height of the image in pixels.
|
|||
/// </summary>
|
|||
public short Height { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of bits per pixel. This number includes
|
|||
/// the Attribute or Alpha channel bits. Common values are 8, 16, 24 and
|
|||
/// 32 but other pixel depths could be used.
|
|||
/// </summary>
|
|||
public byte PixelDepth { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the ImageDescriptor.
|
|||
/// ImageDescriptor contains two pieces of information.
|
|||
/// Bits 0 through 3 contain the number of attribute bits per pixel.
|
|||
/// Attribute bits are found only in pixels for the 16- and 32-bit flavors of the TGA format and are called alpha channel,
|
|||
/// overlay, or interrupt bits. Bits 4 and 5 contain the image origin location (coordinate 0,0) of the image.
|
|||
/// This position may be any of the four corners of the display screen.
|
|||
/// When both of these bits are set to zero, the image origin is the lower-left corner of the screen.
|
|||
/// Bits 6 and 7 of the ImageDescriptor field are unused and should be set to 0.
|
|||
/// </summary>
|
|||
public byte ImageDescriptor { get; } |
|||
|
|||
public static TgaFileHeader Parse(Span<byte> data) |
|||
{ |
|||
return MemoryMarshal.Cast<byte, TgaFileHeader>(data)[0]; |
|||
} |
|||
|
|||
public void WriteTo(Span<byte> buffer) |
|||
{ |
|||
ref TgaFileHeader dest = ref Unsafe.As<byte, TgaFileHeader>(ref MemoryMarshal.GetReference(buffer)); |
|||
|
|||
dest = this; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Registers the image encoders, decoders and mime type detectors for the tga format.
|
|||
/// </summary>
|
|||
public sealed class TgaFormat : IImageFormat<TgaMetadata> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the current instance.
|
|||
/// </summary>
|
|||
public static TgaFormat Instance { get; } = new TgaFormat(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public string Name => "TGA"; |
|||
|
|||
/// <inheritdoc/>
|
|||
public string DefaultMimeType => "image/tga"; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<string> MimeTypes => TgaConstants.MimeTypes; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<string> FileExtensions => TgaConstants.FileExtensions; |
|||
|
|||
/// <inheritdoc/>
|
|||
public TgaMetadata CreateDefaultFormatMetadata() => new TgaMetadata(); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Detects tga file headers.
|
|||
/// </summary>
|
|||
public sealed class TgaImageFormatDetector : IImageFormatDetector |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public int HeaderSize => TgaConstants.FileHeaderLength; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IImageFormat DetectFormat(ReadOnlySpan<byte> header) |
|||
{ |
|||
return this.IsSupportedFileFormat(header) ? TgaFormat.Instance : null; |
|||
} |
|||
|
|||
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) |
|||
{ |
|||
if (header.Length >= this.HeaderSize) |
|||
{ |
|||
// There is no magick bytes in a tga file, so at least the image type
|
|||
// and the colormap type in the header will be checked for a valid value.
|
|||
if (header[1] != 0 && header[1] != 1) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var imageType = (TgaImageType)header[2]; |
|||
return imageType.IsValid(); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors. |
|||
ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the tga image type. The TGA File Format can be used to store Pseudo-Color,
|
|||
/// True-Color and Direct-Color images of various pixel depths.
|
|||
/// </summary>
|
|||
public enum TgaImageType : byte |
|||
{ |
|||
/// <summary>
|
|||
/// No image data included.
|
|||
/// </summary>
|
|||
NoImageData = 0, |
|||
|
|||
/// <summary>
|
|||
/// Uncompressed, color mapped image.
|
|||
/// </summary>
|
|||
ColorMapped = 1, |
|||
|
|||
/// <summary>
|
|||
/// Uncompressed true color image.
|
|||
/// </summary>
|
|||
TrueColor = 2, |
|||
|
|||
/// <summary>
|
|||
/// Uncompressed Black and white (grayscale) image.
|
|||
/// </summary>
|
|||
BlackAndWhite = 3, |
|||
|
|||
/// <summary>
|
|||
/// Run length encoded, color mapped image.
|
|||
/// </summary>
|
|||
RleColorMapped = 9, |
|||
|
|||
/// <summary>
|
|||
/// Run length encoded, true color image.
|
|||
/// </summary>
|
|||
RleTrueColor = 10, |
|||
|
|||
/// <summary>
|
|||
/// Run length encoded, black and white (grayscale) image.
|
|||
/// </summary>
|
|||
RleBlackAndWhite = 11, |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Extension methods for TgaImageType enum.
|
|||
/// </summary>
|
|||
public static class TgaImageTypeExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Checks if this tga image type is run length encoded.
|
|||
/// </summary>
|
|||
/// <param name="imageType">The tga image type.</param>
|
|||
/// <returns>True, if this image type is run length encoded, otherwise false.</returns>
|
|||
public static bool IsRunLengthEncoded(this TgaImageType imageType) |
|||
{ |
|||
if (imageType is TgaImageType.RleColorMapped || imageType is TgaImageType.RleBlackAndWhite || imageType is TgaImageType.RleTrueColor) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Checks, if the image type has valid value.
|
|||
/// </summary>
|
|||
/// <param name="imageType">The image type.</param>
|
|||
/// <returns>true, if its a valid tga image type.</returns>
|
|||
public static bool IsValid(this TgaImageType imageType) |
|||
{ |
|||
switch (imageType) |
|||
{ |
|||
case TgaImageType.NoImageData: |
|||
case TgaImageType.ColorMapped: |
|||
case TgaImageType.TrueColor: |
|||
case TgaImageType.BlackAndWhite: |
|||
case TgaImageType.RleColorMapped: |
|||
case TgaImageType.RleTrueColor: |
|||
case TgaImageType.RleBlackAndWhite: |
|||
return true; |
|||
|
|||
default: |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
/// <summary>
|
|||
/// Provides TGA specific metadata information for the image.
|
|||
/// </summary>
|
|||
public class TgaMetadata : IDeepCloneable |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TgaMetadata"/> class.
|
|||
/// </summary>
|
|||
public TgaMetadata() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TgaMetadata"/> class.
|
|||
/// </summary>
|
|||
/// <param name="other">The metadata to create an instance from.</param>
|
|||
private TgaMetadata(TgaMetadata other) |
|||
{ |
|||
this.BitsPerPixel = other.BitsPerPixel; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of bits per pixel.
|
|||
/// </summary>
|
|||
public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Pixel24; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IDeepCloneable DeepClone() => new TgaMetadata(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tga |
|||
{ |
|||
internal static class TgaThrowHelper |
|||
{ |
|||
/// <summary>
|
|||
/// Cold path optimization for throwing <see cref="ImageFormatException"/>-s
|
|||
/// </summary>
|
|||
/// <param name="errorMessage">The error message for the exception.</param>
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
public static void ThrowImageFormatException(string errorMessage) |
|||
{ |
|||
throw new ImageFormatException(errorMessage); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Cold path optimization for throwing <see cref="NotSupportedException"/>-s
|
|||
/// </summary>
|
|||
/// <param name="errorMessage">The error message for the exception.</param>
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
public static void ThrowNotSupportedException(string errorMessage) |
|||
{ |
|||
throw new NotSupportedException(errorMessage); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.IO; |
|||
|
|||
using BenchmarkDotNet.Attributes; |
|||
|
|||
using ImageMagick; |
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Tests; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Codecs |
|||
{ |
|||
[Config(typeof(Config.ShortClr))] |
|||
public class DecodeTga : BenchmarkBase |
|||
{ |
|||
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); |
|||
|
|||
[Params(TestImages.Tga.Bit24)] |
|||
public string TestImage { get; set; } |
|||
|
|||
[Benchmark(Baseline = true, Description = "ImageMagick Tga")] |
|||
public Size TgaImageMagick() |
|||
{ |
|||
using (var magickImage = new MagickImage(this.TestImageFullPath)) |
|||
{ |
|||
return new Size(magickImage.Width, magickImage.Height); |
|||
} |
|||
} |
|||
|
|||
[Benchmark(Description = "ImageSharp Tga")] |
|||
public Size TgaCore() |
|||
{ |
|||
using (var image = Image.Load<Rgba32>(this.TestImageFullPath)) |
|||
{ |
|||
return new Size(image.Width, image.Height); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.IO; |
|||
|
|||
using BenchmarkDotNet.Attributes; |
|||
|
|||
using ImageMagick; |
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Tests; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Codecs |
|||
{ |
|||
[Config(typeof(Config.ShortClr))] |
|||
public class EncodeTga : BenchmarkBase |
|||
{ |
|||
private MagickImage tgaMagick; |
|||
private Image<Rgba32> tgaCore; |
|||
|
|||
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); |
|||
|
|||
[Params(TestImages.Tga.Bit24)] |
|||
public string TestImage { get; set; } |
|||
|
|||
[GlobalSetup] |
|||
public void ReadImages() |
|||
{ |
|||
if (this.tgaCore == null) |
|||
{ |
|||
this.tgaCore = Image.Load<Rgba32>(TestImageFullPath); |
|||
this.tgaMagick = new MagickImage(this.TestImageFullPath); |
|||
} |
|||
} |
|||
|
|||
[Benchmark(Baseline = true, Description = "Magick Tga")] |
|||
public void BmpSystemDrawing() |
|||
{ |
|||
using (var memoryStream = new MemoryStream()) |
|||
{ |
|||
this.tgaMagick.Write(memoryStream, MagickFormat.Tga); |
|||
} |
|||
} |
|||
|
|||
[Benchmark(Description = "ImageSharp Tga")] |
|||
public void BmpCore() |
|||
{ |
|||
using (var memoryStream = new MemoryStream()) |
|||
{ |
|||
this.tgaCore.SaveAsBmp(memoryStream); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue