Use pdf rendering at the known image size for ccitt and other cases

For ccitt this is relatively slower but still fairly fast overall, avoids the hacky tiff ccitt reading, and handles edge cases (e.g. black/white inversion) properly. Plus this case also handles other situations (e.g. cmyk images) with the correct render size.

Fixes #117
This commit is contained in:
Ben Olden-Cooligan 2023-03-19 16:37:21 -07:00
parent b94e81f16b
commit 0fa3d47edf
6 changed files with 76 additions and 115 deletions

View File

@ -90,6 +90,19 @@ public class PdfBenchmarkTests : ContextualTests
}
}
[BenchmarkFact]
public async Task Import300Naps2Bw()
{
ScanningContext.FileStorageManager = FileStorageManager.CreateFolder("recovery");
var filePath = CopyResourceToFile(PdfResources.image_pdf_bw, "test.pdf");
var pdfExporter = new PdfImporter(ScanningContext);
for (int i = 0; i < 300; i++)
{
await pdfExporter.Import(filePath).ToListAsync();
}
}
[BenchmarkFact]
public async Task Import300NonNaps2()
{

View File

@ -47,10 +47,11 @@ public class PdfiumPdfRendererTests : ContextualTests
{
var path = CopyResourceToFile(PdfResources.image_pdf_cmyk, "test.pdf");
var images = new PdfiumPdfRenderer().Render(ImageContext, path, PdfRenderSize.FromDimensions(788, 525)).ToList();
var images = new PdfiumPdfRenderer().Render(ImageContext, path, PdfRenderSize.Default).ToList();
Assert.Single(images);
ImageAsserts.Similar(ImageResources.dog, images[0], ignoreResolution: true);
// This also verifies that the renderer gets the actual image dpi (72)
ImageAsserts.Similar(ImageResources.dog, images[0]);
}
[Fact]

View File

@ -1,78 +0,0 @@
using NAPS2.Pdf.Pdfium;
namespace NAPS2.Pdf;
internal static class CcittReader
{
private static readonly byte[] TiffBeforeDataLen = { 0x49, 0x49, 0x2A, 0x00 };
private static readonly byte[] TiffBeforeData = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static readonly byte[] TiffBeforeWidth = { 0x07, 0x00, 0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00 };
private static readonly byte[] TiffBeforeHeight = { 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00 };
private static readonly byte[] TiffBeforeBits = { 0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00 };
private static readonly byte[] TiffBeforeRealLen =
{
0x03, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x11, 0x01, 0x04, 0x00, 0x01, 0x00,
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x15, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x17, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00
};
private static readonly byte[] TiffTrailer = { 0x00, 0x00, 0x00, 0x00 };
// Sample full tiff LEN------------------- DATA------------------ WIDTH----------------- HEIGHT---------------- BITS PER COMP--------- REALLEN---------------
// { 0x49, 0x49, 0x2A, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 0x07, 0x00, 0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x77, 0x77, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x88, 0x88, 0x00, 0x00, 0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x11, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x15, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x17, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static IMemoryImage LoadRawCcitt(ImageContext imageContext, byte[] buffer, PdfImageMetadata metadata)
{
// We don't have easy access to a standalone CCITT G4 decoder, so we'll make use of the .NET TIFF decoder
// by constructing a valid TIFF file "manually" and directly injecting the bytestream
var stream = new MemoryStream();
Write(stream, TiffBeforeDataLen);
// The bytestream is 2-padded, so we may need to append an extra zero byte
if (buffer.Length % 2 == 1)
{
Write(stream, buffer.Length + 0x11);
}
else
{
Write(stream, buffer.Length + 0x10);
}
Write(stream, TiffBeforeData);
Write(stream, buffer);
if (buffer.Length % 2 == 1)
{
Write(stream, new byte[] { 0x00 });
}
Write(stream, TiffBeforeWidth);
Write(stream, metadata.Width);
Write(stream, TiffBeforeHeight);
Write(stream, metadata.Height);
Write(stream, TiffBeforeBits);
Write(stream, 1); // bits per component
Write(stream, TiffBeforeRealLen);
Write(stream, buffer.Length);
Write(stream, TiffTrailer);
stream.Seek(0, SeekOrigin.Begin);
return imageContext.Load(stream);
}
private static void Write(MemoryStream stream, byte[] bytes)
{
stream.Write(bytes, 0, bytes.Length);
}
private static void Write(MemoryStream stream, int value)
{
byte[] bytes = BitConverter.GetBytes(value);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
Debug.Assert(bytes.Length == 4);
stream.Write(bytes, 0, bytes.Length);
}
}

View File

@ -11,6 +11,12 @@ internal class PdfBitmap : NativePdfiumObject
Native.FPDFBitmap_CreateEx(width, height, PdfiumNativeLibrary.FPDFBitmap_BGR, scan0, stride));
}
public static PdfBitmap CreateFromPointer(int width, int height, IntPtr scan0, int stride, int format)
{
return new PdfBitmap(
Native.FPDFBitmap_CreateEx(width, height, format, scan0, stride));
}
internal PdfBitmap(IntPtr handle) : base(handle)
{
}
@ -21,12 +27,7 @@ internal class PdfBitmap : NativePdfiumObject
public int Stride => Native.FPDFBitmap_GetStride(Handle);
public ImagePixelFormat Format => Native.FPDFBitmap_GetFormat(Handle) switch
{
PdfiumNativeLibrary.FPDFBitmap_BGR => ImagePixelFormat.RGB24,
PdfiumNativeLibrary.FPDFBitmap_BGRA => ImagePixelFormat.ARGB32,
_ => ImagePixelFormat.Unsupported
};
public int Format => Native.FPDFBitmap_GetFormat(Handle);
public IntPtr Buffer => Native.FPDFBitmap_GetBuffer(Handle);

