Add Encoding.flate_jpeg to recognize deflated JPEG images
FlateDecode+DCTDecode compressed images are essentially deflated JPEGs, typically created by OCRmyPDF's optimizer. This change ensures pdfinfo correctly identifies them and should_visible_page_image_use_jpg treats them as JPEG-origin images, allowing JPEG output when appropriate.
This commit is contained in:
@@ -735,7 +735,8 @@ def ocr_engine_direct(
|
||||
def should_visible_page_image_use_jpg(pageinfo: PageInfo) -> bool:
|
||||
"""Determines whether the visible page image should be saved as a JPEG.
|
||||
|
||||
If all images were JPEGs originally, permit a JPEG as output.
|
||||
If all images were JPEGs originally (including FlateDecode+DCTDecode),
|
||||
permit a JPEG as output.
|
||||
|
||||
Args:
|
||||
pageinfo: The PageInfo object containing information about the page.
|
||||
@@ -744,7 +745,7 @@ def should_visible_page_image_use_jpg(pageinfo: PageInfo) -> bool:
|
||||
A boolean indicating whether the visible page image should be saved as a JPEG.
|
||||
"""
|
||||
return bool(pageinfo.images) and all(
|
||||
im.enc == Encoding.jpeg for im in pageinfo.images
|
||||
im.enc in (Encoding.jpeg, Encoding.flate_jpeg) for im in pageinfo.images
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -108,10 +108,18 @@ class ImageInfo:
|
||||
self._type = 'image'
|
||||
|
||||
self._bpc = int(pim.bits_per_component)
|
||||
try:
|
||||
self._enc = FRIENDLY_ENCODING.get(pim.filters[0])
|
||||
except IndexError:
|
||||
self._enc = None
|
||||
if (
|
||||
len(pim.filters) == 2
|
||||
and pim.filters[0] == '/FlateDecode'
|
||||
and pim.filters[1] == '/DCTDecode'
|
||||
):
|
||||
# Special case: FlateDecode followed by DCTDecode
|
||||
self._enc = Encoding.flate_jpeg
|
||||
else:
|
||||
try:
|
||||
self._enc = FRIENDLY_ENCODING.get(pim.filters[0])
|
||||
except IndexError:
|
||||
self._enc = None
|
||||
|
||||
try:
|
||||
self._color = FRIENDLY_COLORSPACE.get(pim.colorspace or '')
|
||||
|
||||
@@ -36,6 +36,7 @@ class Encoding(Enum):
|
||||
lzw = auto()
|
||||
flate = auto()
|
||||
runlength = auto()
|
||||
flate_jpeg = auto()
|
||||
|
||||
|
||||
FloatRect = tuple[float, float, float, float]
|
||||
|
||||
@@ -131,6 +131,55 @@ def test_jpeg(resources):
|
||||
assert isclose(pdfimage.dpi.x, 150)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flate_jpeg_pdf(outpdf):
|
||||
"""Create a PDF with a FlateDecode+DCTDecode (flate+jpeg) encoded image.
|
||||
|
||||
This simulates what OCRmyPDF's optimizer does when it deflates JPEGs.
|
||||
"""
|
||||
from zlib import compress
|
||||
|
||||
# Create an RGB image and save as JPEG
|
||||
im = Image.new('RGB', (64, 64), color=(128, 64, 192))
|
||||
bio = BytesIO()
|
||||
im.save(bio, format='JPEG')
|
||||
jpeg_data = bio.getvalue()
|
||||
|
||||
# Compress the JPEG data with flate
|
||||
flate_jpeg_data = compress(jpeg_data)
|
||||
|
||||
# Create a PDF with the flate+jpeg image
|
||||
with pikepdf.Pdf.new() as pdf:
|
||||
pdf.add_blank_page(page_size=(72, 72))
|
||||
image_dict = pikepdf.Stream(
|
||||
pdf,
|
||||
flate_jpeg_data,
|
||||
BitsPerComponent=8,
|
||||
ColorSpace=pikepdf.Name.DeviceRGB,
|
||||
Filter=[pikepdf.Name.FlateDecode, pikepdf.Name.DCTDecode],
|
||||
Height=64,
|
||||
Subtype=pikepdf.Name.Image,
|
||||
Type=pikepdf.Name.XObject,
|
||||
Width=64,
|
||||
)
|
||||
objname = pdf.pages[0].add_resource(
|
||||
image_dict, pikepdf.Name.XObject, pikepdf.Name.Im0
|
||||
)
|
||||
pdf.pages[0].Contents = pikepdf.Stream(
|
||||
pdf, b"q 72 0 0 72 0 0 cm %s Do Q" % bytes(objname)
|
||||
)
|
||||
pdf.save(outpdf)
|
||||
return outpdf
|
||||
|
||||
|
||||
def test_flate_jpeg(flate_jpeg_pdf):
|
||||
"""Test that pdfinfo correctly identifies FlateDecode+DCTDecode as flate_jpeg."""
|
||||
pdf = pdfinfo.PdfInfo(flate_jpeg_pdf)
|
||||
|
||||
pdfimage = pdf[0].images[0]
|
||||
assert pdfimage.enc == Encoding.flate_jpeg
|
||||
|
||||
|
||||
def test_form_xobject(resources):
|
||||
filename = resources / 'formxobject.pdf'
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from reportlab.pdfgen.canvas import Canvas
|
||||
|
||||
from ocrmypdf import _pipeline, pdfinfo
|
||||
from ocrmypdf.helpers import Resolution
|
||||
from ocrmypdf.pdfinfo import Encoding
|
||||
|
||||
warnings.filterwarnings(
|
||||
"ignore", category=DeprecationWarning, module="reportlab.lib.rl_safe_eval"
|
||||
@@ -150,3 +151,28 @@ def test_dpi_needed(image, text, vector, result, rgb_image, outdir):
|
||||
)
|
||||
def test_enumerate_compress_ranges(name, input, output):
|
||||
assert output == tuple(_pipeline.enumerate_compress_ranges(input))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'encodings, expected',
|
||||
[
|
||||
# Empty images list returns False
|
||||
([], False),
|
||||
# Single JPEG returns True
|
||||
([Encoding.jpeg], True),
|
||||
# Single flate_jpeg returns True
|
||||
([Encoding.flate_jpeg], True),
|
||||
# Mix of jpeg and flate_jpeg returns True
|
||||
([Encoding.jpeg, Encoding.flate_jpeg], True),
|
||||
# Non-JPEG encoding returns False
|
||||
([Encoding.flate], False),
|
||||
# Mix with non-JPEG returns False
|
||||
([Encoding.jpeg, Encoding.flate], False),
|
||||
([Encoding.flate_jpeg, Encoding.flate], False),
|
||||
],
|
||||
)
|
||||
def test_should_visible_page_image_use_jpg(encodings, expected):
|
||||
"""Test that should_visible_page_image_use_jpg correctly handles flate_jpeg."""
|
||||
pageinfo = Mock()
|
||||
pageinfo.images = [Mock(enc=enc) for enc in encodings]
|
||||
assert _pipeline.should_visible_page_image_use_jpg(pageinfo) == expected
|
||||
|
||||
Reference in New Issue
Block a user