Article technique

Mise à l'échelle des pages PDF à 70% avec la bibliothèque PDF losLab

Les dimensions d'une page PDF sont fixées au moment de la création d'une page, vous ne pouvez donc pas simplement remettre à l'échelle (rescale) le contenu en place de la manière dont vous pourriez redimensionner une image. Le modèle de bibliothèque qui rend la réduction (shrinking) pratique est la capture et le redessin (capture-and-redraw) : soulevez le contenu de chaque page hors du document dans une poignée (handle), créez une nouvelle page vierge à la taille de support d'origine, puis redessinez le contenu capturé dans une boîte englobante (bounding box) réduite. L'espace blanc environnant devient la marge. À l'échelle de 70% sur une page A4, par exemple, 15% de la largeur tombe de chaque côté et la même fraction en haut et en bas, ce qui est exactement ce que produit l'arithmétique de bordure ci-dessous

Comment fonctionne CapturePage

CapturePage prend un numéro de page, promeut le contenu de cette page dans un objet de capture en mémoire et supprime la page de l'arborescence des pages (page tree) du document. Cette suppression est intentionnelle et est la raison pour laquelle la boucle sélectionne toujours la page 1 quel que soit l'index d'itération : une fois que la page 1 est capturée et supprimée, ce qui était la page 2 devient la nouvelle page 1, et ainsi de suite. Si vous incrémentez le sélecteur de page en même temps que le compteur de boucle, vous sauterez une page sur deux et vous vous retrouverez avec la moitié de la sortie attendue

La poignée de capture (capture handle) renvoyée par CapturePage n'est pas une référence de page ; c'est plus comme un instantané de contenu (content snapshot). Elle reste valide jusqu'à ce que vous appeliez DrawCapturedPage ou que vous la libériez explicitement. DrawCapturedPage prend cette poignée plus un rectangle de destination donné sous forme de décalage gauche (left offset), de décalage inférieur, de largeur et de hauteur, le tout en points. La bibliothèque met à l'échelle le contenu capturé pour l'adapter exactement à ce rectangle, en préservant le rapport hauteur/largeur (aspect ratio) uniquement si votre rectangle correspond aux proportions d'origine. Pour une mise à l'échelle uniforme, vous voulez que le rectangle soit de la taille d'origine multipliée par le facteur d'échelle, centré sur la page

Les calculs de centrage

Avec un facteur d'échelle de 70%, les 30% restants de chaque dimension sont répartis également entre les deux côtés. Ainsi, l'encart (inset) horizontal est pageWidth * (1.0 - 0.70) / 2, ce qui correspond à 15% de la largeur, et l'encart vertical suit la même formule en utilisant la hauteur de la page. Le rectangle de destination pour DrawCapturedPage commence alors à (horizBorder, vertBorder) et s'étend sur pageWidth - 2 * horizBorder par pageHeight - 2 * vertBorder. Cette arithmétique n'est pas spécifique à la bibliothèque ; c'est juste la géométrie consistant à ajuster un plus petit rectangle symétriquement à l'intérieur d'un plus grand

Une chose à noter : SetOrigin(1) place l'origine des coordonnées (coordinate origin) en haut à gauche (top-left) plutôt qu'en bas à gauche. Les valeurs de bordure que vous passez à DrawCapturedPage sont mesurées à partir de l'origine que vous définissez, donc si vous changez de mode d'origine entre le chargement et le dessin, le centrage sera décalé (off)

Exemple C#

Le code suivant traite chaque page de Pages.pdf via le cycle de capture et de redessin et écrit le résultat dans newpages.pdf. PDFL est l'objet wrapper ActiveX/COM ajouté au projet à partir de PDFlibDLL64.dll

private void ScalePages_Click(object sender, EventArgs e)
{
    File.Delete("newpages.pdf");

    double pageWidth, pageHeight, horizBorder, vertBorder;
    double scaleFactor = 0.70;
    int capturedPageId, ret;

    PDFL.LoadFromFile("Pages.pdf", "");
    PDFL.SetOrigin(1);

    int numPages = PDFL.PageCount();

    for (int i = 1; i <= numPages; i++)
    {
        // Always select page 1: CapturePage removes the page, so page 2
        // becomes page 1 on the next iteration.
        PDFL.SelectPage(1);

        pageWidth  = PDFL.PageWidth();
        pageHeight = PDFL.PageHeight();

        horizBorder = pageWidth  * (1.0 - scaleFactor) / 2;
        vertBorder  = pageHeight * (1.0 - scaleFactor) / 2;

        capturedPageId = PDFL.CapturePage(1);

        PDFL.NewPage();
        PDFL.SetPageDimensions(pageWidth, pageHeight);

        ret = PDFL.DrawCapturedPage(
            capturedPageId,
            horizBorder, vertBorder,
            pageWidth  - 2 * horizBorder,
            pageHeight - 2 * vertBorder);
    }

    PDFL.SaveToFile("newpages.pdf");
}

