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,
|
generate_pdfa_ps,
|
||||||
speculative_pdfa_conversion,
|
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
|
from ocrmypdf.pluginspec import GhostscriptRasterDevice, OrientationConfidence
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -508,6 +508,49 @@ def calculate_raster_dpi(page_context: PageContext):
|
|||||||
return canvas_dpi, page_dpi
|
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(
|
def rasterize(
|
||||||
input_file: Path,
|
input_file: Path,
|
||||||
page_context: PageContext,
|
page_context: PageContext,
|
||||||
@@ -529,39 +572,13 @@ def rasterize(
|
|||||||
Returns:
|
Returns:
|
||||||
Path: The output PNG file path.
|
Path: The output PNG file path.
|
||||||
"""
|
"""
|
||||||
colorspaces = [
|
|
||||||
GhostscriptRasterDevice.PNGMONO,
|
|
||||||
GhostscriptRasterDevice.PNGGRAY,
|
|
||||||
GhostscriptRasterDevice.PNG256,
|
|
||||||
GhostscriptRasterDevice.PNG16M,
|
|
||||||
]
|
|
||||||
device_idx = 0
|
|
||||||
|
|
||||||
if remove_vectors is None:
|
if remove_vectors is None:
|
||||||
remove_vectors = page_context.options.remove_vectors
|
remove_vectors = page_context.options.remove_vectors
|
||||||
|
|
||||||
output_file = page_context.get_path(f'rasterize{output_tag}.png')
|
output_file = page_context.get_path(f'rasterize{output_tag}.png')
|
||||||
pageinfo = page_context.pageinfo
|
pageinfo = page_context.pageinfo
|
||||||
|
|
||||||
def at_least(colorspace):
|
device = _select_raster_device(pageinfo)
|
||||||
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]
|
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Rasterize with {device}, rotation {correction}, mediabox {pageinfo.mediabox}"
|
f"Rasterize with {device}, rotation {correction}, mediabox {pageinfo.mediabox}"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|||||||
import warnings
|
import warnings
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pikepdf
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from reportlab.lib.units import inch
|
from reportlab.lib.units import inch
|
||||||
@@ -13,8 +14,10 @@ from reportlab.lib.utils import ImageReader
|
|||||||
from reportlab.pdfgen.canvas import Canvas
|
from reportlab.pdfgen.canvas import Canvas
|
||||||
|
|
||||||
from ocrmypdf import _pipeline, pdfinfo
|
from ocrmypdf import _pipeline, pdfinfo
|
||||||
|
from ocrmypdf._pipeline import _select_raster_device
|
||||||
from ocrmypdf.helpers import Resolution
|
from ocrmypdf.helpers import Resolution
|
||||||
from ocrmypdf.pdfinfo import Encoding
|
from ocrmypdf.pdfinfo import Encoding
|
||||||
|
from ocrmypdf.pluginspec import GhostscriptRasterDevice
|
||||||
|
|
||||||
warnings.filterwarnings(
|
warnings.filterwarnings(
|
||||||
"ignore", category=DeprecationWarning, module="reportlab.lib.rl_safe_eval"
|
"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 = Mock()
|
||||||
pageinfo.images = [Mock(enc=enc) for enc in encodings]
|
pageinfo.images = [Mock(enc=enc) for enc in encodings]
|
||||||
assert _pipeline.should_visible_page_image_use_jpg(pageinfo) == expected
|
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