Article technique

Assembler des Scans Recto-Verso en Delphi : Fusion PDF

CollateDocumentsEx dans la bibliothèque PDF Delphi PDFlibPas fusionne plusieurs documents ouverts en un seul document entrelacé. Elle ajoute GroupSize pages de chaque source à chaque tour, accepte une liste de plages de pages par source, et traite une plage descendante telle que 3-1 comme une inversion de cette source. Un seul appel transforme une pile recto et une pile verso inversée en ordre de lecture

Le scénario derrière cette API est banal et extrêmement courant. Un scanner à feuilles avec un chemin recto seul fait défiler toute la pile face vers le bas, puis l opérateur retourne la pile et la fait défiler à nouveau. On se retrouve avec deux PDF : les rectos dans l ordre, les versos dans l ordre inverse. Le fichier que l utilisateur souhaite est un seul document, page 1 recto, page 1 verso, page 2 recto, et ainsi de suite. Cet article traite du problème d ordonnancement et du piège de duplication de ressources qui se cache dessous. Si votre préoccupation porte sur le débit brut de concaténation, voir la fusion PDF rapide par décalage de références au niveau octet ; si les entrées sont trop volumineuses pour tenir entièrement en mémoire, voir la fusion et la division de PDF de plusieurs gigaoctets avec accès direct

Le scanner produit deux piles, dont une à l envers

Assembler n est pas fusionner. Une fusion concatène des plages de pages ; un assemblage les entrelace, et le motif d entrelacement est une propriété du périphérique physique qui a produit l entrée. Se tromper sur le motif ne rend pas le fichier légèrement incorrect, il devient illisible : une page sur deux appartient à une feuille différente. Trois variables décrivent presque tous les cas réels : combien de sources participent à la rotation, combien de pages proviennent de chaque source par tour, et si une source doit être lue à l envers. CollateDocuments couvre les deux premiers points avec un simple tableau de handles de document et un entier GroupSize. CollateDocumentsEx ajoute le troisième en acceptant une liste de plages de pages séparées par des points-virgules, un segment par source, où un segment vide signifie toutes les pages de cette source et une plage descendante l inverse. Les deux fonctions ajoutent à la fin du document actuellement sélectionné et renvoient 1 en cas de succès, 0 en cas de rejet

Pourquoi l assemblage naïf multiplie-t-il la taille du fichier ?

Parce que la table de correspondance qui associe les numéros d objet source aux numéros d objet cible est reconstruite à chaque appel de copie, et tout ce qui est accessible depuis plus d un lot est importé une fois par lot. Dans PDFlibPas, TPDFDocument.CopyPagesFromDoc réinitialise sa NewIndObjList au début de chaque invocation. Cette liste est la seule mémoire dont dispose le copieur sur ce qu il a déjà transféré. Appelez-la une fois avec une plage de dix pages et une police partagée par les dix pages est intégrée une seule fois. Appelez-la dix fois avec une page à chaque fois et cette même police est intégrée dix fois. Cela pèse bien plus lourd pour des scans que pour des documents texte, car une page scannée est un unique gros XObject image et les objets partagés sont ceux qui pèsent réellement : un profil ICC intégré, une chaîne /DecodeParms partagée, un tampon ou filigrane XObject de formulaire appliqué à chaque feuille, la police de la couche de texte OCR. La façon évidente d écrire un assemblage en rotation est une boucle sur les tours, et cette boucle est exactement le cas pathologique

// Do not do this. Each CopyPageRanges call rebuilds the import map,
// so anything the two sources share internally is imported once per
// round instead of once per source.
var
  RoundIndex: Integer;
begin
  for RoundIndex := 1 to 12 do
  begin
    PDF.CopyPageRanges(Fronts, IntToStr(RoundIndex));
    PDF.CopyPageRanges(Backs, IntToStr(13 - RoundIndex));
  end;
end;

Douze tours, deux sources, vingt-quatre tables d importation. Rien ne vous avertit. L ordre des pages est correct, chaque page s affiche, et le seul symptôme est un fichier plusieurs fois plus volumineux que la somme de ses entrées. Sur un lot de 300 pages, le multiplicateur n est pas une erreur d arrondi, c est la différence entre une archive qui tient dans le budget de rétention et une qui n y tient pas

Importer une seule fois, puis réordonner l arbre de pages

La correction consiste à séparer les deux préoccupations que la boucle naïve avait fusionnées. La copie décide quels objets existent dans la cible ; l ordonnancement décide où se placent les pages dans l arbre de pages. CollateDocumentsEx copie chaque source exactement une fois, en un seul appel CopyPagesFromDoc couvrant l intégralité de cette source, de sorte que chaque source obtient une seule table d importation et les ressources partagées ne sont écrites qu une fois. Ce n est qu une fois toutes les sources arrivées que l entrelacement a lieu, et il se fait entièrement via TPDFPageTree.MovePage

Les déplacements de page sont gratuits, dans le sens qui compte ici. La norme ISO 32000-1 §7.7.3 définit l arbre de pages comme une structure équilibrée de dictionnaires de nœuds dont les tableaux /Kids contiennent des références indirectes, avec /Count portant le total de feuilles à chaque nœud. Déplacer une page signifie retirer une référence indirecte d un tableau /Kids, l insérer dans un autre, ajuster les deux valeurs /Count, et repointer le /Parent de la page. Aucun flux de contenu n est touché, aucune ressource n est dupliquée, aucun objet n est créé. L objet page conserve son numéro d objet, ce qui explique aussi pourquoi les numéros d objet restent stables comme dans le remplacement de pages qui préserve les numéros d objet. Il y a un détail supplémentaire qu un déplacement de page naïf traite mal et que MovePage gère correctement. La norme ISO 32000-1 §7.7.3.4 permet à /Resources, /MediaBox, /CropBox et /Rotate d être hérités d un nœud ancêtre plutôt que déclarés sur la page. Une page qui hérite de ses ressources du nœud A puis qui est déplacée sous le nœud B hérite silencieusement d autre chose, ou de rien du tout. MovePage résout donc la valeur héritée et l écrit sur le dictionnaire de la page avant la relocalisation, de sorte que la page emporte ses propres attributs à travers le déplacement

