在Python中提取PDF中的图像而不进行重新采样,可以吗?

115
如何从PDF文档中提取所有图像,以原始分辨率和格式?(即提取tiff为tiff,jpeg为jpeg等,且不进行重新采样)。布局并不重要,我不关心源图像在页面上的位置。

谢谢。那个“PDF中图像是如何存储的”网址无法访问,但这个似乎可以:http://www.jpedal.org/PDFblog/2010/04/understanding-the-pdf-file-format-how-are-images-stored/ - nealmcb
有一个名为JPedal的Java库可以实现此功能,称为PDF Clipped Image Extraction。作者Mark Stephens对于PDF中图像的存储方式有一个简明的高级概述,这可能会帮助构建Python提取器的人。 - matt wilkie
2
将@nealmcb上面的链接移动到https://blog.idrsolutions.com/2010/04/understanding-the-pdf-file-format-how-are-images-stored/。 - Gruber
24个回答

96
你可以使用PyMuPDF模块。这将所有图像输出为.png文件,但是它可以直接使用且速度快。
import fitz
doc = fitz.open("file.pdf")
for i in range(len(doc)):
    for img in doc.getPageImageList(i):
        xref = img[0]
        pix = fitz.Pixmap(doc, xref)
        if pix.n < 5:       # this is GRAY or RGB
            pix.writePNG("p%s-%s.png" % (i, xref))
        else:               # CMYK: convert to RGB first
            pix1 = fitz.Pixmap(fitz.csRGB, pix)
            pix1.writePNG("p%s-%s.png" % (i, xref))
            pix1 = None
        pix = None

点击此处查看更多资源

这是适用于fitz 1.19.6的修改版本:

import os
import fitz  # pip install --upgrade pip; pip install --upgrade pymupdf
from tqdm import tqdm # pip install tqdm

workdir = "your_folder"

for each_path in os.listdir(workdir):
    if ".pdf" in each_path:
        doc = fitz.Document((os.path.join(workdir, each_path)))

        for i in tqdm(range(len(doc)), desc="pages"):
            for img in tqdm(doc.get_page_images(i), desc="page_images"):
                xref = img[0]
                image = doc.extract_image(xref)
                pix = fitz.Pixmap(doc, xref)
                pix.save(os.path.join(workdir, "%s_p%s-%s.png" % (each_path[:-4], i, xref)))
                
print("Done!")

