מאמר טכני

קישורים ב-HotPDF Delphi: טיפים לאנוטציית PrintHyperlink

קישורים ב-PDF הם אנוטציות URI: מלבן המכסה שטח כלשהו בעמוד ואשר, בעת לחיצה, אומר למציג לפתוח כתובת URL. האנוטציה והטקסט שמתחתיה הם אובייקטים בלתי תלויים לגמרי. PrintHyperlink של HotPDF אורז את שניהם בקריאה אחת, מצייר את הטקסט ומחשב את מלבן האנוטציה ממידות הטקסט המשורטט. הנוחות הזו מסתירה פרט שכדאי להבין לפני שאתה כותב קוד לייצור. זה גם לא כל הסיפור: AddURILink מניחה אזור לחיץ מעל תוכן שציירת בעצמך, ו-AddGoToLink מטפלת בניווט פנימי — שניהם מכוסים בהמשך

איך PrintHyperlink עובדת

PrintHyperlink חיה על THPDFPage ומקבלת ארבעה ארגומנטים: קואורדינטות X ו-Y (בנקודות, ראשית בפינה השמאלית התחתונה, Y גדל כלפי מעלה), מחרוזת התווית שיש לצייר, ויעד ה-URL. פנימית היא קוראת ל-TextOut בצבע הקישור הנוכחי, ואז מיד מחשבת את מלבן האנוטציה מ-TextWidth ו-TextHeight לפי מידות הגופן הנוכחיות. משמעות הדבר שהגופן והגודל חייבים להיקבע לפני הקריאה, ואסור להם להשתנות בין ציור התווית להנחת האנוטציה, משום ששניהם נפתרים באותה קריאה

אנטומיה של קריאת PrintHyperlink אחת ב-HotPDF הכותבת שני אובייקטי PDF בלתי תלויים: תווי התווית הנראים שמצוירים על ידי TextOut ומלבן אנוטציית קישור URI המחושב מ-TextWidth ו-TextHeight
תווי התווית ומלבן ה-URI הם אובייקטי PDF נפרדים, ולכן הגופן וצבע הקישור חייבים להתייצב לפני שקריאה אחת כותבת את שניהם

צבע ברירת המחדל הוא clBlue. SetRGBHyperlinkColor משנה אותו עבור קריאות עוקבות בלבד; היא אינה מעדכנת רטרואקטיבית אנוטציות שכבר נכתבו. אם אתה זקוק לצבעים שונים לקבוצות קישורים שונות באותו עמוד, קרא ל-SetRGBHyperlinkColor לפני כל קבוצה ואפס אותו אחריה

הנה מסמך מינימלי שכותב שלושה קישורים בשני צבעים שונים:

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

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

    // כחול ברירת מחדל לקישורים אינפורמטיביים
    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');

    // אדום לקישור הפעולה
    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);  // שחזור ברירת המחדל

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

מלכודת הקואורדינטות

HotPDF משתמש בראשית בפינה השמאלית התחתונה כאשר Y גדל כלפי מעלה, בנקודות (1/72 אינץ׳). עמוד A4 הוא 595 x 842 נק׳; עמוד US Letter הוא 612 x 792 נק׳. Y=750 יושב סמוך לראש עמוד A4, ו-Y=50 יהיה סמוך לשוליים התחתונים. כל מי שמגיע מגרפיקת מסך או מ-HTML מניח את ההפך וממקם את שורת הקישור הראשונה הרחק מחוץ לשטח הנראה

מלבן האנוטציה ש-PrintHyperlink מחשבת משתמש באותה מערכת צירים. אם תסובב אחר כך את העמוד, תשנה את קנה המידה, או תשנה את גודל העמוד בלי לחשב מחדש את ערכי ה-X/Y שלך, הטקסט הנראה והמלבן הלחיץ יתרחקו זה מזה. הקישור "עובד" במובן שלחיצה איפשהו ליד הטקסט מפעילה את ה-URL, אבל האזור החם כבר אינו תואם למה שהקורא רואה. בדוק בגודל העמוד וברמת הזום שאתה מפיץ בפועל, לא רק במכונת הפיתוח ב-100%

