Skip to content

Commit

Permalink
Parse EXIF JPEG headers
Browse files Browse the repository at this point in the history
  • Loading branch information
cyanfish committed Sep 9, 2023
1 parent c8f6d99 commit f460082
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 9 deletions.
10 changes: 10 additions & 0 deletions NAPS2.Sdk.Tests/ImageResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions NAPS2.Sdk.Tests/ImageResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,7 @@
<data name="filled_form_annotated" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\filled_form_annotated.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="dog_exif" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\dog_exif.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
</root>
47 changes: 47 additions & 0 deletions NAPS2.Sdk.Tests/Pdf/JpegFormatHelperTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
33 changes: 33 additions & 0 deletions NAPS2.Sdk.Tests/Pdf/PdfImportExportTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using NAPS2.ImportExport;
using NAPS2.Pdf;
using NAPS2.Sdk.Tests.Asserts;
using Xunit;
Expand Down Expand Up @@ -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);
}
}
Binary file added NAPS2.Sdk.Tests/Resources/dog_exif.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 73 additions & 9 deletions NAPS2.Sdk/Pdf/JpegFormatHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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];
Expand Down Expand Up @@ -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],
Expand All @@ -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);
}
27 changes: 27 additions & 0 deletions NAPS2.Sdk/Util/EndianReader.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit f460082

Please sign in to comment.