Que fait réellement la passe de réordonnancement ?

Elle exécute un tri par sélection contre une sémantique d insertion à un emplacement donné. L ordre relatif au bloc souhaité est calculé d abord : parcourir les sources en rotation, prendre jusqu à GroupSize indices de chacune, ignorer une source épuisée, répéter jusqu à ce que toutes les pages soient placées. Cela produit une permutation sur le bloc ajouté. L appliquer est la partie délicate, car MovePage est une insertion, pas un échange, donc chaque déplacement décale de un tout ce qui se trouve entre l ancienne et la nouvelle position

L implémentation conserve un tableau Current modélisant où se trouve actuellement chaque page ajoutée, balaie vers l avant depuis la position K pour trouver la page qui devrait se trouver en K, effectue le déplacement, puis fait glisser les entrées du tableau pour refléter ce que le déplacement a fait à l arbre. C est O(n au carré) en opérations de tableau et zéro en copies d objet, ce qui est le bon compromis pour cette charge de travail : un assemblage de 500 pages représente un quart de million de permutations d entiers et pas un seul octet de données image dupliquées. Les plages descendantes et les pages répétées ne nécessitent aucun traitement particulier dans cette passe car PLParsePageRangeList est appelée avec le tri désactivé et les doublons autorisés, de sorte que l ordre demandé survit intact à l analyse

Plages inversées et la fusion recto-verso en un seul appel

Avec l inversion exprimée comme une plage, le cas du double passage sur scanner à plat se réduit à un seul appel. Les rectos veulent leur ordre naturel et les versos veulent 12-1, et le premier segment vide avant le point-virgule indique que la première source apporte toutes ses pages

var
  PDF: TPDFlib;
  Target, Fronts, Backs: Integer;
begin
  PDF := TPDFlib.Create;
  try
    Target := PDF.NewDocument;
    if PDF.LoadFromFile('fronts.pdf', '') <> 1 then
      Exit;
    Fronts := PDF.SelectedDocument;
    if PDF.LoadFromFile('backs.pdf', '') <> 1 then
      Exit;
    Backs := PDF.SelectedDocument;
    PDF.SelectDocument(Target);
    // fronts 1..12 in order, backs scanned in reverse: F1 B12 F2 B11 ...
    if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 1 then
      PDF.SaveToFile('duplex.pdf');
  finally
    PDF.Free;
  end;
end;

Deux comportements de cet extrait méritent d être précisés explicitement. Les pages assemblées sont ajoutées au document sélectionné, donc un document créé avec NewDocument apporte sa page vierge initiale avant elles, et vous devez la supprimer si vous ne la voulez pas. Et les sources peuvent être inégales : avec GroupSize à 2 sur une source de trois pages et une de cinq pages, les tours donnent A1 A2 B1 B2, puis A3 B3 B4 lorsque A est presque épuisée, puis B5 seule, car une source épuisée est simplement ignorée plutôt que complétée

Annulation, champs de formulaire, et ce qui ne suit pas

Chaque argument est validé avant que la cible ne soit touchée. Un handle de document manquant, le document sélectionné listé comme sa propre source, un GroupSize inférieur à un, un nombre de segments qui ne correspond pas au nombre de sources, une plage nommant une page que la source ne possède pas : tout cela renvoie 0 sans que la cible soit modifiée. L échec en cours de copie est le cas le plus délicat, et il est géré via le DeletePages public plutôt que le PageTree.DeletePages brut. La raison est précise. La copie s exécute avec MergeFormData activé, donc les champs de formulaire de la source ont déjà été ajoutés au tableau /AcroForm /Fields de la cible au moment où une source ultérieure échoue. Supprimer les pages au niveau de l arbre de pages retirerait les pages porteuses de widgets et laisserait ces références de champ pendantes ; le chemin public dissocie le champ, l esquisse et les références de fil d article en même temps que les pages

if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 0 then
  // Nothing was appended and the target is byte-identical to before.
  // 412 is the copy failure; 0 means the arguments were rejected
  // during validation, before any page was touched.
  Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));

Soyez honnête avec vos utilisateurs quant aux limites. L assemblage transporte les pages, leurs annotations et leurs champs de formulaire, et il fusionne la liste des champs AcroForm, le tableau d ordre de calcul et le dictionnaire de ressources par défaut. Il n emporte pas les signets source : l arbre des esquisses d une pile de rectos scannée est presque toujours vide, donc rien n est perdu dans le cas recto-verso, mais si vous assemblez deux documents rédigés, leurs esquisses restent en arrière et vous devez reconstruire la navigation vous-même. Les destinations nommées qui n existaient que dans le catalogue source sont dans la même situation. Anticipez cela avant de promettre à un client un assemblage sans perte

PDFlibPas livre les fonctions d assemblage avec le reste de sa surface d assemblage de pages, de sorte que le flux de travail du scanner, l extraction basée sur des plages et les chemins pour gros fichiers se trouvent tous derrière un seul composant en Delphi et C++Builder. La référence complète de l API et une version d essai sont sur la page produit losLab Delphi PDF library