Generar pdf con pie/cabecera con python, pdfkit y wkhtmltopdf

Ya llevo varios meses peleándome con pdfkit, al tener que atender otras partes de mi programa lo he ido dejando hasta que realmente he tenido la necesidad de ponerme a hacer funcionar correctamente pdfkit. La documentación que he encontrado por ahí ha sido un poco confusa, además de que estaba teniendo problemas con la versión del wkhtmltopdf y me estaba volviendo loca!

Ahora mismo tengo la necesidad de crear un pdf con sus márgenes, cabecera, pie y numero de página, además no quiero que salga la cabecera y pie de página en la primera página.

Primero de todo tendremos que tener en cuenta que pdfkit hace uso del programa wkhtmltopdf, que está en los repositorios de debian, pero me encuentro que éste no está compilado con qt, como tal al usar según que opciones de la configuración de wkhtmltopdf me soltaba un error similar a éste:

The switch --enable-internal-links, is not support using unpatched qt, and will be ignored.
The switch --footer-center, is not support using unpatched qt, and will be ignored.
The switch --header-html, is not support using unpatched qt, and will be ignored.
The switch --footer-html, is not support using unpatched qt, and will be ignored.

Para ello lo que haremos será primero de todo desinstalar wkhtmlpdf instalado en el sistema e instalar el .deb que nos ofrecen en la página del proyecto de wkhtmltopdf.

# apt remove --purge wkhtmltox
# wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.buster_amd64.deb
# dpkg -i wkhtmltox_0.12.6-1.buster_amd64.deb

UPDATE Para debian 12 bookworm mirar éste repositorio

# wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb
# dpkg -i wkhtmltox_0.12.6.1-3.bookworm_amd64.deb

Para seguir preparando el sistema, vamos a instalar la librería de python de pdfkit que lo usaremos para convertir los ficheros html a pdf.

(venv) laura@melatonina:~/dev/colibri$ pip install pdfkit
(venv) laura@melatonina:~/dev/colibri$ echo "pdfkit==0.6.1" >> requirements.txt

A continuación tendremos que tener en cuenta que vamos a tener que generar 4 ficheros html para ponerlos en cada uno de los sitios, en mi código hago uso de los templates de django, así puedo generar de forma dinámica los ficheros html necesarios:

– página de portada (pdf_cover)
– páginas 2 a n (pdf_body)
– cabecera para las páginas 2 a n (page_header)
– pie de página para los páginas 2 a n (page_footer)

Para que no se muestre la cabera y pie de página en la primera página tendremos que añadir un código javascript tanto en el fichero que generemos para la cabecera como para el pie. Os dejo de ejemplo el trozo de código que estoy usando para generar tanto la cabecera como el pie de página:

Cabecera

< !DOCTYPE html >
< html >
< head >
    < meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / >
    < !-- Bootstrap CSS -- >
    < link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous" >
    < !-- Font Awesome -- >
    < link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous" >
    < !-- Custom CSS -- >
    < link rel="stylesheet" href="https://colibri.capa8.net/static/css/colibri.css" >

    < style type="text/css" >
        body {
            margin: 0;
            padding: 0;
            line-height: 1;
            font-size: 13pt;
            height: 20mm; /* set it to your bottom margin */
        }

        .header-left {
            position: absolute;
            top: 35px;
            bottom: 0;
            left: 0;
            right: 0;
            text-align: left;
        }
        .header-center {
            position: absolute;
            top: 30px;
            bottom: 0;
            left: 0;
            right: 0;
            text-align: center;
        }
        .header-right {
            position: absolute;
            top: 30px;
            bottom: 0;
            left: 0;
            right: 0;
            text-align: right;
        }
    < /style >

    < script >
      function subst() {
        var vars = {};
        var x = document.location.search.substring(1).split('&');
        for (var i in x) {
          var z = x[i].split('=', 2);
          vars[z[0]] = unescape(z[1]);
        }
        var x = ['frompage', 'topage', 'page', 'webpage', 'section', 'subsection', 'subsubsection'];
        for (var i in x) {
          var y = document.getElementsByClassName(x[i]);
          for (var j = 0; j < y.length; ++j) y[j].textContent = vars[x[i]];

          if (vars['page'] == 0) { // If page is 0, set block1 display to none
            document.getElementById("block1").style.display = 'none';
          }
        }
      }
    < /script >
