บทความเทคนิค

PDFlibPas HTML กับ Markdown: decode entity ซ้ำสองรอบ

PDF Library for Delphi (PDFlibPas) รุ่นก่อน v3.539.47 อาจ decode escaped text ซ้ำสองรอบตอนวาด HTML หรือ Markdown ลง PDF DrawHTMLText กับ DrawHTMLTextBox parse HTML แล้ว normalize กลับเป็น HTML แล้ว parse อีกรอบ ข้อความที่เขียนไว้เป็น <unsafe> เลยรอดไปถึงรอบ parse ที่สองในรูปแท็กจริง ตั้งแต่ v3.539.47 entity แต่ละตัวถูก decode ครั้งเดียวเป๊ะ ๆ และข้อความถูก re-escape ทุกจุดที่มันกลับไปเป็น HTML

สถานการณ์ที่เปิดเผยบั๊กนี้ธรรมดามาก help desk ส่งออก ticket เป็น PDF แล้วความคิดเห็นของลูกค้าถูกยัดเข้าเทมเพลต HTML นักพัฒนาทำถูกแล้วที่ escape ความคิดเห็นไว้ <b> จึงกลายเป็น &lt;b&gt; แต่ข้างใน renderer การ escape นั้นถูกล้างทิ้งแบบเงียบ ๆ ความคิดเห็นก็ออกมาเป็นตัวหนา ชื่อแท็กที่ไม่รู้จักก็หายวับไปจากหน้ากระดาษ และ anchor ที่ escape ไว้กลายเป็น link annotation ที่คลิกได้ ไม่มี exception ไม่มีคำเตือน ได้ PDF ที่ถูกต้องสมบูรณ์แต่พูดไม่ตรงกับข้อมูล

ทำไม escaped text ถึงกลายเป็นแท็กจริงใน PDF

escaped text กลายเป็น markup เพราะ renderer รัน parse สองรอบ และขั้น normalize ตรงกลางเขียนข้อความที่ถอดรหัสไปแล้วกลับเป็น HTML โดยไม่ escape มันซ้ำ ทุกการ decode ที่รอบแรกทำไปเลยกลายเป็น syntax มีชีวิตในสายตารอบที่สอง

สองรอบนี้มีอยู่ด้วยเหตุผลจริง รอบแรกสร้างรายการ element ของแท็กกับคำ NormalizeParsedHTML แก้ cascade ของ stylesheet ต่อ: จับกฎจากบล็อก <style> ไปแมตช์กับแต่ละแท็ก รวมกับ attribute style แบบ inline เก็บผลไว้บนแท็ก แล้ว serialize รายการ element ทั้งหมดกลับเป็น string ของ HTML รอบ layout parse string ที่ normalize แล้วนั้น กลไกชุดเดียวกันนี้แหละที่ขับเคลื่อนflexbox, CSS grid และ layout ของ footnote ในการ render HTML ของ PDFlibPas

รอยั้งอยู่ที่วิธี serialize คำ แท็กถูกเขียนกลับจากรูปต้นทางใน source แต่คำถูกเขียนกลับในรูปที่ถอดรหัสแล้ว คำที่รอบแรกถอดจาก &lt;unsafe&gt; เป็น <unsafe> จึงตกลงไปอยู่ใน HTML ที่ normalize แล้วในรูปวงเล็บเหลี่ยมดิบ ๆ และรอบที่สองอ่านมันเป็น element ไป รอบ ๆ บั๊กแกนนี้ยังมีรอยรั่วเล็กสามจุดที่ชี้ไปทางเดียวกัน:

  • &amp; ไม่ได้อยู่ในชุด entity ที่รองรับ R&amp;D จึงถูกพิมพ์ออกมาเป็นตัวอักษรตรง ๆ และไม่มีทางเขียนรูป entity ตรงตัวอย่าง &lt; เป็นข้อความได้เลย
  • ขั้นวาดภาพแทนที่ &nbsp; เป็นรอบที่สอง หลัง parse จบไปแล้ว รูป entity ตรงตัวจึงยังหายได้ตอนท้ายสุด
  • การ escape code ใน Markdown ข้าม ampersand ไป และ dataset exporter escape เฉพาะวงเล็บเหลี่ยม รูป entity ข้างใน code หรือค่าใน cell จึงถูกถอดเป็น markup
