Maîtriser les fichiers PDF

Les fichiers Adobe PDF sont devenus une norme pour les échanges de documents. Ces fichiers peuvent avoir un poids en octets conséquent notamment en raison de la présence d’images. S’il existe des solutions pour transmettre des fichiers lourds (WeTransfer par ex.) il peut s’avérer judicieux de réduire leur taille. Nous pouvons aussi avoir besoin de modifier les métadatas ou de joindre plusieurs fichiers ensembles. Pour cela nous disposons de nombreuses solutions. Celles que nous allons étudier reposent sur Ghostscript (logiciel gratuit) à partir d’un Terminal (bash, zsh, Powershell, etc.) soit à l’aide du code Python.

Utilisation dans le terminal

Nous allons commencer par installer Ghostscript avant de voir les différents usages.

Installation

Windows

Nous installons Ghostscript à partir de la page de téléchargement. Sous Windows 11 nous choisissons la version 64 bit.

La version est installée dans le dossier C:\Program Files\gs. A l’intérieur un sous-repertoire pour la version installée. Par exemple, un sous-répertoire gs10.02.1.

Dans un sous-répertoire bin, nous avons deux exécutables :

  • gswin64.exe qui ouvre Ghostscript dans une fenêtre
  • gswin64c.exe qui ouvre Ghostscript dans le terminal

Par exemple pour ouvrir Ghostscript dans Powershell nous utiliserons un de ces exécutables.

PS C:\Users\gilles> & 'C:\Program Files\gs\gs10.00.0\bin\gswin64c.exe'
GPL Ghostscript 10.0.0 (2022-09-21)
Copyright (C) 2022 Artifex Software, Inc. All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
GS>

Pour une utilisation avec PowerShell il conviendra de prendre garde à la syntaxe. Par exemple pour le paramètre CompatibilityLevel nous écrirons -dCompatibilityLevel='1.4' ou "-dCompatibilityLevel=1.4".

macOS

Pour installer Ghostscript dans sur macOS nous allons passer par le gestionnaire de package brew

% brew update
% brew upgrade
% brew install ghostscript
% brew brew cleanup

La mise à jour de Ghostscript se fait avec le paramètre upgrade

% brew upgrade ghostscript

A noter que pour afficher les fichiers PDF avec Ghostscript nous devons installer xquartz :

% brew install --cask xquartz

L’exécutable de Ghostscript est gs. Pour connaître la version de Ghostscript :

% gs --version

9.56.1

Pour connaître l’emplacement de l’exécutable :

% which gs

/opt/local/bin/gs

Il est possible que le fichier à cet emplacement soit un lien symbolique. Pour vérifier cela nous tapons la ligne de commandes qui suit. Nous constatons que les informations du fichier commence par l (link). -> précise l’emplacement de l’exécutable lié et qui sera exécuté.

% ls -la /opt/local/bin/gs

lrwxr-xr-x 1 root wheel 47 15 déc 11:07 /opt/local/bin/gs -> /opt/homebrew/Cellar/ghostscript/10.02.1/bin/gs

Nous pouvons constater que par exemple la dernière version est installée par brew dans le dossier /opt/homebrew/Cellar/ghostscript. A l’intérieur de cet emplacement nous avons un répertoire par version de ghostscript. Par exemple, nous avons un répertoire 10.01.2. A l’intérieur un sous-répertoire bin avec l’exécutable gs (pour Ghostscript).

% /opt/homebrew/Cellar/ghostscript/10.01.2/bin/gs
GPL Ghostscript 10.01.2 (2023-06-21)
Copyright (C) 2023 Artifex Software, Inc. All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.

Nous pouvons créer un lien symbolique vers l’exécutable pour un appel simplifié. Dans notre cas nous utiliserons `ghostscript` pour appeler l’exécutable `gs`. La commande `ln` avec l’option `-s` crée un lien symbolique vers un fichier.

% sudo rm /opt/local/bin/gs
% sudo ln -s /opt/homebrew/Cellar/ghostscript/10.01.2/bin/gs /opt/local/bin/ghostscript
% which ghostscript
/opt/local/bin/ghostscript

Linux

En fonction de la distribution utilisée, nous utiliserons les lignes de commandes qui suivent.

Pour les distributions à base de Debian
gilles@debian:~$ sudo apt update
gilles@debian:~$ sudo apt install ghostscript
Pour la distribution Arch Linux
[gilles@Archlinux ~]$ sudo pacman -U http://mirror.archlinuxarm.org/aarch64/extra/libpaper-2.1.1-1-aarch64.pkg.tar.xz
[gilles@Archlinux ~]$ sudo pacman -S ghostscript
Pour la distribution Alpine Linux
~ $ sudo apk update
~ $ sudo apk add ghostscript
Pour les distributions à base de Fedora
[gilles@fedora ~]$ sudo yum install ghostscript

L’exécutable de Ghostscript est gs.

Pour construire l’application Ghostscript à partir des sources, nous procédons comme suit.

  • Nous commençons par récupérer un URL valide vers l’archive contant les sources. Pour cela, nous allons sur la page de téléchargement et nous récupérons le lien (par ex. https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs10012/ghostscript-10.01.2.tar.gz]).
  • Dans le terminal nous nous déplaçons dans le répertoire de téléchargement ou dans un répertoire temporaire quelconque. Nous allons décompresser dans ce répertoire les sources. Pour télécharger l’archive dans le terminal nous avons à notre disposition la commande wget. Si elle n’est pas disponible, tous les gestionnaires de paquets la répertorie dans leurs dépôts (par ex. sudo apt install wget).
  • Nous dé-archivons le fichier téléchargé. A partir d’un gestionnaire de fenêtres, en double cliquant dessus nous allons pouvoir le faire automatiquement. Dans le terminal nous utiliserons une ligne de commandes pour dé-archiver : tar -xzf ghostscript-10.01.2.tar.gz. Un sous répertoire est créé. Les sources sont à l’intérieur
  • Nous définissons comme répertoire en cours la racine de ce répertoire. Par ex. cd ghostscript-10.01.2
  • A l’intérieur, nous exécutons le fichier de configuration ./configure. Ce fichier va préparer la compilation à partir des données collectés dans le système (architecture par exemple). Un fichier makefile est créé.
  • Pour lancer la compilation du code nous tapons la commande make. Cette opération peut être plus ou moins longue en fonction de la puissance de l’ordinateur.
  • Enfin, pour installer le code compilé dans notre système, nous exécuterons en mode super utilisateur la commande sudo make install

En résumé, nous pouvons exécuter les commandes suivantes :

[gilles@Archlinux Downloads]$ wget https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs10012/ghostscript-10.01.2.tar.gz
[gilles@Archlinux Downloads]$ tar -xzf ghostscript-10.01.2.tar.gz
[gilles@Archlinux Downloads]$ cd ghostscript-10.01.2
[gilles@Archlinux ghostscript-10.01.2]$ ./configure
[gilles@Archlinux ghostscript-10.01.2]$ make
[gilles@Archlinux ghostscript-10.01.2]$ sudo make install

Options

Ghostscript est une logiciel en ligne de commandes. Par défaut, il va s’exécuter en mode interactif. L’utilisateur doit entrer les commandes à la suite d’un prompt et les valider par appui sur la touche entrée. Pour sortir de ce mode, il faut taper la commande quit.

Pour ce qui nous concerne il est plus intéressant de l’utiliser en mode shell, sans interaction. A la suite du nom de la commande Ghostscript (par ex. gs) nous saisirons une suite de paramètres et d’arguments, ainsi que le nom des fichiers à traiter en entrée et en sortie.

