Paxa Labs
บทความทั้งหมด

25 สิงหาคม 2569 · อ่าน 2 นาที

นับจำนวนหน้าใน PDF โดยไม่ใช้ไลบรารี PDF

ทำไมจุดเชื่อมต่อ OCR ที่คิดเงินตามจำนวนหน้าจึงต้องอ่านจำนวนหน้าเอง และการทำสิ่งนั้นจากตัวไบต์ล้วน ๆ ต้องใช้อะไรบ้าง

จุดเชื่อมต่อ OCR ของเราคิดค่าบริการหน้าละ 6.5 เครดิต และตัดเครดิตก่อนเริ่มประมวลผล การตัดเครดิตก่อนคือรูปแบบเดียวที่ทำให้ API ซึ่งคิดตามปริมาณคืนเงินได้อย่างตรงไปตรงมา ราคาถูกกำหนดตั้งแต่วินาทีที่ผู้เรียกตัดสินใจ ความล้มเหลวหลังจากนั้นย้อนกลับให้อัตโนมัติ และไม่มีใครถูกขอให้เชื่อว่างานที่ได้คุ้มกับที่จ่ายไป

รูปแบบนี้มีข้อกำหนดหนึ่งข้อที่กลายเป็นงานวิศวกรรมจริงจัง คุณต้องรู้ว่าเอกสารมีกี่หน้าก่อนที่จะได้อ่านหน้าไหนเลย โดยรู้จากตัวไบต์ล้วน ๆ ในเส้นทางคำขอที่ถือสิทธิ์การประมวลผลพร้อมกันอยู่แล้ว

ทำไมจึงเขียนขึ้นเอง#

ทางที่ดูชัดที่สุดคือเพิ่มไลบรารี PDF เข้ามา แต่ตัวนับหน้าทำงานในสองรันไทม์ อยู่ในเส้นทางร้อนของการตัดเครดิต และต้องการคุณสมบัติหนึ่งที่ตัวแยกวิเคราะห์อเนกประสงค์จงใจไม่มีให้ ในจุดที่ตัวอื่นเดา มันต้องปฏิเสธ

การเดาเป็นค่าเริ่มต้นที่เป็นมิตรกว่าในเกือบทุกที่ โปรแกรมอ่านที่แสดงผลได้ 47 จาก 50 หน้ามีประโยชน์กว่าโปรแกรมที่ขึ้นข้อผิดพลาด แต่ที่นี่พฤติกรรมเดียวกันแปลว่าคิดเงินผิดจำนวน และข้อผิดพลาดด้านการคิดเงินที่ลูกค้าเป็นคนพบ มีราคาแพงกว่าทุกหน้าที่มันช่วยประหยัดไว้ โมดูลนี้จึงมีสิ่งที่ต้องพึ่งพาเพียงอย่างเดียวคือ node:zlib ซึ่งบันก็มีมาให้ และมีกฎข้อเดียว คือทุกความคลุมเครือจะโยนข้อผิดพลาดออกมา

สายจากไบต์สุดท้ายไปถึงจำนวนหน้า#

ไฟล์ PDF อ่านย้อนจากท้าย ไฟล์จบด้วยคำสำคัญ startxref และตำแหน่งไบต์หนึ่งค่า ตำแหน่งนั้นชี้ไปยังโครงสร้างอ้างอิงไขว้ โครงสร้างนั้นระบุตำแหน่งของอ็อบเจกต์ทุกตัวในไฟล์ หนึ่งในอ็อบเจกต์เหล่านั้นคือแคตตาล็อก แคตตาล็อกระบุต้นไม้ของหน้า และต้นไม้ของหน้าเก็บค่า /Count ไว้

การไล่หาที่ตัวนับต้องทำ
ท้ายไฟล์
 └ startxref  →  ตำแหน่งไบต์
     └ ส่วน xref
         └ trailer /Root
             └ แคตตาล็อก /Pages
                 └ ต้นไม้ของหน้า /Count  →  50

ห้าทอด แต่ละทอดอาจอยู่ในสตรีมอ็อบเจกต์
ที่บีบอัดไว้ และส่วน xref เองก็มีได้สี่รูปแบบ

ลูกศรทุกอันคือจุดที่ไฟล์จะซื่อตรง ผิดแผก หรือมุ่งร้ายก็ได้ ตัวนับจะกวาดหน้าต่างขนาดจำกัดจากท้ายไฟล์เพื่อหา startxref ตัวสุดท้าย เพราะไฟล์หนึ่งอาจมีหลายตัว และมีเพียงตัวสุดท้ายที่ใช้ได้

ตารางเดียวกันในสี่รูปแบบ#

โครงสร้างอ้างอิงไขว้ถูกกำหนดไว้ครั้งเดียวแล้วขยายอีกสามครั้ง ตัวอ่านที่รองรับเฉพาะรูปแบบดั้งเดิมจะล้มเหลวกับเอกสารจำนวนมากที่ผลิตในทศวรรษนี้