מקרה אחד שבו הסטייה מובטחת: אם תקרא ל-PrintHyperlink עם קואורדינטות המתאימות לעמוד A4 ואז תעבור לעמוד בפורמט צר מותאם אישית בלי להתאים את ערכי ה-X/Y, האנוטציה עלולה לסיים מחוץ לעמוד לגמרי. אובייקט האנוטציה עדיין נכתב אל תוך ה-PDF; רוב המציגים גוזמים אותו בשקט, ולכן הקישור פשוט נעלם בלי שום שגיאה

טקסט התווית מול יעד ה-URL

הארגומנטים Text ו-Link בלתי תלויים. אתה יכול לצייר "Download invoice PDF" בזמן שהיעד הוא כתובת HTTPS מלאה עם פרמטרי שאילתה. ההפרדה הזו מכוונת; התווית הנראית צריכה להיות קריאה לבני אדם וה-URL יכול להיות ארוך או להיווצר דינמית

מה שיוצר בעיות הוא כשהתווית היא ה-URL הגולמי עצמו, ובמיוחד ארוך. אם ה-URL נשבר ויזואלית על פני שתי שורות אך מלבן האנוטציה חושב עבור מחרוזת בת שורה אחת, רק השורה הראשונה לחיצה. PrintHyperlink אינה מטפלת בזרימה רב-שורתית; שמור את התווית קצרה מספיק כדי להיכנס לשורה אחת בגודל הגופן וברוחב העמוד הנוכחיים, השתמש בתווית תיאורית קצרה עם ה-URL המלא כיעד, או יישם את המעקף לכל שורה שמוצג בפרק הבא

עבור מסמכים שיאורכבו או יופצו ללא חיבור אינטרנט פעיל, שקול גם אם ה-URL עצמו צריך להופיע בצורה מודפסת אי שם בגוף המסמך, ולא רק כמטא-דאטה של אנוטציה. קורא שמדפיס את ה-PDF על נייר אינו מקבל דבר מאנוטציית URI

מעקף למגבלת הרב-שורות

כשתווית קישור באמת חייבת להשתרע על יותר משורה אחת — URL ארוך שמודפס כלשונו, או משפט שנשבר שצריך להיות לחיץ מקצה לקצה — התיקון הוא להפסיק להתייחס אליו כאל קישור אחד ולהתייחס אליו כאל קישור לכל שורה. כל קריאת PrintHyperlink מחשבת את המלבן שלה מהטקסט שהיא מציירת, ולכן כמה קריאות החולקות את אותו יעד Link מייצרות כמה אנוטציות בגודל נכון שכולן פותחות את אותו URL. הקורא אינו יכול להבחין בהבדל; כל שורה מגיבה ללחיצה

השוואה בין תווית קישור שנשברה ב-HotPDF המקבלת אנוטציה אחת שמכסה רק את שורתה הראשונה לבין קריאת PrintHyperlink אחת לכל שורה משורטטת החולקות את אותו יעד URL
מלבן שחושב עבור שורה אחת נוטש כל המשך שנשבר, בעוד קריאות לכל שורה חולקות יעד ומשאירות את כל הבלוק לחיץ
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;

// שימוש: שבור את התווית במקומות שבהם הפריסה שלך שוברת אותה
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');

פיצול המחרוזת הוא באחריותך: שבור אותה באותם מקומות שבהם היא הייתה נשברת ויזואלית בגופן וברוחב העמודה הנוכחיים, בעזרת TextWidth לבדיקת כל שורה מועמדת. החלופה היא לצייר את הטקסט השבור בעצמך בקריאות TextOut פשוטות ואז להניח מלבן AddURILink אחד מעל כל שורה — המסלול העדיף כשהטקסט כבר מיוצר על ידי לוגיקת שבירת השורות שלך, וזה מביא אותנו אל הפונקציה הזו

AddURILink: אזורים לחיצים מעל כל דבר שציירת

PrintHyperlink היא עטיפת נוחות: היא מציירת תווית משלה וגוזרת את המלבן ממידות התווית הזו. AddURILink היא החצי הנמוך יותר שנחשף ישירות:

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

