Dietpdf : compressez vos PDF

9 05 2009

Si, comme moi, vous êtes toujours en quête de l’octet perdu, je vous propose un petit utilitaire pour compresser vos PDF.

En matière de production de PDF, on trouve de tout : du PDF ultra-léger au PDF poids lourd. Le PDF étant un format composite, des tas de pistes peuvent être explorées pour réduire le poids total.

Pré-requis

Dietpdf nécessite les paquets suivants :

  • libjasper-runtime : Jasper est une bibliothèque permettant de générer des fichiers au format Jpeg 2000, c’est une des clés du gain de place.
  • pdftk : Pdftk est un couteau suisse du PDF. Il est utilisé par dietpdf pour décompresser et recompresser les PDF.
  • imagemagick : Image Magick est utilisé par dietpdf pour convertir du Jpeg CMJN en Jpeg RVB. Les PDF générés par les outils d’Adobe peuvent contenir de tels Jpeg.
  • icc-profiles : Profiles ICC nécessaires à Image Magick pour convertir du CMJN en RVB.

Pour installer ces paquets, tapez la commande :

sudo apt-get install libjasper-runtime pdftk imagemagick icc-profiles

Téléchargement

Télécharger dietpdf

Installation

Copiez le script python dans le répertoire ~/bin sous le nom dietpdf et donnez lui les droits en exécution :

chmod 700 ~/bin/dietpdf

Utilisation

Dietpdf est un utilitaire en ligne de commande. L’appel se fait comme suit :

dietpdf document.pdf

Dietpdf va alors chercher à compresser le fichier PDF donné et va générer un fichier document.opt.pdf (.opt est inséré juste avant l’extension).

Attention : le format PDF étant complexe, je ne peux pas garantir que dietpdf fonctionnera sur tous les fichiers PDF possibles. Si vous avez des exemples ou des infos supplémentaires, je suis preneur.

Notes techniques

Toutes les images Jpeg contenues dans le PDF seront transformées en Jpeg 2000. Cela implique que le PDF passera automatiquement en version 1.5 au minimum.

Les Jpeg CMJN sont d’abord convertis en Jpeg RVB, puis en Jpeg 2000.

Le niveau de compression de Jpeg 2000 est définis à 0.01 (1%). Ça laisse quelques traces sur l’image mais permet de gagner énormément.

Les autres objets sont recompressés à l’aide de la bibliothèque zlib avec un niveau de compression 9 (le maximum). Cela permet de gagner encore quelques octets.

Code source

#!/usr/bin/env python
# coding=utf8
from subprocess import Popen,PIPE
from zlib import compress,decompress
import sys,re

def exec_cmd(commandes,contenu):
  return Popen(commandes,stdin=PIPE,stdout=PIPE).communicate(contenu)[0]

class Metas:
  def __init__(self): pass

  def decode(self,block_meta):
    self.lignes=block_meta.splitlines()
    self.index =1
    self.metas=self.parcours_liste()

  def parcours_liste(self):
    metas={}
    self.index+=1

    while self.index<len(self.lignes) and self.lignes[self.index]!=">>":
      m=re.match("/([A-Za-z0-9]+) +(.+) *",self.lignes[self.index])
      if m:
        metas[m.group(1)]=m.group(2)
      else:
        self.index+=1
        metas[self.lignes[self.index-1].strip()[1:]]=self.parcours_liste()

      self.index+=1

    return metas

  def encode(self):
    return "%s\n%s"%(self.lignes[0],self.parcours_metas(self.metas))

  def parcours_metas(self,metas):
    sortie="<<\n"
    for cle in metas:
      if type(metas[cle])==str:
        sortie+="/%s %s\n"%(cle,metas[cle])
      else:
        sortie+="/%s\n%s\n"%(cle,self.parcours_metas(metas[cle]))

    return sortie+">>"

def jpeg_to_jpeg2000(contenu,compression=0.01):
  return exec_cmd(["jasper","--output-format","jpc","-O","rate="+str(compression)],
                  contenu)

def cmjn_vers_rvb(contenu):
  return exec_cmd(["convert","-","-negate",
                   "-profile","/usr/share/color/icc/ISOcoated.icc",
                   "-profile","/usr/share/color/icc/sRGB.icm","-"],
                   contenu)

nom_pdf_entree=sys.argv[1]

print "Décompression du PDF... ",
contenu   =open(nom_pdf_entree,"rb").read()
entree_unc=exec_cmd(["pdftk","-","output","-","uncompress"],contenu)
print "OK"

# Recherche tous les objets
print "Recherche des objets... ",
objets=re.finditer("([0-9]+) [0-9]+ obj *",entree_unc)
print "OK"

sortie     =''
last_offset=0
re_fin_meta=re.compile(">>\n(stream|endobj)")

entete=re.search("%PDF-([0-9.]+)\n",entree_unc)
if float(entete.group(1))<1.5:
  entree_unc="%PDF-1.5\n"+entree_unc[len(entete.group(0)):]