CommandesDescriptions
gs -hAffiche l’aide et la configuration de base, notamment la liste des périphériques disponibles (device).
-q ou -dQUIETSupprime l’affichage des messages de démarrage (version, copyright)
-fDéfinir un fichier en entrée
-dAffecte une valeur à un paramètre interne. Cette valeur doit avoir un type de données reconnue par Ghostscript comme par exemple false, true ou null. Ce peut être également une valeur numérique.
-sAffecte sous forme de texte une valeur à un paramètre interne
-dNODISPLAYUtilise un périphérique NUL qui ne génère pas de sortie. Cela remplace le périphérique de sortie par défaut ou défini avec -sDEVICE=. A utiliser dans le cas d’une évaluation.
-dPDFSETTINGS=Profile Adobe Distiller pour traiter le fichier en sortie. Ils comportent des préréglages en fonction de la destination. Par ex. /screen. Les valeurs possibles sont : \screen (résolution basse, optimisé pour les écrans), \ebook (résolution moyenne, optimisé pour livre numérique), \printer (résolution élevée, optimisé pour impression), \prepress (optimisé prepress) et \default
-sDEVICE=Défini le périphérique de sortie. C’est par exemple un fichier Adobe PDF avec pdfwrite.
-sPAPERSIZE=Défini les dimensions par défaut des pages. Par ex. -sPAPERSIZE=a4 ou -sPAPERSIZE=legal
-dFIXEDMEDIAFixe les dimensions des médias (images) pour les adapter
-dPDFFitPageAjuste le contenu au dimension de la page
-dDEVICEWIDTHPOINTS=wDéfinir la largeur d’une page en DPI (point par pouce, 1 point = 1/72 de pouce ). Par exemple
-dDEVICEHEIGHTPOINTS=hDéfinir la hauteur d’une page en DPI (point par pouce, 1 point = 1/72 de pouce ). Par exemple -dDEVICEHEIGHTPOINTS=300
-dMonoImageResolution=Résolution en DPI pour les images monochrome. Par ex. -dMonoImageResolution=100
-dColorImageResolution=Résolution en DPI pour les images couleur. Par ex. 100
-dGrayImageResolution=Résolution en DPI pour les images en niveaux de gris. Par ex. 150
-dDownsampleColorImages=Autorise la modification de la résolution des images couleurs. true pour autoriser.
-dColorImageDownsampleType=Défini la méthode de transformation de la résolution des images. Les valeurs possibles sont : /Subsample, /Average et /Bicubic.
-dAntiAliasColorImages=Active le traitement anti-alias des images couleurs. true pour autoriser.
-dDownsampleGrayImages=Autorise la modification de la résolution des images en niveaux de gris. true pour autoriser.
-dGrayImageDownsampleType=Défini la méthode de transformation de la résolution des images. Les valeurs possibles sont : /Subsample, /Average et /Bicubic.
-dAntiAliasGrayImages=Active le traitement anti-alias des images en niveaux de gris. true pour autoriser.
-dDownsampleMonoImages=Autorise la modification de la résolution des images monochromes. true pour autoriser.
-dMonoImageDownsampleType=Défini la méthode de transformation de la résolution des images. Les valeurs possibles sont : /Subsample, /Average et /Bicubic.
-dAntiAliasMonochromeImages=Active le traitement anti-alias des images en monochrome. true pour autoriser.
-dBlackTextAppliquer une couleur noire à tous les contenus textes. Cela ne concerne pas le texte dans les images.
-dBlackVectorAppliquer une couleur noire à tous les contenus vectoriels.
-sOCRLanguage=Définir la langue utilisée pour la reconnaissance de caractères (OCR). Par ex. -sOCRLanguage="fra".
-dOCREngine=Choisir le moteur OCR à utiliser (0 pour Legacy Tesseract et 1 pour LSTM/Neural Network).
-dBATCHN’ouvre pas la console de Ghostscript et traite directement le ou les fichiers en entrée. Ghostscript se ferme automatiquement quand toutes les opérations sont terminées. A utiliser pour l’utilisation dans des scripts
-dNOPROMPTEn mode interactif, masque l’invite pour saisir de Ghostscript
-dNOPAUSEDésactive la pause automatique après chaque page. Il est utilisé conjointement avec Normalement, on -dBATCH.
-sColorConversionStrategy=Défini la stratégie de conversion des couleurs avec une chaîne de caractères. Les valeurs possibles sont : LeaveColorUnchanged, Gray, RGB, ou CMYK.
-dColorConversionStrategy=Défini la stratégie de conversion des couleurs. Les valeurs possibles sont : /LeaveColorUnchanged, /Gray, /RGB, ou /CMYK.
-dCompatibilityLevel=Niveau de compatibilité. Par ex. 1.4
-o ou -sOutputFile=Définir le fichier de sortie avec les options -dBATCH et -dNOPAUSE. Par ex. -o out.pdf ou -sOutputFile=out.pdf
-sCompression=Type de compression None, LZW, Flate, jpeg et RLE
-dDetectDuplicateImages=Regroupe en une seule images les images ayant un hashage identique. La valeur par défaut est à true.
-c "..."Définir une commande Postscript et demander à Ghostscript de l’interpréter. La ou les commandes sont placées entre double guillemets.
-dFirstPage=Début de la page à traiter. Par ex. -dFirstPage=3 pour ignorer les 2 premières pages
-dLastPage=Dernière page à traiter. Par ex. -dLastPage=182 pour ignorer les pages après la page 182
-sPageList=Définir les pages à traiter. Il peut y avoir trois types de valeurs : odd (toutes les pages impaires), even (toutes les pages paires) et une liste de pages. La liste de page peut être formalisée par une liste de numéros de page séparé par une virgule (par ex. -PageList=2,30,42), une plage de page avec le signe moins (par ex. -PageList=2-42), et une liste de pages et de plages de pages (par ex. -PageList=2,7,10-29,40).
-sOwnerPassword=Définir le mot de passe du propriétaire du document. Le document peut être ouvert, mais il sera limité aux permissions accordés par le propriétaire (modification, impression, etc.).
-sUserPassword=Définir le mot de passe de l’utilisateur lors de l’ouvrir de document. S’il n’est pas défini, le document peut être ouvert sans mot de passe. Par contre pour les permissions comme l’impression, il faut le mot de passe du propriétaire (-sOwnerPassword=).
-dEncryptionR=Définir la méthode d’encryptage. Pour une utilisation avec les versions 1.4 et suivantes, nous utiliserons gestionnaire de sécurité révision 3 (-dEncryptionR=3)
-dKeyLength=Définir la longueur en bits de la clef d’encryption. Pour la révision 3 du gestionnaire de sécurité est de 128.
-dPermissions=Définir sous forme de bit les autorisations PDF (impression, copie du texte ou des graphiques, insertion, rotation, suppression de pages, ajout d’annotations ou de signatures, remplissage des champs de formulaire). Par ex. pour n’autoriser que l’impression -dPermissions=196
-sPDFPassword=Définir le mot de passe pour ouvrir un document protégé.

Afficher un PDF

Par exemple, sous Linux, nous indiquons à Ghostscript le fichier à afficher.

[gilles@Archlinux Downloads]$ gs cours-python.pdf

GPL Ghostscript 10.01.2 (2023-06-21)
Copyright (C) 2023 Artifex Software, Inc. All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
Unknown .defaultpapersize: (A4).
Processing pages 1 through 298.

Page 1

>>showpage, press <return> to continue<<

Obtenir des informations

Pour récupérer les informations disponibles dans un fichier PDF et afficher les métadatas nous procéderons comme suit :

% gs -dQUIET -dBATCH -dNODISPLAY -dNOPAUSE -dPDFINFO "merge.pdf"

(null) has 15 pages

Title: ??
Author: Gilles
Subject:
Keywords:
Creator: ??
Producer: GPL Ghostscript 9.56.1
CreationDate: D:20230718174033+02'00'
ModDate: D:20230718174033+02'00'

La date et l’heure sont définies au format D:%Y%m%d%H%M%S ou `D:%Y%m%d%H%M%S%z` (avec prise en compte de la Timezone). Pour convertir une date en utilisant le langage Python nous procéderons ainsi :

from datetime import datetime, timezone
from tzlocal import get_localzone

# PDF Date -> date
dateShortPDF=datetime.strptime('D:20230719101359','D:%Y%m%d%H%M%S')
print(dateShortPDF) # -> 2023-07-19 10:13:59

# PDF Date avec Timezone -> date
dateTimezonePDF = datetime.strptime("D:20231226132119+01'00'".replace("'",':')[:-1],'D:%Y%m%d%H%M%S%z')
print(dateTimezonePDF) # -> 2023-12-26 13:21:19+01:00

# date -> PDF Date
dateActuelle=datetime.strftime(datetime.now(), 'D:%Y%m%d%H%M%S')
print(dateActuelle) # -> 'D:20230719101359'

# date -> PDF Date avec TimeZone
dateTimezoneActuelle = datetime.strftime(datetime.now(timezone.utc).astimezone(get_localzone()), 'D:%Y%m%d%H%M%S%z')
dateTimezoneActuelle = f"{dateTimezoneActuelle[:-2]}'{dateTimezoneActuelle[19:]}'"

Pour afficher le détail des documents PDF il existe une alternative avec Poppler. C’est une bibliothèque open source spécialisée pour les documents PDF.

% brew install poppler

Pour afficher les informations d’un fichier PDF nous passerons son nom en paramètre à pdfinfo.

% pdfinfo "/Users/gilles/Downloads/source.pdf"
Title:
Creator: Adobe Acrobat 22.1
Producer: Adobe Acrobat 22.1 Image Conversion Plug-in
CreationDate: Wed Nov 8 19:01:34 2023 CET
ModDate: Wed Nov 8 19:01:34 2023 CET
Custom Metadata: no
Metadata Stream: yes
Tagged: no
UserProperties: no
Suspects: no
Form: none
JavaScript: no
Pages: 567
Encrypted: no
Page size: 532.8 x 646.08 pts
Page rot: 0
File size: 438948393 bytes
Optimized: yes
PDF version: 1.6