pipeline HTML ของ PDFlibPas สำหรับ DrawHTMLText ที่รอบ parse แรกสร้าง element NormalizeParsedHTML serialize กลับเป็น HTML และรอบ parse ที่สองจัด layout ผลลัพธ์ ก่อน v3.539.47 คำที่ถอดรหัสแล้วถูกเขียนกลับโดยไม่ escape และกลายเป็นแท็กมีชีวิต ตั้งแต่ v3.539.47 ทุกคำถูก re-escape ที่ boundary
คำที่ถอดรหัสแล้วไหลย้อนเข้า parser เป็น syntax เมื่อ normalizer ลืมว่าตัวเองผลิต markup นั่นแหละที่ทำให้ความคิดเห็นที่ escape ไว้กลายเป็นตัวหนาหรืองอก link ขึ้นมา
Input ที่เข้าถึง rendererก่อน v3.539.47ตั้งแต่ v3.539.47
&lt;unsafe&gt;ถูก parse เป็นแท็ก ข้อความไม่เคยถึงหน้ากระดาษ<unsafe> ถูกวาดเป็นข้อความ
&lt;b&gt;x&lt;/b&gt;x ถูกวาดเป็นตัวหนา<b>x</b> ถูกวาดเป็นข้อความ
R&amp;DR&amp;D ถูกพิมพ์ตรงตัวR&D
&amp;lt;&amp;lt; ถูกพิมพ์ตรงตัว&lt;
code span ใน Markdown ที่มี &nbsp;กลายเป็น non-breaking space&nbsp; ถูกวาดเป็นข้อความ
ค่าใน cell ของ dataset เป็น &lt;<&lt;

v3.539.47 ทำให้การ decode entity ของ HTML จบในรอบเดียว

PDFlibPas v3.539.47 ทำให้การ decode entity จบในรอบเดียวด้วยการแก้สามจุดที่ทำงานประสานกัน: parser decode &amp; เป็นตัวสุดท้าย ขั้นวาดภาพไม่ decode อะไรอีก และทุกจุดที่คำที่ถอดรหัสแล้วกลับไปเป็น HTML จะ escape มันซ้ำก่อนเสมอ

ชุด entity ที่รองรับสำหรับ text content ตอนนี้คือ &lt; &gt; &amp; และ &nbsp; อย่างอื่นทั้งหมด รวมถึง numeric reference อย่าง &#65; กับ named entity อย่าง &quot; คงอยู่เป็นข้อความตรงตัว เส้นแบ่งนี้สำคัญกับวิธีที่คุณ escape input ของตัวเอง ดังตัวอย่างด้านล่าง

ลำดับข้างใน decoder คือการแก้จุดแรก ถ้า &amp; ถูก decode เป็นตัวแรก input &amp;lt; จะกลายเป็น &lt; แล้วการแทนที่รอบถัดไปจะเปลี่ยนมันเป็น < เป็นการ decode ซ้ำที่เกิดข้างในรอบเดียว เส้นทางคำ ANSI จึงแทนที่ &lt; &gt; กับ &nbsp; ก่อน แล้วค่อยแทนที่ &amp; เป็นตัวท้าย ampersand ที่มันผลิตออกมาจึงไม่ถูกส่องตรวจซ้ำอีก ส่วนเส้นทางคำ UTF-16 เป็นการสแกนซ้ายไปขวารอบเดียวทีละสองไบต์ เขียนทับ match แต่ละตัวในที่เดิมแล้วเดินข้ามไป ซึ่งให้การการันตีแบบเดียวกันในเชิงโครงสร้าง

