How do I convert markdown files to pdf files

I would like to automatically convert my markdown files to pdf files and use them to e.g. view the content offline. I configure flow and libre office for that and the conversion is working but the pdf content does not have the nice formatting and the images are missing in the pdf.

Is there any way to automatically create pdf files from markdown files with the same formatting and without dropping the images? I am aware that I could use the browser print-to-pdf feature as a workaround but that is not a viable solution for my use case.

Thanks, Wolfgang

1 Like

it’s a bit hacky, but what I did recently is to add them to the collectives app and use the print feature in collectives to print to pdf.

Text is also supposed to have a ‘print’ feature but last time I tried it did not work. but perhaps that bug is solved by now

Hi Daphne, thanks for the hint. Took me a while to discover the print feature in the collectives app though. Print to pdf with nice formatting is working but the image is still missing - only a loading circle is shown in the pdf instead of the actual image.
Screenshot 2023-03-22 185026

1 Like

I liked the question and it motivated me to find a solution.

I have tried everything and this gives the best result with the least effort:

You need the “File Actions app” files_scripts (build the lua php-module as described in Readme)

Then you need pandoc and soffice (Libre Office) on the host system.

Create a “New action” in the admin section of “File actions”, give it a name like “markdown 2 pdf”:

and paste this code-block into the code-window:

local files = get_input_files()

-- finding out nextclouds installation dir with shell_command `pwd`
local current_dir = shell_command('pwd').output
-- Loop through input files
for _, input_file in ipairs(files) do
  local log_filename ='%.[Mm][DdWwNnTtXx]+$', '.pdf') .. '.encoding.txt'
  local outfolder = get_parent(input_file)
  local out_meta = meta_data(outfolder)
-- Check if the logfile already exists
  if exists(outfolder, log_filename) then
    abort('Logfile "' .. outfolder.path .. "/" .. .. "/" .. log_filename .. '" already exists. Skipping...')
    goto continue
  local arguments = {}
  if is_file(input_file) then
    local html_filename ='%.[Mm][DdWwNnTtXx]+$', '.html')
    local out_filename ='%.[Mm][DdWwNnTtXx]+$', '.pdf')
  -- Check if the output file already exists
    if exists(outfolder, out_filename) then
      abort('A file "' .. out_filename .. '" already exists in the target directory. Skipping...')
      goto continue
