2

I have some image files in a folder. I want to have them as a single pdf, one image per page.

I'm currently using Mutool and especially mutool create. I am willing to switch tools (command line, gui or programming library) if you have any you recommend using. Would prefer something that can be automated through scripting or code. In general, I'm looking for a way to extensively and freely manipulate pdf files.

I couldn't get a simple source for what the content stream syntax is (from what I read it's basically a part of the internal pdf format syntax, doesn't seem very user-friendly...), is this really how I'm supposed to use mutool?

Using this content stream file, how do I not resize an image? Using 1 0 0 1 <x> <y> cm for positionning should work (page 206) but it also scales the image to 1x1. And how do I make a page the size of the image in it? How do I "switch" to the next page?

So how can I create a pdf with images using mutool or some other software/code?

1

4 Answers 4

2

You can do this with HexaPDF with a bit of scripting:

require 'hexapdf'

doc = HexaPDF::Document.new
ARGV.each do |image_file|
  image = doc.images.add(image_file)
  iw = image.info.width.to_f
  ih = image.info.height.to_f
  page = doc.pages.add(:A4, orientation: (ih > iw ? :portrait : :landscape))
  pw = page.box(:media).width.to_f
  ph = page.box(:media).height.to_f
  rw, rh = pw / iw, ph / ih
  ratio = [rw, rh].min
  iw, ih = iw * ratio, ih * ratio
  x, y = (pw - iw) / 2, (ph - ih) / 2
  page.canvas.image(image, at: [x, y], width: iw, height: ih)
end
doc.write("output.pdf")

This script takes the image files as arguments and puts each one onto a separate A4 page, scaling the image to fit the page and rotating the page if necessary. If you don't want to scale the image, just set ratio = 1.

2
  • I would consider using HexaPdf if I knew Ruby, currently I really just need to get the job done. Thanks anyway!
    – Dragorn421
    Nov 5, 2019 at 16:44
  • You can just save the script above as something like script.rb, run gem install hexapdf to install HexaPDF (only needed once) and then run ruby script.rb image1.jpg image2.jpg .... This will generate an output.pdf file with all the images. A current Ruby version should be installable from your system package manager.
    – gettalong
    Nov 6, 2019 at 18:22
1

So in fact the mutool run scripts are provided an API (see everything below "JavaScript Shell") for working with pdf and do not require knowing the pdf format (contrarily to what the example scripts hint at), for reference here is a script achieving pdf->one image per page->pdf.

Lack of clear documentation (especially on argument types) made me look at the mutool source code. Most error messages I got were not related to what the problem was.

/*
Flatten a pdf by converting each page to an image and putting them in a new pdf.

Usage: mutool run flatten.js in.pdf out.pdf

Documentation:
https://mupdf.com/docs/manual-mutool-run.html
http://git.ghostscript.com/?p=mupdf.git;a=blob;f=source/tools/murun.c;h=16582b5d83234c880c4c3b67b279a4428d3f2303;hb=refs/heads/master
*/

function array_max(a)
{
    var max = a[0];
    for(var i=1;i<a.length;i++)
    {
        if(a[i] > max)
            max = a[i];
    }
    return max;
}

// scale up render if needed to have non-blurry images up to filling up screen when zooming on a page
var minRenderWidth = 1920;
var minRenderHeight = 1080;

var input = new PDFDocument(scriptArgs[0]);
var pages = input.countPages();

var dw = new DocumentWriter(scriptArgs[1], 'pdf');
for(var i=0;i<pages;i++)
{
    var page = input.loadPage(i);
    var pageBounds = page.bound();
    var scale = array_max([1, minRenderWidth/pageBounds[2], minRenderHeight/pageBounds[3]]);
    var pixmapRender = page.toPixmap(Scale(scale, scale), DeviceRGB);
    var imageRender = new Image(pixmapRender);
    var device = dw.beginPage(pageBounds);
    // translate then scale
    // no idea about how the translate bit works: not sure where the origin for coordinates is (but it works)
    device.fillImage(imageRender, Concat(Scale(pageBounds[2] - pageBounds[0], pageBounds[3] - pageBounds[1]), Translate(2*pageBounds[0], 0)), 1);
    device.close();
    dw.endPage(device);
    print('#' + (i+1) + ' done');
}
dw.close();
0

I recently learned of img2pdf, a Python package. It does what the name suggests, and nothing else: Wrap images in a PDF document. It does not do OCR, it does not resample the image, everything stays as-is. It is very fast. Usage is dead simple, too:

img2pdf img1.png img2.jpg -o out.pdf

img2pdf is also available as a package at least on Debian.

As mentioned on the img2pdf site, Tesseract could also be an option for you if you have JPEGs or PNGs. It has the added benefit of doing OCR, making it possible for you to copy text and also search for it. It will take a lot longer, because OCR takes time. Use it like this:

ls *.jpg | tesseract - output-file-basename pdf

(Note that “output-file-basename” and “pdf” are separate arguments.)

If desired/required, you could use QPDF afterwards to manipulate the document(s).


For example, when using a non-duplex ADF scanner, scan all the front sides first, flip the whole stack, then scan all the back sides in reverse, then combine the two:

qpdf --collate --empty --pages front-sides.pdf 1-z back-sides.pdf z-1 -- out.pdf
-1

Here's how to do it with Python and PyMuPDF:

I had to install the "pymupdf" package that provides the fitz module (NOT the "fitz" package, which is a different package!)

python -m venv .
./bin/pip install pymupdf

(copy pasted code from https://pymupdf.readthedocs.io/en/latest/recipes-images.html#how-to-make-one-pdf-of-all-your-pictures-or-files )

I have modified the code to omit the use of PySimpleGUI as it also requires TK to be installed. I have also added a regex-based filtering to the file list, that allows including only the files that match the regex.

#!./bin/python
import os, fitz, re # "fitz" comes from the "pymupdf" package
#import PySimpleGUI as psg  # for showing a progress bar
doc = fitz.open()  # PDF with the pictures
imgdir = "."  # folder where the pics are
allfiles = os.listdir(imgdir)  # list of them
imglistUNSORTED = list (filter (lambda fn: re.match (r'c-\d+.jpg', fn), allfiles))
imglist = sorted (imglistUNSORTED)
imgcount = len(imglist)  # pic count

for i, f in enumerate(imglist):
    img = fitz.open(os.path.join(imgdir, f))  # open pic as document
    rect = img[0].rect  # pic dimension
    pdfbytes = img.convert_to_pdf()  # make a PDF stream
    img.close()  # no longer needed
    imgPDF = fitz.open("pdf", pdfbytes)  # open stream as PDF
    page = doc.new_page(width = rect.width,  # new page with ...
                       height = rect.height)  # pic dimension
    page.show_pdf_page(rect, imgPDF, 0)  # image fills the page
    #psg.EasyProgressMeter("Import Images",  # show our progress
    #    i+1, imgcount)
    print ('Import Images: ' +str(i+1)+ '/' +str(imgcount))

doc.save("all-my-pics.pdf")

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .