Automatiser la conversion de documents en Rust avec unoserver
Convertissez un document ou un dossier en PDF en faisant appeler le client
unoconvert d’unoserver par Rust. La CLI ci-dessous conserve les noms complets des
fichiers d’entrée, refuse les répertoires de sortie existants et renvoie un code de sortie non nul
si le traitement d’un fichier sélectionné échoue. LibreOffice reste actif dans un processus
accessible sur l’interface de bouclage, tandis que Rust traite les fichiers successivement.
Installer unoserver et les dépendances Rust
Utilisez Linux avec les composants Writer, Calc et Impress de LibreOffice, le pont Python UNO
correspondant et la prise en charge de venv par Python installés. Sur
Debian/Ubuntu, ils sont fournis par les paquets libreoffice,
python3-uno et python3-venv. Installez une chaîne d’outils Rust
maintenue avec Cargo en suivant les
instructions d’installation de Rust.
Les versions testées étaient Rust/Cargo 1.98.1, Python 3.14.7, LibreOffice 26.8.0.3 et unoserver 3.7 ;
la version du compilateur indique l’environnement de test, et cet exemple n’exige pas cette
version exacte.
Créez un projet sans dépendances Cargo. La table vide [workspace] en fait une
racine d’espace de travail autonome,
de sorte que la configuration n’ajoute aucun membre au manifeste d’un projet englobant. Le
sous-shell laisse le répertoire courant de votre terminal inchangé, et
mkdir refuse un projet existant :
(
command -v cargo >/dev/null &&
command -v rustc >/dev/null &&
mkdir doc_converter_rust &&
cd doc_converter_rust &&
mkdir src &&
cat > Cargo.toml <<'TOML'
[package]
name = "doc_converter_rust"
version = "0.1.0"
edition = "2021"
[workspace]
TOML
)
Depuis le répertoire où vous avez créé le projet, exécutez cd doc_converter_rust dans
chaque terminal. Utilisez ce répertoire de projet pour les commandes suivantes. Installez le
client et le serveur aux versions fixées dans l’environnement virtuel de ce projet :
python3 -m venv --system-site-packages .venv &&
.venv/bin/python -c 'import uno' &&
.venv/bin/python -m pip install unoserver==3.7 &&
.venv/bin/unoconvert --version
L’importation d’UNO doit réussir avant de poursuivre l’installation. Si elle échoue, installez le
pont pour cet interpréteur Python précis, puis réexécutez ce bloc ; il réutilise l’environnement
virtuel partiel sans remplacer les fichiers de votre projet. Un environnement
pipx isolé par défaut peut ne pas avoir accès au pont. Le
guide d’installation d’unoserver
explique cette association Python/LibreOffice et indique que macOS et Windows ne sont pas pris en
charge ; ce guide couvre Linux.
Dans un terminal, lancez le processus de travail avec les deux interfaces liées à l’interface de bouclage et des ports distincts :
.venv/bin/python - <<'PY' &&
import socket
for port in (2003, 2002):
with socket.socket() as listener:
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
listener.bind(('127.0.0.1', port))
except OSError as error:
raise SystemExit(f'Loopback port {port} is unavailable: {error}')
PY
.venv/bin/unoserver --interface 127.0.0.1 --port 2003 \
--uno-interface 127.0.0.1 --uno-port 2002 --conversion-timeout 120
Gardez ce terminal ouvert. Après le démarrage, vérifiez le processus depuis le second terminal :
.venv/bin/unoping --host 127.0.0.1 --port 2003
Vous devriez obtenir des informations de version, notamment unoserver 3.7 et LibreOffice. La vérification des ports refuse les services déjà à l’écoute avant de démarrer LibreOffice. Si elle échoue, choisissez une paire de ports libres et adaptez la sonde, les options du serveur et le port du client Rust ci-dessous. Ne vous connectez pas à un service déjà à l’écoute qui n’a pas été identifié. La vérification libère ses sockets avant le démarrage ; si un service se met à l’écoute pendant cet intervalle et que le démarrage échoue, arrêtez le processus enfant LibreOffice restant de ce processus de travail avant de réessayer. Les deux interfaces sont dépourvues d’authentification, et le délai maximal de conversion met fin à LibreOffice et arrête le serveur si une conversion se bloque, comme décrit dans la documentation du client et du serveur. Gardez les deux ports privés. Appuyez sur Ctrl+C dans le terminal du processus de travail pour l’arrêter lorsque vous avez terminé.
Intégrer Rust à unoserver
Enregistrez cette CLI complète dans src/main.rs. Elle accepte un fichier ou un
répertoire et exige un nouveau répertoire de sortie. Chaque PDF conserve le nom complet du fichier
d’entrée, suivi de .pdf, de sorte que report.doc et
report.docx ne peuvent pas entrer en conflit. Les chemins du système de fichiers
restent des chemins natifs au lieu d’être convertis en chaînes avec perte d’information pour les
arguments du processus.
use std::error::Error;
use std::ffi::OsStr;
use std::fs::{self, DirBuilder, File};
use std::io::{self, Read};
use std::os::unix::fs::DirBuilderExt;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn supported(path: &Path) -> bool {
let extension = path.extension().and_then(OsStr::to_str).unwrap_or("").to_ascii_lowercase();
matches!(extension.as_str(), "doc" | "docx" | "odt" | "rtf" | "txt" |
"ppt" | "pptx" | "odp" | "xls" | "xlsx" | "ods" | "csv")
}
fn convert_one(input: &Path, output: &Path) -> Result<()> {
let candidate = output.join(".conversion.pdf");
let conversion = (|| -> Result<()> {
let status = Command::new("unoconvert")
.args(["--host", "127.0.0.1", "--port", "2003", "--host-location", "local"])
.arg(input).arg(&candidate).status()?;
if !status.success() {
return Err(io::Error::other("unoconvert failed").into());
}
let mut header = [0; 5];
File::open(&candidate)?.read_exact(&mut header)?;
if &header != b"%PDF-" {
return Err(io::Error::other("conversion did not produce a PDF").into());
}
let mut filename = input.file_name().ok_or_else(|| io::Error::other("missing filename"))?.to_os_string();
filename.push(".pdf");
// Same-filesystem publication must fail rather than overwrite a destination.
fs::hard_link(&candidate, output.join(filename))?;
Ok(())
})();
if candidate.exists() {
fs::remove_file(&candidate)?;
}
conversion
}
fn run() -> Result<()> {
let args: Vec<_> = std::env::args_os().skip(1).collect();
if args.len() != 2 {
return Err(io::Error::other("Usage: doc_converter_rust <input-file-or-directory> <new-output-directory>").into());
}
let source = PathBuf::from(&args[0]);
let metadata = fs::symlink_metadata(&source)?;
let mut inputs = Vec::new();
if metadata.is_file() && supported(&source) {
inputs.push(source.canonicalize()?);
} else if metadata.is_dir() {
for entry in fs::read_dir(&source)? {
let entry = entry?;
if entry.file_type()?.is_file() && supported(&entry.path()) {
inputs.push(entry.path().canonicalize()?);
}
}
} else {
return Err(io::Error::other("use a supported regular file or directory, not a symlink").into());
}
inputs.sort();
if inputs.is_empty() {
return Err(io::Error::other("no supported documents found").into());
}
let output = PathBuf::from(&args[1]);
DirBuilder::new().mode(0o700).create(&output)?;
let output = output.canonicalize()?;
let mut failures = 0;
for input in inputs {
match convert_one(&input, &output) {
Ok(()) => println!("Converted {}", input.display()),
Err(_) => {
failures += 1;
eprintln!("Conversion failed for {}", input.display());
}
}
}
if failures > 0 {
return Err(io::Error::other(format!("{failures} conversion(s) failed")).into());
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Document conversion failed: {error}");
ExitCode::FAILURE
}
}
}
Avec un véritable fichier DOCX au chemin d’entrée, exécutez :
PATH="$PWD/.venv/bin:$PATH" cargo run -- /absolute/path/example.docx new-pdfs
Vous devriez obtenir une ligne Converted indiquant le fichier d’entrée,
et le fichier de sortie new-pdfs/example.docx.pdf devrait être présent. Ouvrez ce PDF et comparez son texte, son nombre de pages et sa mise
en page avec la source. La vérification %PDF- contrôle uniquement
l’en-tête ; elle ne peut pas prouver que chaque page ou objet intégré a été conservé lors de la
conversion. LibreOffice peut aussi détecter du texte renommé .docx ;
un fichier renommé ne constitue donc pas un test fiable d’échec de conversion.
Exécutez la CLI et le serveur sous le même compte à privilèges restreints, avec accès au même
système de fichiers, comme l’exige --host-location local. Le système de fichiers de sortie
doit prendre en charge les liens physiques ; la création du lien
final échoue si ce nom de fichier existe déjà.
Convertir des dossiers entiers par lot
Le même exécutable traite un dossier par lot, sans récursion et sans lancer un nombre illimité de tâches :
PATH="$PWD/.venv/bin:$PATH" cargo run -- /absolute/path/documents new-batch-pdfs
Il ignore les sous-répertoires, les liens symboliques et les extensions non prises en charge, tente de convertir les fichiers pris en charge dans l’ordre de tri, conserve les résultats réussis et se termine avec un code d’échec si une conversion échoue. Une extension de fichier n’est qu’un filtre initial, pas une preuve que le fichier est valide ou sûr. Ne laissez pas d’autres processus modifier les répertoires d’entrée ou de sortie pendant un traitement.
Pour report.doc et report.docx, vous devriez obtenir les
fichiers distincts report.doc.pdf et report.docx.pdf.
Si la conversion d’un autre fichier sélectionné échoue, ces PDF réussis sont conservés, le nom du
fichier en échec apparaît dans stderr et le traitement par lot se termine avec le code 1.
Réessayez avec un nouveau répertoire de sortie après avoir corrigé le fichier d’entrée ou le
processus de travail ; répéter la commande avec l’ancien répertoire échoue sans remplacer ses PDF.
Gérer les erreurs proprement
- Une entrée manquante, une sélection vide ou un répertoire de sortie existant provoque un échec visible.
- Une conversion échouée ne publie pas son PDF partiel. Vérifiez le code de sortie non nul du traitement par lot, même si certaines conversions précédentes ont réussi.
- Un dépassement du délai maximal côté serveur arrête le processus de travail. Redémarrez-le avant de réessayer et limitez le nombre de tentatives.
- Le client n’est pas un superviseur : ajoutez un délai maximal pour l’ensemble du traitement qui met fin à la CLI et à son processus enfant. Arrêter uniquement le client n’annule pas nécessairement le travail déjà en cours dans LibreOffice.
- Les diagnostics locaux peuvent inclure des chemins ou des détails sur les documents. Conservez-les dans des journaux à accès restreint plutôt que de renvoyer les erreurs brutes du convertisseur via une API web.
La CLI fournie cible un seul processus de travail. Si une application met les téléversements en file d’attente, gardez ce processus derrière les contrôles d’authentification et de stockage de l’application ; exposer XML-RPC permet aux appelants de les contourner. Pour des processus de travail supplémentaires, configurez des profils LibreOffice distincts et des paires de ports XML-RPC/UNO sans chevauchement, puis acheminez explicitement chaque traitement au lieu de démarrer un serveur pour chaque conversion.
Aperçu des formats pris en charge
La prise en charge effective dépend des composants et des filtres LibreOffice installés. Commencez par cette sélection prudente et vérifiez des documents représentatifs :
| Catégorie | Exemples d’entrées | Sortie de cette CLI |
|---|---|---|
| Traitement de texte | DOCX, DOC, ODT, RTF, TXT | |
| Feuilles de calcul | XLSX, XLS, ODS, CSV | |
| Présentations | PPTX, PPT, ODP |
Installez les polices requises et examinez la pagination, les formules, les objets intégrés et le
rendu. Un processus réussi et une vérification de l’en-tête du fichier
%PDF- ne prouvent pas la fidélité visuelle et ne valident pas une signature
numérique.
Pour la conversion de documents via un service géré, consultez le Robot 🤖 /document/convert (English) de Transloadit.
