feat: promote raster device for color/gray image masks; default pngmonod (#1688)
This commit is contained in:
+45
-28
@@ -44,7 +44,7 @@ from ocrmypdf.pdfa import (
|
||||
generate_pdfa_ps,
|
||||
speculative_pdfa_conversion,
|
||||
)
|
||||
from ocrmypdf.pdfinfo import Colorspace, Encoding, FloatRect, PageInfo, PdfInfo
|
||||
from ocrmypdf.pdfinfo import Colorspace, Encoding, FloatRect, Ink, PageInfo, PdfInfo
|
||||
from ocrmypdf.pluginspec import GhostscriptRasterDevice, OrientationConfidence
|
||||
|
||||
try:
|
||||
@@ -508,6 +508,49 @@ def calculate_raster_dpi(page_context: PageContext):
|
||||
return canvas_dpi, page_dpi
|
||||
|
||||
|
||||
def _select_raster_device(pageinfo: PageInfo) -> GhostscriptRasterDevice:
|
||||
"""Choose the minimum raster device that preserves the page's color depth.
|
||||
|
||||
The device escalates from 1-bit mono through grayscale, indexed, and full
|
||||
color as required by the page's images, image masks, and vector content.
|
||||
Image masks are painted with the current fill color, so a mask painted in
|
||||
gray or color escalates the device even though the mask itself is 1-bit.
|
||||
"""
|
||||
colorspaces = [
|
||||
GhostscriptRasterDevice.PNGMONOD,
|
||||
GhostscriptRasterDevice.PNGGRAY,
|
||||
GhostscriptRasterDevice.PNG256,
|
||||
GhostscriptRasterDevice.PNG16M,
|
||||
]
|
||||
device_idx = 0
|
||||
|
||||
def at_least(colorspace):
|
||||
return max(device_idx, colorspaces.index(colorspace))
|
||||
|
||||
for image in pageinfo.images:
|
||||
if image.type_ == 'stencil':
|
||||
# The fill color used to paint the mask, not the 1-bit mask data,
|
||||
# determines the color depth OCR needs.
|
||||
if image.ink == Ink.color:
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNG16M)
|
||||
elif image.ink == Ink.gray:
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNGGRAY)
|
||||
continue
|
||||
if image.bpc > 1:
|
||||
if image.color == Colorspace.index:
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNG256)
|
||||
elif image.color == Colorspace.gray:
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNGGRAY)
|
||||
else:
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNG16M)
|
||||
|
||||
if pageinfo.has_vector:
|
||||
log.debug(f"Page has vector content, using {GhostscriptRasterDevice.PNG16M}")
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNG16M)
|
||||
|
||||
return colorspaces[device_idx]
|
||||
|
||||
|
||||
def rasterize(
|
||||
input_file: Path,
|
||||
page_context: PageContext,
|
||||
@@ -529,39 +572,13 @@ def rasterize(
|
||||
Returns:
|
||||
Path: The output PNG file path.
|
||||
"""
|
||||
colorspaces = [
|
||||
GhostscriptRasterDevice.PNGMONO,
|
||||
GhostscriptRasterDevice.PNGGRAY,
|
||||
GhostscriptRasterDevice.PNG256,
|
||||
GhostscriptRasterDevice.PNG16M,
|
||||
]
|
||||
device_idx = 0
|
||||
|
||||
if remove_vectors is None:
|
||||
remove_vectors = page_context.options.remove_vectors
|
||||
|
||||
output_file = page_context.get_path(f'rasterize{output_tag}.png')
|
||||
pageinfo = page_context.pageinfo
|
||||
|
||||
def at_least(colorspace):
|
||||
return max(device_idx, colorspaces.index(colorspace))
|
||||
|
||||
for image in pageinfo.images:
|
||||
if image.type_ != 'image':
|
||||
continue # ignore masks
|
||||
if image.bpc > 1:
|
||||
if image.color == Colorspace.index:
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNG256)
|
||||
elif image.color == Colorspace.gray:
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNGGRAY)
|
||||
else:
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNG16M)
|
||||
|
||||
if pageinfo.has_vector:
|
||||
log.debug(f"Page has vector content, using {GhostscriptRasterDevice.PNG16M}")
|
||||
device_idx = at_least(GhostscriptRasterDevice.PNG16M)
|
||||
|
||||
device = colorspaces[device_idx]
|
||||
device = _select_raster_device(pageinfo)
|
||||
|
||||
log.debug(
|
||||
f"Rasterize with {device}, rotation {correction}, mediabox {pageinfo.mediabox}"
|
||||
|
||||
@@ -6,6 +6,7 @@ from __future__ import annotations
|
||||
import warnings
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pikepdf
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from reportlab.lib.units import inch
|
||||
@@ -13,8 +14,10 @@ from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
|
||||
from ocrmypdf import _pipeline, pdfinfo
|
||||
from ocrmypdf._pipeline import _select_raster_device
|
||||
from ocrmypdf.helpers import Resolution
|
||||
from ocrmypdf.pdfinfo import Encoding
|
||||
from ocrmypdf.pluginspec import GhostscriptRasterDevice
|
||||
|
||||
warnings.filterwarnings(
|
||||
"ignore", category=DeprecationWarning, module="reportlab.lib.rl_safe_eval"
|
||||
@@ -176,3 +179,39 @@ def test_should_visible_page_image_use_jpg(encodings, expected):
|
||||
pageinfo = Mock()
|
||||
pageinfo.images = [Mock(enc=enc) for enc in encodings]
|
||||
assert _pipeline.should_visible_page_image_use_jpg(pageinfo) == expected
|
||||
|
||||
|
||||
def _make_image_mask_pdf(path, content_fill: bytes):
|
||||
pdf = pikepdf.Pdf.new()
|
||||
pdf.add_blank_page(page_size=(72, 72))
|
||||
mask = pikepdf.Stream(pdf, bytes([0x7E] * 8))
|
||||
mask.Type = pikepdf.Name.XObject
|
||||
mask.Subtype = pikepdf.Name.Image
|
||||
mask.Width = 8
|
||||
mask.Height = 8
|
||||
mask.ImageMask = True
|
||||
mask.BitsPerComponent = 1
|
||||
name = pdf.pages[0].add_resource(mask, pikepdf.Name.XObject)
|
||||
pdf.pages[0].Contents = pikepdf.Stream(
|
||||
pdf, b"q 72 0 0 72 0 0 cm %s %s Do Q" % (content_fill, bytes(name))
|
||||
)
|
||||
pdf.save(path)
|
||||
return path
|
||||
|
||||
|
||||
def test_select_device_gray_mask(tmp_path):
|
||||
p = _make_image_mask_pdf(tmp_path / 'g.pdf', b"0.263 0.263 0.263 rg")
|
||||
pageinfo = pdfinfo.PdfInfo(p)[0]
|
||||
assert _select_raster_device(pageinfo) == GhostscriptRasterDevice.PNGGRAY
|
||||
|
||||
|
||||
def test_select_device_color_mask(tmp_path):
|
||||
p = _make_image_mask_pdf(tmp_path / 'c.pdf', b"0.8 0.2 0.2 rg")
|
||||
pageinfo = pdfinfo.PdfInfo(p)[0]
|
||||
assert _select_raster_device(pageinfo) == GhostscriptRasterDevice.PNG16M
|
||||
|
||||
|
||||
def test_select_device_black_mask_stays_mono(tmp_path):
|
||||
p = _make_image_mask_pdf(tmp_path / 'b.pdf', b"0 g")
|
||||
pageinfo = pdfinfo.PdfInfo(p)[0]
|
||||
assert _select_raster_device(pageinfo) == GhostscriptRasterDevice.PNGMONOD
|
||||
|
||||
Reference in New Issue
Block a user