รูปแบบคืออะไรราคาของการอ่านมัน
ตาราง xref ดั้งเดิมข้อความล้วน แต่ละรายการกว้างคงที่ 20 ไบต์ จัดเป็นส่วนย่อยที่มีเลขกำกับต้องแยกโทเคน และรองรับการขึ้นบรรทัดใหม่สองแบบที่พบในไฟล์จริง
สตรีมอ้างอิงไขว้ข้อมูลชุดเดิมในรูปสตรีมไบนารีที่บีบอัด ใช้ตั้งแต่ PDF 1.5ต้องคลายบีบอัด ย้อนตัวทำนายแถวแบบ PNG ทีละไบต์ แล้วอ่านฟิลด์ที่อาเรย์ /W ประกาศความกว้างไว้
ไฟล์ลูกผสมมีทั้งสองแบบ โดยตารางดั้งเดิมไม่สมบูรณ์อย่างตั้งใจต้องสังเกต /XRefStm ใน trailer แล้วตามไป มิฉะนั้นจะนับได้เพียงบางส่วนอย่างเงียบ ๆ
การแก้ไขแบบเพิ่มทีละชั้นตัวชี้ /Prev ที่ร้อยย้อนกลับไปยังฉบับก่อนหน้าต้องเดินตามสายโดยให้ฉบับใหม่สุดชนะ และตรวจจับการวนซ้ำ

ตัวทำนายคือส่วนที่คนส่วนใหญ่ประหลาดใจ สตรีมอ้างอิงไขว้ไม่ได้ถูกบีบอัดเฉย ๆ ก่อนบีบอัดแต่ละแถวถูกแทนที่ด้วยผลต่างจากแถวก่อนหน้า ด้วยวิธีทำนายแบบเดียวกับที่ PNG ใช้กับเส้นสแกน การคลายบีบอัดจึงได้ผลต่างออกมา การกู้ตารางกลับมาต้องบวกแต่ละแถวคืนเข้ากับแถวก่อนหน้าตามลำดับ ก่อนที่จะอ่านฟิลด์ใดได้

ตัวทำนาย PNG แบบ 12 (Up) ทีละแถว
หลังคลาย   02 | 01 00 0A 00 00
           02 | 00 00 05 00 00
           ^แท็ก ^ไบต์ผลต่าง

row[i] += row[i-1] ทีละไบต์:
กู้คืนได้   01 00 0A 00 00
           01 00 0F 00 00

ความกว้างฟิลด์มาจาก /W รายการสามฟิลด์
จึงอาจเป็น 1/2/1 ไบต์ หรือ 1/4/2 ก็ได้

อ็อบเจกต์ยังอยู่ในสตรีมอ็อบเจกต์ได้ด้วย ตัวสตรีมเองก็เป็นอ็อบเจกต์ ที่ต้องหาตำแหน่งผ่านโครงสร้างอ้างอิงไขว้ชุดเดิม และคลายบีบอัดก่อน จึงจะอ่านเนื้อในได้ การไล่หาแคตตาล็อกจึงอาจต้องคลายบีบอัดสตรีมหนึ่ง เพื่อหาอ็อบเจกต์ที่ระบุสตรีมอีกตัว ซึ่งเก็บต้นไม้ของหน้าไว้ ทั้งหมดนี้ยังไม่ได้แตะพิกเซลแม้แต่จุดเดียว

การปฏิเสธคือคุณสมบัติ#

ตัวนับแยกเหตุผลของความล้มเหลวไว้สิบเอ็ดแบบภายใน และจับคู่กับรหัสสาธารณะสองรหัส เอกสารที่ติดเพียงรหัสผ่านจะได้รับคำตอบตรงตามนั้น เพราะเป็นข้อเท็จจริงที่ผู้เรียกลงมือแก้ได้ ส่วนเหตุผลที่เหลือมาถึงในรหัสเดียว ความไม่สมมาตรนี้ตั้งใจไว้ ผู้เรียกจำเป็นต้องรู้ว่าเอกสารอ่านไม่ได้ ส่วนเหตุผลควรอยู่ในบันทึกของเรา ซึ่งเป็นข้อมูลวินิจฉัย และไม่ควรกลายเป็นคำเชิญให้มาทดลองหยั่งเชิงตัวแยกวิเคราะห์