View File

@ -17,6 +17,7 @@ internal class PdfiumNativeLibrary : Unmanaged.NativeLibrary
public static PdfiumNativeLibrary Instance => LazyInstance.Value;
public const int FPDFBitmap_Gray = 1;
public const int FPDFBitmap_BGR = 2;
public const int FPDFBitmap_BGRA = 4;

View File

@ -11,7 +11,7 @@ internal static class PdfiumImageExtractor
if (imageObj != null)
{
var metadata = imageObj.ImageMetadata;
var image = GetImageFromObject(imageContext, imageObj, metadata);
var image = GetImageFromObject(imageContext, page, imageObj, metadata);
if (image != null)
{
image.SetResolution((int) Math.Round(metadata.HorizontalDpi), (int) Math.Round(metadata.VerticalDpi));
@ -23,7 +23,15 @@ internal static class PdfiumImageExtractor
// TODO: This could be wrong if the image object has a mask, but GetRenderedBitmap does a re-encode which we don't really want
// Ideally we would be do this conditionally based on the presence of a mask, if pdfium could provide us that info
private static IMemoryImage? GetImageFromObject(ImageContext imageContext, PdfPageObject imageObj,
private static IMemoryImage? GetImageFromObject(ImageContext imageContext, PdfPage page, PdfPageObject imageObj,
PdfImageMetadata metadata)
{
// Otherwise we render the entire page with the known image dimensions
return ExtractRawImageData(imageContext, imageObj, metadata) ??
RenderPdfPageToNewImage(imageContext, page, metadata);
}
private static IMemoryImage? ExtractRawImageData(ImageContext imageContext, PdfPageObject imageObj,
PdfImageMetadata metadata)
{
if (metadata.Colorspace is not (Colorspace.DeviceRgb or Colorspace.DeviceGray or Colorspace.Indexed))
@ -36,11 +44,6 @@ internal static class PdfiumImageExtractor
{
// If the image has transparency, that implies the bitmap has a mask, so we need to use GetRenderedBitmap
// to apply the mask and get the correct image.
using var pdfBitmap = imageObj.GetRenderedBitmap();
if (pdfBitmap.Format is ImagePixelFormat.RGB24 or ImagePixelFormat.ARGB32)
{
return CopyPdfBitmapToNewImage(imageContext, pdfBitmap, metadata);
}
return null;
}
// First try and read the raw image data, this is most efficient if we can handle it
@ -50,6 +53,7 @@ internal static class PdfiumImageExtractor
}
if (imageObj.HasImageFilters("FlateDecode"))
{
// TODO: Add tests for these cases to PdfiumPdfRendererTests
if (metadata.BitsPerPixel == 24 && metadata.Colorspace == Colorspace.DeviceRgb)
{
return LoadRaw(imageContext, imageObj.GetImageDataDecoded(), metadata, ImagePixelFormat.RGB24,
@ -61,35 +65,54 @@ internal static class PdfiumImageExtractor
SubPixelType.Bit);
}
}
if (imageObj.HasImageFilters("CCITTFaxDecode"))
{
return CcittReader.LoadRawCcitt(imageContext, imageObj.GetImageDataDecoded(), metadata);
}
// If we can't read the raw data ourselves, we can try and rely on Pdfium to materialize a bitmap, which is a
// bit less efficient
// TODO: Maybe add support for black & white here too, with tests
// TODO: Also this won't have test coverage if everything is covered by the "raw" tests, maybe either find a
// test case or just have a switch to test this specifically
// TODO: Is 32 bit even possible here? As alpha is implemented with masks
if (metadata.BitsPerPixel == 24 || metadata.BitsPerPixel == 32)
{
using var pdfBitmap = imageObj.GetBitmap();
if (pdfBitmap.Format is ImagePixelFormat.RGB24 or ImagePixelFormat.ARGB32)
{
return CopyPdfBitmapToNewImage(imageContext, pdfBitmap, metadata);
}
}
// Otherwise we fall back to relying on Pdfium to render the whole page which is least efficient and won't have
// the correct DPI
// Previously we also had a way to load the raw CCITTFaxDecode filter with a custom CcittReader class, but that
// failed in some cases (https://github.com/cyanfish/naps2/issues/117)
return null;
}
private static IMemoryImage RenderPdfPageToNewImage(ImageContext imageContext, PdfPage page,
PdfImageMetadata metadata)
{
// This maintains the correct image dimensions/resolution as we have that info from the metadata.
using var pdfBitmap = RenderPdfPageToBitmap(page, metadata);
return CopyPdfBitmapToNewImage(imageContext, pdfBitmap, metadata);
}
private static unsafe PdfBitmap RenderPdfPageToBitmap(PdfPage page, PdfImageMetadata imageMetadata)
{
var w = imageMetadata.Width;
var h = imageMetadata.Height;
var (subPixelType, format) = imageMetadata.BitsPerPixel switch
{
1 or 8 => (SubPixelType.Gray, PdfiumNativeLibrary.FPDFBitmap_Gray),
24 => (SubPixelType.Bgr, PdfiumNativeLibrary.FPDFBitmap_BGR),
32 => (SubPixelType.Bgra, PdfiumNativeLibrary.FPDFBitmap_BGRA),
_ => throw new ArgumentException()
};
var pixelInfo = new PixelInfo(w, h, subPixelType);
var buffer = new byte[pixelInfo.Length];
fixed (byte* ptr = buffer)
{
var pdfiumBitmap = PdfBitmap.CreateFromPointer(w, h, (IntPtr) ptr, pixelInfo.Stride, format);
pdfiumBitmap.FillRect(0, 0, w, h, PdfBitmap.WHITE);
pdfiumBitmap.RenderPage(page, 0, 0, w, h);
return pdfiumBitmap;
}
}
private static IMemoryImage CopyPdfBitmapToNewImage(ImageContext imageContext, PdfBitmap pdfBitmap,
PdfImageMetadata imageMetadata)
{
var dstImage = imageContext.Create(pdfBitmap.Width, pdfBitmap.Height, pdfBitmap.Format);
var (targetPixelFormat, subPixelType) = imageMetadata.BitsPerPixel switch
{
1 => (ImagePixelFormat.BW1, SubPixelType.Gray),
8 => (ImagePixelFormat.Gray8, SubPixelType.Gray),
24 => (ImagePixelFormat.RGB24, SubPixelType.Bgr),
32 => (ImagePixelFormat.ARGB32, SubPixelType.Bgra),
_ => throw new ArgumentException()
};
var dstImage = imageContext.Create(pdfBitmap.Width, pdfBitmap.Height, targetPixelFormat);
dstImage.SetResolution(imageMetadata.HorizontalDpi, imageMetadata.VerticalDpi);
var subPixelType = pdfBitmap.Format == ImagePixelFormat.ARGB32 ? SubPixelType.Bgra : SubPixelType.Bgr;
var srcPixelInfo = new PixelInfo(pdfBitmap.Width, pdfBitmap.Height, subPixelType, pdfBitmap.Stride);
new CopyBitwiseImageOp().Perform(pdfBitmap.Buffer, srcPixelInfo, dstImage);
return dstImage;