Articol tehnic

Hyperlinkuri HotPDF în Delphi: adnotări PrintHyperlink

Hyperlinkurile din PDF sunt adnotări URI: un dreptunghi care acoperă o zonă de pagină și care, la clic, îi spune vizualizatorului să deschidă o adresă. Adnotarea și textul de sub ea sunt obiecte complet independente. Metoda PrintHyperlink din HotPDF le împachetează pe amândouă într-un singur apel, desenând textul și calculând dreptunghiul adnotării din metricile textului randat. Acea comoditate ascunde un detaliu pe care merită să îl înțelegeți înainte de a scrie cod de producție. Și nu este toată povestea: AddURILink plasează o zonă clicabilă peste conținut desenat de dumneavoastră, iar AddGoToLink se ocupă de navigarea internă — ambele tratate mai jos

Cum funcționează PrintHyperlink

PrintHyperlink stă pe THPDFPage și primește patru argumente: coordonatele X și Y (în puncte, cu originea în stânga jos și Y crescând în sus), șirul de etichetă de desenat și adresa țintă. Pe plan intern apelează TextOut cu culoarea curentă de hyperlink, apoi calculează imediat dreptunghiul adnotării din TextWidth și TextHeight, la metricile curente ale fontului. Asta înseamnă că fontul și dimensiunea trebuie setate înainte de apel și nu trebuie să se schimbe între desenarea etichetei și plasarea adnotării, pentru că amândouă se rezolvă în același apel

Anatomia unui singur apel HotPDF PrintHyperlink care scrie două obiecte PDF independente: glifele vizibile ale etichetei, desenate de TextOut, și dreptunghiul unei adnotări de legătură URI, calculat din TextWidth și TextHeight
Glifele etichetei și dreptunghiul URI sunt obiecte PDF separate, motiv pentru care fontul și culoarea de hyperlink trebuie să fie stabilite înainte ca un singur apel să le scrie pe amândouă

Culoarea implicită este clBlue. SetRGBHyperlinkColor o schimbă doar pentru apelurile următoare; nu actualizează retroactiv adnotările deja scrise. Dacă aveți nevoie de culori diferite pentru grupuri de legături diferite pe aceeași pagină, apelați SetRGBHyperlinkColor înaintea fiecărui grup și resetați-o după

Iată un document minimal care scrie trei legături cu două culori diferite:

procedure CreateLinkedReport(const FileName: string);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.BeginDoc;

    Pdf.CurrentPage.SetFont('Arial', [], 11);

    // Albastru implicit pentru legăturile informative
    Pdf.CurrentPage.TextOut(50, 750, 0, 'Reference links:');
    Pdf.CurrentPage.PrintHyperlink(50, 720, 'Product page', 'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');
    Pdf.CurrentPage.PrintHyperlink(50, 695, 'Online manual', 'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');

    // Roșu pentru legătura de acțiune
    Pdf.CurrentPage.SetRGBHyperlinkColor(clRed);
    Pdf.CurrentPage.PrintHyperlink(50, 660, 'Purchase license', 'https://www.loslab.com/en-us/buy-hotpdf-fastspring.html');
    Pdf.CurrentPage.SetRGBHyperlinkColor(clBlue);  // revine la valoarea implicită

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Capcana coordonatelor

HotPDF folosește o origine în stânga jos, cu Y crescând în sus, în puncte (1/72 dintr-un inch). O pagină A4 are 595 x 842 pt; o pagină US Letter are 612 x 792 pt. Y=750 stă lângă partea de sus a unei pagini A4, iar Y=50 ar fi lângă marginea de jos. Oricine vine dinspre grafica de ecran sau dinspre HTML presupune inversul și plasează primul rând de legături direct în afara zonei vizibile

Dreptunghiul de adnotare pe care îl calculează PrintHyperlink folosește același sistem de coordonate. Dacă ulterior rotiți pagina, o scalați sau îi schimbați dimensiunea fără să recalculați valorile X/Y, textul vizibil și dreptunghiul clicabil se vor îndepărta unul de altul. Legătura „funcționează”, în sensul că un clic undeva lângă text declanșează adresa, dar zona activă nu mai corespunde cu ce vede cititorul. Testați pe dimensiunea de pagină și pe nivelul de mărire cu care livrați, nu doar pe mașina de dezvoltare la 100%