เหตุผลภายในจับอะไรได้
password_requiredรหัสผ่านที่ไม่มีก็เปิดเอกสารไม่ได้ ส่วนการเข้ารหัสเพื่อจำกัดสิทธิ์อย่างเดียว แบบที่ผู้ออกหนังสือรับรองใช้ ระบบถอดรหัสและนับหน้าให้ตามปกติ ส่วนเอกสารที่ขอรหัสผ่านจะถูกปฏิเสธก่อนตัดเครดิต เพราะการนับหน้าไว้เท่ากับสร้างคู่ตัดเครดิตกับคืนเครดิตขึ้นมาเปล่า ๆ
encryptedวิธีเข้ารหัสที่ตัวอ่านนี้ยังไม่รองรับ เช่น การเข้ารหัสด้วยกุญแจสาธารณะ เอกสารจะนับเป็นอ่านไม่ได้
cycleตำแหน่ง xref ที่ถูกเยี่ยมซ้ำ สาย /Prev ที่วนกลับ
too_deepส่วน xref เกิน 64 ส่วน ค่าซ้อนกันเกิน 64 ชั้น หรือสายอ้างอิงเกิน 32 ทอด
bad_startxrefไม่มี startxref ในหน้าต่างท้ายไฟล์ หรือตำแหน่งชี้ออกนอกไฟล์
unsupported_filterสายฟิลเตอร์ที่ตัวอ่านนี้ไม่ได้รองรับ
bad_catalog bad_pages bad_countสายไล่ไปจบที่สิ่งที่ไม่ตรงกับที่มันอ้างว่าเป็น

การคลายบีบอัดถูกจำกัดผลลัพธ์ไว้ที่ 32 MB เพราะสตรีมอ้างอิงไขว้คืออ็อบเจกต์ที่บีบอัดมาจากคนแปลกหน้า และ inflateSync ก็ยินดีจะผลิตข้อมูลขนาดกิกะไบต์จากไม่กี่กิโลไบต์ถ้ามีคนขอ

หมายเหตุ

ไม่มีทางถอยไปกวาดหาเครื่องหมายขึ้นหน้าใหม่ในไฟล์ ทางถอยนั้นให้ตัวเลขที่ถูกเป็นบางครั้ง และตัวเลขที่ถูกเป็นบางครั้งแย่กว่าการแจ้งข้อผิดพลาด เพราะข้อผิดพลาดมองเห็นได้ ส่วนการคิดเงินผิดมองไม่เห็น

เอกสารที่ระบุจำนวนหน้าของตัวเองไม่ตรงความจริง#

/Count เป็นค่าที่อยู่ในไฟล์ และไฟล์ก็ระบุเรื่องของตัวเองผิดได้ ตัวนับตรวจจับเรื่องนั้นไม่ได้ และไม่พยายามตรวจ การตรวจเกิดขึ้นที่ปลายทางแทน คือที่ขอบเขตของผู้ให้บริการประมวลผล ผลการอ่านต้องกลับมาด้วยจำนวนหน้าเท่ากับที่นับและคิดเงินไว้พอดี มิฉะนั้นการส่งมอบจะถูกปฏิเสธและคืนเครดิต

นี่คือรูปแบบคำตอบทั่วไปเมื่อการตรวจแบบราคาถูกทำให้แน่นหนาไม่ได้ ให้วางการตรวจราคาถูกไว้ในที่ของมัน แล้ววางการตรวจครั้งที่สองที่แม่นยำไว้ตรงจุดที่ความจริงปรากฏ

รูปภาพง่ายกว่า และเส้นแบ่งความเชื่อถือก็เป็นแบบเดียวกัน#

ไฟล์ PNG JPEG และ WebP เป็นสามในรูปแบบที่รับได้ 4 แบบ และนับเป็นหนึ่งหน้าต่อไฟล์ สิ่งที่สำคัญตรงนั้นคือการดมรูปแบบไฟล์ ระบบอ่านรูปแบบจากไบต์เริ่มต้น และมองข้ามฟิลด์ที่ผู้เรียกประกาศไว้ ฟิลด์ที่ประกาศคือคำกล่าวอ้างของคนแปลกหน้า ส่วนตัวไบต์คือตัวเอกสาร รูปแบบที่ดมได้คือสิ่งที่บันทึกลงข้อมูลการใช้งานและสิ่งที่ส่งต่อไปยังผู้ให้บริการ ทั้งสามอย่างจึงขัดแย้งกันไม่ได้

สิ่งที่การแลกเปลี่ยนนี้ซื้อมาได้จริง#

การคิดเงินตามจำนวนหน้าดูเหมือนรูปแบบราคาที่ง่ายที่สุดเท่าที่มี และเมื่อมองจากภายนอกมันก็เป็นเช่นนั้นจริง ความซับซ้อนไม่ได้หายไป มันย้ายเข้าไปอยู่ในโมดูลเดียวที่ไม่มีสิ่งอื่นให้พึ่งพา พร้อมชุดทดสอบที่สร้างตัวอย่างขึ้นจากโปรแกรม ซึ่งเป็นที่ที่กรณีล้มเหลวมาถึงในรูปของการทดสอบ และไม่มีใครต้องมาโต้เถียงกัน

แนวทางนี้เปิดให้ทุกคนที่คิดเงินตามเอกสารทำได้ และเป็นส่วนที่ควรค่าแก่การนำไปใช้ คือตัดสินว่าราคาขึ้นอยู่กับปริมาณอะไร ทำให้ปริมาณนั้นอ่านได้ก่อนจะผูกมัดตัวเองกับมัน และปฏิเสธเมื่ออ่านไม่ได้