pdf2html: CMYK-блоки с ext=png на деле JPEG — декодировать через Pixmap

На «Запускаем Ansible» 3-го изд. 10 из 593 картинок — блоки с
b["ext"]=="png" и colorspace=4, но реальные байты — CMYK JPEG (FFD8,
подтверждено file). Запись b["image"] как есть под именем .png давала файл,
который pngquant и часть читалок отказывались декодировать.

_image_png() декодирует сырые байты через pymupdf.Pixmap (MuPDF определяет
формат по содержимому, не по чужому ext), при colorspace не Gray/RGB
конвертирует в RGB, и всегда отдаёт настоящий PNG. На отказе декодирования —
откат к исходным байтам, не падать.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
chesirecatt
2026-10-05 12:01:05 +03:00
parent 33845bd47a
commit 5d957e7be9
+21 -1
View File
@@ -45,6 +45,26 @@ SOFT_HYPHEN = "\u00ad"
OCR = False OCR = False
def _image_png(raw):
"""Байты картинки блока -> настоящий PNG.
b["ext"] врёт: на «Запускаем Ansible» 3-го изд. блоки с ext="png" и
colorspace=4 на деле — CMYK JPEG (magic FFD8, подтверждено `file`), и
PNG-инструменты (pngquant) отказываются их декодировать с именем .png.
Вместо доверия заявленному формату декодируем через Pixmap (MuPDF сам
распознаёт реальный формат по данным) и всегда отдаём RGB PNG — тогда
имя файла (.png) не расходится с содержимым и CMYK не ловит инверсию
цвета в читалках, которые не ждут Adobe-JPEG с четырьмя каналами.
"""
try:
pix = pymupdf.Pixmap(raw)
if pix.colorspace is not None and pix.colorspace.n not in (1, 3):
pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
return pix.tobytes("png")
except Exception:
return raw # не смогли декодировать — пишем как есть, лучше так, чем ничего
def style(font, flags=0): def style(font, flags=0):
"""Полужирный и курсив: сначала флаги, затем имя — имена бывают пустыми.""" """Полужирный и курсив: сначала флаги, затем имя — имена бывают пустыми."""
f = font.lower() f = font.lower()
@@ -330,7 +350,7 @@ for pno, page in enumerate(doc):
continue # подложка отсканированной полосы: текст берём из OCR-слоя continue # подложка отсканированной полосы: текст берём из OCR-слоя
img_n += 1 img_n += 1
name = "img%02d.png" % img_n name = "img%02d.png" % img_n
(IMGDIR / name).write_bytes(b["image"]) (IMGDIR / name).write_bytes(_image_png(b["image"]))
if open_para: if open_para:
parts.append(open_para) parts.append(open_para)
open_para = None open_para = None