如何使用PDFSharp从PDF中提取FlateDecoded图像

5

我该如何用PDFSharp从PDF文档中提取FlateDecoded格式的图片(例如PNG)?

我在PDFSharp的一个示例中发现了这个评论:

// TODO: You can put the code here that converts vom PDF internal image format to a
// Windows bitmap
// and use GDI+ to save it in PNG format.
// [...]
// Take a look at the file
// PdfSharp.Pdf.Advanced/PdfImage.cs to see how we create the PDF image formats.

有人有解决这个问题的办法吗?
谢谢你们的回复。
编辑:因为我不能在8小时内回答自己的问题,所以我这样做:
感谢您的非常快速的回复。
我添加了一些代码到“ExportAsPngImage”方法中,但我没有得到想要的结果。它只是提取了更多的图像(png),它们没有正确的颜色并且失真了。
这是我的实际代码:
PdfSharp.Pdf.Filters.FlateDecode flate = new PdfSharp.Pdf.Filters.FlateDecode();
        byte[] decodedBytes = flate.Decode(bytes);

        System.Drawing.Imaging.PixelFormat pixelFormat;

        switch (bitsPerComponent)
        {
            case 1:
                pixelFormat = PixelFormat.Format1bppIndexed;
                break;
            case 8:
                pixelFormat = PixelFormat.Format8bppIndexed;
                break;
            case 24:
                pixelFormat = PixelFormat.Format24bppRgb;
                break;
            default:
                throw new Exception("Unknown pixel format " + bitsPerComponent);
        }

        Bitmap bmp = new Bitmap(width, height, pixelFormat);
        var bmpData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, pixelFormat);
        int length = (int)Math.Ceiling(width * bitsPerComponent / 8.0);
        for (int i = 0; i < height; i++)
        {
            int offset = i * length;
            int scanOffset = i * bmpData.Stride;
            Marshal.Copy(decodedBytes, offset, new IntPtr(bmpData.Scan0.ToInt32() + scanOffset), length);
        }
        bmp.UnlockBits(bmpData);
        using (FileStream fs = new FileStream(@"C:\Export\PdfSharp\" + String.Format("Image{0}.png", count), FileMode.Create, FileAccess.Write))
        {
            bmp.Save(fs, System.Drawing.Imaging.ImageFormat.Png);
        }

这是正确的方法吗?还是我应该选择另一种方法?非常感谢!


关于24 bpp:一种格式是RGB,另一种是BGR。因此,Marshal.Copy不能使用,您必须在复制时交换字节。因此颜色会出现错误。 您没有将BMP数据对齐到DWORD边界。这应该可以解释图像失真的原因。 - I liked the old Stack Overflow
请注意,bmpData.Scan0.ToInt32()在64位系统上会失败,需要更改为ToInt64 - JeffreyABecker
6个回答

5

我知道这个回答可能晚了几年,但也许它会对其他人有所帮助。

在我的情况下,畸变是因为image.Elements.GetInteger(PdfImage.Keys.BitsPerComponent)似乎没有返回正确的值。正如Vive la déraison在你的问题下指出的那样,你得到了使用Marshal.Copy的BGR格式。因此,在执行Marshal.Copy之后反转字节并旋转位图将完成工作。

最终代码看起来像这样:

private static void ExportAsPngImage(PdfDictionary image, ref int count)
    {
        int width = image.Elements.GetInteger(PdfImage.Keys.Width);
        int height = image.Elements.GetInteger(PdfImage.Keys.Height);

        var canUnfilter = image.Stream.TryUnfilter();
        byte[] decodedBytes;

        if (canUnfilter)
        {
            decodedBytes = image.Stream.Value;
        }
        else
        {
            PdfSharp.Pdf.Filters.FlateDecode flate = new PdfSharp.Pdf.Filters.FlateDecode();
            decodedBytes = flate.Decode(image.Stream.Value);
        }

        int bitsPerComponent = 0;
        while (decodedBytes.Length - ((width * height) * bitsPerComponent / 8) != 0)
        {
            bitsPerComponent++;
        }

        System.Drawing.Imaging.PixelFormat pixelFormat;
        switch (bitsPerComponent)
        {
            case 1:
                pixelFormat = System.Drawing.Imaging.PixelFormat.Format1bppIndexed;
                break;
            case 8:
                pixelFormat = System.Drawing.Imaging.PixelFormat.Format8bppIndexed;
                break;
            case 16:
                pixelFormat = System.Drawing.Imaging.PixelFormat.Format16bppArgb1555;
                break;
            case 24:
                pixelFormat = System.Drawing.Imaging.PixelFormat.Format24bppRgb;
                break;
            case 32:
                pixelFormat = System.Drawing.Imaging.PixelFormat.Format32bppArgb;
                break;
            case 64:
                pixelFormat = System.Drawing.Imaging.PixelFormat.Format64bppArgb;
                break;
            default:
                throw new Exception("Unknown pixel format " + bitsPerComponent);
        }

        decodedBytes = decodedBytes.Reverse().ToArray();

        Bitmap bmp = new Bitmap(width, height, pixelFormat);
        BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);
        int length = (int)Math.Ceiling(width * (bitsPerComponent / 8.0));
        for (int i = 0; i < height; i++)
        {
            int offset = i * length;
            int scanOffset = i * bmpData.Stride;
            Marshal.Copy(decodedBytes, offset, new IntPtr(bmpData.Scan0.ToInt32() + scanOffset), length);
        }
        bmp.UnlockBits(bmpData);
        bmp.RotateFlip(RotateFlipType.Rotate180FlipNone);
        bmp.Save(String.Format("exported_Images\\Image{0}.png", count++), System.Drawing.Imaging.ImageFormat.Png);
    }

代码可能需要一些优化,但在我的情况下它能正确地导出FlateDecoded图像。

1
对我来说,image.Stream.Value返回了一个字节数组,其中每一行像素都以空字节结尾。为了从带有额外空字节的字节数组中输出图像,我进行了以下更改: 我设置了int bitsPerComponent = decodedBytes.Length / height * 8 / width;。 我设置了int length = decodedBytes.Length / height;。 我在Marshal.Copy方法中使用了bmpData.Stride作为长度参数。 而且应该是Array.Reverse(decodedBytes); - CoryCoolguy
我不理解 while 循环。计算背后的逻辑是什么?在我的情况下,图像为 244*244,每秒钟传输速率为 1bps(根据 PDF 信息)。解码数据的长度为 7808。第一轮它将 bpc 增加到 1。第二轮差异将会是 366,所以再次增加。从那里开始,差异将不断增加,导致无限循环。我的问题吗?在我的情况下,解码数据似乎已经正确解码(我希望如此)。 - StanE
指针转换对我来说是错误的。它必须是bmpData.Scan0.ToInt64()! - hawaii

1

到目前为止...我的代码...它可以处理许多png文件,但无法处理来自Adobe Photoshop且颜色空间为索引的文件:

    private bool ExportAsPngImage(PdfDictionary image, string SaveAsName)
        {
            int width = image.Elements.GetInteger(PdfSharp.Pdf.Advanced.PdfImage.Keys.Width);
            int height = image.Elements.GetInteger(PdfSharp.Pdf.Advanced.PdfImage.Keys.Height);
            int bitsPerComponent = image.Elements.GetInteger(PdfSharp.Pdf.Advanced.PdfImage.Keys.BitsPerComponent);
            var ColorSpace = image.Elements.GetArray(PdfImage.Keys.ColorSpace);
System.Drawing.Imaging.PixelFormat pixelFormat= System.Drawing.Imaging.PixelFormat.Format24bppRgb; //24 just for initalize

            if (ColorSpace is null) //no colorspace.. bufferedimage?? is in BGR order instead of RGB so change the byte order. Right now it works
            {
                byte[] origineel_byte_boundary = image.Stream.UnfilteredValue;
                bitsPerComponent = (origineel_byte_boundary.Length) / (width * height);
                switch (bitsPerComponent)
                {
                    case 4:
                        pixelFormat = System.Drawing.Imaging.PixelFormat.Format32bppPArgb;
                        break;
                    case 3:
                        pixelFormat = System.Drawing.Imaging.PixelFormat.Format24bppRgb;
                        break;
                    default:
                        {
                            MessageBox.Show("Unknown pixel format " + bitsPerComponent, "Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                            return false;
                        }
                        break;
                }
                Bitmap bmp = new Bitmap(width, height, pixelFormat); //copy raw bytes to "master" bitmap so we are out of pdf format to work with 
                System.Drawing.Imaging.BitmapData bmd = bmp.LockBits(new Rectangle(0, 0, width, height), System.Drawing.Imaging.ImageLockMode.WriteOnly, pixelFormat);
                System.Runtime.InteropServices.Marshal.Copy(origineel_byte_boundary, 0, bmd.Scan0, origineel_byte_boundary.Length);
                bmp.UnlockBits(bmd);
                Bitmap bmp2 = new Bitmap(width, height, pixelFormat);
                for (int indicex = 0; indicex < bmp.Width; indicex++)
                {
                    for (int indicey = 0; indicey < bmp.Height; indicey++)
                    {
                        Color nuevocolor = bmp.GetPixel(indicex, indicey);
                        Color colorintercambiado = Color.FromArgb(nuevocolor.A, nuevocolor.B, nuevocolor.G, nuevocolor.R);
                        bmp2.SetPixel(indicex, indicey, colorintercambiado);
                    }
                }
                using (FileStream fs = new FileStream(SaveAsName, FileMode.Create, FileAccess.Write))
                {
                    bmp2.Save(fs, System.Drawing.Imaging.ImageFormat.Png);
                }
                bmp2.Dispose();
                bmp.Dispose();
            }
            else
            {
// this is the case of photoshop... work needs to be done here. I ´m able to get the color palette but no idea how to put it back or create the png file... 
                switch (bitsPerComponent)
                {
                    case 4:
                        pixelFormat = System.Drawing.Imaging.PixelFormat.Format32bppArgb;
                        break;
                    default:
                        {
                            MessageBox.Show("Unknown pixel format " + bitsPerComponent, "Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                            return false;
                        }
                        break;
                }
                if ((ColorSpace.Elements.GetName(0) == "/Indexed") && (ColorSpace.Elements.GetName(1) == "/DeviceRGB"))
                {
                    //we need to create the palette
                    int paletteColorCount = ColorSpace.Elements.GetInteger(2);
                    List<System.Drawing.Color> paletteList = new List<Color>();
                    //Color[] palette = new Color[paletteColorCount+1]; // no idea why but it seams that there´s always 1 color more. ¿transparency?
                    PdfObject paletteObj = ColorSpace.Elements.GetObject(3);
                    PdfDictionary paletteReference = (PdfDictionary)paletteObj;
                    byte[] palettevalues = paletteReference.Stream.Value;
                    for (int index = 0; index < (paletteColorCount + 1); index++)
                    {
                        //palette[index] = Color.FromArgb(1, palettevalues[(index*3)], palettevalues[(index*3)+1], palettevalues[(index*3)+2]); // RGB
                        paletteList.Add(Color.FromArgb(1, palettevalues[(index * 3)], palettevalues[(index * 3) + 1], palettevalues[(index * 3) + 2])); // RGB
                    }                  
                }
            }
            return true;
        }

还有一种颜色是白色,它默认包含在内以显示它而不是透明像素。 - hsCode

1
这是我完成此任务的完整代码。
我正在从PDF中提取UPS运输标签,因此我预先知道格式。如果您提取的图像类型未知,则需要检查bitsPerComponent并相应处理。在此处,我仅处理第一页上的第一个图像。
注意:我使用TryUnfilter进行“解压缩”,它使用应用的任何过滤器并直接对数据进行解码。无需显式调用“Deflate”。
    var file = @"c:\temp\PackageLabels.pdf";

    var doc = PdfReader.Open(file);
    var page = doc.Pages[0];

    {
        // Get resources dictionary
        PdfDictionary resources = page.Elements.GetDictionary("/Resources");
        if (resources != null)
        {
            // Get external objects dictionary
            PdfDictionary xObjects = resources.Elements.GetDictionary("/XObject");
            if (xObjects != null)
            {
                ICollection<PdfItem> items = xObjects.Elements.Values;

                // Iterate references to external objects
                foreach (PdfItem item in items)
                {
                    PdfReference reference = item as PdfReference;
                    if (reference != null)
                    {
                        PdfDictionary xObject = reference.Value as PdfDictionary;
                        // Is external object an image?
                        if (xObject != null && xObject.Elements.GetString("/Subtype") == "/Image")
                        {
                            // do something with your image here 
                            // only the first image is handled here
                            var bitmap = ExportImage(xObject);
                            bmp.Save(@"c:\temp\exported.png", System.Drawing.Imaging.ImageFormat.Bmp);
                        }
                    }
                }
            }
        }
    }

使用这些辅助函数。
    private static Bitmap ExportImage(PdfDictionary image)
    {
        string filter = image.Elements.GetName("/Filter");
        switch (filter)
        {
            case "/FlateDecode":
                return ExportAsPngImage(image);

            default:
                throw new ApplicationException(filter + " filter not implemented");
        }
    }

    private static Bitmap ExportAsPngImage(PdfDictionary image)
    {
        int width = image.Elements.GetInteger(PdfImage.Keys.Width);
        int height = image.Elements.GetInteger(PdfImage.Keys.Height);
        int bitsPerComponent = image.Elements.GetInteger(PdfImage.Keys.BitsPerComponent);   

        var canUnfilter = image.Stream.TryUnfilter();
        var decoded = image.Stream.Value;

        Bitmap bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
        BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);
        Marshal.Copy(decoded, 0, bmpData.Scan0, decoded.Length);
        bmp.UnlockBits(bmpData);

        return bmp;
    }

PDF中的图像具有字节对齐的行,Windows位图图像具有DWORD对齐的行。在8 BPP情况下,如果宽度是4的倍数,则可以正常工作。为了支持任何宽度,必须为每一行调用“MarshalCopy”。感谢您的代码-您已经提到它是一个单一的解决方案,而不是通用解决方案。 - I liked the old Stack Overflow
找了这么多年,发现排名第一的库竟然不能做这么简单的事情,真是令人沮丧。所以,一旦我把基础工作做好了,就想分享出来。如果你有更好的改进意见,请随意编辑我的答案。 - Simon_Weaver
在我的情况下,TryUnfilter返回false。这可能是什么原因? - Saurabh Harwande
@SaurabhHarwande 我的猜测是这只意味着它不需要被过滤,但也不会有任何伤害。 - Simon_Weaver

1
要获取Windows BMP,您只需创建位图头,然后将图像数据复制到位图中即可。PDF图像是按字节对齐的(每个新行都从一个字节边界开始),而Windows BMP是按DWORD对齐的(每个新行都从DWORD边界开始(由于历史原因,一个DWORD为4个字节))。
位图头所需的所有信息都可以在过滤器参数中找到,或者可以计算得出。
颜色调色板是PDF中的另一个FlateEncoded对象。您还需要将其复制到BMP中。
这必须针对几种格式完成(每像素1位,8 bpp,24 bpp,32 bpp)。

0

PDF 可能包含具有掩码和不同颜色空间选项的图像,因此简单解码图像对象可能在某些情况下无法正常工作。

因此,代码还需要检查 PDF 中的图像掩码(/ImageMask)和图像对象的其他属性(以查看图像是否应该使用反转颜色或使用索引颜色),以便重新创建类似于 PDF 中显示的图像。请参阅官方 PDF 参考 中的图像对象、/ImageMask 和 /Decode 字典。

不确定 PDFSharp 是否能够在 PDF 中找到图像掩码对象,但是 iTextSharp 能够访问图像掩码对象(请参见 PdfName.MASK 对象类型)。

商业工具如 PDF Extractor SDK 能够提取原始形式和“呈现形式”中的图像。

我为 ByteScout 工作,是 PDF Extractor SDK 的制造商


-1

也许不能直接回答问题,但是从 PDF 提取图像的另一个选择是使用 FreeSpire.PDF,它可以轻松地从 PDF 中提取图像。它作为 Nuget 包 https://www.nuget.org/packages/FreeSpire.PDF/ 可用。它们处理所有图像格式并可导出为 PNG。其示例代码如下:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using Spire.Pdf;

namespace ExtractImagesFromPDF
{
    class Program
    {
        static void Main(string[] args)
        {
            //Instantiate an object of Spire.Pdf.PdfDocument
            PdfDocument doc = new PdfDocument();
            //Load a PDF file 
            doc.LoadFromFile("sample.pdf");
            List<Image> ListImage = new List<Image>();
            for (int i = 0; i < doc.Pages.Count; i++)
            {
                // Get an object of Spire.Pdf.PdfPageBase
                PdfPageBase page = doc.Pages[i];
                // Extract images from Spire.Pdf.PdfPageBase
                Image[] images = page.ExtractImages();
                if (images != null && images.Length > 0)
                {
                    ListImage.AddRange(images);
                }

            }
            if (ListImage.Count > 0)
            {
                for (int i = 0; i < ListImage.Count; i++)
                {
                    Image image = ListImage[i];
                    image.Save("image" + (i + 1).ToString() + ".png", System.Drawing.Imaging.ImageFormat.Png);
                }
                System.Diagnostics.Process.Start("image1.png");
            }  
        }
    }
}

(代码取自https://www.e-iceblue.com/Tutorials/Spire.PDF/Spire.PDF-Program-Guide/How-to-Extract-Image-From-PDF-in-C.html


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接