ลำดับของ decoder ใน PDFlibPas เมื่อเจอ entity ต่อกันอย่าง &amp;lt;: ถอดรหัส ampersand ก่อนจะยุบมันเป็นวงเล็บเหลี่ยมจริงข้างในรอบเดียว ขณะที่การถอดรหัส lt gt และ nbsp ก่อน ampersand คงรูปเขียนตรงตัวไว้ ข้อความจึงถึงหน้ากระดาษโดยถูกถอดรหัสครั้งเดียวเป๊ะ ๆ
ampersand คือ escape character มันจึงต้องถูก decode เป็นตัวสุดท้ายและถูก escape เป็นตัวแรก ไม่อย่างนั้นรอบเดียวก็ decode ซ้ำได้

การแก้จุดที่สองคือถอดการแทนที่ &nbsp; ตอนปลายทางออกจากขั้นวาดภาพ การ decode เป็นหน้าที่ของ parser และไม่มีที่อื่น คำที่ไหลถึง line breaker จึงเป็นข้อความสุดท้ายแล้ว

การแก้จุดที่สามคือกฎ boundary NormalizeParsedHTML ตอนนี้ escape & < และ > ในทุกคำที่ถอดรหัสแล้วก่อนแปะมันต่อท้าย HTML ที่ normalize รอบที่สองถอดกลับได้ข้อความเดิมเป๊ะ ผลสุทธิ์ตลอด pipeline จึงเหลือ decode ครั้งเดียว string ต่อเนื่องก็ยึดกฎเดียวกัน คำที่ไม่ลงกล่องถูก escape ก่อนแปะเข้า LeftOverText ส่วนที่เหลือของ remainder ถูกก๊อปมาจาก HTML ที่ normalize แล้วซึ่งอยู่ในรูป escaped อยู่แก่แล้ว loop ที่เก็บคำ leftover ยังถูกผูกด้วยจำนวนคำด้วย แทนที่จะเป็น repeat loop แบบเดิมที่เดินล้ำเลยคำสุดท้ายได้

ทำไมการ escape UTF-16BE ถึงใช้ byte-level replace ไม่ได้

การ escape UTF-16BE ใช้ byte-level replace ไม่ได้ เพราะรูปสองไบต์ของ ampersand สามารถคร่อมอักขระที่ไม่เกี่ยวกันสองตัวได้ หน่วยงานเดียวที่ถูกต้องคือ code unit 16 บิตทั้งตัว

renderer เก็บคำ Unicode เป็น UTF-16 แบบ big-endian อัดอยู่ใน byte string ไบต์สูงมาก่อน ampersand คือ 00 26 ลองยก U+0100 (ตัว A ใหญ่ลาตินพร้อม macron ไบต์ 01 00) ตามด้วย U+2603 (ตัวหิมะ ไบต์ 26 03) ลำดับไบต์คือ 01 00 26 03 และไบต์ที่สองกับสามอ่านได้ 00 26 การค้นหาด้วยไบต์หา #0'&' จะเจอ ampersand ที่ไม่มีอยู่จริง ต่อไบต์ของ &amp; เข้าไปกลางอักขระสองตัว แล้วฉีกทุกอักขระถัดไปเหลื่อมไปหนึ่งไบต์

อันตรายของการ escape UTF-16BE ใน PDFlibPas ที่ไบต์ 01 00 26 03 ของ U+0100 กับ U+2603 มีรูปแบบ 00 26 คร่อมอักขระสองตัว การค้นหา ampersand ด้วยไบต์จึงต่อ entity เข้าไปกลาง code point การสแกนแบบ code unit ทดสอบเฉพาะ offset คู่
การค้นด้วยไบต์เจอ ampersand ที่ไม่มีอักขระไหนเคยมี ทำงานบน code unit ทั้งตัว อย่าแตะ byte buffer ของ UTF-16 ดิบ ๆ