היא כותבת רק את האנוטציה — שום טקסט אינו מצויר ושום צבע אינו משתנה. ה-Rectangle מפורש באותו מרחב קואורדינטות כמו קריאות הציור שלך, ולכן אתה יכול לעשות שימוש חוזר בדיוק באותם ערכי X/Y שהעברת ל-TextOut או לקריאת תמונה. זה הופך אותה לכלי הנכון בכל פעם שהתוכן הנראה כבר קיים: אזור חם על תמונה, תא בטבלה, בלוק טקסט שצויר קודם, או שורה אחת של פסקה שנשברה כמו במעקף שלמעלה. האנוטציה נושאת מסגרת ברוחב אפס, ולכן שום דבר נראה אינו משתנה; האזור הלחיץ הוא בדיוק המלבן שציינת

הפונקציה מחזירה את מילון האנוטציה כ-THPDFDictionaryObject. רוב הקוראים משליכים את התוצאה, אבל שמירתה מאפשרת לך להתאים את רשומות האנוטציה לפני שהמסמך נכתב

שני פרטי תאימות בנויים פנימה. במצבי PDF/A דגל ההדפסה של האנוטציה נקבע כפי שהתקנים האלה דורשים. תחת PDFUACompliance הפרמטר Description חייב להיות מחרוזת שאינה ריקה — הוא הופך לרשומת /Contents של האנוטציה, וזה מה שטכנולוגיה מסייעת מכריזה עבור הקישור — והקריאה מעלה חריגה במקום לפלוט בשקט קובץ שאינו תואם. PrintHyperlink קדמה לכלל הזה ואינה מצרפת תיאור, ולכן עבור פלט PDF/UA צייר את התווית עם TextOut והנח את האנוטציה עם AddURILink בתוספת תיאור בעל משמעות

כלל ההכרעה פשוט: השתמש ב-PrintHyperlink כשהקישור הוא פיסת טקסט קצרה שעדיין לא ציירת; השתמש ב-AddURILink כשהאזור הלחיץ מוגדר על ידי תוכן שאתה מצייר או מודד בעצמך

ניווט פנימי עם AddGoToLink

כתובות URL חיצוניות הן רק מחצית ממה שאנוטציות קישור עושות. המחצית השנייה היא ניווט בתוך המסמך — תוכן עניינים שקופץ אל פרקים, הפניות צולבות בין חלקים. HotPDF חושף זאת דרך AddGoToLink:

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

שלוש סמנטיקות ראויות לניסוח מדויק, שכן אף אחת מהן אינה ניתנת לניחוש מהחתימה. TargetPageIndex מבוסס אפס: העמוד הראשון של המסמך הוא עמוד 0, בהתאמה ל-CurrentPageNumber. עמוד היעד חייב כבר להתקיים כשאתה מבצע את הקריאה; אם האינדקס מחוץ לתחום, הפרוצדורה חוזרת בלי להוסיף אנוטציה — בלי חריגה, בלי קישור, בלי אזהרה. עבור תוכן עניינים שמצביע קדימה, צור תחילה את כל העמודים, ואז חזור והוסף את הקישורים

YPos בוחר את המיקום האנכי בעמוד היעד, באותו מרחב קואורדינטות כמו קריאות הציור שלך. ברירת המחדל -1 (כל ערך שלילי) כותבת קואורדינטת יעד ריקה, ואומרת למציג לשמור על המיקום האנכי הנוכחי שלו כשהוא נוחת בעמוד היעד. העבר ערך אי-שלילי והמציג יגלול כך שהמיקום הזה יישב בראש החלון — השתמש בקואורדינטת ה-Y של הכותרת שאליה אתה מקשר. הזום תמיד נשאר ללא שינוי. כמו ב-AddURILink, Description חייב להיות לא ריק תחת PDFUACompliance והוא הופך לטקסט החלופי של הקישור

