3

I've got some long-standing code in a Django code base that reads in a PDF and uses Wand to take a screenshot of the first page of the PDF, which is then displayed on the website. We recently migrated servers (an upgrade from Ubuntu 22 LTS to 24 LTS), and something broke, and I can't for the life of me figure it out.

First, some potentially useful information:

  • OS: Ubuntu 24 LTS
  • Python 3.12.3
  • Django 5.2.4
  • Wand 0.6.13
  • Web server: nginx 1.24.0
  • gunicorn version: 23.0.0
  • We are not using Docker. This Django app is running directly on the server with a local virtual environment.

The PDF-to-PNG code is on the admin side of the web app. Here's the heart of it:

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()

When I upload a PDF to the admin site for processing, I'm getting this error:

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.

I have tried everything I can think of after a ton of searching, but nothing is working:

  • I do have ghostscript installed:
$ gs --version
10.02.1
$ which gs
/usr/bin/gs
  • My ImageMagick policy.xml contains the default content found in the policy-debian.xml file that's included with the ImageMagick package, with the notable exception of ensuring that <policy domain="coder" rights="read|write" pattern="PDF" /> is in the policy.xml. I can verify that the PDF policy is properly set:
$ 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
  • Conversion of the same PDF file does work when I do it manually with both ghostscript and imagemagick (as suggested here):
$ 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

and

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

Both correctly create page-001.png when using this test PDF.

  • And finally, when doing this manually within my Django shell (i.e., within the same venv that nginx uses), it also works properly:
$ ./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
  • One thing that is a bit bizarre is that when I list the ImageMagick delegates, gs is not listed. This is the only thing I can think of that might be causing the issue, but I can't figure out how to get it listed:
$ 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

Note that this is not a typo; there are 2 DELEGATES lines here, and neither contains gs.

To reiterate, this code has worked perfectly for many years prior to this server migration/upgrade, so all this leads me to believe that it must be some configuration file (ImageMagick, nginx?) somewhere outside of my code, but I just can't nail it down. I'm really hoping that one of you might have some insights.

Thanks in advance!

[edit]

Here are some responses to comments below:

  • I don't see pdf listed when I do convert -version. Should I?
$ convert -version
Version: ImageMagick 6.9.12-98 Q16 x86_64 18038 https://legacy.imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC Modules OpenMP(4.5)
Delegates (built-in): bzlib djvu fftw fontconfig freetype heic jbig jng jp2 jpeg lcms lqr ltdl lzma openexr pangocairo png raw tiff webp wmf x xml zlib
  • convert -list configure | grep pdf doesn't return any results. I don't see pdf<=>eps....

  • I didn't have <policy domain="module" rights="none" pattern="{PS,PDF,XPS}" /> in my policy.xml. I added that line and set rights to read|write, but I'm still getting the same error. Setting it to read actually introduced a PolicyError: attempt to perform an operation not allowed by the security policy `PDF' @ error/module.c/OpenModule/1293.

40
  • 2
    in question you could add information what version of ImageMagick is installed convert -version. It gives me Version: ImageMagick 6.9.12-98 Q16 x86_64 18038 https://legacy.imagemagick.org .... Delegates (built-in): bzlib djvu fftw fontconfig freetype heic jbig jng jp2 jpeg lcms lqr ltdl lzma openexr pangocairo png raw tiff webp wmf x xml zlib. When I run convert -list configure | grep DELEGATES then I get empty screen. But when I ru convert -list configure | grep pdf then I get pdf<=>eps "gs' -sstdout=%%stderr .... Commented Jul 30 at 10:35
  • 1
    Does convert -version list pdf as a Delegate? Do you have your policy to be able to read PDF/PS/EPS? <policy domain="module" rights="none" pattern="{PS,PDF,XPS}" /> set this to read Commented Jul 30 at 15:39
  • @fmw42 I just added some additional information in the post above in response to your comment. I don't see pdf listed as a delegate. Adding that additional policy line unfortunately didn't fix it. Commented Jul 31 at 0:08
  • @furas I just added some additional information in the post above in response to your comment. Looks like I'm missing the pdf<=>eps line that you're getting in the config. Commented Jul 31 at 0:08
  • 1
    I believe sudo apt install libmagickcore-6.q16-6-extra will solve, use sudo apt update and sudo systemctl restart gunicorn as side measures. If that doesn't work edit /etc/systemd/system/gunicorn.service and add line Environment="PATH=/path/to/your/venv/bin:/usr/bin" /usr/bin is where we have gs, while the other path is venv bin. Commented Aug 20 at 13:12

1 Answer 1

1

Wand delegates some tasks to ImageMagick, which delegates PDF tasks to GhostScript and return again. So it is faster and easier to avoid the delegates problem by Wand ask Ghostscript direct to return the desired ImageBlob.

Thus all you need to resolve the issue, is to swap out the code block entry but ensure the return is as expected.

Here is a suitable emulation of a direct call and perfect return.

Set gs to your own installation and set the required options as shown.

enter image description here

# ---- PERSONAL Preamble for testing the function DO NOT ADD TO function LEAVE THIS OUT
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 OUT



# ---- Replacement Function that returns dotted call: first_page_png.make_blob()  
# ---- Substitute lines from "with Image(filename=pdf_location) as pdf:"

# 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.")
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you! This isn’t quite the solution that worked for me, but I’m making it as the answer anyway since the actual answer is buried in the comments under my OP. What worked for me was a suggestion from KJ: ensure that the Environment in my gunicorn settings included a PATH variable that included /usr/bin. Turns out that my gunicorn PATH only included the path to my venv. Including /usr/bin and then restarting the gunicorn service fixed my issue immediately.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.