import re, subprocess, os, shutil # The two poppler tools a PDF image needs, found on PATH -- never by an # absolute path: /usr/bin/pdfinfo exists on a Debian-family Linux and # nowhere else (Homebrew puts poppler under /opt/homebrew/bin, MSYS2 under # C:/msys64/ucrt64/bin), and a user on those systems saw "No such file" # with no hint of what to install. Mirrors latex_util.cpp's report of a # missing xelatex. def _tool(name): path = shutil.which(name) if path is None: raise Exception( f'The "{name}" command required to use a PDF file as an image is not ' 'installed. It is part of poppler: on Ubuntu "sudo apt-get install ' 'poppler-utils", with Homebrew "brew install poppler", on Windows ' 'MSYS2\'s "pacman -S mingw-w64-ucrt-x86_64-poppler" and ' 'C:\\msys64\\ucrt64\\bin on the Path') return path def dimensions_from_prolog(filename): mediabox_pat = re.compile(r"/MediaBox\s+\[(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\]", re.S) width_pat = re.compile(r"/Width\s+(\d+)", re.S) height_pat = re.compile(r"/Height\s+(\d+)", re.S) width = None height = None with open(filename, mode="rb") as fp: line = fp.readline().decode("ascii") while line: match = mediabox_pat.match(line) if match: x1, y1, x2, y2 = [int(e) for e in match.groups()] width = x2 - x1 height = y2 - y1 else: match = width_pat.match(line) if match: width = int(match.group(1)) else: match = height_pat.match(line) if match: height = int(match.group(1)) if width and height: break line = fp.readline().decode("ascii") return width, height def dimensions_from_pdfinfo(filename): sp = subprocess.Popen( [_tool("pdfinfo"), filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = sp.communicate() out = out.decode("ascii", errors="replace") width, height = None, None if out: pat = re.compile(r"Page size:\s+([\d.]+) x ([\d.]+) pts", re.S) match = pat.search(out) if match: width, height = [int(round(float(e))) for e in match.groups()] return width, height # Doesn't agree with pdfinfo; why? # from pdf2image import convert_from_path # def dimensions_from_pdf2image(filename): # width, height = None, None # if pdf2image_imported: # img = convert_from_path(filename)[0] # width, height = img.size # return width, height def dimensions(filename): # The prolog scan answers for a PDF whose MediaBox is in its first # lines; a compressed or binary prolog either raises (a non-ASCII byte) # or answers None, and EITHER is pdfinfo's turn -- the None case once # fell straight through to the error below without asking it. width, height = None, None try: width, height = dimensions_from_prolog(filename) except Exception: pass if width is None or height is None: width, height = dimensions_from_pdfinfo(filename) if width is None or height is None: raise Exception(f'Could not determine the width and height of "{filename}"') return width, height def convert(pdf_filename, image_filename): if image_filename.endswith(".jpg"): arg1 = "-jpeg" elif image_filename.endswith(".png"): arg1 = "-png" else: raise Exception(f'PDF file "{pdf_filename}" can only be converted to PNG or JPEG') arg2 = "-singlefile" command = _tool("pdftoppm") basename = os.path.splitext(image_filename)[0] sp = subprocess.Popen( [command, arg1, arg2, pdf_filename, basename], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = sp.communicate() if sp.returncode != 0 or not os.path.exists(image_filename): raise Exception(f'Error creating image file from "{pdf_filename}": ' f'{err.decode("utf-8", errors="replace").strip()}')