diff --git a/src/ImageSharp/Common/Helpers/HexConverter.cs b/src/ImageSharp/Common/Helpers/HexConverter.cs
new file mode 100644
index 0000000000..c55e9bbd9d
--- /dev/null
+++ b/src/ImageSharp/Common/Helpers/HexConverter.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Common.Helpers
+{
+ internal static class HexConverter
+ {
+ ///
+ /// Parses a hexadecimal string into a byte array without allocations. Throws on non-hexadecimal character.
+ /// Adapted from https://source.dot.net/#System.Private.CoreLib/Convert.cs,c9e4fbeaca708991.
+ ///
+ /// The hexadecimal string to parse.
+ /// The destination for the parsed bytes. Must be at least .Length / 2 bytes long.
+ /// The number of bytes written to .
+ public static int HexStringToBytes(ReadOnlySpan chars, Span bytes)
+ {
+ if ((chars.Length % 2) != 0)
+ {
+ throw new ArgumentException("Input string length must be a multiple of 2", nameof(chars));
+ }
+
+ if ((bytes.Length * 2) < chars.Length)
+ {
+ throw new ArgumentException("Output span must be at least half the length of the input string");
+ }
+ else
+ {
+ // Slightly better performance in the loop below, allows us to skip a bounds check
+ // while still supporting output buffers that are larger than necessary
+ bytes = bytes.Slice(0, chars.Length / 2);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static int FromChar(int c)
+ {
+ // Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.
+ // This doesn't actually allocate.
+ ReadOnlySpan charToHexLookup = new byte[]
+ {
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
+ 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
+ 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
+ 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 255
+ };
+
+ return c >= charToHexLookup.Length ? 0xFF : charToHexLookup[c];
+ }
+
+ // See https://source.dot.net/#System.Private.CoreLib/HexConverter.cs,4681d45a0aa0b361
+ int i = 0;
+ int j = 0;
+ int byteLo = 0;
+ int byteHi = 0;
+ while (j < bytes.Length)
+ {
+ byteLo = FromChar(chars[i + 1]);
+ byteHi = FromChar(chars[i]);
+
+ // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern
+ // is if either byteHi or byteLo was not a hex character.
+ if ((byteLo | byteHi) == 0xFF)
+ {
+ break;
+ }
+
+ bytes[j++] = (byte)((byteHi << 4) | byteLo);
+ i += 2;
+ }
+
+ if (byteLo == 0xFF)
+ {
+ i++;
+ }
+
+ if ((byteLo | byteHi) == 0xFF)
+ {
+ throw new ArgumentException("Input string contained non-hexadecimal characters", nameof(chars));
+ }
+
+ return j;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 182d6e7bea..708cd7f5f4 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -11,6 +11,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
+using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
@@ -1093,7 +1094,7 @@ namespace SixLabors.ImageSharp.Formats.Png
tempExifBuf = new byte[exifHeader.Length];
}
- HexStringToBytes(dataSpan.Slice(0, exifHeader.Length * 2), tempExifBuf);
+ HexConverter.HexStringToBytes(dataSpan.Slice(0, exifHeader.Length * 2), tempExifBuf);
if (!tempExifBuf.AsSpan().Slice(0, exifHeader.Length).SequenceEqual(exifHeader))
{
// Exif header in the hex data is not valid
@@ -1115,7 +1116,7 @@ namespace SixLabors.ImageSharp.Formats.Png
lineSpan = dataSpan.Slice(0, newlineIndex);
}
- i += HexStringToBytes(lineSpan, exifBlob.AsSpan().Slice(i));
+ i += HexConverter.HexStringToBytes(lineSpan, exifBlob.AsSpan().Slice(i));
dataSpan = dataSpan.Slice(newlineIndex + 1);
}
@@ -1163,93 +1164,6 @@ namespace SixLabors.ImageSharp.Formats.Png
#pragma warning restore IDE0022 // Use expression body for methods
}
- ///
- /// Parses a hexadecimal string into a byte array without allocations. Throws on non-hexadecimal character.
- /// Adapted from https://source.dot.net/#System.Private.CoreLib/Convert.cs,c9e4fbeaca708991.
- ///
- /// The hexadecimal string to parse.
- /// The destination for the parsed bytes. Must be at least .Length / 2 bytes long.
- /// The number of bytes written to .
- private static int HexStringToBytes(ReadOnlySpan chars, Span bytes)
- {
- if ((chars.Length % 2) != 0)
- {
- throw new ArgumentException("Input string length must be a multiple of 2", nameof(chars));
- }
-
- if ((bytes.Length * 2) < chars.Length)
- {
- throw new ArgumentException("Output span must be at least half the length of the input string");
- }
- else
- {
- // Slightly better performance in the loop below, allows us to skip a bounds check
- // while still supporting output buffers that are larger than necessary
- bytes = bytes.Slice(0, chars.Length / 2);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- static int FromChar(int c)
- {
- // Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.
- // This doesn't actually allocate.
- ReadOnlySpan charToHexLookup = new byte[]
- {
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
- 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
- 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
- 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 255
- };
-
- return c >= charToHexLookup.Length ? 0xFF : charToHexLookup[c];
- }
-
- // See https://source.dot.net/#System.Private.CoreLib/HexConverter.cs,4681d45a0aa0b361
- int i = 0;
- int j = 0;
- int byteLo = 0;
- int byteHi = 0;
- while (j < bytes.Length)
- {
- byteLo = FromChar(chars[i + 1]);
- byteHi = FromChar(chars[i]);
-
- // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern
- // is if either byteHi or byteLo was not a hex character.
- if ((byteLo | byteHi) == 0xFF)
- {
- break;
- }
-
- bytes[j++] = (byte)((byteHi << 4) | byteLo);
- i += 2;
- }
-
- if (byteLo == 0xFF)
- {
- i++;
- }
-
- if ((byteLo | byteHi) == 0xFF)
- {
- throw new ArgumentException("Input string contained non-hexadecimal characters", nameof(chars));
- }
-
- return j;
- }
-
///
/// Sets the in to ,
/// or copies exif tags if already contains an .