2
这个非常好用!(首先需要安装pip install pymudf - Basj
18
对于想知道为什么上述安装失败的谷歌用户,请执行“pip install pymupdf”。 - VSZM
12
不要使用pip install pymupdf,尝试使用pip install PyMuPDF 更多信息 - Damotorie
1
使用这段代码时,我遇到了“RuntimeError:pixmap must be grayscale or rgb to write as png”错误,请问有人可以帮忙吗? - vault
9
@vault 这条评论已经过时了。你应该将“if pix.n < 5”更改为“if pix.n - pix.alpha < 4”,因为原来的条件无法正确地识别CMYK图像。 - Oringa
显示剩余7条评论

61
在Python中使用pypdf和Pillow库非常简单。
from pypdf import PdfReader

reader = PdfReader("example.pdf")
for page in reader.pages:
    for image in page.images:
        with open(image.name, "wb") as fp:
            fp.write(image.data)

请注意:PyPDF2已不推荐使用。请使用pypdf。


1
这里有一个相关问题链接 - vishvAs vAsuki
1
能够为我找到图片,但它们被裁剪/缩放错误,全部是黑白的并且有水平线 :( - Petri
@matt wilkie,问题不在于Sylvain的答案。如果你追溯代码,你会发现PyPDF2的作者没有实现那两个过滤器,就像在这个链接的348和353行中所看到的一样:https://github.com/mstamy2/PyPDF2/blob/master/PyPDF2/filters.py - rmutalik
1
这里的大多数评论可能应该被删除,因为它们已经过时了:(1)在过去的几个月中,PyPDF2比PyPDF4得到了更好的维护;(2)PyPDF2修复了几个长期存在的漏洞;(3)PyPDF2刚刚获得了一个更简单的接口来访问图像。 - Martin Thoma
1
@MartinThoma,它在版本2.12.1上无错误运行。 - Joe
显示剩余5条评论

32

在PDF文件中,图片通常以原始格式存储。例如,插入jpg格式的PDF文件将在其某个位置有一系列字节,当这些字节被提取出来时,它们就是一个有效的jpg文件。您可以使用此方法非常简单地从PDF中提取字节范围。我之前曾经写过关于此的文章,并提供了示例代码:从PDF中提取JPG图片


1
谢谢Ned。看起来我需要的特定PDF文件并没有使用JPEG in-situ,但如果有其他匹配的东西出现,我会保留你的示例。 - matt wilkie
3
你能否解释一下代码中的一些内容?例如,为什么要先搜索“stream”,然后再搜索 startmark?你可以直接搜索 startmark,因为这是 JPG 的起始标记,不是吗?另外,startfix 变量有什么意义,因为你根本没有改变它。 - user3599803
这对我想从中提取图像的PDF文件完美地起作用了。(如果有帮助的话,我将他的代码保存为.py文件,然后安装/使用Python 2.7.18来运行它,将我的PDF路径作为单个命令行参数传递。) - matt

25

使用PyPDF2在Python中处理CCITTFaxDecode过滤器:

import PyPDF2
import struct

"""
Links:
PDF format: http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf
CCITT Group 4: https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.6-198811-I!!PDF-E&type=items
Extract images from pdf: https://dev59.com/L3E85IYBdhLWcg3wnU0d
Extract images coded with CCITTFaxDecode in .net: https://dev59.com/5HE85IYBdhLWcg3wtV_1
TIFF format and tags: http://www.awaresystems.be/imaging/tiff/faq.html
"""


def tiff_header_for_CCITT(width, height, img_size, CCITT_group=4):
    tiff_header_struct = '<' + '2s' + 'h' + 'l' + 'h' + 'hhll' * 8 + 'h'
    return struct.pack(tiff_header_struct,
                       b'II',  # Byte order indication: Little indian
                       42,  # Version number (always 42)
                       8,  # Offset to first IFD
                       8,  # Number of tags in IFD
                       256, 4, 1, width,  # ImageWidth, LONG, 1, width
                       257, 4, 1, height,  # ImageLength, LONG, 1, lenght
                       258, 3, 1, 1,  # BitsPerSample, SHORT, 1, 1
                       259, 3, 1, CCITT_group,  # Compression, SHORT, 1, 4 = CCITT Group 4 fax encoding
                       262, 3, 1, 0,  # Threshholding, SHORT, 1, 0 = WhiteIsZero
                       273, 4, 1, struct.calcsize(tiff_header_struct),  # StripOffsets, LONG, 1, len of header
                       278, 4, 1, height,  # RowsPerStrip, LONG, 1, lenght
                       279, 4, 1, img_size,  # StripByteCounts, LONG, 1, size of image
                       0  # last IFD
                       )

pdf_filename = 'scan.pdf'
pdf_file = open(pdf_filename, 'rb')
cond_scan_reader = PyPDF2.PdfFileReader(pdf_file)
for i in range(0, cond_scan_reader.getNumPages()):
    page = cond_scan_reader.getPage(i)
    xObject = page['/Resources']['/XObject'].getObject()
    for obj in xObject:
        if xObject[obj]['/Subtype'] == '/Image':
            """
            The  CCITTFaxDecode filter decodes image data that has been encoded using
            either Group 3 or Group 4 CCITT facsimile (fax) encoding. CCITT encoding is
            designed to achieve efficient compression of monochrome (1 bit per pixel) image
            data at relatively low resolutions, and so is useful only for bitmap image data, not
            for color images, grayscale images, or general data.

            K < 0 --- Pure two-dimensional encoding (Group 4)
            K = 0 --- Pure one-dimensional encoding (Group 3, 1-D)
            K > 0 --- Mixed one- and two-dimensional encoding (Group 3, 2-D)
            """
            if xObject[obj]['/Filter'] == '/CCITTFaxDecode':
                if xObject[obj]['/DecodeParms']['/K'] == -1:
                    CCITT_group = 4
                else:
                    CCITT_group = 3
                width = xObject[obj]['/Width']
                height = xObject[obj]['/Height']
                data = xObject[obj]._data  # sorry, getData() does not work for CCITTFaxDecode
                img_size = len(data)
                tiff_header = tiff_header_for_CCITT(width, height, img_size, CCITT_group)
                img_name = obj[1:] + '.tiff'
                with open(img_name, 'wb') as img_file:
                    img_file.write(tiff_header + data)
                #
                # import io
                # from PIL import Image
                # im = Image.open(io.BytesIO(tiff_header + data))
pdf_file.close()

1
这对我来说立即有效,而且速度非常快!!所有的图像都倒置了,但是我能够用OpenCV解决这个问题。我一直在使用ImageMagick的convertsubprocess调用它,但速度非常慢。感谢分享这个解决方案。 - crld
4
其他地方指出,你的tiff_header_struct应该为'<' + '2s' + 'H' + 'L' + 'H' + 'HHLL' * 8 + 'L'。特别要注意结尾处的'L' - Dispenser
1
有关此问题的任何帮助请访问以下链接:https://dev59.com/GbPma4cB1Zd3GeqPsItv - Aakash Basu

18

我希望有人能找到一个不依赖于子系统上安装pdfimages的Python模块。 - user1717828
它不会按页面输出图像。 - Alok Nayak
2
pdfimages 经常在处理由多层组成的图像时失败,输出单独的层而不是整个图像。 - swestrup
@swestrup,你找到这个问题的解决方案了吗? - CVname
1
@CVname - 哎呀,很抱歉,我没有。 - swestrup

10

我更喜欢使用矿车,因为它非常易于使用。以下片段展示了如何从PDF中提取图像:

#pip install minecart
import minecart

pdffile = open('Invoices.pdf', 'rb')
doc = minecart.Document(pdffile)

page = doc.get_page(0) # getting a single page

#iterating through all pages
for page in doc.iter_pages():
    im = page.images[0].as_pil()  # requires pillow
    display(im)

嗨,我的矿车运行得很完美,但我有一个小问题:有时图像的布局会改变(水平->垂直)。你有任何想法如何避免这种情况吗?谢谢! - Sha Li
3
使用我的小推车,我得到了以下错误信息:pdfminer.pdftypes.PDFNotImplementedError: 不支持的过滤器:/CCITTFaxDecode。 - Javi12
显示未定义。 - Azhar Uddin Sheikh
我收到了 AttributeError: 模块 'pdfminer.pdfparser' 没有属性 'PDFDocument' 的错误。 - swestrup

10

PikePDF 可以用非常少的代码实现这一点:

from pikepdf import Pdf, PdfImage

filename = "sample-in.pdf"
example = Pdf.open(filename)

for i, page in enumerate(example.pages):
    for j, (name, raw_image) in enumerate(page.images.items()):
        image = PdfImage(raw_image)
        out = image.extract_to(fileprefix=f"{filename}-page{i:03}-img{j:03}")
< p > extract_to 会根据 PDF 中图像的编码方式自动选择文件扩展名。

如果您想要,您还可以在提取图像时输出一些关于它们的详细信息:

        # Optional: print info about image
        w = raw_image.stream_dict.Width
        h = raw_image.stream_dict.Height
        f = raw_image.stream_dict.Filter
        size = raw_image.stream_dict.Length

        print(f"Wrote {name} {w}x{h} {f} {size:,}B {image.colorspace} to {out}")

这可以打印类似于

Wrote /Im1 150x150 /DCTDecode 5,952B /ICCBased to sample2.pdf-page000-img000.jpg
Wrote /Im10 32x32 /FlateDecode 36B /ICCBased to sample2.pdf-page000-img001.png
...

参见文档,了解更多关于图片的用法,包括如何在PDF文件中替换它们。


虽然通常情况下这个方法很有效,但是需要注意以下几种类型的图片无法使用此方法进行提取:


我测试了一下,它完全符合我的需求,谢谢!但是有一个问题,filter = raw_image.stream_dict.Filter 会报错,因为 filter 是一个函数。当我更改名称时,仍然会出现错误,NotImplementedError: don't know how to __str__ this object。我还没有弄清楚 .filter 的数据类型是什么。 - Hobbes
感谢您的评论。我将filter重命名为f,以避免与Python内置的filter()函数冲突。对我来说,raw_image.stream_dict.Filterpikepdf.objects.Object的一个实例; 它似乎有一个to_json()方法,如果str()不能满足您的要求,那么您可以尝试此方法。但是,PDF规范还表明Filter也可能是列表,这可能是您所看到的内容的一部分? 这将特定于您正在尝试使用它的PDF。 您可以尝试print(type(f))print(dir(f)),以查看 f的类型、属性和方法。 - andrewdotn
3
这看起来现在是最简单且最有效的答案。如果我在尝试使用PyPDF实现之前看到它就好了!需要注意的一件事是,当我尝试导出JBIG2数据时,pikepdf崩溃了,因此我安装了jbig2decconda install jbig2dec),它可以很好地工作。上面的代码会直接保存图像数据(如果可能:DCTDecode > jpg,JPXDecode > jp2,CCITTFaxDecode > tif),否则会以无损PNG格式保存(JBIG2Decode,FlateDecode)。我认为这已经做得很好了。 - Matthias Fripp
对于 Windows,我使用 Visual Studio 编译了 jbig2dec 文件并将其放置在 Windows 目录中。源代码在此处:https://jbig2dec.com/. 在 bat 文件中:call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars32.bat" "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.30.30704\bin\Hostx86\x86\nmake.exe" msvc.mak - Rufat
我在一个包含56页图片的文档上尝试了这个方法,但只在第53页找到了一张图片。不知道问题出在哪里。 - swestrup
显示剩余2条评论

8

以下是我2019年的版本,用递归方式从PDF中获取所有图像,并使用PIL读取它们。与Python 2/3兼容。我还发现有时PDF中的图像可能会被zlib压缩,因此我的代码支持解压缩。

#!/usr/bin/env python3
try:
    from StringIO import StringIO
except ImportError:
    from io import BytesIO as StringIO
from PIL import Image
from PyPDF2 import PdfFileReader, generic
import zlib


def get_color_mode(obj):

    try:
        cspace = obj['/ColorSpace']
    except KeyError:
        return None

    if cspace == '/DeviceRGB':
        return "RGB"
    elif cspace == '/DeviceCMYK':
        return "CMYK"
    elif cspace == '/DeviceGray':
        return "P"

    if isinstance(cspace, generic.ArrayObject) and cspace[0] == '/ICCBased':
        color_map = obj['/ColorSpace'][1].getObject()['/N']
        if color_map == 1:
            return "P"
        elif color_map == 3:
            return "RGB"
        elif color_map == 4:
            return "CMYK"


def get_object_images(x_obj):
    images = []
    for obj_name in x_obj:
        sub_obj = x_obj[obj_name]

        if '/Resources' in sub_obj and '/XObject' in sub_obj['/Resources']:
            images += get_object_images(sub_obj['/Resources']['/XObject'].getObject())

        elif sub_obj['/Subtype'] == '/Image':
            zlib_compressed = '/FlateDecode' in sub_obj.get('/Filter', '')
            if zlib_compressed:
               sub_obj._data = zlib.decompress(sub_obj._data)

            images.append((
                get_color_mode(sub_obj),
                (sub_obj['/Width'], sub_obj['/Height']),
                sub_obj._data
            ))

    return images


def get_pdf_images(pdf_fp):
    images = []
    try:
        pdf_in = PdfFileReader(open(pdf_fp, "rb"))
    except:
        return images

    for p_n in range(pdf_in.numPages):

        page = pdf_in.getPage(p_n)

        try:
            page_x_obj = page['/Resources']['/XObject'].getObject()
        except KeyError:
            continue

        images += get_object_images(page_x_obj)

    return images


if __name__ == "__main__":

    pdf_fp = "test.pdf"

    for image in get_pdf_images(pdf_fp):
        (mode, size, data) = image
        try:
            img = Image.open(StringIO(data))
        except Exception as e:
            print ("Failed to read image with PIL: {}".format(e))
            continue
        # Do whatever you want with the image

1
这段代码对我来说完全可用,几乎没有修改。谢谢。 - xax
1
PyPDF2现在支持开箱即用的图像提取功能。 - Martin Thoma

7

我已经苦苦挣扎了几周,这里的许多答案都对我有所帮助,但总有些东西缺失,显然没有人在这里遇到过使用jbig2编码图像的问题。

在我要扫描的一组PDF文件中,使用jbig2编码的图像非常普遍。

据我所知,有许多复印/扫描机将纸张扫描并转换为充满jbig2编码图像的PDF文件。

因此,经过多天的测试,我决定采用dkagedal很久以前提出的答案。

以下是我在Linux上的逐步操作(如果您有其他操作系统,建议使用 Linux Docker,这样会更容易):

第一步:

apt-get install poppler-utils

然后我能够运行命令行工具 pdfimages,就像这样:

pdfimages -all myfile.pdf ./images_found/

使用上述命令,您将能够提取myfile.pdf中包含的所有图片,并将它们保存在images_found文件夹中(您需要在之前创建images_found文件夹)。
在列表中,您会发现几种类型的图片,如png、jpg、tiff等;这些都可以使用任何图形工具轻松阅读。
然后,您会看到一些名为“-145.jb2e”和“-145.jb2g”的文件。
这两个文件包含了一个以jbig2编码的图像,分别保存在头文件和数据文件中。
我曾经花费很多天时间试图找出如何将这些文件转换为可读格式,最终我找到了这个叫做jbig2dec的神奇工具。
因此,首先您需要安装这个神奇的工具:
apt-get install jbig2dec

然后您可以运行:

jbig2dec -t png -145.jb2g -145.jb2e

你将最终能够将提取出的所有图像转换为有用的内容。

祝你好运!


1
这是有用的信息,应该记录并分享,就像你刚刚做的那样。点赞。但是我建议您发布自己的新问题,然后自己回答,因为它没有涉及到在Python中如何实现,这是这个问题的重点。(随意交叉链接这些帖子,因为它们是相关的。) - matt wilkie
嗨@mattwilkie,感谢您的建议,这是问题链接:https://dev59.com/IUcFtIcB2Jgan1znsMQ2 - Marco
1
如果你想要一个更“Pythonic”的方法,你也可以使用在 另一个回答 中提到的 PikePDF 解决方案。如果你安装了 jbig2dec (可以使用 conda 完成),它也会自动将 jbig2 图像转换为 png。 - Matthias Fripp

6

我从@sylvain的代码开始。

存在一些缺陷,例如getData函数出现异常NotImplementedError: unsupported filter /DCTDecode,或者代码在某些页面上无法找到图像,因为它们位于比页面更深的级别。

这是我的代码:

import PyPDF2

from PIL import Image

import sys
from os import path
import warnings
warnings.filterwarnings("ignore")

number = 0

def recurse(page, xObject):
    global number

    xObject = xObject['/Resources']['/XObject'].getObject()

    for obj in xObject:

        if xObject[obj]['/Subtype'] == '/Image':
            size = (xObject[obj]['/Width'], xObject[obj]['/Height'])
            data = xObject[obj]._data
            if xObject[obj]['/ColorSpace'] == '/DeviceRGB':
                mode = "RGB"
            else:
                mode = "P"

            imagename = "%s - p. %s - %s"%(abspath[:-4], p, obj[1:])

            if xObject[obj]['/Filter'] == '/FlateDecode':
                img = Image.frombytes(mode, size, data)
                img.save(imagename + ".png")
                number += 1
            elif xObject[obj]['/Filter'] == '/DCTDecode':
                img = open(imagename + ".jpg", "wb")
                img.write(data)
                img.close()
                number += 1
            elif xObject[obj]['/Filter'] == '/JPXDecode':
                img = open(imagename + ".jp2", "wb")
                img.write(data)
                img.close()
                number += 1
        else:
            recurse(page, xObject[obj])



try:
    _, filename, *pages = sys.argv
    *pages, = map(int, pages)
    abspath = path.abspath(filename)
except BaseException:
    print('Usage :\nPDF_extract_images file.pdf page1 page2 page3 …')
    sys.exit()


file = PyPDF2.PdfFileReader(open(filename, "rb"))

for p in pages:    
    page0 = file.getPage(p-1)
    recurse(p, page0)

print('%s extracted images'% number)

这段代码在使用“/ICCBased”、“/FlateDecode”过滤的图像上失败,报错信息为img = Image.frombytes(mode, size, data) ValueError: not enough image data - GrantD71
2
@GrantD71 我不是专家,以前从未听说过ICCBased。而且如果您没有提供输入,就无法再现错误。 - Labo
2
我修改了你的代码,使其能够在Python 2和3上运行。我还实现了Ronan Paixão的/Indexed更改。我还将过滤器if / elif更改为“in”而不是等于。我有一个带有/ Filter类型['/ ASCII85Decode','/ FlateDecode']的PDF。我还将函数更改为返回图像块而不是写入文件。更新后的代码可以在此处找到:https://gist.github.com/gstorer/f6a9f1dfe41e8e64dcf58d07afa9ab2a - Gerald
@Gerald 太棒了,谢谢!我会查看代码并更新我的Dropbox :) - Labo
1
PyPDF2现在支持开箱即用的图像提取功能。 - Martin Thoma
显示剩余6条评论

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