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 ใช้กับเส้นสแกน การคลายบีบอัดจึงได้ผลต่างออกมา การกู้ตารางกลับมาต้องบวกแต่ละแถวคืนเข้ากับแถวก่อนหน้าตามลำดับ ก่อนที่จะอ่านฟิลด์ใดได้
หลังคลาย 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 แบบ และนับเป็นหนึ่งหน้าต่อไฟล์ สิ่งที่สำคัญตรงนั้นคือการดมรูปแบบไฟล์ ระบบอ่านรูปแบบจากไบต์เริ่มต้น และมองข้ามฟิลด์ที่ผู้เรียกประกาศไว้ ฟิลด์ที่ประกาศคือคำกล่าวอ้างของคนแปลกหน้า ส่วนตัวไบต์คือตัวเอกสาร รูปแบบที่ดมได้คือสิ่งที่บันทึกลงข้อมูลการใช้งานและสิ่งที่ส่งต่อไปยังผู้ให้บริการ ทั้งสามอย่างจึงขัดแย้งกันไม่ได้
สิ่งที่การแลกเปลี่ยนนี้ซื้อมาได้จริง#
การคิดเงินตามจำนวนหน้าดูเหมือนรูปแบบราคาที่ง่ายที่สุดเท่าที่มี และเมื่อมองจากภายนอกมันก็เป็นเช่นนั้นจริง ความซับซ้อนไม่ได้หายไป มันย้ายเข้าไปอยู่ในโมดูลเดียวที่ไม่มีสิ่งอื่นให้พึ่งพา พร้อมชุดทดสอบที่สร้างตัวอย่างขึ้นจากโปรแกรม ซึ่งเป็นที่ที่กรณีล้มเหลวมาถึงในรูปของการทดสอบ และไม่มีใครต้องมาโต้เถียงกัน
แนวทางนี้เปิดให้ทุกคนที่คิดเงินตามเอกสารทำได้ และเป็นส่วนที่ควรค่าแก่การนำไปใช้ คือตัดสินว่าราคาขึ้นอยู่กับปริมาณอะไร ทำให้ปริมาณนั้นอ่านได้ก่อนจะผูกมัดตัวเองกับมัน และปฏิเสธเมื่ออ่านไม่ได้