for objet in objets:
  identifiant=objet.group(1)
  offset     =objet.start()

  fin_meta     =re_fin_meta.search(entree_unc,offset)
  if fin_meta.group(1)=="endobj": continue

  block_meta   =entree_unc[offset:fin_meta.start()+2]
  metas        =Metas()
  metas.decode(block_meta)
  if "Length" not in metas.metas: continue

  print "[%s]"%(identifiant),

  offset_stream=fin_meta.start()+len(fin_meta.group(0))+1
  rien_fait    =True

  stream_length=int(metas.metas['Length'])
  stream       =entree_unc[offset_stream:offset_stream+stream_length]

  if rien_fait and "Filter" in metas.metas and metas.metas["Filter"]=="/FlateDecode":
    print "Recompression",
    stream_opt=compress(decompress(stream),9)
    rien_fait=False

  if rien_fait and "Subtype" in metas.metas and metas.metas["Subtype"]=="/Image":
    if "ColorSpace" not in metas.metas:
      colorimetrie="RGB"
    else:
      colorimetrie="col. inconnue ("+metas.metas["ColorSpace"]+")"
      if metas.metas["ColorSpace"]=="/DeviceRGB" : colorimetrie="RGB"
      if metas.metas["ColorSpace"]=="/DeviceCMYK": colorimetrie="CMYK"
      if metas.metas["ColorSpace"]=="/DeviceGray": colorimetrie="Gray"

    print "%s×%s, %s"%(metas.metas["Width"],metas.metas["Height"],colorimetrie),

    if "Filter" in metas.metas and metas.metas["Filter"]=="/DCTDecode":
      if colorimetrie.startswith("col. inconnue") or colorimetrie=="CMYK":
        stream=cmjn_vers_rvb(stream)
        metas.metas["ColorSpace"]="/DeviceRGB"

      # Conversion de l'image en JPEG 2000
      stream_opt           =jpeg_to_jpeg2000(stream,0.01)
      metas.metas["Filter"]="/JPXDecode"
      print "[JP2K]",

      if len(stream)>len(stream_opt):
        if "Decode" in metas.metas: metas.metas.pop("Decode")
        rien_fait=False

  if rien_fait and "Filter" not in metas.metas:
    print "Compression",
    stream_opt=compress(stream,9)
    if len(stream)>len(stream_opt+"/Filter /FlateDecode\n"):
      metas.metas["Filter"]="/FlateDecode"
      rien_fait=False

  if not rien_fait:
    metas.metas["Length"]=str(len(stream_opt))
    sortie+=entree_unc[last_offset:offset]
    sortie+=metas.encode()+"\nstream\n"+stream_opt+"\n"

    last_offset=offset+len(block_meta+"\nstream\n")+stream_length
    print "→ %d octets vers %d octets"%(len(stream),len(stream_opt))
  else:
    print "→ rien à faire"

sortie+=entree_unc[last_offset:]

print "Recompression du PDF... ",
sortie_compresse=exec_cmd(["pdftk","-","output","-","compress"],sortie)
print "OK"

print "Écriture du fichier optimisé... ",
nom_pdf_sortie=re.sub("\.pdf$",".opt.pdf",nom_pdf_entree)
open(nom_pdf_sortie,"wb").write(sortie_compresse)
print "OK"

Actions

Information

5 responses

14 06 2009
Olivier

Bonjour,

Je vous remercie pour cet excellent script !
Je cherchais à alléger mes pdf sous linux et je suis enchanté du résultat🙂

Bravo et merci !

4 04 2010
Gegetel

Bonjour Zigazou,
Bravo et merci pour votre script dietpdf que j’ai découvert hier et essayé aussitôt. Je peux parfaitement l’utiliser tel quel, quitte à modifier le taux de compression que je trouve beaucoup trop élevé, ce qui n’est pas trop difficile même pour un grand ignorant comme moi. Il me faut toutefois vous signaler un problème qui affecte les images qui ne sont pas initialement au format jpeg.
En effet, je fabrique avec pdfLaTeX des documents pdf dont chaque page contient une seule image occupant toute la page et rien d’autre. Quelques unes de ces images, avec des photographies en couleurs, sont au format jpeg mais la plupart, qui représentent principalement des pages de texte scannées, sont au format png, en mode indexé ou non. Après traitement par dietpdf, les images jpeg sont bien converties en JPEG2000 et s’affichent parfaitement avec n’importe quel lecteur mais les images png sont devenues invisibles : chaque page est blanche.:-/
Je me contente donc de traiter les pages en jpeg, ce qui m’oblige à me livrer à une petite gymnastique de découpage/reconstitution de pdf avec ce brave pdftk.
Si le sujet vous intéresse, je vous fournirai avec plaisir plus d’explications et un exemple.
Attention toutefois si vous me répondez : Je risque de vous demander des fonctionnalités supplémentaires.😉
Bien cordialement,
GC

10 04 2010
zigazou

Bonjour et désolé pour le retard de ma réponse !

Pour le coup des PNG, j’avoue que je n’ai pas eu beaucoup de PDF de ce type sous la main pour tester.

Pourriez-vous m’en envoyer un exemple ? Ça me permettra de regarder ce que je peux faire pour corriger le souci.

Merci

10 04 2010
Gegetel

Réponse avec exemple envoyée par messagerie. Merci et bon courage.

6 07 2010
Gegetel

Hello Zigazou,

Avez vous avancé sur les png ? De mon côté, je suis en train d’apprendre les rudiments de Python mais je suis encore très loin de pouvoir vous aider, et je le regrette.
Par ailleurs, je découvre aujourd’hui une autre lacune : Les images jpeg en niveaux de gris ne bénéficient pas du traitement par dietpdf. Est-ce normal ?
Enfin, pour terminer sur une demande peut-être plus facile à satisfaire, pourriez vous inclure un dialogue permettant à l’utilisateur de choisir son degré de compression ?

Bien cordialement,
GC

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s




%d blogueurs aiment cette page :