-- meta_data part:
    local in_meta = meta_data(input_file)
    local metadata_args = {
-- obtained from shell_command('pwd')
      "\"nextcloud_dir    = " .. current_dir .. "\"",
      "\"in_file          = " .. .. "\"",
      "\"in_mimetype      = " .. in_meta.mimetype .. "\"",
      "\"local_path       = " .. out_meta.local_path .. "\"",
      "\"html_filename    = " .. html_filename .. "\"",
      "\"out_filename     = " .. out_filename .. "\"",
      "\"out_storage_path = " .. out_meta.storage_path .. "/" .. out_filename .. "\"",
-- Concatenate the metadata_args array into a single string with spaces between each argument
    local arguments_string = table.concat(metadata_args, " ")
    table.insert(arguments, arguments_string)

  -- Construct the command string with quotes around each argument
  local command = "/usr/local/bin/nc-md-to-pdf " .. table.concat(arguments, " ")

  -- Run the command
  local result = shell_command(command)

  -- Create debug file with the same name as the output file but with extra .encoding.txt extension
  local output_file = new_file(outfolder, log_filename, result.output .. "\n" .. result.errors)
  if output_file == nil then
    abort("Could not create logfile: " .. log_filename)

Activate and store it.

Now create a bash-script
with this content:


echo "$(date +"%Y-%m-%d %H:%M:%S %Z")"

if [ $# -lt 10 ]; then digits=1
elif [ $# -lt 100 ]; then digits=2
else digits=3

# iterate over the arguments
for (( i=0; i<=$#; i++ )); do
  # format the index with leading zeros
  index=$(printf "%0*d" $digits $i)
  # get the i-th argument
  # echo the argument with its index
  echo "  -  arg $index = $arg"
  echo "$arg" | grep -q "nextcloud_dir" && NCC="${arg##*= }/occ"
# in:
  echo "$arg" | grep -q "in_file" && in_file="${arg##*= }"
  echo "$arg" | grep -q "in_mimetype" && in_mt="${arg##*= }"
  echo "$arg" | grep -q "local_path" && local_path="${arg##*= }"
# out:
  echo "$arg" | grep -q "html_filename" && html_fn="${arg##*= }"
  echo "$arg" | grep -q "out_filename" && out_fn="${arg##*= }"
  echo "$arg" | grep -q "out_storage_path" && out_sp="${arg##*= /}"

mime_type=$(file -bL --mime-type "$in_lp")
echo "information gathered by file:"
echo "  file                         = $(file -bL "$in_lp")"
echo "     --mime-type               = $mime_type"
echo "NCC           = $NCC"
echo "in_file       = $in_file"
echo "in_mt         = $in_mt"
echo "local_path    = $local_path"
echo "html_fn       = $html_fn"
echo "out_fn        = $out_fn"
echo "out_sp        = $out_sp"

#exit 0
# cd into infile dir to be as relative to images as the md file
cd "$local_path"

if [ "$in_mt" = "text/markdown" ]; then
  if pandoc -f markdown -t html "$in_file" -o "$html_fn" && soffice --headless --convert-to pdf "$html_fn"; then
    rm "$html_fn"
    $NCC files:scan --verbose --no-interaction --path="$out_sp"
  echo "$in_file seems not to be a markdown file, skipping"

exit 0

(last updated 2023.03.24 12:50 CET)

and make it executable:
chmod +x /usr/local/bin/nc-md-to-pdf

It creates the pdf file and a log file for debug purposes.

This is a quick sketch and can be fleshed out as you like. So did I not yet work on image resize templates.

You can trigger it from the context menu but it is possible to make a background job with the flow mechanism, as described here

1 Like

Wow! Great work.

Also, doing this locally is really easy with pandoc, just:

pandoc -o output.pdf

Nice to have it done on the server, though.

For reference:

Of course I have tried that too but it needs about 1 GB on LateX dependencies and then still not able to handle emojis.

1 Like

Of course there is the integrated html_to_pdf() function in the files_script App but the complexity of the mPDF configuration overwelmed me.
But I’ll definitely try that solution when I get in the mood for it…

Hi @ernolf ,

thanks for your effort. I tried the solution today but I’m always getting an error when I try to convert the file.

2023-03-30 16:25:33 CEST
  -  arg 0 = /usr/local/bin/nc-md-to-pdf
  -  arg 1 = nil

information gathered by file:
  file                         = directory
     --mime-type               = inode/directory
NCC           = 
in_file       = 
in_mt         = 
local_path    = 
html_fn       = 
out_fn        = 
out_sp        = 
 seems not to be a markdown file, skipping

Any idea what’s going wrong?

Thanks and br, Wolfgang

There are not passed any arguments. The error arises in the “File actions” part.
Double check that the lua-code is exactly as I stated above.

Hi @ernolf,

I finally found some time to debug and fix the issue caused by a PHP version mismatch on the server.

Your solution is working but have still some issues. When converting a markdown file to pdf I am getting a permission error from the files:scan job and the pdf is not shown in the folder.

... using filter : writer_web_pdf_Export
/usr/local/bin/nc-md-to-pdf: line 52: /var/www/html/nextcloud/occ: Permission denied

This is probably caused by a racing condition because pdf generation has not been finished yet.
Doing a manual rescan afterward (occ files:scan --all) reveals the generated pdf file in the folder.

The second issue is that the images in the pdf are not scaled to fit the page size as in the md viewer but get cropped on the right side.

Nevertheless, I think this is still a viable solution. Thanks a lot for the effort you have put into answering the question. I will mark the question as resolved.

Thanks and best regards,

1 Like