diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/1310ba77-4dbf-4d8d-a8f4-5ba59d1221a7.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/1310ba77-4dbf-4d8d-a8f4-5ba59d1221a7.jp2 new file mode 100644 index 000000000..28d07f716 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/1310ba77-4dbf-4d8d-a8f4-5ba59d1221a7.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/2cee5bb6-f845-4ac1-8156-a899075c0b46.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/2cee5bb6-f845-4ac1-8156-a899075c0b46.jp2 new file mode 100644 index 000000000..02c34e244 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/2cee5bb6-f845-4ac1-8156-a899075c0b46.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/33fb977a-e3da-48da-ad51-89af637ab736.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/33fb977a-e3da-48da-ad51-89af637ab736.jp2 new file mode 100644 index 000000000..7d254bcce Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/33fb977a-e3da-48da-ad51-89af637ab736.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/371028e4-aea3-4e1d-b76b-47b763922e2f.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/371028e4-aea3-4e1d-b76b-47b763922e2f.jp2 new file mode 100644 index 000000000..9f77c82bc Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/371028e4-aea3-4e1d-b76b-47b763922e2f.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/804344c3-2c63-4e9c-b7c2-8c64a14d885b.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/804344c3-2c63-4e9c-b7c2-8c64a14d885b.jp2 new file mode 100644 index 000000000..999925fe4 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/804344c3-2c63-4e9c-b7c2-8c64a14d885b.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/82811cfb-9a70-475d-8338-f20df0acd052.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/82811cfb-9a70-475d-8338-f20df0acd052.jp2 new file mode 100644 index 000000000..b35bfafce Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/82811cfb-9a70-475d-8338-f20df0acd052.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/82828826-f624-4f22-8421-f8c4adac43a3.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/82828826-f624-4f22-8421-f8c4adac43a3.jp2 new file mode 100644 index 000000000..b1116d1c3 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/82828826-f624-4f22-8421-f8c4adac43a3.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/8e95baf6-874e-431c-9cbc-d735ccabac0c.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/8e95baf6-874e-431c-9cbc-d735ccabac0c.jp2 new file mode 100644 index 000000000..077eb8203 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/8e95baf6-874e-431c-9cbc-d735ccabac0c.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/91a217c9-79bb-4a4b-934b-1362344f6b89.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/91a217c9-79bb-4a4b-934b-1362344f6b89.jp2 new file mode 100644 index 000000000..18f7635c9 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/91a217c9-79bb-4a4b-934b-1362344f6b89.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/99a9ea0e-c407-4336-96a0-85023f46c231.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/99a9ea0e-c407-4336-96a0-85023f46c231.jp2 new file mode 100644 index 000000000..5eb723dd1 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/99a9ea0e-c407-4336-96a0-85023f46c231.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/9ac01df9-6623-4d14-89fd-e9934d1a6c7e.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/9ac01df9-6623-4d14-89fd-e9934d1a6c7e.jp2 new file mode 100644 index 000000000..fb917327c Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/9ac01df9-6623-4d14-89fd-e9934d1a6c7e.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/9d5c783a-c001-40e9-91b9-630c71804a77.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/9d5c783a-c001-40e9-91b9-630c71804a77.jp2 new file mode 100644 index 000000000..eb580f2b7 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/9d5c783a-c001-40e9-91b9-630c71804a77.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/9df64d7b-4003-4d0d-8f68-b5f88de781b7.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/9df64d7b-4003-4d0d-8f68-b5f88de781b7.jp2 new file mode 100644 index 000000000..16e6dfa5a Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/9df64d7b-4003-4d0d-8f68-b5f88de781b7.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/a6105bfd-3ace-4d6b-b2dc-f9ce4022832b.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/a6105bfd-3ace-4d6b-b2dc-f9ce4022832b.jp2 new file mode 100644 index 000000000..6305ac42c Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/a6105bfd-3ace-4d6b-b2dc-f9ce4022832b.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/b650c344-bc4d-427a-94af-cfed04136f67.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/b650c344-bc4d-427a-94af-cfed04136f67.jp2 new file mode 100644 index 000000000..35d537cf2 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/b650c344-bc4d-427a-94af-cfed04136f67.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/c390b9c7-a562-42bf-a592-7a7b29819a6a.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/c390b9c7-a562-42bf-a592-7a7b29819a6a.jp2 new file mode 100644 index 000000000..9442506b8 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/c390b9c7-a562-42bf-a592-7a7b29819a6a.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/d6b4b35c-0ceb-47fe-aba8-4360acb49fcb.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/d6b4b35c-0ceb-47fe-aba8-4360acb49fcb.jp2 new file mode 100644 index 000000000..05cc748dc Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/d6b4b35c-0ceb-47fe-aba8-4360acb49fcb.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/deff000e-a14a-40fd-bf39-88ce11745260.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/deff000e-a14a-40fd-bf39-88ce11745260.jp2 new file mode 100644 index 000000000..a4f9a7e2f Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/deff000e-a14a-40fd-bf39-88ce11745260.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/e29266a2-201a-4ad6-9725-ca1b7c22224d.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/e29266a2-201a-4ad6-9725-ca1b7c22224d.jp2 new file mode 100644 index 000000000..ab796cafc Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/e29266a2-201a-4ad6-9725-ca1b7c22224d.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/eae2cabb-f520-4be5-932f-fb19fce5b2f2.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/eae2cabb-f520-4be5-932f-fb19fce5b2f2.jp2 new file mode 100644 index 000000000..162984880 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/eae2cabb-f520-4be5-932f-fb19fce5b2f2.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/eb62f062-6567-48b2-b04d-6d90de120f07.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/eb62f062-6567-48b2-b04d-6d90de120f07.jp2 new file mode 100644 index 000000000..102d8d08b Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/eb62f062-6567-48b2-b04d-6d90de120f07.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/ed5c585f-590e-4585-9ce7-25976f589ca8.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/ed5c585f-590e-4585-9ce7-25976f589ca8.jp2 new file mode 100644 index 000000000..adb435196 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/ed5c585f-590e-4585-9ce7-25976f589ca8.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/ef0af08b-04d1-4a3e-a9d8-7916b9826f5d.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/ef0af08b-04d1-4a3e-a9d8-7916b9826f5d.jp2 new file mode 100644 index 000000000..f8db9be65 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/ef0af08b-04d1-4a3e-a9d8-7916b9826f5d.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/fd42e6a0-5c7a-4eb2-b0e3-474cfde067a6.jp2 b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/fd42e6a0-5c7a-4eb2-b0e3-474cfde067a6.jp2 new file mode 100644 index 000000000..1bbb65118 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/Jpx/fd42e6a0-5c7a-4eb2-b0e3-474cfde067a6.jp2 differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Jpeg2000HelperTests.cs b/src/UglyToad.PdfPig.Tests/Images/Jpeg2000HelperTests.cs new file mode 100644 index 000000000..7bd5a5a2c --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/Images/Jpeg2000HelperTests.cs @@ -0,0 +1,38 @@ +namespace UglyToad.PdfPig.Tests.Images +{ + using PdfPig.Images; + using System; + + public class Jpeg2000HelperTests + { + private static readonly Lazy DocumentFolder = new Lazy(() => Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Images", "Files", "Jpx"))); + + public static IEnumerable GetAllDocuments + { + get + { + return Directory.GetFiles(DocumentFolder.Value, "*.jp2").Select(x => new object[] { Path.GetFileName(x) }); + } + } + + [Fact] + public void GetJp2BitsPerComponent_ThrowsException_WhenInputIsTooShort() + { + Assert.Throws(() => Jpeg2000Helper.GetJp2BitsPerComponent(new byte[11])); + } + + [Fact] + public void GetJp2BitsPerComponent_ThrowsException_WhenSignatureBoxIsInvalid() + { + Assert.Throws(() => Jpeg2000Helper.GetJp2BitsPerComponent(new byte[12])); + } + + [Theory] + [MemberData(nameof(GetAllDocuments))] + public void GetJp2BitsPerComponent_ReturnsCorrectBitsPerComponent_WhenValidInput(string path) + { + byte[] image = File.ReadAllBytes(Path.Combine(DocumentFolder.Value, path)); + Assert.Equal(8, Jpeg2000Helper.GetJp2BitsPerComponent(image)); + } + } +} diff --git a/src/UglyToad.PdfPig.Tests/UglyToad.PdfPig.Tests.csproj b/src/UglyToad.PdfPig.Tests/UglyToad.PdfPig.Tests.csproj index 0b32cf771..f4e40de56 100644 --- a/src/UglyToad.PdfPig.Tests/UglyToad.PdfPig.Tests.csproj +++ b/src/UglyToad.PdfPig.Tests/UglyToad.PdfPig.Tests.csproj @@ -132,6 +132,78 @@ PreserveNewest + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + diff --git a/src/UglyToad.PdfPig/Images/Jpeg2000Helper.cs b/src/UglyToad.PdfPig/Images/Jpeg2000Helper.cs new file mode 100644 index 000000000..83c052063 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Jpeg2000Helper.cs @@ -0,0 +1,103 @@ +namespace UglyToad.PdfPig.Images +{ + using System; + using System.Buffers.Binary; + + internal static class Jpeg2000Helper + { + /// + /// Get bits per component values for Jp2 (Jpx) encoded images (first component). + /// + public static byte GetJp2BitsPerComponent(ReadOnlySpan jp2Bytes) + { + // Ensure the input has at least 12 bytes for the signature box + if (jp2Bytes.Length < 12) + { + throw new InvalidOperationException("Input is too short to be a valid JP2 file."); + } + + // Verify the JP2 signature box + uint length = BinaryPrimitives.ReadUInt32BigEndian(jp2Bytes.Slice(0, 4)); + uint type = BinaryPrimitives.ReadUInt32BigEndian(jp2Bytes.Slice(4, 4)); + uint magic = BinaryPrimitives.ReadUInt32BigEndian(jp2Bytes.Slice(8, 4)); + + if (length != 0x0000000C || type != 0x6A502020 || magic != 0x0D0A870A) + { + throw new InvalidOperationException("Invalid JP2 signature box."); + } + + // Proceed to parse JP2 boxes + return ParseBoxes(jp2Bytes.Slice(12)); + } + + private static byte ParseBoxes(ReadOnlySpan jp2Bytes) + { + int offset = 0; + while (offset < jp2Bytes.Length) + { + if (offset + 8 > jp2Bytes.Length) + { + throw new InvalidOperationException("Invalid JP2 box structure."); + } + + // Read box length and type + uint boxLength = BinaryPrimitives.ReadUInt32BigEndian(jp2Bytes.Slice(offset, 4)); + uint boxType = BinaryPrimitives.ReadUInt32BigEndian(jp2Bytes.Slice(offset + 4, 4)); + + // Check for the contiguous codestream box ('jp2c') + if (boxType == 0x6A703263) // 'jp2c' in ASCII + { + // Parse the codestream to find the SIZ marker + return ParseCodestream(jp2Bytes.Slice(offset + 8)); + } + + // Move to the next box + offset += (int)(boxLength > 0 ? boxLength : 8); // Box length of 0 means the rest of the file + } + + throw new InvalidOperationException("Codestream box not found in JP2 file."); + } + + private static byte ParseCodestream(ReadOnlySpan codestream) + { + int offset = 0; + while (offset + 2 <= codestream.Length) + { + // Read marker (2 bytes) + ushort marker = BinaryPrimitives.ReadUInt16BigEndian(codestream.Slice(offset, 2)); + + // Check for SIZ marker (0xFF51) + if (marker == 0xFF51) + { + if (offset + 38 > codestream.Length) + { + throw new InvalidOperationException("Invalid SIZ marker structure."); + } + + // Skip marker length (2 bytes), capabilities (4 bytes), and reference grid size (8 bytes) + // Skip image offset (8 bytes), tile size (8 bytes), and tile offset (8 bytes) + offset += 38; + + // Read number of components (2 bytes) + ushort numComponents = BinaryPrimitives.ReadUInt16BigEndian(codestream.Slice(offset, 2)); + + offset += 2; + if (numComponents < 1) + { + throw new InvalidOperationException("Invalid number of components in SIZ marker."); + } + + // Read bits per component for the first component (1 byte per component) + byte bitsPerComponent = codestream[offset]; + + // Bits per component is stored as (bits - 1) + return ++bitsPerComponent; + } + // Move to the next marker + offset += 2; + } + + throw new InvalidOperationException("SIZ marker not found in JPEG2000 codestream."); + } + } +} diff --git a/src/UglyToad.PdfPig/XObjects/XObjectFactory.cs b/src/UglyToad.PdfPig/XObjects/XObjectFactory.cs index 13b927fb8..16d699f6d 100644 --- a/src/UglyToad.PdfPig/XObjects/XObjectFactory.cs +++ b/src/UglyToad.PdfPig/XObjects/XObjectFactory.cs @@ -8,6 +8,7 @@ using Graphics; using Graphics.Colors; using Graphics.Core; + using Images; using Tokenization.Scanner; using Tokens; using Util; @@ -52,19 +53,36 @@ public static XObjectImage ReadImage(XObjectContentRecord xObject, var isJpxDecode = dictionary.TryGet(NameToken.Filter, pdfScanner, out NameToken filterName) && filterName.Equals(NameToken.JpxDecode); - int bitsPerComponent = 0; - if (!isImageMask && !isJpxDecode) + int bitsPerComponent; + if (isImageMask) { - if (!dictionary.TryGet(NameToken.BitsPerComponent, pdfScanner, out NumericToken? bitsPerComponentToken)) + bitsPerComponent = 1; + } + else + { + if (isJpxDecode) { - throw new PdfDocumentFormatException($"No bits per component defined for image: {dictionary}."); + // Optional for JPX + if (dictionary.TryGet(NameToken.BitsPerComponent, pdfScanner, out NumericToken? bitsPerComponentToken)) + { + bitsPerComponent = bitsPerComponentToken.Int; + System.Diagnostics.Debug.Assert(bitsPerComponent == Jpeg2000Helper.GetJp2BitsPerComponent(xObject.Stream.Data.Span)); + } + else + { + bitsPerComponent = Jpeg2000Helper.GetJp2BitsPerComponent(xObject.Stream.Data.Span); + System.Diagnostics.Debug.Assert(new int[] { 1, 2, 4, 8, 16 }.Contains(bitsPerComponent)); + } } + else + { + if (!dictionary.TryGet(NameToken.BitsPerComponent, pdfScanner, out NumericToken? bitsPerComponentToken)) + { + throw new PdfDocumentFormatException($"No bits per component defined for image: {dictionary}."); + } - bitsPerComponent = bitsPerComponentToken.Int; - } - else if (isImageMask) - { - bitsPerComponent = 1; + bitsPerComponent = bitsPerComponentToken.Int; + } } var intent = xObject.DefaultRenderingIntent;