Un caz în care abaterea este garantată: dacă apelați PrintHyperlink cu coordonate potrivite pentru o pagină A4 și apoi treceți la o pagină cu format îngust personalizat fără să ajustați valorile X/Y, adnotarea poate ajunge complet în afara paginii. Obiectul de adnotare tot este scris în PDF; majoritatea vizualizatoarelor îl decupează în tăcere, deci legătura pur și simplu dispare, fără nicio eroare

Textul etichetei față de adresa țintă

Argumentele Text și Link sunt independente. Puteți desena „Descărcați factura PDF” în timp ce ținta este o adresă HTTPS complet calificată, cu parametri de interogare. Acea separare este intenționată; eticheta vizibilă ar trebui să fie lizibilă de om, iar adresa poate fi lungă sau generată dinamic

Ce creează probleme este atunci când eticheta este chiar adresa brută, mai ales una lungă. Dacă adresa se împarte vizual pe două rânduri, dar dreptunghiul de adnotare a fost calculat pentru un șir de un singur rând, doar primul rând este clicabil. PrintHyperlink nu tratează fluxul pe mai multe rânduri; păstrați eticheta destul de scurtă încât să încapă pe un rând la dimensiunea curentă a fontului și la lățimea paginii, folosiți o etichetă scurtă și descriptivă cu adresa completă drept țintă sau aplicați soluția pe rânduri arătată în secțiunea următoare

Pentru documentele care vor fi arhivate sau distribuite fără o conexiune activă la internet, cântăriți și dacă adresa în sine ar trebui să apară tipărită undeva în corpul documentului, nu doar ca metadate de adnotare. Un cititor care tipărește PDF-ul pe hârtie nu obține nimic dintr-o adnotare URI

Ocolirea limitării la un singur rând

Când o etichetă de legătură chiar trebuie să se întindă pe mai mult de un rând — o adresă lungă tipărită ca atare sau o frază împărțită care ar trebui să fie clicabilă de la un capăt la altul — corectura este să nu o mai tratați ca pe o singură legătură, ci ca pe o legătură pentru fiecare rând. Fiecare apel PrintHyperlink își calculează dreptunghiul din textul pe care îl desenează, așa că mai multe apeluri care împart aceeași țintă Link produc mai multe adnotări dimensionate corect, care deschid toate aceeași adresă. Cititorul nu poate simți diferența; fiecare rând răspunde la clic

Comparație între o etichetă de hyperlink HotPDF împărțită pe rânduri, care primește o singură adnotare ce acoperă doar primul rând, și câte un apel PrintHyperlink pentru fiecare rând randat, toate cu aceeași adresă țintă
Un dreptunghi calculat pentru un singur rând lasă pe dinafară fiecare continuare, în timp ce apelurile pe rânduri împart o țintă și păstrează clicabil întregul bloc
procedure PrintWrappedHyperlink(Page: THPDFPage; X, TopY, LineStep: Single;
  const Lines: array of AnsiString; const Link: AnsiString);
var
  I: Integer;
begin
  for I := 0 to High(Lines) do
    Page.PrintHyperlink(X, TopY - I * LineStep, Lines[I], Link);
end;

// Utilizare: împărțiți eticheta acolo unde aranjamentul o rupe pe rânduri
Pdf.CurrentPage.SetFont('Arial', [], 10);
PrintWrappedHyperlink(Pdf.CurrentPage, 50, 400, 14,
  ['https://www.loslab.com/en-us/pdf-library/',
   'delphi-pdf-component.html'],
  'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');

Împărțirea șirului este responsabilitatea dumneavoastră: rupeți-l în aceleași poziții în care s-ar rupe vizual la fontul și lățimea de coloană curente, folosind TextWidth pentru a testa fiecare rând candidat. Alternativa este să desenați singur textul împărțit cu simple apeluri TextOut și apoi să așezați câte un dreptunghi AddURILink peste fiecare rând — calea mai bună atunci când textul este deja produs de propria dumneavoastră logică de împărțire pe rânduri, ceea ce ne aduce la acea funcție

AddURILink: zone clicabile peste orice ați desenat

PrintHyperlink este un înveliș de comoditate: își desenează propria etichetă și derivă dreptunghiul din metricile acelei etichete. AddURILink este jumătatea de nivel jos, expusă direct:

function AddURILink(Rectangle: TRect; const URL: AnsiString;
  const Description: AnsiString = ''): THPDFDictionaryObject;