Les dimensions de la page (page size) sont en points. Il y a 72 points dans un inch (dpi ou dot per inch) ou 72 pixels dans un inch (ppi ou pixel per inch). Un inch (pouce) mesure 2.54 centimètres.

Pour les convertir nous appliquons la formule suivante : valeur en point multipliée par 1/72 et multipliée par 2.54 pour obtenir une valeur en centimètres. Dans notre exemple, nous évaluons la largeur 532.8 soit 18.796 cm. La hauteur sera de 22.792 cm.

\[largeur = {\text{points} * 2.54\;\text{cm} \over 72\;\text{dpi}} = {532.9 * 2.54\;\text{cm} \over 72\;\text{dpi}} = 18.796\;\text{ cm}\]

Pour connaître le détail des images d’un document PDF, nous exécuterons la ligne suivante :

% pdfimages -list "/Users/gilles/Downloads/source.pdf"

page num type width height color comp bpc enc interp object ID x-ppi y-ppi size ratio
--------------------------------------------------------------------------------------------
1 0 image 2220 2692 rgb 3 8 jpeg no 3564 0 300 300 945K 5.4%
2 1 image 2220 2692 rgb 3 8 jpeg no 4 0 300 300 497K 2.8%
3 2 image 2220 2692 rgb 3 8 jpeg no 8 0 300 300 581K 3.3%
4 3 image 2220 2692 rgb 3 8 jpeg no 12 0 300 300 666K 3.8%
...

Dans ce tableau nous identifions la largeur (pixels), hauteur (pixels), mode couleur (RGB ou niveaux de gris) et la densité de points par pouce (inch) en largeur (x) et en hauteur (y). Nous pouvons évaluer la largeur et la hauteur d’une image en centimètres :

\[largeur = {2220 * 2.54\;\text{cm} \over 300\;\text{dpi}} = 18.796\;\text{ cm}\]
\[hauteur = {2692 * 2.54\;\text{cm} \over 300\;\text{dpi}} = 22.792\;\text{ cm}\]

Pour connaître le nombre de points par pouce à partir d’une largeur en pixels et en centimètres, nous écrirons la formule suivante :

\[\text{dpi} = {\text{Nbr pixels} * 2.54\;\text{cm} \over \text{longueur en cm}} = {2220\;\text{pixels} * 2.54\;\text{cm} \over 18.796\;\text{cm}} = 300\;\text{dpi}\]

Pour connaître le nombre de pixels à partir des DPI et de la longueur en centimètres.

\[\text{pixels} = {\text{dpi} * \text{longueur en cm} \over 2.54\;\text{cm}} = {300\;\text{dpi} * 18.796\;\text{cm} \over 2.54\;\text{cm}} = 2220\;\text{pixels}\]

A partir de ces informations, nous pouvons ajuster un document PDF pour une impression optimale. Par exemple pour imprimer en 300 dpi une page (A4 21 cm x 29.7 cm) avec une image pleine plage, la résolution en pixels sera :

\[largeur = {300 * 21\;\text{cm} \over 2.54\;\text{cm}} = 2480\;\text{pixels}\]
\[hauteur = {300 * 29.7\;\text{cm} \over 2.54\;\text{cm}} = 3508\;\text{pixels}\]

Pour aller plus loin sur le sujet, pour une visualisation optimum sur un écran spécifique (je pense notamment à une liseuse ou une tablette), il faut évaluer la résolution idéale.