< /head >
< body onload="subst()" >
    < div id="block1" >
        < div class="header-left" >
            {{ inform.snapshot.company.name }}
        < /div >
        < div class="header-center" >
             
        < /div >
        < div class="header-right" >
            {{ inform.snapshot.certificate_company.certificate|object_name:request.language }}
            < br >
            {{ inform.snapshot.creation|date:"d/m/Y" }}
        < /div >
    < /div >
< /body >
< /html >

Pie de página

< !DOCTYPE html >
< html >
< head >
    < meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / >
    < !-- Bootstrap CSS -- >
    < link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous" >
    < !-- Font Awesome -- >
    < link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous" >
    < !-- Custom CSS -> -- >
    < link rel="stylesheet" href="https://colibri.capa8.net/static/css/colibri.css" >

    < style type="text/css" >
        body {
            margin: 0;
            padding: 0;
            line-height: 1;
            font-size: 13pt;
            height: 20mm; /* set it to your bottom margin */
        }

        .footer-left {
            position: absolute;
            bottom: 55px;
            left: 0;
            right: 0;
            text-align: left;
        }
        .footer-center {
            position: absolute;
            bottom: 55px;
            left: 0;
            right: 0;
            text-align: center;
        }
        .footer-right {
            position: absolute;
            bottom: 40px;
            left: 0;
            right: 0;
            text-align: right;
        }
    < /style >

    < script >
      function subst() {
        var vars = {};
        var x = document.location.search.substring(1).split('&');
        for (var i in x) {
          var z = x[i].split('=', 2);
          vars[z[0]] = unescape(z[1]);
        }
        var x = ['frompage', 'topage', 'page', 'webpage', 'section', 'subsection', 'subsubsection'];
        for (var i in x) {
          var y = document.getElementsByClassName(x[i]);
          for (var j = 0; j < y.length; ++j) y[j].textContent = vars[x[i]];

          if (vars['page'] == 0) { 
            document.getElementById("block2").style.display = 'none';
          }else{
              document.getElementById("currentpage").innerHTML=vars['page'];
              document.getElementById("allpages").innerHTML=vars['topage'];
          }
        }
      }
    < /script >