Ea scrie doar adnotarea — nu se desenează niciun text și nu se schimbă nicio culoare. Rectangle este interpretat în același spațiu de coordonate ca apelurile dumneavoastră de desenare, deci puteți refolosi exact valorile X/Y pe care le-ați transmis lui TextOut sau unui apel de imagine. Asta o face instrumentul potrivit ori de câte ori conținutul vizibil există deja: o zonă activă pe o imagine, o celulă de tabel, un bloc de text desenat mai devreme sau un rând dintr-un paragraf împărțit, ca în soluția de mai sus. Adnotarea poartă o bordură cu lățime zero, deci nimic vizibil nu se schimbă; regiunea clicabilă este exact dreptunghiul pe care îl indicați

Funcția returnează dicționarul adnotării ca THPDFDictionaryObject. Majoritatea apelanților aruncă rezultatul, dar păstrarea lui vă lasă să ajustați intrările adnotării înainte ca documentul să fie scris

Două detalii de conformitate sunt integrate. În modurile PDF/A, indicatorul de tipărire al adnotării este setat așa cum cer acele standarde. Sub PDFUACompliance, parametrul Description trebuie să fie un șir nevid — el devine intrarea /Contents a adnotării, adică exact ce anunță tehnologia asistivă pentru legătură — iar apelul ridică o excepție în loc să emită în tăcere un fișier neconform. PrintHyperlink este anterior acelei reguli și nu atașează nicio descriere, deci pentru ieșire PDF/UA desenați eticheta cu TextOut și plasați adnotarea cu AddURILink, cu o descriere cu sens

Regula de decizie este simplă: folosiți PrintHyperlink atunci când legătura este o bucată scurtă de text pe care nu ați desenat-o încă; folosiți AddURILink atunci când regiunea clicabilă este definită de conținut pe care îl desenați sau îl măsurați chiar dumneavoastră

Navigare internă cu AddGoToLink

Adresele externe sunt doar jumătate din ce fac adnotările de legătură. Cealaltă jumătate este navigarea în interiorul documentului — un cuprins care sare la capitole, referințe încrucișate între secțiuni. HotPDF expune asta prin AddGoToLink:

procedure AddGoToLink(Rectangle: TRect; TargetPageIndex: Integer;
  YPos: Single = -1; const Description: AnsiString = '');

Merită enunțate precis trei semantici, de vreme ce niciuna nu se ghicește din semnătură. TargetPageIndex pornește de la zero: prima pagină a documentului este pagina 0, în acord cu CurrentPageNumber. Pagina țintă trebuie să existe deja când faceți apelul; dacă indexul este în afara intervalului, procedura se întoarce fără să adauge vreo adnotare — nicio excepție, nicio legătură, niciun avertisment. Pentru un cuprins care arată înainte, creați întâi toate paginile, apoi reveniți și adăugați legăturile

YPos alege poziția verticală de pe pagina țintă, în același spațiu de coordonate ca apelurile dumneavoastră de desenare. Valoarea implicită -1 (orice valoare negativă) scrie o coordonată de destinație nulă, spunându-i vizualizatorului să își păstreze poziția verticală curentă când ajunge pe pagina țintă. Transmiteți o valoare nenegativă și vizualizatorul derulează astfel încât acea poziție să stea în partea de sus a ferestrei — folosiți coordonata Y a titlului spre care faceți legătura. Nivelul de mărire rămâne întotdeauna neschimbat. Ca și la AddURILink, Description trebuie să fie nevid sub PDFUACompliance și devine textul alternativ al legăturii

HotPDF: cuprins cu legături construit cu AddGoToLink, care arată salturile cu TargetPageIndex pornit de la zero de la pagina de cuprins spre paginile de capitol, unde fiecare titlu ajunge în partea de sus a ferestrei
Dreptunghiurile se întind dincolo de text, ca rândurile întregi să răspundă, iar un Y fix de aterizare așază fiecare titlu de capitol în partea de sus a ferestrei
procedure BuildLinkedTOC(const FileName: string);
const
  Chapters: array[0..2] of string =
    ('Introduction', 'Installation', 'API Reference');
