mirror of
https://github.com/cyanfish/naps2.git
synced 2024-09-11 15:26:55 +03:00
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:
parent
b94e81f16b
commit
0fa3d47edf
@ -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()
|
||||
{
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user