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:
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:
$ gs --version
10.02.1
$ which gs
/usr/bin/gs
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
$ 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.
$ ./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
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:
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
.
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.
# ---- 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.")