var
  Pdf: THotPDF;
  I, Y: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.BeginDoc;                        // pagina 0 devine pagina de cuprins

    // Creați întâi paginile de capitol, ca țintele legăturilor să existe
    for I := 0 to High(Chapters) do
    begin
      Pdf.AddPage;                       // paginile 1..3
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
      Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
    end;

    // Reveniți la pagina 0 și desenați intrările de cuprins cu legăturile lor
    Pdf.CurrentPageNumber := 0;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Contents');
    Pdf.CurrentPage.SetFont('Arial', [], 11);

    Y := 720;
    for I := 0 to High(Chapters) do
    begin
      Pdf.CurrentPage.TextOut(70, Y, 0, Chapters[I]);
      Pdf.CurrentPage.AddGoToLink(
        Rect(70, Y + 14, 300, Y - 3),    // acoperă intrarea cu spațiu în plus
        I + 1,                           // pornit de la zero: capitolele sunt paginile 1..3
        780,                             // aterizează cu titlul în partea de sus
        AnsiString('Go to ' + Chapters[I]));
      Y := Y - 25;
    end;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Fiecare intrare primește un dreptunghi mai lat decât textul, ca tot rândul să răspundă la indicator, iar fiecare legătură aterizează cu titlul de capitol (desenat la Y=780) în partea de sus a ferestrei. Dacă inserați ulterior o pagină înaintea capitolelor, fiecare TargetPageIndex se deplasează cu unu; calculați indicii din bucla dumneavoastră de creare a paginilor, în loc să îi scrieți fix în cod

Un exemplu complet de generare a unui document

Tiparul de mai jos arată un scenariu mai realist: generarea unui raport scurt cu o secțiune de antet, text de corp și un rând de legături în subsol, totul din cod, nu dintr-un formular cu câmpuri TEdit:

procedure GenerateProductSheet(
  const FileName, ProductName, ProductURL, SupportURL: string);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Compression := cmFlateDecode;
    Pdf.BeginDoc;

    // Antet
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 750, 0, WideString(ProductName));

    // Substituent pentru paragraful de corp
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');

    // Legături în subsol
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 80, 0, 'Links:');
    Pdf.CurrentPage.PrintHyperlink(50, 60, 'Product page', ProductURL);
    Pdf.CurrentPage.PrintHyperlink(200, 60, 'Support', SupportURL);

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Observați că SetFont este apelat înaintea fiecărui grup de apeluri de text. Fontul nu persistă peste AddPage, iar dacă uitați să îl setați înainte de PrintHyperlink pe o pagină nouă, dreptunghiul de adnotare va fi calculat față de metricile implicite ale paginii, care pot să difere de ce vă așteptați

Unde diferă tratarea adnotărilor de la un vizualizator la altul

Adnotările URI din PDF sunt definite în ISO 32000-1 §12.6.4.7, iar orice vizualizator conform ar trebui să le respecte. În practică, câteva comportamente diferă de la un vizualizator la altul. Adobe Acrobat arată o solicitare de securitate la primul clic pentru adresele care nu sunt în lista de domenii de încredere; multe browsere și cititoare ușoare nu o fac. Unele vizualizatoare PDF de întreprindere, în medii restricționate, dezactivează complet adnotările URI prin politică, deci un clic nu face nimic, fără vreo eroare vizibilă. Aplicațiile PDF mobile diferă între ele prin faptul că deschid legăturile în vizualizatorul web al aplicației sau le predau browserului sistemului

Niciuna dintre acestea nu este un defect pe care să îl puteți repara din partea de generare; sunt decizii de politică ale vizualizatoarelor. Ce puteți face este să scrieți etichete de legătură care fac adresa vizibilă și în corpul documentului, astfel încât un cititor dintr-un mediu restricționat să poată copia adresa manual. Adnotarea este comoditatea; textul este soluția de rezervă

Încă un detaliu pe care merită să îl știți: adnotările URI din PDF nu poartă implicit nicio subliniere vizuală. Sublinierea pe care o vedeți în majoritatea vizualizatoarelor este desenată de vizualizatorul însuși, pe baza tipului de adnotare, nu de o glifă din fluxul de conținut. Dacă aveți nevoie de o subliniere fizică ce supraviețuiește tipăririi către un randor neinteractiv sau conversiei din PDF în imagine, desenați-o explicit cu LineTo și Stroke, la decalajul Y potrivit sub linia de bază a textului. Aceea este o operație de desenare separată, nu ceva de care se ocupă PrintHyperlink în locul dumneavoastră

API-ul de hyperlinkuri prezentat aici face parte din HotPDF Delphi Component pentru Delphi și C++Builder