Python Wand: MagickReadImage возвращает значение false, но не вызывает исключения ImageMagick

У меня есть некоторый давний код в базе кода Django, который считывается в формате PDF и использует Wand, чтобы сделать снимок экрана первой страницы PDF, который затем отображается на экране. веб-сайт. Недавно мы перенесли серверы (обновление с Ubuntu 22 LTS до 24 LTS), и что-то сломалось, и я ни за что на свете не смогу в этом разобраться.

Сначала немного потенциально полезной информации:

  • ОПЕРАЦИОННАЯ система: Ubuntu 24 LTS
  • Python 3.12.3
  • Джанго 5.2.4
  • Жезл 0.6.13
  • Веб-сервер: nginx 1.24.0
  • версия gunicorn: 23.0.0
  • Мы не используем Docker. Это приложение Django запускается непосредственно на сервере в локальной виртуальной среде.

Код для преобразования PDF в PNG находится на стороне администратора веб-приложения. Вот в чем суть:

with Image(filename=pdf_location) as pdf:
  with Image(pdf.sequence[0]) as first_page_pdf:
    with first_page_pdf.convert('png') as first_page_png:
      first_page_png.background_color = Color('white')
      first_page_png.alpha_channel = 'remove'
      return first_page_png.make_blob()

Когда я загружаю PDF-файл на сайт администратора для обработки, я получаю эту ошибку:

MagickReadImage returns false, but did not raise ImageMagick exception. This can occur when a delegate is missing, or returns EXIT_SUCCESS without generating a raster.

После долгих поисков я перепробовал все, что смог придумать, но ничего не работает:

  • У меня установлен ghostscript:
$ gs --version
10.02.1
$ which gs
/usr/bin/gs
  • Мой ImageMagick policy.xml содержит содержимое по умолчанию, найденное в файле policy-debian.xml, который входит в состав пакета ImageMagick, за заметным исключением того, что <policy domain="coder" rights="read|write" pattern="PDF" /> находится в policy.xml. Я могу убедиться, что политика в отношении PDF настроена правильно:
$ identify -list policy

Path: /etc/ImageMagick-6/policy.xml
  Policy: Resource
    name: disk
    value: 2GiB
  Policy: Resource
    name: map
    value: 2048MiB
  Policy: Resource
    name: memory
    value: 1024MiB
  Policy: Resource
    name: area
    value: 256MP
  Policy: Resource
    name: height
    value: 32KP
  Policy: Resource
    name: width
    value: 32KP
  Policy: Undefined
    rights: None
  Policy: Path
    rights: None
    pattern: @*
  Policy: Delegate
    rights: None
    pattern: URL
  Policy: Delegate
    rights: None
    pattern: HTTPS
  Policy: Delegate
    rights: None
    pattern: HTTP
  Policy: Coder
    rights: Read Write
    pattern: PDF

Path: [built-in]
  Policy: Undefined
    rights: None
  • Преобразование одного и того же PDF-файла работает ли, когда я выполняю это вручную с помощью ghostscript и imagemagick (как предлагается здесь):
$ gs -sDEVICE=pngalpha -o page-%03d.png -r120 pdf-test.pdf
GPL Ghostscript 10.02.1 (2023-11-01)
Copyright (C) 2023 Artifex Software, Inc.  All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
Processing pages 1 through 1.
Page 1
Loading font ArialMT (or substitute) from /usr/share/ghostscript/10.02.1/Resource/Font/NimbusSans-Regular

и

$ convert -density 120 pdf-test.pdf page-%03d.png

Оба правильно создают page-001.png при использовании этого тестового PDF-файла.

  • И, наконец, когда я выполняю это вручную в моей оболочке Django (т.е. в том же venv, который использует nginx), это также работает должным образом:
$ ./manage_dev.py shell
19 objects imported automatically (use -v 2 for details).

Python 3.12.3 (main, Jun 18 2025, 17:59:45) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from wand.image import Image, Color
>>> with Image(filename='pdf-test.pdf') as pdf:
...   with Image(pdf.sequence[0]) as first_page_pdf:
...     with first_page_pdf.convert('png') as first_page_png:
...       first_page_png.background_color = Color('white')
...       first_page_png.alpha_channel = 'remove'
...       blob = first_page_png.make_blob()
...       with open('screenshot.png', 'wb') as png:
...         png.write(blob)
...
24420
  • Немного странно то, что когда я перечисляю делегатов ImageMagick, gs в списке нет. Это единственное, что я могу придумать, что может быть причиной проблемы, но я не могу понять, как включить это в список:
$ convert -list configure | grep DELEGATES
DELEGATES      bzlib djvu fftw fontconfig freetype heic jbig jng jpeg lcms lqr lzma openexr openjp2 pango png ps raw tiff webp wmf x xml zlib zstd
DELEGATES      bzlib djvu fftw fontconfig freetype heic jbig jng jp2 jpeg lcms lqr ltdl lzma openexr pangocairo png raw tiff webp wmf x xml zlib

Обратите внимание, что это не опечатка; здесь 2 строки DELEGATES, и ни одна из них не содержит gs.

Еще раз повторю, что этот код отлично работал в течение многих лет до переноса / обновления сервера, так что все это наводит меня на мысль, что это должен быть какой-то конфигурационный файл (ImageMagick, nginx?) где-то за пределами моего кода, но я просто не могу его определить. Я очень надеюсь, что у кого-нибудь из вас появятся какие-то идеи.

Заранее спасибо!

enter image description here

# ---- PERSONAL Preamble for testing the function DO NOT ADD TO function LEAVE THIS ALONE
import pymupdf, os, subprocess, tempfile, base64
gs = os.environ.get("GS_EXE") or r"C:\Users\WDAGUtilityAccount\Desktop\Apps\PDF\gs\10.05.1\bin\gs.exe"
def create_hello_world_pdf():
    fd, pdf_location = tempfile.mkstemp(suffix=".pdf"); os.close(fd)  # Close the file descriptor immediately
    doc = pymupdf.open(); page = doc.new_page(); page.insert_text((200, 72), "HELLO WORLD", fontsize=20)
    doc.save(pdf_location); doc.close()
    return pdf_location
# ---- PERSONAL Preamble for testing the function DO NOT ADD TO function LEAVE THIS ALONE



# ---- Replacement Function that returns dotted call: first_page_png.make_blob()

# OP required white background, no alpha
gs_opts = "-dSAFER -sDEVICE=png16m -r120 -dFirstPage=1 -dLastPage=1 -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -o"

def first_page_png_make_blob(pdf_location):
    with tempfile.TemporaryDirectory() as tmpdir:
        png_path = os.path.join(tmpdir, "page.png")
        cmd = [gs] + gs_opts.split() + [png_path, pdf_location]
        subprocess.run(cmd, check=True)
        class FirstPagePNG:
            def make_blob(self):
                with open(png_path, "rb") as f:
                    return f.read()
        first_page_png = FirstPagePNG()
        return first_page_png.make_blob()

# ---- TEST: Prove first_page_png.make_blob() returns valid PNG

pdf_location = create_hello_world_pdf()
blob = first_page_png_make_blob(pdf_location)
encoded = base64.b64encode(blob).decode("utf-8")
html = f"""
<html><body><h2>Proof: PNG from dotted call first_page_png.make_blob()</h2><img src="data:image/png;base64,{encoded}" /></body></html>
"""
with open("proof.html", "w", encoding="utf-8") as f:
    f.write(html)
print("PNG proof written to proof.html — open it in your browser to verify.")
Вернуться на верх