Savoir si un PDF a besoin d’OCR
Décider, dans une pile de PDF, quelles pages envoyer à la reconnaissance de caractères et lesquelles se lisent déjà.
Pas besoin d’IA
GratuitInstantanéVos données ne sortent pas
Le verdict
N0, parce que la décision tient dans deux nombres que le fichier porte déjà — les caractères rendus et les images tracées —, et que le prix de se tromper est asymétrique : payer l’OCR d’une page lisible coûte de l’argent, déclarer lisible une page scannée perd son contenu. Le seuil de 120 caractères existe pour cette raison, et la fiche montre ce qu’il change : une page scannée qui porte un en-tête de 28 caractères est un scan, là où la règle « y a-t-il du texte », avec un seuil bas, la déclare lisible. Montez à N1 le jour où vous trouvez, parmi les pages déclarées lisibles, du texte qui n’en est pas : il se note sur les paires de lettres de vos propres pages, et il attrape la page cassée que N0 laisse passer. Sachez ce qu’il coûte avant de monter : il refuse aussi les annexes financières, qui sont une part loin d’être négligeable d’un fonds documentaire, et elles partiront toutes à la reconnaissance de caractères.
Caractères et images comptés page par page, par une bibliothèque PDF
Pas besoin d’IA
Règle et algorithme classiqueN0GratuitInstantané
Python
"""
Trier une pile de PDF entre les pages qui ont besoin d'OCR et celles qui n'en
ont pas besoin.
Niveau N0. L'OCR et les modèles de vision se facturent à la page, et la
plupart des pages d'un fonds documentaire n'ont besoin ni de l'un ni de
l'autre : un PDF écrit par un logiciel de comptabilité, un traitement de
texte ou un pilote d'impression porte déjà son texte, exact, à côté de ses
instructions de tracé. Lire ce texte ne coûte rien et ne peut pas se tromper,
puisqu'aucune reconnaissance n'a eu lieu.
La décision se prend donc page par page, et non document par document. Un
contrat exporté d'un traitement de texte avec une page de signature scannée à
la fin, c'est un seul fichier qui contient deux sortes de pages, et envoyer
l'ensemble à l'OCR fait payer les pages qui se lisaient déjà.
Deux nombres par page suffisent, et c'est cette part qu'un modèle remplacerait
par une devinette : combien de caractères hors espaces la page rend, et
combien d'images elle trace. Du texte et pas d'image, c'est une page à
laisser tranquille. Une image et presque pas de texte, c'est un scan — et
« presque » compte, parce qu'une page scannée porte souvent un en-tête ou un
numéro de page en vrai texte, qu'une règle qui demande seulement « y a-t-il
du texte » range parmi les pages lisibles.
`pypdf` lit la page ici, `pdf.js` en JavaScript. Ni l'un ni l'autre n'est chargé de
reconnaître quoi que ce soit.
"""
from __future__ import annotations
import io
# A page of an ordinary document yields a few thousand characters; a header, a
# reference or a page number yields a few dozen. Anything under this, with an
# image on the page, is a scan wearing a label. Raise it for dense documents,
# lower it for forms.
MIN_CHARACTERS = 120
def triage_pages(pdf_bytes, *, min_characters: int = MIN_CHARACTERS, reader=None) -> dict:
"""
Say, page by page, which ones have to go to OCR.
The answer is a plan: `needs_ocr` is the list of page numbers to pay for,
`readable` the ones to read for free, `unreadable` the ones that carry
neither text nor image and that OCR would not help either.
`reader` is injected by the tests; in production it is `pypdf.PdfReader`.
"""
if reader is None: # imported here so the rest of this file needs nothing
from pypdf import PdfReader as reader
try:
document = reader(io.BytesIO(pdf_bytes))
pages = list(document.pages)
except Exception as error: # noqa: BLE001 - a broken file is an answer too
return {"pages": [], "needs_ocr": [], "readable": [], "unreadable": [],
"reason": f"this file could not be opened as a PDF: {error}"}
report = []
for number, page in enumerate(pages, start=1):
text = page.extract_text() or ""
characters = sum(1 for character in text if not character.isspace())
images = len(page.images)
report.append({
"page": number,
"characters": characters,
"images": images,
"verdict": _verdict(characters, images, min_characters),
})
return {
"pages": report,
"needs_ocr": [p["page"] for p in report if p["verdict"] == "scan"],
"readable": [p["page"] for p in report if p["verdict"] == "text"],
"unreadable": [p["page"] for p in report if p["verdict"] == "blank"],
"reason": None,
}
def _verdict(characters: int, images: int, min_characters: int) -> str:
# The order matters. A page with enough text is readable whatever it also
# draws; below that, an image is what OCR is for, and nothing at all is a
# page OCR would return empty — which is worth knowing before paying.
if characters >= min_characters:
return "text"
return "scan" if images else "blank"JavaScript
/**
* Trier une pile de PDF entre les pages qui ont besoin d'OCR et celles qui n'en
* ont pas besoin.
*
* Niveau N0. L'OCR et les modèles de vision se facturent à la page, et la
* plupart des pages d'un fonds documentaire n'ont besoin ni de l'un ni de
* l'autre : un PDF écrit par un logiciel de comptabilité, un traitement de
* texte ou un pilote d'impression porte déjà son texte, exact, à côté de ses
* instructions de tracé. Lire ce texte ne coûte rien et ne peut pas se tromper,
* puisqu'aucune reconnaissance n'a eu lieu.
*
* La décision se prend donc page par page, et non document par document. Un
* contrat exporté d'un traitement de texte avec une page de signature scannée à
* la fin, c'est un seul fichier qui contient deux sortes de pages, et envoyer
* l'ensemble à l'OCR fait payer les pages qui se lisaient déjà.
*
* Deux nombres par page suffisent, et c'est cette part qu'un modèle remplacerait
* par une devinette : combien de caractères hors espaces la page rend, et
* combien d'images elle trace. Du texte et pas d'image, c'est une page à
* laisser tranquille. Une image et presque pas de texte, c'est un scan — et
* « presque » compte, parce qu'une page scannée porte souvent un en-tête ou un
* numéro de page en vrai texte, qu'une règle qui demande seulement « y a-t-il
* du texte » range parmi les pages lisibles.
*
* `pdf.js` lit la page ici, `pypdf` en Python. Ni l'un ni l'autre n'est chargé de
* reconnaître quoi que ce soit.
*/
// A page of an ordinary document yields a few thousand characters; a header, a
// reference or a page number yields a few dozen. Anything under this, with an
// image on the page, is a scan wearing a label. Raise it for dense documents,
// lower it for forms.
export const MIN_CHARACTERS = 120;
/**
* Say, page by page, which ones have to go to OCR.
*
* The answer is a plan: `needs_ocr` is the list of page numbers to pay for,
* `readable` the ones to read for free, `unreadable` the ones that carry
* neither text nor image and that OCR would not help either.
*
* `load` is injected by the tests; in production it is pdf.js.
*/
export async function triagePages(pdfBytes, { minCharacters = MIN_CHARACTERS, load } = {}) {
// Imported here, and only here: nothing else in this file needs it.
const pdfjs = load ?? (await import('pdfjs-dist/legacy/build/pdf.mjs'));
const painted = new Set([
pdfjs.OPS.paintImageXObject,
pdfjs.OPS.paintInlineImageXObject,
pdfjs.OPS.paintImageMaskXObject,
pdfjs.OPS.paintImageXObjectRepeat,
]);
const report = [];
try {
const document = await pdfjs.getDocument({
data: new Uint8Array(pdfBytes), isEvalSupported: false,
}).promise;
for (let number = 1; number <= document.numPages; number += 1) {
/* eslint-disable no-await-in-loop -- pdf.js hands pages out one at a time */
const page = await document.getPage(number);
const content = await page.getTextContent();
const operators = await page.getOperatorList();
/* eslint-enable no-await-in-loop */
const text = content.items.map((item) => item.str ?? '').join('');
const characters = [...text].filter((character) => !/\s/u.test(character)).length;
const images = operators.fnArray.filter((code) => painted.has(code)).length;
report.push({ page: number, characters, images, verdict: verdictOf(characters, images, minCharacters) });
}
} catch (error) {
return { pages: [], needs_ocr: [], readable: [], unreadable: [],
reason: `this file could not be opened as a PDF: ${error.message}` };
}
const numbers = (kind) => report.filter((p) => p.verdict === kind).map((p) => p.page);
return {
pages: report,
needs_ocr: numbers('scan'),
readable: numbers('text'),
unreadable: numbers('blank'),
reason: null,
};
}
function verdictOf(characters, images, minCharacters) {
// The order matters. A page with enough text is readable whatever it also
// draws; below that, an image is what OCR is for, and nothing at all is a
// page OCR would return empty — which is worth knowing before paying.
if (characters >= minCharacters) return 'text';
return images ? 'scan' : 'blank';
}Essayer
Ce code ne peut pas tourner dans un navigateur. Les sorties ci-dessous sont calculées à la construction du site, en exécutant le vrai code.
Cinq documents, et le plan de tri que l’extrait en tire. Les sorties sont celles du code, exécutées à la construction du site : rien n’est écrit à la main.
Un contrat exporté d’un traitement de texte
Donné
Deux pages de texte, aucune image : rien à payer.
Ce qu’il renvoie
0 page(s) à envoyer à l’OCR, 2 à lire gratuitement
Deux pages de texte, aucune image : rien à payer.
Le détail
Page Caractères Images Décision 1 224 0 se lit 2 224 0 se lit Un document entièrement scanné
Donné
Deux pages d’image, aucun caractère : les deux partent à l’OCR.
Ce qu’il renvoie
2 page(s) à envoyer à l’OCR, 0 à lire gratuitement
Deux pages d’image, aucun caractère : les deux partent à l’OCR.
Le détail
Page Caractères Images Décision 1 0 1 à l’OCR 2 0 1 à l’OCR Un contrat dont une seule page est scannée
Donné
Trois pages, une seule à payer : c’est ce que le tri par document fait perdre.
Ce qu’il renvoie
1 page(s) à envoyer à l’OCR, 2 à lire gratuitement
Trois pages, une seule à payer : c’est ce que le tri par document fait perdre.
Le détail
Page Caractères Images Décision 1 224 0 se lit 2 0 1 à l’OCR 3 224 0 se lit Une page scannée qui porte un en-tête en vrai texte
Donné
Vingt-huit caractères et une image : une règle qui demande « y a-t-il du texte » la déclarerait lisible.
Ce qu’il renvoie
1 page(s) à envoyer à l’OCR, 0 à lire gratuitement
Vingt-huit caractères et une image : une règle qui demande « y a-t-il du texte » la déclarerait lisible.
Le détail
Page Caractères Images Décision 1 28 1 à l’OCR Une page dont la police n’a pas de table de caractères
Donné
Cent quatre-vingt-trois caractères, aucune image : rangée parmi les pages lisibles.
Ce qu’il renvoie
0 page(s) à envoyer à l’OCR, 1 à lire gratuitement
Cent quatre-vingt-trois caractères, aucune image : rangée parmi les pages lisibles.
Le détail
Page Caractères Images Décision 1 183 0 se lit Là, ça casseLa page rend « #$%&!*+,-./0123 » là où elle affiche un paragraphe, et le compte de caractères ne voit pas la différence : elle est déclarée lisible. C’est le point de rupture de ce niveau, et c’est ce que le niveau N1 attrape en notant les paires de lettres contre celles de vos pages sûres.Voir le niveau au-dessus
Ce que ça implique
- Vos données ne sortent pas
- Résultat toujours identique
- Se teste unitairement, cas par cas
- Dépend d’une bibliothèque
- Empreinte négligeable
Ce que cela ajoute à votre périmètre réglementaire
- Aucun périmètre spécifique ajouté : le document est lu dans votre processus et n’est transmis à personne
- Le tri décide de ce qui sortira ensuite : une page déclarée lisible ne partira pas chez un prestataire d’OCR, une page déclarée à l’OCR y partira
Point de rupture
Une couche texte illisible compte comme du texte : une page dont la police ne porte pas de table de caractères s’extrait en « #$%&!*+,-./0123 » là où elle affiche un paragraphe, et ses 183 caractères la rangent parmi les pages lisibles. Le témoin est dans le même test : une page de contrat ordinaire en rend 224 et reçoit exactement le même verdict.
Quand passer au niveau suivant
Vous relisez des pages déclarées lisibles et vous trouvez du texte qui n’est pas du texte : des suites de ponctuation, des lettres sans mots, une page entière qu’aucune recherche plein texte ne retrouve.
Les quatre options
- RecommandéN0Caractères et images comptés page par page, par une bibliothèque PDFGratuit · Instantané
- N1Paires de lettres, apprises sur vos propres pages lisiblesnégligeable · Instantané
- N2Niveau sans objet ici
- N3Niveau sans objet ici
Les autres options
N1Paires de lettres, apprises sur vos propres pages lisiblesnégligeable · Instantané
Paires de lettres, apprises sur vos propres pages lisibles
Modèle classique légerN1négligeableInstantané
Python
"""
Distinguer une couche texte utilisable d'une couche cassée, sur les pages qui
prétendent en avoir une.
Niveau N1. Le niveau N0 compte des caractères, et une couche texte faite des
mauvais caractères en compte tout autant : un PDF dont la police ne porte pas
de table de caractères, ou en porte une fausse, s'extrait en index de glyphes
lus comme de l'ASCII — « #$%&!*+,-./0123 » là où la page affiche un
paragraphe. N0 déclare cette page lisible, et elle ne l'est pas.
Distinguer la langue de cela, c'est le travail d'un modèle de caractères, et
c'est le plus petit qui existe : la fréquence de chaque paire de lettres.
« qu » et « es » sont fréquents en français, « q# » et « &! » ne le sont pas,
et une page se note à la ressemblance de ses paires avec celles des pages que
vous tenez déjà pour bonnes.
Rien n'est livré avec ce modèle. On donne à `fit` le texte de vos propres
pages lisibles — N0 vient de les lister —, si bien que la référence est faite
de vos documents, dans votre langue, avec votre vocabulaire. Le seuil vient du
même endroit : la note la plus basse du lot d'entraînement, avec une marge,
plutôt qu'un nombre choisi ici. `fit` refuse un lot vide : un modèle ajusté
sur rien déclarerait tout lisible, et c'est l'erreur coûteuse de cette fiche.
La marge, elle, est un réglage par défaut et non une constante. Une page du
lot d'entraînement passe son propre seuil par construction, donc elle ne dit
rien : le demi-nat vient de quatre pages de prose administrative française
retenues **hors** du lot — un avenant, un article du code civil, un accusé de
réception, un en-tête de facture —, qui notent entre −2,42 et −2,83 contre un
lot dont la pire page note −2,48. La plus éloignée est à 0,35 en dessous : à
trois dixièmes elle était refusée. Gardez quelques-unes de vos pages lisibles
hors de `fit`, notez-les, et déplacez ce nombre jusqu'à ce qu'elles passent
toutes : c'est le seul réglage qui dise quelque chose de vos documents.
Ce que cela ne sait pas faire, c'est lire. Une page de chiffres, un tableau de
montants, un formulaire à champs courts : ce sont de vrais textes qu'aucun
modèle de lettres n'appellera de la langue, et la note le dit. Il sépare les
lettres du bruit, pas le sens du non-sens.
"""
from __future__ import annotations
import math
# The letters this model counts. Anything else — digits, punctuation, symbols
# — becomes a single « other » class, which is exactly what a broken character
# map produces in quantity.
ALPHABET = "abcdefghijklmnopqrstuvwxyzàâäçéèêëîïôöùûüÿœæ "
OTHER = "#"
SYMBOLS = ALPHABET + OTHER
# How far below the worst training page a page may score before it is called
# unreadable.
#
# Half a nat per pair, and the number comes from pages that are NOT in the
# training set: four ordinary French administrative pages — an amendment, an
# article of the civil code, an acknowledgement letter, an invoice heading —
# score between −2.42 and −2.83 against a model fitted on six contract pages
# whose worst is −2.48. The furthest sits 0.35 below it, so three tenths would
# refuse it, and did. Half a nat clears it and still refuses an English page,
# 0.65 below, and a table of amounts, 1.66 below.
#
# It is a default, not a constant: it was measured on six training pages and
# four held-out ones, which is small. Keep a handful of your own readable pages
# out of `fit`, score them, and move this number until they all pass — that is
# the only calibration that says anything about your own documents.
MARGIN = 0.5
def normalise(text: str) -> str:
"""Lower case, and every symbol outside the alphabet folded into one."""
return "".join(c if c in ALPHABET else OTHER for c in text.lower())
def fit(pages) -> dict:
"""Count the letter pairs of pages you trust, and set the threshold from them."""
counts = {a: {b: 1 for b in SYMBOLS} for a in SYMBOLS} # add-one smoothing
for page in pages:
clean = normalise(page)
for first, second in zip(clean, clean[1:]):
counts[first][second] += 1
totals = {a: sum(row.values()) for a, row in counts.items()}
model = {"log_probability": {a: {b: math.log(counts[a][b] / totals[a]) for b in SYMBOLS}
for a in SYMBOLS}}
scores = [score(page, model) for page in pages]
if not scores:
# A model fitted on nothing would answer « readable » to everything,
# which is the expensive mistake of this entry: a page declared
# readable that is not one goes into the index empty, and nobody looks
# at it again.
raise ValueError("fit needs at least one page you have read yourself")
model["threshold"] = min(scores) - MARGIN
return model
def score(text: str, model: dict) -> float:
"""Mean log probability of the letter pairs, or -inf on a page too short."""
clean = normalise(text)
pairs = list(zip(clean, clean[1:]))
if not pairs:
return float("-inf")
table = model["log_probability"]
return sum(table[a][b] for a, b in pairs) / len(pairs)
def is_readable(text: str, model: dict) -> dict:
"""Say whether this text layer looks like the pages the model was fitted on."""
value = score(text, model)
return {"readable": value >= model["threshold"], "score": value,
"threshold": model["threshold"]}JavaScript
/**
* Distinguer une couche texte utilisable d'une couche cassée, sur les pages qui
* prétendent en avoir une.
*
* Niveau N1. Le niveau N0 compte des caractères, et une couche texte faite des
* mauvais caractères en compte tout autant : un PDF dont la police ne porte pas
* de table de caractères, ou en porte une fausse, s'extrait en index de glyphes
* lus comme de l'ASCII — « #$%&!*+,-./0123 » là où la page affiche un
* paragraphe. N0 déclare cette page lisible, et elle ne l'est pas.
*
* Distinguer la langue de cela, c'est le travail d'un modèle de caractères, et
* c'est le plus petit qui existe : la fréquence de chaque paire de lettres.
* « qu » et « es » sont fréquents en français, « q# » et « &! » ne le sont pas,
* et une page se note à la ressemblance de ses paires avec celles des pages que
* vous tenez déjà pour bonnes.
*
* Rien n'est livré avec ce modèle. On donne à `fit` le texte de vos propres
* pages lisibles — N0 vient de les lister —, si bien que la référence est faite
* de vos documents, dans votre langue, avec votre vocabulaire. Le seuil vient du
* même endroit : la note la plus basse du lot d'entraînement, avec une marge,
* plutôt qu'un nombre choisi ici. `fit` refuse un lot vide : un modèle ajusté
* sur rien déclarerait tout lisible, et c'est l'erreur coûteuse de cette fiche.
*
* La marge, elle, est un réglage par défaut et non une constante. Une page du
* lot d'entraînement passe son propre seuil par construction, donc elle ne dit
* rien : le demi-nat vient de quatre pages de prose administrative française
* retenues **hors** du lot — un avenant, un article du code civil, un accusé de
* réception, un en-tête de facture —, qui notent entre −2,42 et −2,83 contre un
* lot dont la pire page note −2,48. La plus éloignée est à 0,35 en dessous : à
* trois dixièmes elle était refusée. Gardez quelques-unes de vos pages lisibles
* hors de `fit`, notez-les, et déplacez ce nombre jusqu'à ce qu'elles passent
* toutes : c'est le seul réglage qui dise quelque chose de vos documents.
*
* Ce que cela ne sait pas faire, c'est lire. Une page de chiffres, un tableau de
* montants, un formulaire à champs courts : ce sont de vrais textes qu'aucun
* modèle de lettres n'appellera de la langue, et la note le dit. Il sépare les
* lettres du bruit, pas le sens du non-sens.
*/
// The letters this model counts. Anything else — digits, punctuation, symbols
// — becomes a single « other » class, which is exactly what a broken character
// map produces in quantity.
export const ALPHABET = "abcdefghijklmnopqrstuvwxyzàâäçéèêëîïôöùûüÿœæ ";
export const OTHER = '#';
const SYMBOLS = [...(ALPHABET + OTHER)];
// How far below the worst training page a page may score before it is called
// unreadable.
//
// Half a nat per pair, and the number comes from pages that are NOT in the
// training set: four ordinary French administrative pages — an amendment, an
// article of the civil code, an acknowledgement letter, an invoice heading —
// score between −2.42 and −2.83 against a model fitted on six contract pages
// whose worst is −2.48. The furthest sits 0.35 below it, so three tenths would
// refuse it, and did. Half a nat clears it and still refuses an English page,
// 0.65 below, and a table of amounts, 1.66 below.
//
// It is a default, not a constant: it was measured on six training pages and
// four held-out ones, which is small. Keep a handful of your own readable
// pages out of `fit`, score them, and move this number until they all pass —
// that is the only calibration that says anything about your own documents.
export const MARGIN = 0.5;
/** Lower case, and every symbol outside the alphabet folded into one. */
export function normalise(text) {
return [...text.toLowerCase()].map((c) => (ALPHABET.includes(c) ? c : OTHER)).join('');
}
/** Count the letter pairs of pages you trust, and set the threshold from them. */
export function fit(pages) {
const counts = new Map(SYMBOLS.map((a) => [a, new Map(SYMBOLS.map((b) => [b, 1]))])); // add-one
for (const page of pages) {
const clean = normalise(page);
for (let i = 0; i + 1 < clean.length; i += 1) {
const row = counts.get(clean[i]);
row.set(clean[i + 1], row.get(clean[i + 1]) + 1);
}
}
const logProbability = new Map();
for (const [a, row] of counts) {
const total = [...row.values()].reduce((sum, n) => sum + n, 0);
logProbability.set(a, new Map([...row].map(([b, n]) => [b, Math.log(n / total)])));
}
const model = { logProbability, threshold: -Infinity };
const scores = pages.map((page) => score(page, model));
if (scores.length === 0) {
// A model fitted on nothing would answer « readable » to everything, which
// is the expensive mistake of this entry: a page declared readable that is
// not one goes into the index empty, and nobody looks at it again.
throw new RangeError('fit needs at least one page you have read yourself');
}
model.threshold = Math.min(...scores) - MARGIN;
return model;
}
/** Mean log probability of the letter pairs, or -Infinity on a page too short. */
export function score(text, model) {
const clean = normalise(text);
if (clean.length < 2) return -Infinity;
let total = 0;
for (let i = 0; i + 1 < clean.length; i += 1) {
total += model.logProbability.get(clean[i]).get(clean[i + 1]);
}
return total / (clean.length - 1);
}
/** Say whether this text layer looks like the pages the model was fitted on. */
export function isReadable(text, model) {
const value = score(text, model);
return { readable: value >= model.threshold, score: value, threshold: model.threshold };
}Ce que ça implique
- Vos données ne sortent pas
- Résultat toujours identique
- Se teste unitairement, cas par cas
- Aucun fournisseur dans la boucle
- Empreinte négligeable
Ce que cela ajoute à votre périmètre réglementaire
- Aucun périmètre spécifique ajouté : le modèle est ajusté sur vos documents, dans votre infrastructure, et n’est transmis à personne
- Le lot d’entraînement est fait de vos pages : ce qu’il contient reste dans le modèle sous forme de fréquences de paires de lettres
Point de rupture
Il sépare les lettres du bruit, pas le sens du non-sens : un tableau de montants — « 12/01/2026 1 250,00 4 300,50 » — marque −4,10 pour un seuil à −2,98, comme la page cassée, alors que c’est du texte parfaitement lu. Témoin dans le même test : une lettre commerciale absente du lot d’entraînement marque −2,83.
Quand passer au niveau suivant
Vos documents ne sont pas de la prose — bordereaux, tableaux, annexes financières, formulaires à champs courts — et le score refuse des pages que personne n’a besoin de relire.
N2Niveau sans objet ici
Niveau sans objet ici
Petit modèle spécialisé auto-hébergéN2
Un classifieur d’images de page — scan contre page numérique — existe et fonctionne, mais il demande de rendre chaque page en image avant de décider, c’est-à-dire de payer le travail le plus lourd de la chaîne pour savoir s’il faut le faire. Les deux nombres que lit le niveau N0 sont dans le fichier, sans rendu, et ils répondent à la même question.
N3Niveau sans objet ici
Niveau sans objet ici
API de LLM généralisteN3
Envoyer l’image de chaque page à un modèle de vision pour savoir s’il faut l’envoyer à un modèle de vision est le seul niveau qui se contredit en une phrase : l’appel qu’on cherche à éviter est celui qu’il faut passer pour décider. Il ajoute en outre une sortie de données par page sur un fonds que le tri avait précisément pour but de garder chez vous.
Pour aller plus loin
- pypdf — la bibliothèque de lecture PDF en Python
- pdf.js — la bibliothèque de lecture PDF de Mozilla
- ISO 32000-1 (PDF 1.7), la spécification publiée par Adobe
- Lire le texte d’une page scannée — la fiche qui traite l’étape suivante
Source des chiffres
- pypdf, documentation de extract_text (consultée) : ce que la bibliothèque rend, et ce qu’elle ne reconstruit pas
- pdf.js, API getTextContent et getOperatorList (consultée) : les éléments de texte d’une page et les opérateurs de tracé, dont ceux qui peignent une image
- ISO 32000-1, section 9.10 (consultée) : la table ToUnicode, et ce qui arrive à l’extraction quand elle manque
Métadonnées
Information générale, tenue à jour au mieux. Ce n’est pas un avis juridique. Faites valider votre situation par un professionnel.