นี่ไม่ใช่มุมหลังของกรณีพิเศษ อักขระใดก็ได้ที่ไบต์ต่ำเป็นศูนย์ยืมครึ่งแรกให้ได้ U+4E00 หนึ่งในอักษร CJK ที่พบบ่อยที่สุดก็เข้าข่าย วงเล็บเหลี่ยมมีความเสี่ยงแบบเดียวกัน: 00 3C กับ 00 3E โผล่ทุกครั้งที่อักขระพวกนี้ตามด้วยตัวจาก U+3C00 ถึง U+3EFF ใน CJK Extension A การแก้ใน EscapeHTMLWord แกะไบต์ออกเป็น WideString escape ทีละอักขระแล้วอัดกลับ decoder ฝั่งรับปลอดภัยอยู่แล้วเพราะทดสอบรูปแบบเฉพาะที่ boundary คู่ของ code unit

กฎเดียวกันใช้กับโค้ดของคุณเอง ถ้าคุณถือ text UTF-16 ในรูป TBytes สักครั้ง อย่างหลัง TEncoding.BigEndianUnicode.GetBytes อย่าไปค้นหามันด้วยรูปแบบไบต์ แปลงกลับเป็น string แล้วทำงานบนอักขระเถอะ

code block ใน Markdown กับ export dataset: escape ampersand ก่อนเป็นตัวแรก

ตั้งแต่ v3.539.47 ผู้ผลิต HTML ทั้งสองตัวข้างใน PDFlibPas ทั้ง Markdown converter และ dataset exporter escape ampersand ก่อนวงเล็บเหลี่ยม การ decode รอบเดียวใน renderer จึงคืนข้อความต้นฉบับกลับมาเป๊ะ ๆ

ใน MarkdownToHTML code span แบบ inline กับ code block แบบ fenced หรือย่อหน้า ตอนนี้ map & เป็น &amp; < เป็น &lt; และ > เป็น &gt; ส่วนช่องว่างกลายเป็น &nbsp; และ tab กลายเป็นสี่ตัวต่อกันเพื่อคงย่อหน้า โปรแกรม Markdown ธรรมดา escape เฉพาะวงเล็บเหลี่ยม HTML ดิบในร้อยแก้วจึงฉีดแท็กไม่เข้า แต่ผู้เขียนยังเขียน &amp; ตั้งใจได้ ตรงตามที่ผู้เขียน Markdown คาดหวัง DrawMarkdownText กับ DrawMarkdownTextBox ใช้การแปลงชุดเดียวกัน code จึงโผล่ใน PDF ตรงตามที่พิมพ์:

uses
  System.SysUtils, PDFlibrary;

procedure RenderCodeSample;
var
  Lib: TPDFlib;
  Md, Html: WideString;
begin
  Md := 'Comparison helper:' + sLineBreak + sLineBreak +
        '```' + sLineBreak +
        'if (A < B) and (Flags <> 0) then' + sLineBreak +
        '  WriteLn(''&lt;tag&gt; &amp; R&amp;D'');' + sLineBreak +
        '```';
  Lib := TPDFlib.Create;
  try
    // ส่องดู HTML: ใน code, '&' กลายเป็น '&amp;' และ '<' กลายเป็น '&lt;'
    Html := Lib.MarkdownToHTML(Md);
    Lib.SetOrigin(1);            // origin มุมบนซ้าย Y โตลงล่าง
    Lib.SetMeasurementUnits(0);  // หน่วยเป็น point
    // หน้าแสดง code ตรงตามที่พิมพ์ รวมถึงรูปเขียน entity ด้วย
    Lib.DrawMarkdownText(50, 50, 495, Md);
    Lib.SaveToFile('code-sample.pdf');
  finally
    Lib.Free;
  end;
end;

dataset exporter คือเคสที่สอนใจ ก่อน v3.539.47 มัน escape เฉพาะวงเล็บเหลี่ยม และตั้งใจแบบนั้น: renderer ไม่ decode &amp; การ escape ampersand จึงจะพิมพ์ &amp; ออกมาในทุก cell ที่มีมัน workaround นั้นถูกต้องกับ renderer รุ่นเก่า แต่ผิดในภาพรวม เพราะค่า cell ที่บังเอิญมี &lt; ถูกถอดเป็น < ไป พอ renderer ถูกแก้ exporter จึง escape & ก่อน และค่าอย่าง R&D &lt; &amp; &nbsp; ก็ลงไปอยู่ใน PDF ครบตรงตัว ถ้าคุณสร้างรายงานด้วยวิธีนี้ walkthrough ในการ export TDataSet เป็นรายงาน PDF ใน Delphi เล่าส่วนที่เหลือของ exporter ให้ครบ

ทำไม ampersand ต้องมาก่อน คุ้มค่าที่จะเล่าให้ชัดสักครั้ง escape < ก่อนจะได้ &lt; ต่อด้วย escape & รอบสอง มันจะกลายเป็น &amp;lt; ซึ่งการ decode รอบเดียวที่ถูกต้องจะแสดงเป็น &lt; แทนที่จะเป็น < ห่วงโซ่แทนที่แบบไล่ลำดับจะถูกต้องก็ต่อเมื่อ escape character เองถูกจัดการก่อนสิ่งที่จะมาผลิตมัน

ควร escape text ที่ไม่น่าเชื่อถืออย่างไรสำหรับ DrawHTMLTextBox

สำหรับการ render HTML ของ PDFlibPas ให้ escape text content ที่ไม่น่าเชื่อถือด้วยการแทนที่ & ก่อน แล้ว < แล้ว > ครบพอดีหนึ่งรอบ และกันข้อมูลที่ไม่น่าเชื่อถือออกจากค่า attribute ให้สนิท

uses
  System.SysUtils, PDFlibrary;

// escape text ที่ไม่น่าเชื่อถือสำหรับ text content ของ HTML ใน PDFlibPas
// '&' ต้องถูกแทนที่ก่อน ไม่อย่างนั้น ampersand ข้างใน
// '&lt;' ที่ผลิตไปแล้วจะถูก escape ซ้ำรอบสอง
function EscapeHTMLText(const S: string): string;
begin
  Result := StringReplace(S, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
  Result := StringReplace(Result, '>', '&gt;', [rfReplaceAll]);
end;

procedure RenderTicket(const CustomerComment: string);
var
  Lib: TPDFlib;
  Html: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.SetMeasurementUnits(0);
    Html := '<p><b>Customer comment</b></p>' +
            '<p>' + EscapeHTMLText(CustomerComment) + '</p>';
    Lib.DrawHTMLText(50, 50, 495, Html);
    Lib.SaveToFile('ticket.pdf');
  finally
    Lib.Free;
  end;
end;

บน v3.539.47 ความคิดเห็นอย่าง Try <a href="https://example.com">this</a> & &lt;b&gt; โผล่บนหน้ากระดาษตรงตัวทีละอักขระ ก่อน v3.539.47 input ที่ escape แล้วชุดเดียวกันอาจผลิต link annotation มีชีวิตออกมา ซึ่งเป็นส่วนที่เปลี่ยนอาการแสดงผลเพี้ยนให้กลายเป็นปัญหาด้านความปลอดภัย ความคิดเห็นใน ticket ไม่ควรมีสิทธิ์ปลูก URL คลิกได้ลงในเอกสารที่ทีมงานของคุณเชื่อถือ

สังเกตสิ่งที่ฟังก์ชันนี้ไม่ escape escaper HTML แบบทั่วไปยังแปลง " เป็น &quot; และ ' เป็น &#39; ซึ่งถูกต้องสำหรับ browser แต่การถอดรหัส text ของ PDFlibPas รู้จักแค่สี่ entity ที่ยกมาข้างบน สองตัวนั้นจึงถูกพิมพ์ออกมาตรง ๆ เป็น &quot; กับ &#39; เครื่องหมายคำพูดไม่เป็นอันตรายใน text content มันมีผลแค่ข้างในค่า attribute เท่านั้น และ renderer ไม่ decode entity ใน attribute เลย ดีไซน์ที่ปลอดภัยจึงไม่ใช่ escaper ที่เก่งขึ้น แต่เป็นกฎ: ข้อมูลที่ไม่น่าเชื่อถือห้ามเข้า href src หรือ style ถ้าปลายทางลิงก์ต้องมาจากข้อมูลผู้ใช้จริง ๆ ก็ validate เองกับ allow-list ของ scheme กับอักขระ แล้วปฏิเสธทุกอย่างที่มีเครื่องหมายคำพูดหรือวงเล็บเหลี่ยม

หมายเหตุตอนอัปเกรดสองข้อที่ไหลตามมาจากการแก้นี้:

  • ถ้าโค้ดของคุณเลิก escape & เพราะรุ่นเก่าพิมพ์ &amp; ออกมาตรง ๆ ใส่มันกลับเข้าไป ไม่งั้น text ของผู้ใช้ที่มี &lt; ตอนนี้จะแสดงเป็น < ยังไม่เป็นอันตรายแต่ไม่ใช่สิ่งที่ผู้ใช้พิมพ์มาแล้ว
  • อย่า escape สองรอบ text ที่ผ่าน escaper สองตัวจะ render < เป็นรูปเขียนที่มองเห็นได้ &lt; หา boundary เดียวที่ข้อมูลของคุณเข้าสู่ string ของ HTML แล้ว escape ตรงนั้นจุดเดียวพอ

แบ่งหน้าด้วย LeftOverText โดยไม่ทำ escapes พัง

DrawHTMLTextBox คืน HTML ที่ไม่ลงกล่อง ซึ่งมักถูกเรียกว่า LeftOverText และตั้งแต่ v3.539.47 remainder นี้คงรูปเขียน entity ตรงตัวกับวงเล็บเหลี่ยมที่ escape ไว้เมื่อคุณส่งมันเข้ากล่องถัดไป กฎสำหรับ caller ง่ายมาก: ส่งกลับไปแบบไม่แตะต้อง

const
  BoxLeft = 50;
  BoxTop = 50;
  BoxWidth = 495;    // คำนวณมาสำหรับหน้า A4 หน่วย point
  BoxHeight = 740;
  MaxPages = 500;

procedure RenderLongHTML(Lib: TPDFlib; const Html: WideString);
var
  Rest: WideString;
  Pages: Integer;
begin
  Lib.SetOrigin(1);
  Lib.SetMeasurementUnits(0);
  Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Html);
  Pages := 1;
  while (Rest <> '') and (Pages < MaxPages) do
  begin
    Lib.NewPage;
    Inc(Pages);
    // LeftOverText เป็น engine HTML ที่ escape ไปแล้ว: ห้าม escape หรือ unescape ซ้ำ
    Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Rest);
  end;
  if Rest <> '' then
    raise Exception.CreateFmt('Content still left after %d pages', [MaxPages]);
end;

ถือ remainder เป็นกล่องดำ มันคือ HTML ที่ normalize โดย engine แล้ว แก้ style เรียบร้อย อย่าเอาไปผ่าน escaper ของคุณเอง อย่าถอดรหัสมัน และอย่าสอด text ผู้ใช้ลงไปข้างใน cap จำนวนหน้าคือประกันราคาถูก ถ้า element บางตัวไม่มีวันลงกล่องได้ loop ที่ไร้ cap ก็ไม่มีทางออกตามธรรมชาติ

Markdown มีกลไกต่อเนื่องของตัวเอง DrawMarkdownTextBox คืน token ที่ขึ้นต้นด้วย marker ภายใน เพื่อให้ call ถัดไปข้ามการแปลงได้ ส่งมันกลับเข้า DrawMarkdownTextBox หรือ DrawMarkdownText อย่าส่งเข้าช่องทางฝั่ง HTML ซึ่งจะวาด marker ออกมาเป็นข้อความ

บทเรียนกลาง ๆ: decode ครั้งเดียว re-encode ทุก boundary

pipeline ใดก็ตามที่ parse text เอาผลไป serialize กลับเป็น syntax เดิม แล้ว parse ซ้ำ ต้องถือว่าการ decode เป็นปฏิบัติการที่เกิดขึ้นตรงจุดเดียวเป๊ะ ๆ และต้อง re-encode ทุก boundary ที่ text ที่ถอดรหัสแล้วกลับไปเป็น syntax อีกครั้ง template engine, HTML sanitizer และห่วงโซ่ Markdown เป็น HTML เป็น PDF มีรูปทรงแบบนี้ร่วมกัน และพังแบบเดียวกันเมื่อ serializer ลืมว่าตัวเองผลิต markup

อาการทำนายได้เมื่อรู้รูปทรง re-encode น้อยไปเปลี่ยนข้อมูลเป็น syntax ซึ่งเป็นทิศของ injection encode มากไป หรือ decoder ที่รันสองรอบ แสดงรูปเขียน entity ให้ผู้อ่านเห็นหรือกลืนมันหาย ซึ่งเป็นทิศของการแสดงผล แก้ทีละทิศมักทำอีกทิศพัง นั่นเหตุผลที่การแก้ของ PDFlibPas ต้องเพิ่มการ decode &amp; จัดลำดับใหม่ ถอดการ decode ตอนปลายทางออก และเพิ่มการ re-escape ใน release เดียว หลักการเดียวกันวิ่งสวนทางเมื่อเนื้อหา PDF ถูก export เป็น structured text อย่างในการ export PDF เป็น Markdown และ DOCX เชิง semantics จาก Delphi ที่ทุกอักขระตรงตัวต้องถูก escape ให้ syntax ปลายทางครบพอดีหนึ่งรอบ

เช็คลิสต์สรุปด่วน

  • อัปเกรดเป็น PDFlibPas v3.539.47 ขึ้นไปถ้าคุณ render HTML หรือ Markdown ที่มีข้อมูลผู้ใช้ปนอยู่
  • escape text content ด้วย & ก่อน แล้ว < และ > อย่าแปลงเครื่องหมายคำพูดสำหรับ text ของ PDFlibPas
  • escape ครั้งเดียว ตรงจุดเดียวที่ข้อมูลเข้าสู่ string ของ HTML
  • กันค่าที่ไม่น่าเชื่อถือออกจาก href src และ style หรือ validate กับ allow-list ก่อน
  • คาดหวังการ decode เฉพาะ &lt; &gt; &amp; กับ &nbsp; ใน text entity อื่นคงอยู่ตรงตัว
  • ส่ง LeftOverText กลับเข้า DrawHTMLTextBox แบบไม่แตะต้อง และผูก loop หน้าด้วย cap
  • ส่ง continuation token ของ Markdown เข้า DrawMarkdownTextBox หรือ DrawMarkdownText เท่านั้น
  • อย่าค้นหา byte buffer ของ UTF-16 ด้วยรูปแบบไบต์เด็ดขาด ทำงานบน code unit ทั้งตัว

การ render HTML กับ Markdown, การ export รายงาน dataset และส่วนที่เหลือของ layout engine มาพร้อมซอร์ส Pascal native ของ PDF Library for Delphi สำหรับ Delphi และ Free Pascal ดูหน้า product ของ PDFlibPas สำหรับ edition, platform ที่รองรับ และดาวน์โหลดเวอร์ชันทดลอง