Exemple Delphi

La version Delphi utilise TPDFlib directement plutôt qu'à travers la couche COM, mais la séquence d'appels est identique. Une différence pratique est la garde du fichier de sortie (output file guard) : FileExists plus DeleteFile au lieu de File.Delete, car SaveToFile échouera si la destination est verrouillée par une exécution précédente toujours ouverte dans une visionneuse

procedure TForm1.ScalePagesClick(Sender: TObject);
var
  PDFLib: TPDFlib;
  pageWidth, pageHeight, horizBorder, vertBorder: Double;
  scaleFactor: Double;
  capturedPageId, ret, numPages, i: Integer;
begin
  if FileExists('newpages.pdf') then
    DeleteFile('newpages.pdf');

  scaleFactor := 0.70;

  PDFLib := TPDFlib.Create;
  try
    PDFLib.LoadFromFile('Pages.pdf', '');
    PDFLib.SetOrigin(1);

    numPages := PDFLib.PageCount();

    for i := 1 to numPages do
    begin
      PDFLib.SelectPage(1);

      pageWidth  := PDFLib.PageWidth();
      pageHeight := PDFLib.PageHeight();

      horizBorder := pageWidth  * (1.0 - scaleFactor) / 2;
      vertBorder  := pageHeight * (1.0 - scaleFactor) / 2;

      capturedPageId := PDFLib.CapturePage(1);

      PDFLib.NewPage();
      PDFLib.SetPageDimensions(pageWidth, pageHeight);

      ret := PDFLib.DrawCapturedPage(
        capturedPageId,
        horizBorder, vertBorder,
        pageWidth  - 2 * horizBorder,
        pageHeight - 2 * vertBorder);
    end;

    PDFLib.SaveToFile('newpages.pdf');
  finally
    PDFLib.Free;
  end;
end;

Ce que le facteur d'échelle contrôle réellement

La valeur 0.70 signifie ici que le contenu rendu occupe 70% de chaque dimension de page, non pas que le fichier représente 70% de sa taille d'octet d'origine. La taille du fichier après cette opération dépend de la complexité du contenu d'origine ; une page avec de grandes images ne rétrécira pas proportionnellement car les données de pixels sont redessinées à la même résolution dans une zone plus petite. Si la compression au niveau des octets est l'objectif, la bonne approche est LinearizeFile ou un ré-enregistrement avec compression de flux (stream compression), et non une mise à l'échelle géométrique

Le chiffre de 70% n'est pas non plus une limite stricte (hard limit). N'importe quelle valeur entre 0.0 et 1.0 fonctionne, et des valeurs supérieures à 1.0 agrandissent le contenu au-delà de la limite de page d'origine, ce qui coupe (clips) au bord de la boîte de support (media box) à moins que vous n'augmentiez également les dimensions de la page. Les documents de tailles mixtes sont gérés naturellement car PageWidth et PageHeight sont interrogés par page avant le calcul de la bordure, de sorte qu'un document où les pages impaires (odd pages) sont A4 et les pages paires (even pages) sont A3 produira une sortie correctement centrée sur chaque taille de page sans aucun cas spécial (special casing)

Où les choses peuvent mal tourner

Deux modes de défaillance (failure modes) apparaissent dans la pratique. Le premier est un fichier de sortie laissé ouvert dans une visionneuse PDF lors d'une exécution précédente : SaveToFile échouera ou écrira zéro octet selon la plateforme, et la nouvelle sortie n'atterrit jamais. La garde de suppression de fichier (file-delete guard) en haut de la fonction gère cela pour le développement, mais dans un pipeline de production, écrire dans un chemin temporaire et renommer en cas de succès est plus sûr

Le second est l'inadéquation (mismatch) du nombre de pages. Étant donné que CapturePage supprime les pages du document au fur et à mesure de leur traitement, le nombre que vous lisez depuis PageCount() avant la boucle est la limite correcte sur laquelle itérer. Appeler PageCount() à l'intérieur de la boucle renverrait un nombre décroissant à chaque passage et se terminerait plus tôt, laissant les dernières pages non traitées. La variable de boucle dans les exemples ne sert que de compteur d'itérations restantes ; elle n'est jamais utilisée pour sélectionner une page, car la page à sélectionner est toujours 1 pour la raison expliquée précédemment

Les appels de manipulation de page (page manipulation calls) présentés ici, y compris CapturePage, DrawCapturedPage et SetPageDimensions, font partie de la bibliothèque PDF losLab pour Delphi, C#, VB.NET et C++