Par exemple pour un écran 24″ full hd (1920 x 1080) nous aurons une densité de pixels de 92 [PPI](https://en.wikipedia.org/wiki/Pixel_density) (Pixels per inch).

\[ppi = {\text{largeur en pixels} \over \text{largeur de l’écran en pouce (inch)}}\]

Attention, il s’agit de la largeur d’un écran en pouce, et non pas de la diagonale. Par exemple pour un écran Huawei MateView 28.2″ nous avons approximativement une largeur de 23.95 pouces pour une résolution horizontale de 3840 pixels.

\[ppi = {3840\;\text{pixels} \over 23.95\;\text{pouce (inch)}} = 160 \;\text{ppi}\]

Pour un liseuse, par exemple une Amazon Kindle Scribe 10.2″ (1860×2480 pixels), nous avons une densité de 300 PPI. Pour récupérer la largeur en pouce, nous appliquerons la formule suivante :

\[largeur en pouces = {\text{largeur en pixels} \over \text{ppi}} = {1860\over300} = 6.2 \text{ pouces}\]

Pour obtenir une longueur en centimètre, nous devons multiplier une valeur en pouce par 2.54 cm.

Ces formules peuvent également servir pour évaluer les dimensions idéales en pixels d’une image pour une impression en 300 dpi sur du papier en 10 x 15 cm :

\[\text{largeur en pixels} = { 300 \text{ dpi} * 10 \text{ cm} \over 2.54 \text{ cm} } = 1181 \text{ pixels}\] \[\text{hauteur en pixels} = { 300 \text{ dpi} * 15 \text{ cm} \over 2.54 \text{ cm} } = 1772 \text{ pixels}\]

La résolution en mégapixels de cette image est évaluée avec la formule suivante :

\[\text{résolution en mpx} = {{1181\text{ pixels} * 1772\text{ pixels}} \over 1000000\text{ pixels}} = 2.10 \text{ mégapixels}\]

Inversement pour connaître à partir des dimensions en pixels d’une photo, nous pouvons calculer pour une valeur de DPI la dimension d’impression maximale. Par exemple pour un fichier JPEG issu d’un boitier hybride Sony ILCE-7M4 de 33 mpx (7008 × 4672), les dimensions à 200 DPI sont :

\[\text{largeur en cm} = { 7008 \text{ dpi} * 2.54\text{ cm} \over 200\text{ dpi} } = 89\text{ cm}\] \[\text{hauteur en cm} = { 4672 \text{ dpi} * 2.54\text{ cm} \over 200\text{ dpi} } = 59\text{ cm}\]

Modifier les métadatas

Les métadatas disponibles par défaut sont :

MétadatasDescriptions
/TitleTitre du document
/AuthorAuteur du document
/SubjectSujet ou objet du document
/KeywordsMots-clefs séparés par une virgule
/ModDateDate de modification
/CreatorCréateur
/ProducerOutil de création du document
/CreationDateDate de création

Le principe pour modifier les métadatas du document est le suivant :

  • ajouter le paramètre -c
  • entre doubles guillemets, nous commençons par [
  • chaque métadata à modifier est insérée avec la nouvelle valeur placée entre parenthèses.
  • avant les doubles guillemets de fin nous ajoutons /DOCINFO pdfmark

Par exemple : -c "[/Title (Mon document PDF) /Producer (Gilles) /DOCINFO pdfmark"

% gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sColorConversionStrategy=Gray -dDownsampleGrayImages=true -dGrayImageResolution=150 -sGrayImageDownsampleType=Subsample -dCompatibilityLevel=1.4 -sOutputFile=/Users/gilles/output.pdf /Users/gilles/input.pdf -c "[/Title (Mon document PDF) /Producer (Gilles) /DOCINFO pdfmark"

Déverrouiller

Un document PDF peut être protégé par un mot de passe en ouverture. Pour récupérer par exemple des informations sur ce fichier, nous devrons transmettre le mot de passe avec -sPDFPassword=.

% gs -dQUIET -dBATCH -dNODISPLAY -dNOPAUSE -sPDFPassword=XXXXXXXX -dPDFINFO "crypte.pdf"

(null) has 2 pages

Title: /var/www/kalilab-tmp/ktag8f9l8f.rtf
Author: Laurent Schlegel
Creator: Ted: http://www.nllgg.nl/Ted
Producer: Ted: http://www.nllgg.nl/Ted
CreationDate: D:202108121018
ModDate: D:202109141311

Pour définir un mot de passe à l’ouverture d’un fichier nous utiliserons les options cumulées -sOwnerPassword= (mot de passe administrateur pour permettre le contrôle de la lecture) et -sUserPassword= (mot de passe qui sera demandé à l’ouverture).

gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -dCompatibilityLevel=2.0 -sOwnerPassword=test -sUserPassword=mot-de-passe -sOutputFile="/Users/gilles/Downloads/dossier-transforme.pdf" -f "/Users/gilles/Downloads/dossier.pdf"

Protéger

Les documents PDF peuvent être protégés en lecture, mais il est également possible certaines actions. Il est possible d’interdire l’impression d’un document (même si on peut toujours imprimer une copie d’écran) ou la modification.

Pour les possesseurs de MacOS ces permissions sont visibles dans l’inspecteur (⌘I).

Techniquement, pour protéger un document PDF en modulant les permissions en fonction des actions autorisées, nous devons assigner à la variable Permissions une valeur sur 32 bits. Cette valeur est le résultat de l’activation ou non des bits. Le 1er bit est à droite.

BitsDescriptions
1-2Mettre à 0
3Autoriser l’impression mettre à 1
4Autoriser les modifications (insertion, rotation, ou suppression de page)
5Autoriser la copie/extraction non contrôlée par le bit 10
6Autoriser les annotations et le remplissage de formulaire. Bit 4 défini afin d’autoriser la forme interactive
7-8Mettre à 1 (bits réservés)
9Autoriser le remplissage des champs de formulaire existants
10Autoriser la copie des textes et des graphiques
11Mettre à 0
12Pour activer l’impression haute résolution mettre à 1
13-32Mettre à 0 pour le gestionnaire de sécurité révision 3, sinon à 1 pour la révision 2.

Il est possible de construire la valeur à l’aide de Python. Dans le REPL, nous pouvons saisir une valeur binaire (les bits sont précédés des caractères 0b). Par exemple ici nous allons autoriser tout en activant les bits 3 à 6, ainsi que les bits 9 et 10.

% python3
Python 3.12.0 (v3.12.0:0fb18b02c8, Oct  2 2023, 09:45:56) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 0b1111111100
1020

Une fois notre valeur définie de permissions, nous devons ajouter un mot de passe propriétaire et définir des paramètres d’encryption. Le mot de passe se définit avec -sOwnerPassword=.

% /opt/local/bin/gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -dCompatibilityLevel=2.0 -sOwnerPassword=test -dEncryptionR=3 -dKeyLength=128 -dPermissions=1020 -sOutputFile="/Users/gilles/Downloads/pdf_reduce/0-Dossier vente Aquarelle.pdf" -f "/Users/gilles/Downloads/pdf1/0-Dossier vente Aquarelle.pdf"

Joindre plusieurs fichiers

Nous allons joindre plusieurs fichiers PDF en un seul. Nous en profitons pour convertir toutes les pages en niveau de gris.

% gs -sDEVICE=pdfwrite -dNOPAUSE -sColorConversionStrategy=Gray -dCompatibilityLevel=1.4 -sOutputFile='merge.pdf' -dBATCH 'source1.pdf' 'source2.pdf' 'source3.pdf'

Convertir en PDF/A

Un document PDF/A (Portable Document Format Archivable) est une variante normalisée ISO. Ce type de document a été créé pour réaliser un archivage long et apporter des garanties de conservation. Ghostscript propose d’ajouter le paramètre -dPDFA=1.

% gs -dPDFA=1 -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sColorConversionStrategy=Gray -dDownsampleGrayImages=true -dGrayImageResolution=150 -dGrayImageDownsampleType=/Bicubic -dCompatibilityLevel=1.4 -sOutputFile="/Users/gilles/Downloads/destination.pdf" "/Users/gilles/Downloads/source.pdf"

Orientation

Une propriété -dAutoRotatePages= a pour mission d’activer ou non l’auto-rotation. Si nous lui passons en valeur /PageByPage ou /All, les pages seront redressées automatiquement, et ce quelque soit l’orientation, de manière à faciliter la lecture. Par contre, cela ne modifiera pas l’orientation de la page. Cela ne joue que sur l’affichage. Pour désactiver l’auto-rotation nous affecterons la valeur /None. Dans ce cas la valeur affectée à setpagedevice est utilisée.

Pour changer physiquement l’orientation des pages nous devons ajouter du code Postscript au document. Pour cela, nous allons utiliser la commande -c "<</Orientation {orientation}>> setpagedevice" avec {orientation} représentée par une valeur :

  • 0 pour aucune rotation (portrait)
  • 1 pour 90° vers en sens inverse des aiguilles d’une montre (landscape ou paysage)
  • 2 pour 180° (upside down ou à l’envers)
  • 3 pour 90° dans le sens des aiguilles d’une montre (seascape)
/opt/local/bin/gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -dAutoRotatePages=/None -dCompatibilityLevel=2.0 -sOutputFile="/Users/gilles/Downloads/pdf_reduce/dossier.pdf" -c "<</Orientation 3>> setpagedevice" -f "/Users/gilles/Downloads/pdf/dossier.pdf"

L’injection du code Postscript doit se faire avant l’importation du fichier source. Le commutateur -f doit précéder le fichier source.

Redimensionnement

Un document PDF peut comporter des pages de dimensions différentes. Si à l’affichage cela ne pose pas de problème, l’impression d’un document va générer des difficultés. Le bac d’alimentation ne comporte en général qu’un seul format de papier.

Avec Ghostscript nous avons deux approches pour redimensionner les pages.

La première consiste à utiliser un format pré-existant comme par exemple le format A4. Les formats sont transmis en argument de l’option -sDEFAULTPAPERSIZE=.

% /opt/local/bin/gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sPAPERSIZE=a4 -dFIXEDMEDIA -dPDFFitPage -dCompatibilityLevel=2.0 -sOutputFile="/Users/gilles/Downloads/pdf_reduce/dossier.pdf" "/Users/gilles/Downloads/pdf/dossier.pdf"
FormatsLargeursHauteursNormes
a0 841 1189ISO STANDARD
a1 594 841ISO STANDARD
a2 420 594ISO STANDARD
a3 297 420ISO STANDARD
a4 210 297ISO STANDARD
a4small 210 297ISO STANDARD
a5 148 210ISO STANDARD
a6 105 148ISO STANDARD
a7 74 105ISO STANDARD
a8 52 74ISO STANDARD
a9 37 52ISO STANDARD
a10 26 37ISO STANDARD
isob0 1000 1414ISO STANDARD
isob1 707 1000ISO STANDARD
isob2 500 707ISO STANDARD
isob3 353 500ISO STANDARD
isob4 250 353ISO STANDARD
isob5 176 250ISO STANDARD
isob6 125 176ISO STANDARD
c0 917 1297ISO STANDARD
c1 648 917ISO STANDARD
c2 458 648ISO STANDARD
c3 324 458ISO STANDARD
c4 229 324ISO STANDARD
c5 162 229ISO STANDARD
c6 114 162ISO STANDARD
11×17279432US STANDARD
ledger432279US STANDARD
legal216356US STANDARD
letter216279US STANDARD
lettersmall216279US STANDARD
archE9141219US STANDARD
archD610914US STANDARD
archC457610US STANDARD
archB305457US STANDARD
archA229305US STANDARD
jisb010301456JIS STANDARD
jisb17281030JIS STANDARD
jisb2515728JIS STANDARD
jisb3364515JIS STANDARD
jisb4257364JIS STANDARD
jisb5182257JIS STANDARD
jisb6128182JIS STANDARD
flsa216330Autres
flse216330Autres
halfletter140216Autres
hagaki100148Autres
Format de page – Dimension en millimètres

La deuxième approche consiste à définir les dimensions en utilisant les options -dDEVICEWIDTHPOINTS=<largeur> et -dDEVICEHEIGHTPOINTS=<hauteur>. Les dimensions sont en points (1 pouce/inch = 72 points et 1 pouce/inch = 2.54 centimètres). Par exemple, pour appliquer un format de page de 10 x 10 cm nous pour vous exécuter la commande qui suit :

% /opt/local/bin/gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -dDEVICEWIDTHPOINTS=283.46 -dDEVICEHEIGHTPOINTS=283.46 -dFIXEDMEDIA -dPDFFitPage -dCompatibilityLevel=2.0 -sOutputFile="/Users/gilles/Downloads/pdf_reduce/dossier.pdf" "/Users/gilles/Downloads/pdf/dossier.pdf"
\[largeur = {10 \;\text{cm} * 72 \;\text{points} \over 2.54\;\text{cm}} = 283.46\;\text{points}\]

Il faut pas oublier d’ajouter les options -dFIXEDMEDIA -dPDFFitPage pour adapter les pages au format.

Réduction de la taille des documents

Profile Distiller

L’utilisation de profiles inspirés d’Adobe Distiller est une approche simple pour réduire le poids d’un document. Ces profiles utilisent des réglages adaptés en fonction de l’appareil qui affichera le PDF.

Pour afficher la liste des profiles supportés par l’option -dPDFSETTINGS=... nous utiliserons la commande suivante sous macOS ou Linux :

% gs -dNODISPLAY -c ".distillersettings {exch ==only ( ) print ==} forall quit"
GPL Ghostscript 10.02.1 (2023-11-01)
Copyright (C) 2023 Artifex Software, Inc.  All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
/ebook -dict-
/printer -dict-
/default -dict-
/prepress -dict-
/screen -dict-
/PSL2Printer -dict-

Sous Windows, dans Powershell nous utiliserons la même commande.

PS C:\Users\gille> & 'C:\Program Files\gs\gs10.00.0\bin\gswin64c.exe' -dNODISPLAY -c ".distillersettings {exch ==only ( ) print ==} forall quit"
GPL Ghostscript 10.0.0 (2022-09-21)
Copyright (C) 2022 Artifex Software, Inc. All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
/default -dict-
/prepress -dict-
/screen -dict-
/PSL2Printer -dict-
/ebook -dict-
/printer -dict-

Pour afficher les paramètres par défaut d’un profile comme par exemple /screen :

% gs -q -dNODISPLAY -c ".distillersettings /screen get {exch ==only ( ) print ===} forall quit" | sort
/AutoRotatePages /PageByPage
/CannotEmbedFontPolicy /Warning
/ColorACSImageDict << /ColorTransform 1 /Blend 1 /HSamples [2 1 1 2] /VSamples [2 1 1 2] /QFactor 0.76 >>
/ColorConversionStrategy /sRGB
/ColorImageDownsampleType /Average
/ColorImageResolution 72
/CompatibilityLevel 1.5
/CreateJobTicket false
/DoThumbnails false
/EmbedAllFonts true
/GrayACSImageDict << /ColorTransform 1 /Blend 1 /HSamples [2 1 1 2] /VSamples [2 1 1 2] /QFactor 0.76 >>
/GrayImageDownsampleType /Average
/GrayImageResolution 72
/MonoImageDownsampleType /Subsample
/MonoImageResolution 300
/NeverEmbed [/Courier /Courier-Bold /Courier-Oblique /Courier-BoldOblique /Helvetica /Helvetica-Bold /Helvetica-Oblique /Helvetica-BoldOblique /Times-Roman /Times-Bold /Times-Italic /Times-BoldItalic /Symbol /ZapfDingbats]
/PreserveEPSInfo false
/PreserveOPIComments false
/PreserveOverprintSettings false
/UCRandBGInfo /Remove

A la suite d’un paramètre de sortie (ici screen) nous pouvons modifier certains paramètres comme la résolution de sortie pour les images (ici pour les images monochrome, niveau de gris et couleur).

% gs -dPDFSETTINGS=/screen -sDEVICE=pdfwrite -dMonoImageResolution=150
-dColorImageResolution=100 -dGrayImageResolution=150
-dBATCH -dNOPROMPT -dNOPAUSE -sOutputFile="/Users/gilles/Documents/Archives/temporaire/pdf-out.pdf" "/Users/gilles/Documents/Archives/temporaire/pdf-in.pdf"

Niveaux de gris

Pour réduire le poids d’un document, une approche consiste à transformer toutes les images en niveaux de gris. En effet, une image avec cette colorimétrie utilise 256 niveaux de gris. Le nombre d’informations stockées pour une image sera réduit.

Nous personnaliserons la ligne de commandes pour forcer ce type de conversion.

  • -sColorConversionStrategy=Gray pour appliquer une conversion vers du niveaux de gris à toutes les images.
  • -dGrayImageResolution=100 pour modifier la densité de points par pouce (DPI ou nombre de points imprimés sur une ligne). Un pouce (inch) est égale à 2.54 cm. Une image de 10 cm x 5 cm aura pour une densité de 200 dpi aura environ 787 points en largeur (10 cm / 2.54 x 200 dpi). Plus la résolution en DPI est élevée, plus l’image sera volumineuse. A contrario, une résolution faible allégera le poids de l’image avec en contre-partie une détérioration de la qualité.
  • -dDownsampleGrayImages=true pour activer le changement de résolution.

Pour réduire la taille de l’image nous allons jouer sur le nombre de DPI.

% gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sColorConversionStrategy=Gray -dDownsampleGrayImages=true -dGrayImageResolution=100 -dCompatibilityLevel=1.4 -sOutputFile="/Users/gilles/Downloads/destination.pdf" "/Users/gilles/Downloads/source.pdf"

Pour les utilisateurs de MacOS (et Linux) il est toujours possible d’intégrer une commande au shell qui

Pour les utilisateurs du shell ZSH nous nous plaçons à la racine de l’utilisateur, et nous ouvrons le fichier .zshrc (pour Bash nous éditerons .bash_profile).

% cd
% nano .zshrc
Ghostscript pdf adobe python

Nous ajoutons ensuite le code suivant à la fin du fichier. Pour sauvegarder nous faisons les touches CTRL+X puis Y et entrée.

pdfcompress()
{
   gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sColorConversionStrategy=Gray -dDownsampleGrayImages=true -dGrayImageResolution=100 -dCompatibilityLevel=2.0 -sOutputFile=${1%.*}_reduce.pdf $1
}

Pour essayer, nous ouvrons un nouveau terminal. Nous tapons simplement pdfcompress suivi du nom du fichier à compresser.

Noir et blanc

Pour des documents PDF ne comportant que des objets textes, il est possible de forcer les couleurs au noir avec -dBlackText. Cela n’affectera pas les images.

gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -dBlackText -dBlackVector -dCompatibilityLevel=2.0 -sOutputFile="/Users/gilles/Downloads/test-nb.pdf" -f "/Users/gilles/Downloads/test.pdf"

Pour les documents PDF comportant des images, la solution repose sur des opérations successives. La première consiste à utiliser le device

Device : pngmono, pngmonod, png16m, png16, png256, png48, pnggray

% mkdir img
% gs -q -dBATCH -dNOPAUSE -sDEVICE=png16m -r300 -sOutputFile=img/out_%03d.png -f "/Volumes/seagate/Download/Ebook/Informatique/eni csharp 10 et visual studio code.pdf"

Récupère dans une variable toutes les images

% files=$(find ./img -type f -name "*.png")
% sources="";for file in `find ./img -type f -name "*.jpg"`; do sources="${sources} (${file}) viewJPEG showpage" done
% 

Reconnaissance de caractères (OCR)

Ghostscript intègre depuis la version 9.54 un moteur de reconnaissance de caractères basé sur Tesseract et Leptonica. La reconnaissance se fait sur des médias images (par exemple issues d’un scanner).

La reconnaissance est intégrée sous forme de devices :

  • ocr pour une conversion d’un fichier PDF vers un fichier texte. Les images sont convertis en niveaux de gris pour être analysées par Tesseract. Le fichier texte est encodé en UTF-8.
  • hocr pour une conversion d’un fichier PDF vers un fichier XML au standard hOCR
  • pdfocr24 (RGB), pdfocr32 (CMYK) ou pdfocr8 (niveau de gris) pour une conversion d’un fichier PDF vers un fichier PDF sans modifier le fichier source. Les images sont conservées comme tel. Le texte reconnu est ajouté au fichier. Il ne sera pas disponible à l’affichage (invisible-text-only PDF). Il sera par contre reconnu pour la recherche et le copier/coller dans le document.

Tesseract offre plusieurs moteurs pour opérer la reconnaissance. Pour choisir un moteur avec Ghostscript nous avons à notre disposition l’option -dOCREngine=. Par défaut le moteur LSTM/neural network est utilisé (-dOCREngine=1). Nous pouvons choisir d’utiliser le moteur Legacy Tesseract (-dOCREngine=0).

Pour fonctionner, Tesseract a besoin de fichiers de données spécifiques appelés traineddata. Ces fichiers sont essentiels pour adapter la reconnaissance en fonction de chaque langue. Il existe des variantes qui favorise la rapide ou la qualité.

Les fichiers sont stockés dans un répertoire appelé tessdata. Le chemin d’accès à ce répertoire doit être mémorisé dans un variable locale.

% export TESSDATA_PREFIX=/Users/gilles/tessdata

Alternativement, le chemin d’accès peut aussi être renseigné comme paramètre de Ghostscript avec l’option --permit-file-read=.

% /opt/local/bin/gs -dNOPAUSE -dBATCH --permit-file-read=/Users/gilles/tessdata -sDEVICE=ocr ...

Par défaut, la langue anglaise est utilisée avec le fichier eng.traineddata. Pour modifier la langue utilisée, nous utiliserons l’option -sOCRLanguage= avec en valeur le code de la langue à utiliser.

% /opt/local/bin/gs -dNOPAUSE -dBATCH -sDEVICE=ocr -sOCRLanguage="fra" --permit-file-read=/Users/gilles/tessdata -sOutputFile="/Users/gilles/Downloads/rapport.txt" -f "/Users/gilles/Downloads/rapport.pdf"

L’utilisation de plusieurs langues est possible. L’option -sOCRLanguage= sera alors écrite par exemple -sOCRLanguage="fra+eng" pour utiliser le français et l’anglais.

Dans le répertoire tessdata il n’est pas nécessaire de copier tous les fichiers avec extension traineddata. Nous pouvons y placer que les fichiers pour les langues qu’il est envisagé de reconnaître.

Il est possible d’adapter la résolution des images utilisées avec le moteur OCR avec l’option -r<dpi> ou -r<dpi horizontal>x<dpi vertical>. -r<dpi> applique la même résolution en horizontal et en vertical. Attention, plus les DPI seront élevés, plus la reconnaissance sera longue. Il faut appliquer une valeur équilibrée. Cette option est équivalente à -dDEVICEXRESOLUTION=<dpi> et -dDEVICEYRESOLUTION=<dpi> (la résolution horizontale et verticale sur le device utilisé).

% /opt/local/bin/gs -dNOPAUSE -dBATCH -sDEVICE=ocr -r200 --permit-file-read=/Users/gilles/tessdata -sOutputFile="/Users/gilles/Downloads/rapport.txt" -f "/Users/gilles/Downloads/rapport.pdf"

Automatisation

Il existe de nombreuses possibilités en matière d’automatisation. L’utilisation du terminal est une de ces solutions. Ce n’est pas une approche facile pour le néophyte, mais son utilisation peut s’avérer un puissant allié pour réduire la taille des fichiers.

Avec le terminal

Windows Powershell

Sous Windows, en utilisant PowerShell nous pouvons automatiser la réduction de la taille des fichiers. Nous créons un fichier texte reduction.ps1 dans lequel nous intégrons ces lignes. Sans faire un cours sur PowerShell, notons les points suivants :

  • Get-ChildItem récupère dans un tableau tous les fichiers (y compris dans les sous-dossiers) avec extension .pdf.
  • $gs précise l’emplacement de Ghostscript.
  • $destinationFolder identifie le dossier de destination où tous les fichiers réduits seront créés.
  • ForEach parcoure les éléments du tableau $files. Chaque élément est placé dans une variable $file.
  • $destination comporte le nom complet du fichier à créer (dossier et fichier).
  • if est utilisé pour ne réduire que les fichiers de 100 Mo et plus. C’est facultatif. Nous pouvons supprimer ce test et les accolades associées.
  • & $gs ... exécute Ghostscript avec les paramètres pour chaque élément de la liste $files.
$files= Get-ChildItem -Path "D:\travail" -Filter *.pdf -Recurse -ErrorAction SilentlyContinue -Force
$gs = 'C:\Program Files\gs\gs10.02.1\bin\gswin64c.exe'
$destinationFolder = "d:\pdf_reduits"
ForEach ($file in $files) {
    $destination = [io.path]::Combine($destinationFolder,$file)
    If ((($file.Length) / 1024 / 1024) -ge 100) {
        & $gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sColorConversionStrategy=Gray -dDownsampleGrayImages=true -dGrayImageResolution=150 "-dCompatibilityLevel=1.4" -o $destination $file.FullName
    }
}

Pour exécuter ce script dans PowerShell nous devons autoriser l’utilisateur actuel. Pour cela nous ouvrons le terminal en mode administrateur et nous tapons la commande suivante :

PS C:\Users\gilles> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

Shell MacOS/Linux

Nous allons créer un fichier texte que nous nommerons reduction.sh. Cela se fait automatiquement par la ligne de commandes qui suit :

% nano reduction.sh

A l’intérieur de ce fichier nous allons créer une fonction récursive pour parcourir tous les sous-répertoires d’un emplacement spécifié lors de l’appel de la fonction. L’écriture de script pour le terminal utilise un langage particulièrement abscon. Il faut être vigileant à bien reproduire la syntaxe.

#!/bin/bash

sourceFolder=/Users/gilles/Downloads/pdf
destinationFolder=/Users/gilles/Downloads/pdf_reduce/

recherche () {
for chemin in "$1"/*; do
    if [ -d "$chemin" ]; then
        walk_dir "$chemin"
    elif [ -e "$chemin" ]; then
        case "$chemin" in
            *.pdf|*.PDF)
                destination=$destinationFolder$(basename "$chemin")
                gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sColorConversionStrategy=Gray -dDownsampleGrayImages=true -dGrayImageResolution=150 -dCompatibilityLevel=1.4 -o "${destination}" "${chemin}"
        esac
    fi
    done
}

recherche "$sourceFolder"

Le script pourra être exécuté tout simplement.

% bash reduction.sh

Pour rendre exécutable le script nous exécuterons cette commande.

% sudo chmod +x reduction.sh

L’exécution du script se fera de cette façon :

% ./reduction.sh

Nous pouvons modifier le script pour définir dans la ligne de commandes en paramètres les dossiers de recherche ($1) et de destination ($2).

#!/bin/bash
sourceFolder=$1
destinationFolder=$2
...

Nous lançons le script en ajoutant les chemins source et destination (avec un / à la fin).

% ./reduce.sh /Users/gilles/Downloads/pdf_source /Users/gilles/Downloads/pdf_destination/

Pour afficher la liste de tous les fichiers PDF contenus dans un dossier et ses sous-dossiers nous avons à notre disposition la commande find.

% find /Users/gilles/Downloads/pdf -type f -name '*.pdf' -exec printf "%s\n" {} +

Sous MacOS et Linux il existe de nombreuses autres possibilités pour automatiser en ligne de commandes la réduction de la taille des fichiers. Au lieu d’utiliser un script avec une boucle, nous pouvons le faire en une seule ligne de commandes en utilisant find. Cette commande est capable d’exécuter une ou plusieurs commandes en communiquant chaque nom de fichier trouvé (récupérable avec {}). basename est utilisé pour extraire le nom du fichier sans extension.

% find /Users/gilles/Downloads/pdf_source -type f -name "*.pdf" -exec bash -c 'destination="/Users/gilles/Downloads/pdf_destination/"$(basename "$0" .pdf); gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sColorConversionStrategy=Gray -dDownsampleGrayImages=true -dGrayImageResolution=150 -dCompatibilityLevel=1.4 -o $destination".reduce.pdf" "$0";' {} \;

En mode terminal, nous pouvons également utiliser des variables locales (destinationDossier et sourceDossier) pour renseigner la ligne de commandes.

% destinationDossier="/Users/gilles/Downloads/pdf_reduce/"
% sourceDossier="/Users/gilles/Downloads/pdf"
% find $sourceDossier -type f -name "*.pdf" -exec zsh -c 'destination=$1$(basename "$0" .pdf)".reduce.pdf"; gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sColorConversionStrategy=Gray -dDownsampleGrayImages=true -dGrayImageResolution=150 -dCompatibilityLevel=1.4 -o $destination "$0";' {} $destinationDossier \;

Avec Python

Nous allons créer un programme Python pour automatiser la réduction de tous les fichiers PDF d’un dossier. Nous pouvons donner à cette application le nom reduce_gs.

Sur mon dépôt GitHub.com nous pouvons retrouver un ensemble d’exemples notamment les interactions entre la librairie libgs et Python.

Installation d’un environnement

Windows

Pour exécuter cette application, il est fortement conseillé de créer un environnement virtuel.

PS C:\Users\gilles\Documents\Python Scripts\pdf> python -m venv .venv

Pour activer l’environnement dans Powershell nous écrirons les commandes suivantes :

PS C:\Users\gilles\Documents\Python Scripts\pdf> .\.venv\Scripts\Activate.ps1
(.venv) PS C:\Users\gilles\Documents\Python Scripts\pdf> pip list
Package Version
----------------- -------
pip 23.3.2
python-utils 3.8.1
setuptools 65.5.0

Pour fonctionner ce script a besoin de bibliothèques auxiliaires. Nous pouvons les installer directement par une commande pip.

(.venv) PS C:\Users\gilles\Documents\Python Scripts\pdf> python install tqdm

Pour mettre à jour le package, nous ajouterons un paramètre --upgrade :

(.venv) PS C:\Users\gilles\Documents\Python Scripts\pdf> pip install --upgrade tqdm

Nous pouvons aussi utiliser un fichier texte que nous appellerons requirements.txt auquel nous ajouterons la ligne suivante :

tqdm==4.66.1

Pour installer les bibliothèques, nous utiliserons le fichier requirements.txt avec la commande pip.

(.venv) PS C:\Users\gilles\Documents\Python Scripts\pdf> pip install -r requirements.txt
Mac OS

Dans le système d’Apple nous allons utiliser l’environnement Python Conda. Nous pouvons utiliser les distributions Anaconda ou Miniconda pour utiliser Conda. Elles sont compatibles avec tous les systèmes d’exploitation (Windows, Mac Os ou Linux). Cela permet une meilleure intégration d’un environnement de travail Python.

Une fois installée, nous allons travailler dans des environnements virtuels. Pour obtenir la liste des environnements présents sur le système, nous utiliserons dans le Terminal les instructions qui suivent :

% conda env list

ou

% conda info --envs

Nous pouvons également obtenir des informations sur l’environnement. Ici, nous sommes en présence d’un environnement sous Mac OS Silicon (arm).

% conda info
	active environment : base
	active env location : /Users/gilles/opt/anaconda3
	shell level : 1
	user config file : /Users/gilles/.condarc
	populated config files : /Users/gilles/.condarc
	conda version : 4.13.0
	conda-build version : 3.21.8
	python version : 3.9.12.final.0
	virtual packages : __osx=13.3.1=0
	__unix=0=0
	__archspec=1=arm64
	base environment : /Users/gilles/opt/anaconda3  (writable)
	conda av data dir : /Users/gilles/opt/anaconda3/etc/conda
	conda av metadata url : None
	channel URLs : https://repo.anaconda.com/pkgs/main/osx-arm64
	https://repo.anaconda.com/pkgs/main/noarch
	https://repo.anaconda.com/pkgs/r/osx-arm64
	https://repo.anaconda.com/pkgs/r/noarch
	package cache : /Users/gilles/opt/anaconda3/pkgs
	/Users/gilles/.conda/pkgs
	envs directories : /Users/gilles/opt/anaconda3/envs
	/Users/gilles/.conda/envs
	platform : osx-arm64
	user-agent : conda/4.13.0 requests/2.27.1 CPython/3.9.12 Darwin/22.4.0 OSX/13.3.1
	UID:GID : 501:20
	netrc file : None
	offline mode : False

L’environnement de développement Python peut être mis à jour.

% conda update conda

Si nous utilisons la distribution Anaconda, nous pouvons également utiliser la commande suivante :

% conda update anaconda

Pour développer, nous devons créer un environnement virtuel. Pour cela, nous utilisons la commande create, suivi de l’argument -n pour donner un nom à l’environnement. Optionnellement, nous pouvons définir la version de Python à installer.

% conda create -n reduce_pdf python=3.11

ou

% conda create --name reduce_pdf python=3.11

Nous pouvons également intégrer des bibliothèques à la création de l’environnement. -c conda-forge est utilisé pour définir le dépôt de bibliothèque à utiliser.

% conda create -n reduce_pdf -c conda-forge python=3.11 tqdm

Une bibliothèque peut être installé par la suite lorsque l’environnement est en cours d’utilisation.

% conda install -c conda-forge tqdm

Si nous souhaitons mettre à jour un package spécifique nous utiliserons la commande update suivi du ou des noms de packages.

% conda update tdqm

Les dépendances (packages) sont également mises à jour. Tout est automatique, il n’est pas nécessaire de s’en inquiéter. update propose quelques options supplémentaires :

  • --no-deps pour ne pas mettre à jour les dépendances
  • --force-reinstall pour forcer la mise à jour même si le package est à jour
  • --dry-run pour simuler la mise à jour
  • --yes pour répondre automatiquement oui à toute question

La mise à jour de tous les packages d’un environnement se fait de cette manière.

% conda update -all

L’utilisation d’un environnement nécessite son activation. Pour faire cela, nous avons la commande :

% conda activate reduce_pdf

Pour quitter l’environnement nous procéderons comme suit :

% conda deactivate

Pour supprimer un environnement de développement :

% conda env remove --name reduce_pdf

Pour supprimer tous les environnements à un emplacement spécifique, nous utiliserons l’argument --prefix.

%  conda env remove --prefix /Users/gilles/envs 

Code complet

Pour utiliser ce code nous devons l’insérer dans un fichier Python. Vous pouvez le faire avec n’importe quel éditeur texte ou de code. Personnellement je développe en Python sous Microsoft Visuel Code.

Nous pouvons le faire dans le Terminal de MacOS, en utilisant l’utilitaire Nano et en copier le code.

% nano reduce_gs.py

Pour sortir de Nano, appuyez sur le raccourci CTRL + X puis validez par Y et la touche Entrée.

Pour exécuter le code dans Terminal, nous utiliserons l’exécutable Python avec en argument le chemin d’accès au fichier reduce_gs.py. Par exemple, pour lancer le script présent dans le répertoire actif.

% python ./reduce_gs.py

Le listing complet est reproduit ci-après. Il s’agit d’une version simplifiée. Sur le dépôt GitHub vous retrouvez une version étoffée avec des options supplémentaires.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

##################################################
## reduction de la taille des PDF
##################################################
## Author: Gilles BIHAN
## Copyright: Copyright 2024, Converio.fr
## Version: 1.0.0
## Email: gilles.bihan@converio.fr
## Status: Dev
##################################################

import os, subprocess, re, shutil, argparse
from platform import system

from tqdm import tqdm

if system() in [ 'Linux', 'Darwin']:
    ghostscript = shutil.which("gs")
elif system() in ['Windows']:
    ghostscript = shutil.which("gswin64c.exe")

def numberOfPage(source):
    cmd = [ghostscript, "-dQUIET", "-dBATCH", "-dNODISPLAY", "-dNOPAUSE", "-dPDFINFO", "-dFirstPage=1","-dLastPage=1", source]
    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = proc.communicate()
    ret = re.findall('([0-9]{1,})',str(stderr))
    if len(ret)>0:
        return re.search('([0-9]{1,})',str(stderr))[0]
    return 0

def cli(cmd, source, pages):
    fichier = "[{:^20s}] ".format(os.path.basename(source)[:20])
    with tqdm(total=int(pages)) as bar:
        bar.set_description_str(fichier)
        p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True)
        while True:
            chunk = p.stdout.readline()
            if chunk=='':
                break
            m = re.search("Page (?P<page>[0-9]{1,})",chunk)
            if m:
                bar.update(1)
                

def reduction(dossierSource, dossierDestination, dpi=150, version="1.4", replace=False):
    global ghostscript
    source_size = 0
    dest_size = 0
    nbr_file = 0

    for path, subdirs, files in os.walk(dossierSource):
        fichiers = [file for file in files if os.path.splitext(file)[1]==".pdf"]
        for fichier in fichiers:
            source = os.path.join(path, fichier)
            destination = source.replace(os.path.dirname(source),dossierDestination)
            
            if os.path.exists(destination) and not replace:
                continue

            destinationFinale = destination
            if replace:
                destination=re.sub('(?i)'+re.escape(".pdf"), lambda m:  ".tmp", destination)

            cmd = [ghostscript, '-sDEVICE=pdfwrite', '-dNOPAUSE', '-dBATCH', 
                    '-sColorConversionStrategy=Gray', '-dDownsampleGrayImages=true', f"-dGrayImageResolution={dpi}", f"-dCompatibilityLevel={version}",
                    f"-sOutputFile={destination}",
                    f"{source}"]

            try:
                cli(cmd,source=source,pages=numberOfPage(source=source))
                source_size += os.stat(source).st_size
                dest_size += os.stat(destination).st_size

                if replace and os.path.exists(destinationFinale):
                    os.remove(destinationFinale)
                    shutil.move(destination,destinationFinale)
                elif replace:
                    shutil.move(destination,destinationFinale)
                    
            except FileNotFoundError:
                if os.path.exists(source):
                    shutil.copyfile(source,destination)
            except Exception as e:
                print(e)

            nbr_file += 1

    return nbr_file,source_size,dest_size 

if __name__ == "__main__":
    description="Réduction de la taille des fichiers PDF"
    parser = argparse.ArgumentParser(description=description,exit_on_error=False)
    parser.add_argument("source", help="Dossier source",type=str)
    parser.add_argument("destination", help="Dossier de destination",type=str)
    parser.add_argument("-d","--dpi", help="Points par pouce (défaut=150)",type=int,default=150)
    parser.add_argument("-v","--version", help="Version du document PDF (défaut=1.4)",type=str,default="1.4",)
    parser.add_argument("-r","--replace", help="Points par pouce (défaut=150)",type=str, choices=['o','oui','y', 'yes', 'n','non','no'],default='n')
    try:
        args = parser.parse_args()
        replace = False if args.replace.startswith("n") else True
        if ghostscript is None:
            print("Ghostscript n'a pas été trouvé. Veuillez l'installer.")
        elif os.path.exists(args.source):       
            if not os.path.exists(args.destination):
                os.makedirs(args.destination)
            nbr_file,source_size,dest_size = reduction(dossierSource=args.source, 
                                                    dossierDestination=args.destination, 
                                                        dpi=args.dpi, version=args.version, replace=replace)
                
            print(f"Nbr de fichiers: {nbr_file}\tSources: {source_size / (1024*1024):.2f} mbytes\tDestinations: {dest_size / (1024*1024):.2f} mbytes soit gain de {max(0,1-(dest_size/source_size)):.2%}")
    except argparse.ArgumentError as e:
        print(e)
    except ZeroDivisionError:
        print("Aucun fichier n'a été traité")
    except Exception as e:
        print(e)

Utilisation

Pour afficher l’aide de la commande nous ajoutons le paramètre -h.

(.venv) PS C:\Users\gilles\Documents\Python Scripts\pdf> python .\reduce_gs\reduce_gs.py -h

python reduce_gs.py -h
usage: reduce_gs.py [-h] [-d DPI] [-v VERSION] [-r {o,oui,y,yes,n,non,no}] source destination

Réduction de la taille des fichiers PDF

positional arguments:
  source                Dossier source
  destination           Dossier de destination

options:
  -h, --help            show this help message and exit
  -d DPI, --dpi DPI     Points par pouce (défaut=150)
  -v VERSION, --version VERSION
                        Version du document PDF (défaut=1.4)
  -r {o,oui,y,yes,n,non,no}, --replace {o,oui,y,yes,n,non,no}
                        Points par pouce (défaut=150)

Pour fonctionner nous devons a minima préciser un dossier source et un dossier destination. Avec le script Python, nous procéderons comme suit :

% python reduce_gs.py ~/Documents/Factures /Users/gilles/Factures

Avec le fichier exécutable, l’utilisation de la commande sera :

(.venv) PS C:\Users\gilles\Documents\Python Scripts\pdf> reduce_gs.exe ~/Documents/Factures /Users/gilles/Factures

Avec le paramètre -r nous pouvons choisir de ne pas remplacer les fichiers existants dans le dossier destination.

(.venv) PS C:\Users\gilles\Documents\Python Scripts\pdf> python reduce_gs.py "C:\Users\gilles\OneDrive\Documents\Factures\" "C:\Users\gilles\Documents\Factures\" -r non

Pour modifier la densité de points par pouce nous utiliserons le paramètre -d suivi de la valeur désirée. Rappelons nous que plus la valeur est basse, plus la qualité des images est dégradée. Inversement, il est inutile d’augmenter les DPI au delà de la valeur utilisée par les images sources.

Dans Windows, l’accès a un disque réseau nécessite dans Powershell de se connecter avec des identifiants valides. Pour cela nous devons au préalable lancer la ligne qui suit :

net use \\192.168.1.14\mondisque /user:gilles

Compiler les sources

Les sources de Ghostscript sont disponibles de deux manières : avec une archive TAR compressée ; avec un dépôt distant à dupliquer localement.

Archive

Le code source se télécharge à partir du site de Ghostscript. Il est commun pour toutes les plateformes (Windows, MacOs, Linux). Nous récupérons une archive TAR avec une compression GZIP. Le numéro de version est précisé dans le nom du fichier (ghostscript-10.02.1.tar.gz pour la version 10.02.1)

% wget https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs10021/ghostscript-10.02.1.tar.gz
% tar xvf ghostscript-10.02.1.tar.gz 
% cd ghostscript-10.02.1
%

Dans Linux, nous mettons à jour le système, puis nous installons le compilateur GCC.

% sudo apt update &amp;&amp; sudo apt upgrade
% sudo apt install build-essential

Dans MacOs, si nous n’utilisons pas Xcode, nous devons installer les utilisateurs de développement en ligne de commandes. Nous ouvrons le terminal et nous exécutons cette commande.

gilles@MBP-de-Gilles ~ %  xcode-select --install

Une fenêtre apparaît et nous cliquons sur le bouton Installer. Nous validons le contrat de licence. L’installation de Command Line Developer Tools se fait automatiquement. Nous pouvons suivre la progression. A la fin, gcc est installé.

Pour vérifier la bonne installation de GCC, nous taperons la commande suivante :

$ gcc -v
gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04) 
gilles@MBP-de-Gilles ~ % gcc -v
Apple clang version 13.1.6 (clang-1316.0.21.2.5)
Target: arm64-apple-darwin21.4.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

Ensuite nous allons exécuter la configuration pour préparer les fichiers makefile.

% ./configure

Pour afficher l’aide du script de configuration nous exécuterons la commande ./configure --help dans le terminal.

Pour lancer la compilation de l’exécutable Ghostscript nous lançons la commande ci-après.

% make

Pour supprimer tous les fichiers de compilation nous avons à notre disposition make clean. A la suite nous allons pouvoir relancer tout le processus de compilation avec une génération de tous les fichiers intermédiaires. Pour exécuter l’exécutable sur le système, nous lancerons la commande make install à la suite de la compilation. Le fichier exécutable est disponible dans le dossier bin.

Sur MacOS, il peut être utile de connaître la compatibilité de l’exécutable (x86_64 ou Arm64). Nous utilisons l’instruction file.

% cd bin
gilles@MBP-de-Gilles bin % ls
gs
gilles@MBP-de-Gilles bin % file gs 
gs: Mach-O 64-bit executable arm64

Pour lancer la compilation de la librairie partagée Ghostscript, nous exécuterons la commande make so dans le terminal. Pour nettoyer les fichiers de compilation, nous avons la commande make soclean. Les librairies compilées sont disponibles dans le dossier sobin. Sous Linux, nous trouverons des fichiers avec extension so. Par exemple pour la version 10.02, nous aurons la librairie libgs.so.10.02, et deux liens symboliques vers la librairie (libgs.so et libgs.so.10). Sous MacOs, les librairies auront une extension dylib. Pour la version 10.02 nous aurons la librairie libgs.dylib.10.02 et deux liens symboliques (libgs.dylib.10.02 ou libgs.dylib.10). Contrairement à l’exécutable gs (qui intègre la librairie dans l’exécutable), la librairie partagée peut être utilisé avec deux exécutables : gsc et gsx. Le premier est identique à gs. Le deuxième, gsx, utilise en sortie un périphérique d’affichage s’appuyant sur un widget GTK+.

% gsc -v
GPL Ghostscript 10.02.1 (2023-11-01)
Copyright (C) 2023 Artifex Software, Inc.  All rights reserved.

Pour installer localement la librairie partagée nous exécuterons en mode super utilisateur sudo make install-so dans le terminal.

Dépôt distant

Nous pouvons aussi compiler Ghostscript à partir d’un dépôt distant (Git). Bien entendu il faut au préalable avoir installé Git.

gilles@MBP-de-Gilles % git clone git://git.ghostscript.com/ghostpdl.git
Cloning into 'ghostpdl'...
remote: Enumerating objects: 231573, done.
remote: Counting objects: 100% (8266/8266), done.
remote: Compressing objects: 100% (5125/5125), done.
remote: Total 231573 (delta 4424), reused 4834 (delta 3122), pack-reused 223307
Receiving objects: 100% (231573/231573), 243.30 MiB | 10.57 MiB/s, done.
Resolving deltas: 100% (182471/182471), done.

Nous devons exécuter autogen.sh pour préparer localement le dépôt. Sous MacOs, nous devons au préalable installer via brew deux outils supplémentaires.

% brew install autoconf automake

Il nous reste à nous placer à la racine du dépôt local, et d’exécuter successivement

gilles@MBP-de-Gilles % cd ghostpdl
gilles@MBP-de-Gilles ghostpdl % ./autogen.sh 
gilles@MBP-de-Gilles ghostpdl % ./configure
gilles@MBP-de-Gilles ghostpdl % make

Une fois compilée, le binaire de Ghostscript est placé dans le répertoire bin.

gilles@MBP-de-Gilles ghostpdl % ./bin/gs 
GPL Ghostscript 10.03.0 (2023-09-13)
Copyright (C) 2023 Artifex Software, Inc.  All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
GS>quit

Pour synchroniser localement avec le dépôt distant nous exécuterons à la racine du dépôt local la commande git pull dans le terminal.

gilles@MBP-de-Gilles ghostpdl % git pull

Pour les utilisateurs de Visual Studio Code, pour référencer la librairie libgs, nous pouvons ajouter à une variable d’environnement GSAPI_LIB au fichier de configuration. Cette variable pointera sur le fichier.

{
     // Utilisez IntelliSense pour en savoir plus sur les attributs possibles.
     // Pointez pour afficher la description des attributs existants.
     // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387
     "version": "0.2.0",
     "configurations": [
         {
             "name": "Débogueur Python : Fichier actuel",
             "type": "debugpy",
             "request": "launch",
             "program": "${file}",
             "console": "integratedTerminal",
             "env": {
                 "GSAPI_LIB":"/Users/gilles/Downloads/ghostscript-10.02.1/sobin/libgs.dylib.10.02"
             }
         }
     ]
 }

Windows

Dans le dossier Windows de l’archive Ghostscript nous pouvons ouvrir dans Microsoft Visual Studio 2022 le fichier solution GhostPDL.sln. Pour obtenir la librairie nous devons générer la solution complète (Menu Générer -> Générer la solution) ou Ctrl+Maj+B. Par défaut, la version debug sera générée. Les fichiers résultants se trouveront dans le dossier debugbin.

Avant d’exécuter le script Python, nous devons définir une variable d’environnement GSAPI_LIB. Dans Powershell nous procéderons comme cela :

PS > $env:GSAPI_LIB="C:\Users\gilles\Downloads\ghostscript-10.02.1\debugbin\gsdll64.dll"

Pour obtenir la liste des variables d’environnement dans Powershell nous utiliseront la ligne qui suit :

PS > dir env:

Dans l’invite de commandes (cmd) nous utiliserons l’instruction set :

> set GSAPI_LIBDIR=C:\Users\gilles\Downloads\ghostscript-10.02.1\debugbin\gsdll64.dll

Pour vérifier la présence de la variable nous utilisons sans argument l’instruction set.

Leave a Comment

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *