มีงานส่งมาถึงคุณ ให้นำชุด statement ที่เรนเดอร์ไว้แล้วมาปิดเลขบัญชี แล้วส่งออกแบบสองหน้าต่อแผ่นเพื่อประหยัดกระดาษ ทั้งสองครึ่งของงานนี้คือการผ่าตัด content stream บน PDF ที่คุณไม่ได้สร้างมา จึงไม่มี canvas ของหน้าที่ใช้งานง่ายให้วาด และไม่มีตัวจัดการฟอนต์ให้พึ่งพา คุณกำลังแก้ object graph ของเอกสารที่โหลดเข้ามาโดยตรง ด้วยการต่อโอเปอเรเตอร์วาดแบบดิบลงบนหน้าที่เครื่องมืออื่นเป็นคนจัดวาง HotPDF เปิดทางเข้าไว้แค่สองจุด และจุดที่ดูไม่อันตรายคือจุดที่อันตรายที่สุด
HotPDF เป็นคอมโพเนนต์ PDF แบบ native VCL สำหรับ Delphi และ C++Builder API สำหรับเอกสารที่โหลดเข้ามาในรอบเก้าเพิ่มเมธอดแรกที่ สร้าง เนื้อหาใหม่บนหน้าที่คุณเปิดจากดิสก์ แทนหน้าที่คุณสร้างจากศูนย์ สองเมธอดที่เกี่ยวข้องในบทความนี้คือ RedactLoadedRect ซึ่งวาดสี่เหลี่ยมทึบปิดทับบริเวณหนึ่ง และ StitchLoadedPage ซึ่งย่อหนึ่งหน้าแล้ววาดลงบนอีกหน้า ทั้งคู่ทำงานโดยเขียนโอเปอเรเตอร์ content-stream ของ ISO 32000-1 §8.5 ลงในสตรีม /Contents ของหน้า การเข้าใจว่าโอเปอเรเตอร์เหล่านั้นทำอะไร และที่สำคัญพอๆ กันคือสิ่งที่มันไม่ได้ทำ คือเส้นแบ่งระหว่างเครื่องมือที่ใช้งานได้กับการรั่วไหลของข้อมูล
การต่อโอเปอเรเตอร์เข้ากับหน้าที่โหลดมา
เมื่อคุณสร้างหน้าด้วย HotPDF API ปกติ คอมโพเนนต์จะเป็นเจ้าของ content stream และ serialize การเรียก TextOut กับการวาดเวกเตอร์ให้คุณ หน้าที่โหลดมาจะต่างออกไป /Contents ของมันเป็นสตรีมเดิมที่อาจแชร์อยู่ อาจเป็นส่วนหนึ่งของ content array และคุณต้องสอดเข้าไปโดยไม่ทำลายของเดิม รอบเก้ารวม helper เล็กๆ สามตัวที่ทำให้ปลอดภัย NewIndirectStream จัดสรร THPDFStreamObject แบบ indirect ใหม่พร้อมบัฟเฟอร์ว่างและรายการ /Length 0; ResolveLoadedStream ไล่ indirect reference ลงไปยังสตรีมจริง; และ AppendLoadedStream เขียนไบต์ดิบต่อท้ายสตรีมแล้วเขียน /Length ใหม่เพื่อให้วัตถุที่บันทึกยังคงถูกต้อง
รูปแบบที่เมธอดสาธารณะทั้งสองใช้เหมือนกัน คือหา /Contents ของหน้า แกะให้เหลือสตรีม แล้วถ้ายังไม่มีสตรีมที่ใช้งานได้ก็สร้างขึ้นมาและแนบเข้าไป จากนั้นต่อโอเปอเรเตอร์ลงไป เพราะไบต์ใหม่ถูกเติมที่ท้ายสตรีม โมเดล painter's model จึงรับประกันว่ามันจะแสดงทับทุกอย่างที่เลย์เอาต์ต้นฉบับวาดไว้ ลำดับการเติมนี้คือกลไกทั้งหมดเบื้องหลังสี่เหลี่ยมปิดบัง และก็เป็นเหตุผลที่สี่เหลี่ยมนั้นไม่ใช่อย่างที่คนส่วนใหญ่เข้าใจ
RedactLoadedRect: สี่เหลี่ยมทึบปิดทับ ไม่ใช่การลบ
RedactLoadedRect รับดัชนีหน้าแบบเริ่มจากศูนย์ พิกัด user space สี่ค่า และคอมโพเนนต์สีสามค่าในช่วง 0 ถึง 1:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('statement.pdf') > 0 then
begin
// Cover the account-number band on page 1 with solid black.
// Coordinates are PDF user space: origin bottom-left, points.
Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
Pdf.SaveLoadedDocument('statement-covered.pdf');
end;
finally
Pdf.Free;
end;
end;
ภายในเมธอดจะส่งโอเปอเรเตอร์สามตัวลง content stream คือการตั้งค่าสีเติมใน DeviceRGB (r g b rg) เส้นทางสี่เหลี่ยม (x y w h re) และการ fill (f) ความกว้างและความสูงคำนวณจาก X2 - X1 และ Y2 - Y1 ดังนั้นคุณส่งมุมตรงข้ามสองมุมแล้วให้เมธอดคำนวณขอบเขตเอง ถ้าส่ง 0, 0, 0 สำหรับสีจะได้แถบสีดำ ถ้าส่ง 1, 1, 1 จะได้แถบสีขาวที่กลมกลืนกับหน้าขาว พิกัดคือ user space ของหน้าที่โหลดมาเอง ซึ่งหมายความว่าจุดกำเนิดอยู่มุมล่างซ้าย และหน่วยเป็น points และยังหมายความว่าคุณต้องรู้ /MediaBox ของหน้าเพื่อวางอะไรให้แม่นยำ GetLoadedPageBox พร้อม pbMediaBox จะให้ข้อมูลนั้น
อ่านตรงนี้ให้ดีสองครั้ง สี่เหลี่ยมที่เติมสีลงไปปิดเนื้อหาในเชิงภาพเท่านั้น มันไม่ได้ลบเนื้อหาจริง ข้อความ รูปภาพ หรือเวกเตอร์ที่อยู่ใต้สี่เหลี่ยมนั้นยังคงอยู่ใน PDF ยังคงอยู่ใน object graph ยังคงดึงออกมาได้โดยใครก็ตามที่คัดลอกหน้า รันตัวแยกข้อความ หรือแค่ลบสี่เหลี่ยมของคุณออกจาก content stream นี่คือการปิดบังเชิงภาพ ไม่ใช่ redaction ในความหมายทางกฎหมายหรือความปลอดภัย ถ้าคุณกำลังซ่อนข้อมูลที่อ่อนไหวจริงๆ เช่นเลขบัญชี เวชระเบียน ตัวตน หรือข้อมูลที่อยู่ภายใต้ข้อกำกับ การเอากล่องดำไปทับแล้วส่งไฟล์ออกไปคือการปล่อยข้อมูลรั่วที่รอวันถูกพบ การ redaction ที่แท้จริงต้องลบ object ของเนื้อหาที่อยู่ข้างใต้ ไม่ใช่ทาสีทับมัน
ชื่อเมธอดบอกว่า "Redact" และนั่นเป็นคำเตือนที่มีประโยชน์ว่าผลลัพธ์จะถูกอ่านผิดอย่างไร ไม่ใช่คำสัญญาว่ามันลบอะไรได้บ้าง implementation เองก็พูดตรงไปตรงมาในคอมเมนต์ของมัน โดยเรียกตัวเองว่า "visual redaction primitive" และบอกว่าการ redaction แบบลบเนื้อหาต้องใช้ content-stream interpreter ที่ไล่และเขียนโอเปอเรเตอร์เดิมใหม่ HotPDF path สำหรับเอกสารที่โหลดมาจึงไม่ได้ทำแบบนั้นในจุดนี้ ดังนั้นกฎที่ปลอดภัยจึงแคบมาก ใช้ RedactLoadedRect เพื่อการปิดบังเชิงตกแต่งที่ไม่อ่อนไหว เช่น ซ่อน draft watermark ล้างพื้นที่ก่อนจับภาพหน้าจอ หรือปิดโลโก้ที่ล้าสมัยบน proof ภายใน เมื่อใดก็ตามที่สิ่งใต้กล่องมีความหมายถ้าหลุดออกไป เมธอดนี้คือเครื่องมือที่ผิด และคำตอบที่ถูกคือสร้างเอกสารใหม่โดยไม่ใส่ข้อมูลนั้น หรือใช้ pipeline ที่ลบเนื้อหาได้จริง
StitchLoadedPage: ย่อ เลื่อน แล้ววาด
การจัด N-up เป็นปัญหาที่เป็นมิตรกว่า เพราะไม่มีอะไรถูกซ่อน มีแต่ถูกจัดใหม่ StitchLoadedPage รับดัชนีหน้าปลายทาง ดัชนีหน้าต้นทาง ค่า offset X/Y และตัวคูณสเกล แล้วมันจะวาดหน้าต้นทางลงบนหน้าปลายทางที่ตำแหน่งและขนาดนั้น:
// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);
// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);
สตริงโอเปอเรเตอร์ที่มันต่อท้ายคือชุด transform-and-paint มาตรฐาน q เพื่อบันทึก graphics state เมทริกซ์ cm ที่วางสเกลไว้บนเส้นทแยงมุมและ offset ไว้ในช่อง translation /StitchSrc Do เพื่อเรียก external object และ Q เพื่อคืน state คู่ q/Q สำคัญเพราะมันแยก transform ออก ทำให้หน้าที่ stitch ไว้ไม่รั่วระบบพิกัดไปยังสิ่งที่ถูกต่อท้ายภายหลัง เมธอดนี้ยังป้องกันความผิดพลาดที่เห็นได้ชัด เช่น ดัชนีเกินช่วง หน้าปลายทางเท่ากับหน้าต้นทาง สเกลไม่เป็นบวก ซึ่งมันจะ clamp เป็น 1.0 แล้วออกแบบเงียบๆ แทนการโยน error ดังนั้นควรตรวจ input เอง เพราะ no-op ที่เงียบดูเหมือนสำเร็จทุกอย่าง
StitchLoadedPageSideBySide เป็นเพียงตัวช่วยบางๆ บนเมธอดหลัก มันอ่านความกว้างของ media box ของหน้าปลายทาง แบ่งครึ่ง แล้วเรียก StitchLoadedPage ด้วยค่า half-width นั้นเป็น X offset และใช้สเกลคงที่ 0.5 เพื่อวางหน้าต้นทางลงครึ่งขวา ค่า 0.5 ที่กำหนดตายตัวนี้สมมติว่าหน้าต้นทางกับหน้าปลายทางมีความกว้างเท่ากัน ถ้าไม่เท่ากัน หน้าต้นทางจะไม่พอดีกับครึ่งของมันอย่างเรียบร้อย และคุณควรใช้ StitchLoadedPage แบบทั่วไปพร้อมสเกลที่คำนวณเองจาก media box ทั้งสองหน้า
กลยุทธ์ XObject แบบย่อและข้อแลกเปลี่ยนกับ ISO
ตรงนี้คือจุดที่ implementation เลือกทางลัดอย่างตั้งใจ ซึ่งคุณต้องรู้ก่อนจะเชื่อผลลัพธ์ข้าม viewer การจัด N-up ที่ถูกต้องจะห่อ content ของหน้าต้นทางไว้ใน Form XObject ซึ่งเป็น drawable object แบบปิดที่ ISO 32000-1 §8.10.1 บอกว่าต้องมี /Type /XObject, /Subtype /Form, และ clipping box /BBox ของตัวเอง แต่ stitch ในรอบเก้าของ HotPDF ไม่ได้สร้าง wrapper แบบนั้น แทนที่จะทำอย่างนั้น มันลงทะเบียน page dictionary ของหน้าต้นทางเองไว้ใต้ /Resources /XObject ของหน้าปลายทางด้วยชื่อ StitchSrc แล้ววาดด้วย Do page dictionary และ Form XObject มีโมเดลเนื้อหาคล้ายกันพอสมควร ทั้งคู่พึ่งพา content stream และ resource dictionary จึงทำให้ reader จำนวนมากเรนเดอร์ผลลัพธ์ได้
แต่มันไม่ใช่ Form XObject ที่สอดคล้องกับมาตรฐาน มันไม่มีตัวบ่งชี้ /Subtype /Form และไม่มี /BBox ของตัวเอง ซึ่งหมายความว่า consumer ที่เคร่งครัดมีสิทธิ์ที่จะละ Do ทิ้ง หรือ clip มันต่างจากที่คุณคาดไว้ TechnicalNotes สำหรับรอบนี้บอกตรงๆ ว่าแนวทางนี้ "renders under most readers" แต่ "not a strictly ISO-compliant Form XObject" และการทำให้ตรงข้อกำหนดเต็มรูปแบบต้องสร้าง Form XObject stream จริงเป็นขั้นตอนแยก ดังนั้นให้มองผลลัพธ์ stitch เหมือนโครงสร้างที่ไม่เป็นมาตรฐาน ตรวจใน viewer ที่ลูกค้าใช้จริง ไม่ใช่แค่เครื่องของคุณเอง และถ้าคุณต้องการ PDF สำหรับเก็บถาวรหรือผ่าน strict validator ก็อย่าใช้ทางนี้ หลักเดียวกันนี้ใช้กับสิ่งที่คุณสร้างบน object graph ที่โหลดมา ซึ่งเป็นเหตุผลที่การทำ การตรวจ preflight ของ PDF ใน Delphi ควรอยู่ใน release pipeline ทุกครั้งที่คุณแก้ไขเอกสารด้วยโค้ด
ส่วนที่เหมาะใช้ และส่วนที่ไม่เหมาะ
ทั้งสองเมธอดเป็นเครื่องมือของ content stream ดังนั้น mental model จึงเหมือนกับการวาดโดยตรง ถ้าคุณเคยสร้างหน้าจากศูนย์ด้วยคอมโพเนนต์นี้ โอเปอเรเตอร์เวกเตอร์และสีที่อยู่เบื้องหลังการเรียกเหล่านี้จะดูคุ้นจาก การวาดบน canvas ของ HotPDF ใน Delphi ความต่างมีแค่ว่าตรงนี้คุณกำลังต่อท้ายสตรีมที่คนอื่นเป็นคนเขียนไว้ ไม่ใช่ของที่คุณเป็นเจ้าของเอง เก็บขอบเขตสามข้อไว้ในใจ:
- การปิดบังเป็นเพียงเชิงภาพ
RedactLoadedRectวาดทับเนื้อหาและไม่เคยลบมัน สำหรับสิ่งที่อ่อนไหว ให้สร้างต้นฉบับใหม่หรือใช้การลบเนื้อหาจริง กล่องดำไม่ใช่การป้องกัน - การ stitch ไม่สอดคล้องมาตรฐานโดยดีไซน์ หน้าต้นทางถูกอ้างถึงเป็น pseudo-XObject โดยไม่มี
/Subtype /Formและ/BBoxตาม §8.10.1 ดังนั้นต้องยืนยันการแสดงผลใน viewer เป้าหมายของคุณ และหลีกเลี่ยงเมื่อจำเป็นต้องผ่านการตรวจที่เคร่งครัด - พิกัดคือ user space ของหน้า จุดกำเนิดอยู่มุมล่างซ้าย หน่วยเป็น points และขับเคลื่อนด้วย media box ของหน้าเอง อ่านกล่องด้วย
GetLoadedPageBoxก่อนวางอะไรลงไป เพราะหน้าที่คุณโหลดมาอาจไม่ใช่ขนาดที่คุณคิด
ถ้าใช้ภายใต้ขอบเขตเหล่านี้ คู่มือนี้ครอบคลุม workflow ที่ใช้งานได้จริง คือจัดหน้าใหม่สำหรับพิมพ์ ปิดบังพื้นที่ที่ไม่เป็นความลับ และเขียนผลลัพธ์กลับด้วย SaveLoadedDocument โดยไม่ต้องเรนเดอร์ใหม่ทั้งเอกสาร loaded-document API ที่รวม primitive สำหรับ stitch และ mask เหล่านี้มาพร้อมกับ HotPDF Component สำหรับ Delphi และ C++Builder ควบคู่ไปกับเมธอด form-field, annotation และ FDF จากรอบเดียวกัน