diff --git a/NAPS2.Sdk.Tests/ImageResources.Designer.cs b/NAPS2.Sdk.Tests/ImageResources.Designer.cs
index 1f57116620..f04d2e42f0 100644
--- a/NAPS2.Sdk.Tests/ImageResources.Designer.cs
+++ b/NAPS2.Sdk.Tests/ImageResources.Designer.cs
@@ -299,6 +299,16 @@ internal static byte[] dog_clustered_gray {
}
}
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] dog_exif {
+ get {
+ object obj = ResourceManager.GetObject("dog_exif", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+
///
/// Looks up a localized resource of type System.Byte[].
///
diff --git a/NAPS2.Sdk.Tests/ImageResources.resx b/NAPS2.Sdk.Tests/ImageResources.resx
index b2aa6bc974..895ab75983 100644
--- a/NAPS2.Sdk.Tests/ImageResources.resx
+++ b/NAPS2.Sdk.Tests/ImageResources.resx
@@ -295,4 +295,7 @@
Resources\filled_form_annotated.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Resources\dog_exif.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
\ No newline at end of file
diff --git a/NAPS2.Sdk.Tests/Pdf/JpegFormatHelperTests.cs b/NAPS2.Sdk.Tests/Pdf/JpegFormatHelperTests.cs
new file mode 100644
index 0000000000..3905f8cf32
--- /dev/null
+++ b/NAPS2.Sdk.Tests/Pdf/JpegFormatHelperTests.cs
@@ -0,0 +1,47 @@
+using NAPS2.Pdf;
+using Xunit;
+
+namespace NAPS2.Sdk.Tests.Pdf;
+
+public class JpegFormatHelperTests
+{
+ [Fact]
+ public void Jfif()
+ {
+ var header = JpegFormatHelper.ReadHeader(new MemoryStream(ImageResources.dog));
+ Assert.NotNull(header);
+ Assert.Equal(788, header.Width);
+ Assert.Equal(525, header.Height);
+ Assert.Equal(3, header.NumComponents);
+ Assert.Equal(72, header.HorizontalDpi);
+ Assert.Equal(72, header.VerticalDpi);
+ Assert.True(header.HasJfifHeader);
+ }
+
+ [Fact]
+ public void JfifGrey()
+ {
+ var header = JpegFormatHelper.ReadHeader(new MemoryStream(ImageResources.dog_gray));
+ Assert.NotNull(header);
+ Assert.Equal(788, header.Width);
+ Assert.Equal(525, header.Height);
+ Assert.Equal(1, header.NumComponents);
+ Assert.Equal(72, header.HorizontalDpi);
+ Assert.Equal(72, header.VerticalDpi);
+ Assert.True(header.HasJfifHeader);
+ }
+
+ [Fact]
+ public void Exif()
+ {
+ var header = JpegFormatHelper.ReadHeader(new MemoryStream(ImageResources.dog_exif));
+ Assert.NotNull(header);
+ Assert.Equal(788, header.Width);
+ Assert.Equal(525, header.Height);
+ Assert.Equal(3, header.NumComponents);
+ Assert.Equal(72, header.HorizontalDpi);
+ Assert.Equal(72, header.VerticalDpi);
+ Assert.False(header.HasJfifHeader);
+ Assert.True(header.HasExifHeader);
+ }
+}
\ No newline at end of file
diff --git a/NAPS2.Sdk.Tests/Pdf/PdfImportExportTests.cs b/NAPS2.Sdk.Tests/Pdf/PdfImportExportTests.cs
index 0b981ccc1e..ad8fa250d8 100644
--- a/NAPS2.Sdk.Tests/Pdf/PdfImportExportTests.cs
+++ b/NAPS2.Sdk.Tests/Pdf/PdfImportExportTests.cs
@@ -1,3 +1,4 @@
+using NAPS2.ImportExport;
using NAPS2.Pdf;
using NAPS2.Sdk.Tests.Asserts;
using Xunit;
@@ -204,4 +205,36 @@ public async Task ImportVariousAndExport(OcrTestConfig config)
PdfAsserts.AssertDoesNotContainText("Sized for printing unscaled", _exportPath);
}
}
+
+ [Fact]
+ public async Task ImportJpegExportWithoutEncoding()
+ {
+ SetUpFileStorage();
+
+ var path = CopyResourceToFile(ImageResources.dog, "image.jpg");
+ var images = await new ImageImporter(ScanningContext).Import(path).ToListAsync();
+ Assert.Single(images);
+
+ await _exporter.Export(_exportPath, images);
+
+ var renderer = new PdfiumPdfRenderer();
+ var pdfImage = renderer.Render(ImageContext, _exportPath, PdfRenderSize.Default).Single();
+ ImageAsserts.Similar(ImageResources.dog, pdfImage, 0);
+ }
+
+ [Fact]
+ public async Task ImportExifJpegExportWithoutEncoding()
+ {
+ SetUpFileStorage();
+
+ var path = CopyResourceToFile(ImageResources.dog_exif, "image.jpg");
+ var images = await new ImageImporter(ScanningContext).Import(path).ToListAsync();
+ Assert.Single(images);
+
+ await _exporter.Export(_exportPath, images);
+
+ var renderer = new PdfiumPdfRenderer();
+ var pdfImage = renderer.Render(ImageContext, _exportPath, PdfRenderSize.Default).Single();
+ ImageAsserts.Similar(ImageResources.dog_exif, pdfImage, 0);
+ }
}
\ No newline at end of file
diff --git a/NAPS2.Sdk.Tests/Resources/dog_exif.jpg b/NAPS2.Sdk.Tests/Resources/dog_exif.jpg
new file mode 100644
index 0000000000..d7423932b5
Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/dog_exif.jpg differ
diff --git a/NAPS2.Sdk/Pdf/JpegFormatHelper.cs b/NAPS2.Sdk/Pdf/JpegFormatHelper.cs
index 9b8c2f9042..423ec6e64f 100644
--- a/NAPS2.Sdk/Pdf/JpegFormatHelper.cs
+++ b/NAPS2.Sdk/Pdf/JpegFormatHelper.cs
@@ -4,13 +4,12 @@ internal static class JpegFormatHelper
{
// JPEG format doc: https://github.com/corkami/formats/blob/master/image/jpeg.md
- public static JpegHeader? ReadHeader(FileStream stream)
+ public static JpegHeader? ReadHeader(Stream stream)
{
var sig1 = stream.ReadByte();
var sig2 = stream.ReadByte();
if (sig1 != 0xFF || sig2 != 0xD8) return null;
- var header = new JpegHeader(0, 0, 0, 0, 0);
- bool isJfif = false;
+ var header = new JpegHeader(0, 0, 0, 0, 0, false, false);
while (true)
{
var marker = stream.ReadByte();
@@ -23,7 +22,7 @@ internal static class JpegFormatHelper
if (stream.Read(buf, 0, len) < len) return null;
if (type == 0xE0 && len >= 12 && buf is [0x4A, 0x46, 0x49, 0x46, 0x00, ..]) // Application data (JFIF)
{
- isJfif = true;
+ header = header with { HasJfifHeader = true };
var units = buf[7];
var hRes = buf[8] * 256 + buf[9];
var vRes = buf[10] * 256 + buf[11];
@@ -52,12 +51,76 @@ internal static class JpegFormatHelper
};
}
}
+ if (type == 0xE1 && len >= 12 && buf is [0x45, 0x78, 0x69, 0x66, 0x00, 0x00, ..]) // Application data (EXIF)
+ {
+ // https://docs.fileformat.com/image/exif/
+
+ // 0x4949 = little-endian, 0x4d4d = big-endian
+ var flipEnd = (buf[6] == 0x49 && buf[7] == 0x49) != BitConverter.IsLittleEndian;
+ var reader = new EndianReader(flipEnd);
+
+ var number = reader.ReadInt16(buf, 8);
+
+ if (number == 42)
+ {
+ header = header with { HasExifHeader = true };
+
+ var ifdOffset = reader.ReadInt32(buf, 10);
+ var dirCount = reader.ReadInt16(buf, ifdOffset + 6);
+
+ double xRes = 0;
+ double yRes = 0;
+ int resUnit = 0;
+
+ for (int i = 0; i < dirCount; i++)
+ {
+ var offset = ifdOffset + 8 + i * 12;
+
+ var tag = reader.ReadInt16(buf, offset);
+
+ if (tag == 282)
+ {
+ var valueOffset = reader.ReadInt32(buf, offset + 8);
+ var num = reader.ReadInt32(buf, valueOffset + 6);
+ var den = reader.ReadInt32(buf, valueOffset + 10);
+ xRes = num / (double) den;
+ }
+ if (tag == 283)
+ {
+ var valueOffset = reader.ReadInt32(buf, offset + 8);
+ var num = reader.ReadInt32(buf, valueOffset + 6);
+ var den = reader.ReadInt32(buf, valueOffset + 10);
+ yRes = num / (double) den;
+ }
+ if (tag == 296)
+ {
+ resUnit = reader.ReadInt16(buf, offset + 8);
+ }
+ }
+
+ if (xRes > 0 && yRes > 0)
+ {
+ if (resUnit == 3) // cm
+ {
+ header = header with
+ {
+ HorizontalDpi = xRes * 2.54,
+ VerticalDpi = yRes * 2.54
+ };
+ }
+ else // inch
+ {
+ header = header with
+ {
+ HorizontalDpi = xRes,
+ VerticalDpi = yRes
+ };
+ }
+ }
+ }
+ }
if (type == 0xC0 && len >= 6) // Start of frame
{
- // JPEGs can come with several header varieties: JFIF-only, EXIF-only, or JFIF+EXIF.
- // As long as we have a JFIF header we should have accurate resolution information.
- // If it's EXIF-only we're not currently able to parse that.
- if (!isJfif) return null;
return header with
{
Height = buf[1] * 256 + buf[2],
@@ -68,5 +131,6 @@ internal static class JpegFormatHelper
}
}
- public record JpegHeader(double HorizontalDpi, double VerticalDpi, int Width, int Height, int NumComponents);
+ public record JpegHeader(double HorizontalDpi, double VerticalDpi, int Width, int Height, int NumComponents,
+ bool HasJfifHeader, bool HasExifHeader);
}
\ No newline at end of file
diff --git a/NAPS2.Sdk/Util/EndianReader.cs b/NAPS2.Sdk/Util/EndianReader.cs
new file mode 100644
index 0000000000..c36741ae31
--- /dev/null
+++ b/NAPS2.Sdk/Util/EndianReader.cs
@@ -0,0 +1,27 @@
+using System.Buffers.Binary;
+
+namespace NAPS2.Util;
+
+public class EndianReader
+{
+ private readonly bool _reverseEndianness;
+
+ public EndianReader(bool reverseEndianness)
+ {
+ _reverseEndianness = reverseEndianness;
+ }
+
+ public int ReadInt16(byte[] buf, int offset)
+ {
+ var value = BitConverter.ToInt16(buf, offset);
+ if (_reverseEndianness) value = BinaryPrimitives.ReverseEndianness(value);
+ return value;
+ }
+
+ public int ReadInt32(byte[] buf, int offset)
+ {
+ var value = BitConverter.ToInt32(buf, offset);
+ if (_reverseEndianness) value = BinaryPrimitives.ReverseEndianness(value);
+ return value;
+ }
+}
\ No newline at end of file