< /head >
< body onload="subst()" >
    < div id="block2" >
        < div class="footer-left" >
                © {{ year }} · Capa8 (https://capa8.net)
        < /div >
        < div class="footer-center" >
                Page
                < span id="currentpage" >< /span >
                 of
                < span id="allpages" >< /span >
        < /div>
        < div class="footer-right" >
            < img src="https://colibri.capa8.net/static/img/cert/iso26000.jpg" width="22" >
              
            < img src="https://colibri.capa8.net/static/img/cert/iso27001.jpg" width="30" >
              
            < img src="https://colibri.capa8.net/static/img/cert/iso37001.jpg" width="30" >
              
            < a href="https://aspertic.org/" target="_blank" rel="noopener noreferrer" >< img src="https://colibri.capa8.net/static/img/2019_aspertic_espigues_70.png" width="40" >< /a >
            < img src="https://colibri.capa8.net/static/img/cert/eni.jpg" width="44" >
            < img src="https://colibri.capa8.net/static/img/cert/ens.jpg" width="56" >
            < img src="https://colibri.capa8.net/static/img/cert/icab_es.jpg" width="30" >
        < /div >
    < /div >
< /body >
< /html >

A continuación os dejo el código, al final del código voy a comentar algunas cosas.

from django.http import HttpResponse
from django.template.loader import get_template
import pdfkit, os
from django.conf import settings

path_wkthmltopdf = r'/usr/local/bin/wkhtmltopdf'
config = pdfkit.configuration(wkhtmltopdf=path_wkthmltopdf)

_inform = InformSnapshot.objects.get(pk=int(value))
_sections = InformSectionSnapshot.objects.filter(inform_snapshot=_inform).order_by('order')

_path = Config.objects.get(name="core.working_directory").value


# render header
template = get_template('web/documents_pdf.html')
page_header = template.render({'nav': {'menu': "pdf-header"}, 'inform': _inform, 'request': request})
text_file = open(_path + "/web/templates/web/pdf/header.html", "w")
text_file.write(page_header)
text_file.close()

# render footer
_year = datetime.datetime.now().year
template = get_template('web/documents_pdf.html')
page_footer = template.render({'nav': {'menu': "pdf-footer"}, 'year': _year, 'request': request})
text_file = open(_path + "/web/templates/web/pdf/footer.html", "w")
text_file.write(page_footer)
text_file.close()

# render body
nav = {'menu': 'inform-pdf', 'location': 'pdf'}
template = get_template('web/documents.html')
page_body = template.render({'nav': nav, 'request': request, 'inform': _inform, 'sections': _sections})


# configure PDF pages
options = {
    'dpi': '300',
    'page-size': 'A4',
    'encoding': "UTF-8",
    'margin-top': '0.5in',
    'margin-right': '0.5in',
    'margin-bottom': '0.5in',
    'margin-left': '0.7in',
    'enable-internal-links': '',
    #'footer-center': '[page] of [topage]',
    'header-html': _path + '/web/templates/web/pdf/header.html',
    'footer-html': _path + '/web/templates/web/pdf/footer.html',
    'page-offset': '-1',
    #'dump-outline': _path + '/web/templates/web/pdf/outline.xslt'
    'header-spacing': '4',
    'footer-spacing': '2'
}

_static = settings.STATIC_ROOT

css = [
    _static + '/css/colibri.css'
]

# generate pdf
pdf = pdfkit.from_string(page_body, False, options, configuration=config, css=css)

# remove temporary pdf
os.remove(_path + "/web/templates/web/pdf/header.html")
os.remove(_path + "/web/templates/web/pdf/footer.html")

# generate pdf to download
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename=inform_' + str(_inform.code) + '.pdf'
return response

Desearía remarcar algunas de las opciones de wkthmltopdf que definimos en las variables options_cover y options_body que no se mencionan mucho en la documentación

'dpi': '300',
'enable-internal-links': '',
'footer-center': '[page] of [topage]',
'header-html': _path + '/web/templates/web/pdf/header.html',
'footer-html': _path + '/web/templates/web/pdf/footer.html'

dpi: Tendremos que añadirlo para que la página no quede fea, por ejemplo me encuentro que si no defino los dpi, las card de boostrap4 me quedan feas, si añado ésto, no :)
enable-internal-links: lo que hace es que los links que se añadan a la página sean con ruta relativa y no ruta absoluta, de ésta forma podemos generar un índice y con los hyperlinks ir a cada una de las secciones (me molaría poder saber en qué página se encuentra cada sección y poder añadirla al índice, pero lo dejo para otro momento o quizás nunca hehe)
footer-center: Lo usaremos para añadir el número actual de página y todas las páginas en el centro
header-html/footer-html: Definiremos la ruta absoluta donde se encuentran los ficheros html generados automáticamente.
page-offset: En qué numero de página empezará el documento, le diremos -1 así la primera página (portada) será la 0 y empezará a contar desde la página de índice.

Recomiendo mirar aquí para ver otras opciones disponibles.

Fijo que me olvido de contar muchas cosas, pero aquí dejo un trozo de código que funciona en python3, con django 2.2.6 y wkhtmltopdf 0.12.6.

Y ésto es todo, espero que os haya sido de ayuda :)

5 Comments

  1. Pingback: Múltiples idiomas en django – Blackhold

  2. Como establecer la altura al header y que el contenido de pdf no se me sobreponga sobre el header

    Respon
  3. Pingback: Compilar wkhtmltopdf para debian bookworm - Blackhold

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *

Aquest lloc utilitza Akismet per reduir els comentaris brossa. Apreneu com es processen les dades dels comentaris.