HotPDF: תוכן עניינים מקושר שנבנה עם AddGoToLink המראה קפיצות TargetPageIndex מבוססות אפס מעמוד התוכן אל עמודי הפרקים שבהם כל כותרת נוחתת בראש החלון
המלבנים משתרעים מעבר לטקסט כך ששורות שלמות מגיבות, ו-Y נחיתה קבוע ממקם כל כותרת פרק בראש החלון
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;                        // עמוד 0 הופך לעמוד התוכן

    // צור תחילה את עמודי הפרקים כדי שיעדי הקישורים יתקיימו
    for I := 0 to High(Chapters) do
    begin
      Pdf.AddPage;                       // עמודים 1..3
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
      Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
    end;

    // חזור לעמוד 0 וצייר את רשומות התוכן עם הקישורים שלהן
    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),    // מכסה את הרשומה עם ריפוד
        I + 1,                           // מבוסס אפס: הפרקים הם עמודים 1..3
        780,                             // נחיתה עם הכותרת בראש
        AnsiString('Go to ' + Chapters[I]));
      Y := Y - 25;
    end;

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

כל רשומה מקבלת מלבן רחב מהטקסט כך שכל השורה מגיבה למצביע, וכל קישור נוחת כשכותרת הפרק (המצוירת ב-Y=780) בראש החלון. אם תוסיף אחר כך עמוד לפני הפרקים, כל TargetPageIndex יזוז באחד; חשב אינדקסים מלולאת יצירת העמודים שלך במקום לקבע אותם בקוד

דוגמה מלאה ליצירת מסמך

התבנית שלהלן מציגה תרחיש מציאותי יותר: יצירת דוח קצר עם מקטע כותרת, טקסט גוף, ושורת קישורים בתחתית, הכול מקוד ולא מטופס עם שדות 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;

    // כותרת עליונה
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 750, 0, WideString(ProductName));

    // מציין מקום לפסקת הגוף
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');

    // קישורי כותרת תחתונה
    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;

שים לב ש-SetFont נקראת לפני כל קבוצת קריאות טקסט. הגופן אינו נשמר על פני AddPage, ואם תשכח לקבוע אותו לפני PrintHyperlink בעמוד חדש, מלבן האנוטציה יחושב מול מידות ברירת המחדל של העמוד, שעשויות להיות שונות ממה שאתה מצפה

היכן טיפול באנוטציות משתנה בין מציגים

אנוטציות URI של PDF מוגדרות ב-ISO 32000-1 §12.6.4.7, וכל מציג תואם אמור לפעול לפיהן. בפועל, כמה התנהגויות שונות ממציג למציג. Adobe Acrobat מציג בקשת אבטחה בלחיצה הראשונה עבור כתובות שאינן ברשימת הדומיינים המהימנים; דפדפנים רבים וקוראים קלילים אינם עושים זאת. חלק ממציגי ה-PDF הארגוניים בסביבות נעולות משביתים אנוטציות URI לגמרי כמדיניות, ולכן לחיצה אינה עושה דבר, בלי שגיאה נראית. יישומי PDF לנייד נבדלים בשאלה אם הם פותחים קישורים בתצוגת האינטרנט של היישום או מעבירים אותם לדפדפן המערכת

אף אחד מאלה אינו באג שאתה יכול לתקן מצד היצירה; אלה החלטות מדיניות של המציג. מה שאתה כן יכול לעשות הוא לכתוב תוויות קישור שהופכות את ה-URL לנראה גם בגוף המסמך, כך שקורא בסביבה מוגבלת עדיין יוכל להעתיק את הכתובת ידנית. האנוטציה היא הנוחות; הטקסט הוא הגיבוי

פרט נוסף שכדאי לדעת: אנוטציות URI של PDF אינן נושאות שום קו תחתון חזותי כברירת מחדל. הקו התחתון שאתה רואה ברוב המציגים מצויר על ידי המציג עצמו על סמך סוג האנוטציה, ולא על ידי תו בזרם התוכן. אם אתה זקוק לקו תחתון פיזי ששורד הדפסה למחולל לא אינטראקטיבי או המרת PDF לתמונה, צייר אותו במפורש עם LineTo ו-Stroke בהיסט ה-Y המתאים מתחת לקו הבסיס של הטקסט. זו פעולת ציור נפרדת, ולא משהו ש-PrintHyperlink מטפלת בו עבורך

ממשק הקישורים שמוצג כאן הוא חלק מ-HotPDF Delphi Component עבור Delphi ו-C++Builder