আপনি যদি কখনও কোনো PDF থেকে বোল্ড বা ইটালিক টেক্সট বের করার চেষ্টা করে থাকেন, তবে আপনি সম্ভবত একটি regex দিয়ে শুরু করেছিলেন। এটি একটি সহজ সমাধান বলে মনে হয়। ফন্ট নামের মধ্যে “Bold” শব্দটি খুঁজুন, টেক্সটটিকে চিহ্নিত করুন এবং এগিয়ে যান। এই কৌশলটি আপনাকে কেবল ততক্ষণ পর্যন্ত কাজ দেবে যতক্ষণ না আপনি একটি মিথ্যা আত্মবিশ্বাস পাচ্ছেন। তারপর কেউ একই ডকুমেন্ট Acrobat-এর অন্য কোনো ভার্সন থেকে, অথবা LibreOffice থেকে, অথবা কোনো print-to-PDF ড্রাইভার থেকে এক্সপোর্ট করে, আর তখনই আপনার কোড করা প্রতিটি ধারণা ভেঙে পড়ে।

কেন ফন্ট নাম মিথ্যা বলে

PDF.js আপনাকে ABCDEF+TimesNewRomanPS-BoldMT-এর মতো স্ট্রিং দেবে। প্রথম ছয়টি অক্ষর হলো এক্সপোর্ট করার সময় যুক্ত করা একটি র‍্যান্ডম প্রিফিক্স, যা ফাইলটি প্রতিবার নতুন করে তৈরি করার সময় পরিবর্তিত হয়। আপনার পার্সারকে (parser) এই প্রিফিক্সের ওপর নির্ভর করানো মানে হলো অনিশ্চিত কিছুর ওপর বাজি ধরা। অন্যান্য এক্সপোর্টারগুলো আরও কম সাহায্যকারী। কিছু ক্ষেত্রে Font12 বা F1-এর মতো সাধারণ আইডেন্টিফায়ার দেখা যায়। এই লেবেলগুলোর কোনো অর্থগত (semantic) তাৎপর্য নেই; এগুলো কেবল অভ্যন্তরীণ রিসোর্স ট্যাগ যা ফাইলটি লেখার সময় কাছাকাছি ছিল।

যেহেতু Portable Document Format-কে টেক্সট এক্সট্রাকশন সহজ করার জন্য ডিজাইন করা হয়নি, তাই ফন্ট নামগুলো মূলত এমবেডেড (embedded) বা সাবসেটেড (subsetted) রিসোর্সের রেফারেন্স মাত্র। এগুলো স্টাইল শনাক্ত করার জন্য কোনো স্থিতিশীল API হিসেবে কাজ করার জন্য তৈরি করা হয়নি। আপনি যখন Bold বা Italic সাবস্ট্রিং খোঁজার জন্য একটি regex লেখেন, তখন আপনি আসলে এমন একটি লেবেল স্ক্র্যাপ করছেন যা নির্মাতা অ্যাপ্লিকেশনটি তার ইচ্ছামতো ফরম্যাট করতে পারে। আপনি প্রকৃত টাইপোগ্রাফিক প্রপার্টিজ পড়ছেন না। আপনি কেবল একটি ফাইল-নামকরণ পদ্ধতি পড়ছেন, আর নিয়ম বা কনভেনশন কোনো চুক্তি নয়।

লেবেল নয়, ডেসক্রিপ্টর পড়ুন

প্রকৃত তথ্য অন্য কোথাও থাকে। PDF.js-এ প্রতিটি পেজ অবজেক্ট commonObjs এক্সপোজ করে, যা একটি ম্যাপ এবং এতে পেজ রেন্ডার করার জন্য প্রয়োজনীয় আসল ফন্ট ডেসক্রিপ্টরগুলো থাকে। লাইব্রেরিটি যখন একটি পেজ পার্স করে, তখন এটি এই ম্যাপটিকে আসল ফন্ট অবজেক্ট দিয়ে পূর্ণ করে। সেই অবজেক্টগুলো bold এবং italic-এর জন্য বুলিয়ান (boolean) প্রপার্টি প্রদান করে। এই বুলিয়ানগুলো নাম থেকে অনুমান করা হয় না। এগুলো PDF-এর ভেতরে এমবেডেড ফন্ট ডেসক্রিপ্টর থেকে আসে, যা গ্লিফ মেট্রিক্স, OS/2 table flags এবং টাইপসেটার ডকুমেন্টে যে সিম্বলিক ডেসক্রিপ্টরগুলো লিখে রেখেছে তা থেকে উদ্ভূত।

এর মানে হলো আপনি অনুমান করা বন্ধ করতে পারেন। একটি পেজের টেক্সট আইটেমগুলো প্রসেস করার আগে, page.commonObjs-এর ওপর ইটারেট করে একটি fontStyleMap তৈরি করুন। প্রতিটি ফন্ট ID-এর জন্য একটি অবজেক্ট সংরক্ষণ করুন যা ফন্ট অবজেক্ট দ্বারা প্রদান করা প্রকৃত .bold এবং .italic প্রপার্টিগুলো রেকর্ড করে। পরবর্তীতে, যখন আপনি প্রতিটি টেক্সট আইটেম প্রসেস করবেন, তখন আপনার ম্যাপে তার ফন্ট রেফারেন্সটি খুঁজুন এবং আগে থেকে গণনা করা ফ্ল্যাগগুলো পড়ুন। আপনি হঠাৎ করেই একটি নিশ্চিত (deterministic) উত্তর পেয়ে যাবেন যা এক্সপোর্টের কারণে পরিবর্তিত হবে না।

আপনি একটি ফলব্যাক (fallback) রাখতে পারেন। যদি ডেসক্রিপ্টরটি কোনোভাবে অনুপস্থিত বা অসম্পূর্ণ থাকে, তবে ফন্ট নাম থেকে প্রিফিক্স এবং র‍্যান্ডম ট্যাগগুলো সরিয়ে ফেলুন এবং যা অবশিষ্ট থাকে তার ওপর একটি রক্ষণশীল regex চালান। তবে এটি হওয়া উচিত আপনার শেষ উপায়, প্রাথমিক লজিক নয়। নির্ভরযোগ্যতার ক্ষেত্রে এর পার্থক্যটি বিশাল। যেখানে নাম-ভিত্তিক পার্সিং বিভিন্ন এক্সপোর্টারের কারণে ভেঙে পড়ে, সেখানে ডেসক্রিপ্টর-ভিত্তিক পার্সিং স্থির থাকে কারণ এটি ফাইলটিকে জিজ্ঞাসা করে যে এতে আসলে কী আছে।

যখন ফন্টও মিথ্যা বলে: সিন্থেটিক স্টাইল

এমনকি ডেসক্রিপ্টরও ভুল করতে পারে। কিছু PDF ক্রিয়েটর আলাদা ইটালিক টাইপফেস এমবেড করার ঝামেলা করতে চায় না। পরিবর্তে, তারা সোজা রোমান ফন্টটি নিয়ে একটি ট্রান্সফর্ম ম্যাট্রিক্স দিয়ে সেটিকে বাঁকিয়ে দেয়। ডিজাইন টুল বা পুরনো ওয়ার্ড প্রসেসর দ্বারা তৈরি ফাইলে এটি সাধারণ, যারা টাইপোগ্রাফিক বিশুদ্ধতার চেয়ে ফাইলের আকারের ওপর বেশি গুরুত্ব দেয়।

PDF.js-এর প্রতিটি টেক্সট আইটেমে একটি transform অ্যারে থাকে, যা একটি ছয়-উপাদান বিশিষ্ট অ্যাফাইন ম্যাট্রিক্স যা গ্লিফ কোঅর্ডিনেট সিস্টেমকে পেজ কোঅর্ডিনেট সিস্টেমে ম্যাপ করে। সেই অ্যারের তৃতীয় উপাদানটি হরিজন্টাল শিয়ার (horizontal shear) নিয়ন্ত্রণ করে। যখন সেই মানটি শূন্যের চেয়ে বেশি হয়, তখন রেন্ডারার টেক্সটটিকে যান্ত্রিকভাবে বাঁকিয়ে দেয়। আপনি যদি কেবল ফন্ট ডেসক্রিপ্টরের ওপর বিশ্বাস করেন, তবে আপনি এই টেক্সটটিকে সোজা রোমান হিসেবে শ্রেণীবদ্ধ করবেন। আপনি যদি ম্যাট্রিক্সটি পরীক্ষা করেন, তবে আপনি সিন্থেটিক ইটালিক ধরতে পারবেন এবং সঠিকভাবে চিহ্নিত করতে পারবেন। ওভারপ্রিন্টিং দ্বারা তৈরি সিন্থেটিক বোল্ডের ক্ষেত্রেও একই লজিক প্রযোজ্য, যদিও শুধুমাত্র জ্যামিতির মাধ্যমে এটি শনাক্ত করা কঠিন। বাঁকানো টেক্সটের ক্ষেত্রে, শিয়ার মানটিই হলো আপনার নিশ্চিত প্রমাণ।

আন্ডারলাইন আঁকা হয়, ঘোষণা করা হয় না

বোল্ড এবং ইটালিক হলো ফন্টের প্রপার্টি। আন্ডারলাইন তা নয়। PDF-এ, একটি আন্ডারলাইন হলো একটি ভেক্টর পাথ। রেন্ডারার টেক্সট বেসলাইনের কাছাকাছি একটি সরু অনুভূমিক রেখা আঁকার কমান্ড দেয়। এটি একটি গ্রাফিক্যাল এলিমেন্ট যা গ্লিফগুলোর নিচে অবস্থান করে, এটি কোনো cmap-এ সংরক্ষিত ক্যারেক্টার অ্যাট্রিবিউট নয়।

এই পার্থক্যটি গুরুত্বপূর্ণ কারণ ফন্ট ডিসক্রিপ্টর (font descriptor) পড়ার মাধ্যমে আন্ডারলাইন শনাক্ত করা সম্ভব নয়। আপনাকে পেজের র (raw) ড্রয়িং অপারেটর অথবা জ্যামিতিক-স্তরের (geometry-level) আউটপুটে এমন ছোট অনুভূমিক রেখা (horizontal line segments) খুঁজতে হবে যা সঠিক দূরত্বে বেসলাইনের সমান্তরালে অবস্থান করে। যখন আপনার এক্সট্রাকশন ইঞ্জিন কোনো টেক্সট রানের নিচে এমন একটি সেগমেন্ট খুঁজে পায়, তখন আপনি সেই রানটিকে আন্ডারলাইন হিসেবে ট্যাগ করেন। এটিকে একটি আলাদা ডিটেকশন লেয়ার হিসেবে বিবেচনা করা আপনার ডেটা মডেলকে নির্ভুল রাখে: বোল্ড (bold) এবং ইটালিক (italic) হলো ফন্টের সহজাত বৈশিষ্ট্য, অন্যদিকে আন্ডারলাইন হলো ডকুমেন্টের মাধ্যমে রেন্ডার করা একটি বাহ্যিক সাজসজ্জা।

একটি কার্যকর পাইপলাইন

একটি পরিচ্ছন্ন এক্সট্রাকশন সিস্টেম বিভিন্ন কাজকে আলাদা আলাদা লেয়ারে বিভক্ত করে। প্রথমে, একটি জ্যামিতিক ওয়ার্কার (geometry worker) পুরো পেজটি পর্যবেক্ষণ করে। এটি আপনার fontStyleMap তৈরি করতে page.commonObjs কুয়েরি করে, সিন্থেটিক ইটালিক (synthetic italics) শনাক্ত করতে প্রতিটি টেক্সট আইটেমের ট্রান্সফর্ম অ্যারে (transform array) পরীক্ষা করে এবং আন্ডারলাইন খুঁজে পেতে কাছাকাছি থাকা ভেক্টর পাথগুলো (vector paths) স্ক্যান করে। এর আউটপুট হলো একটি পরিচ্ছন্ন ইন্টারমিডিয়েট স্ট্রাকচার যাকে textMeta বলা যেতে পারে, যেখানে প্রতিটি টেক্সট রান তিনটি সাধারণ বুলিয়ান (boolean) মান বহন করে: bold, italic, এবং underline।

এরপর, একটি টেক্সট রিবিল্ডার (text rebuilder) সেই স্ট্রাকচারটি গ্রহণ করে এবং মার্ক-আপ করা আউটপুট প্রদান করে। এখানে নেস্টিং অর্ডার (nesting order) অত্যন্ত গুরুত্বপূর্ণ। সঠিক হায়ারার্কিতে আন্ডারলাইন থাকবে সবচেয়ে বাইরে, তারপর ইটালিক, এবং সবশেষে বোল্ড থাকবে সবচেয়ে ভেতরে। এর মানে হলো একটি সম্পূর্ণ স্টাইল করা রান হবে <u><i><b>text</b></i></u>। এই ক্রমটি ইনভ্যালিড HTML ওভারল্যাপ প্রতিরোধ করে এবং ব্রাউজার ও ডকুমেন্ট কনভার্টারগুলোর মধ্যে রেন্ডারিং সামঞ্জস্যপূর্ণ রাখে। এটি টাইপোগ্রাফিক লজিককেও প্রতিফলিত করে: সাজসজ্জা (decoration) সিম্যান্টিক এমফাসিসকে (semantic emphasis) ঘিরে রাখে, আর সিম্যান্টিক এমফাসিস স্ট্রাকচারাল ওয়েটকে (structural weight) ঘিরে রাখে।

এই পদ্ধতির শক্তি হলো এটি শুধুমাত্র সেই তথ্যগুলোই ব্যবহার করে যা PDF নিজেই নিজের সম্পর্কে জানে। এতে কোনো OCR, ক্লাউড ভিশন সার্ভিস বা রাস্টারাইজড পিক্সেল থেকে স্টাইল অনুমান করার জন্য কোনো মেশিন লার্নিং মডেলের প্রয়োজন হয় না। আপনি ফাইলের নিজস্ব সিম্যান্টিক লেয়ারটি পড়ছেন, যা জ্যামিতি এবং মেটাডেটার মাধ্যমে প্রকাশ করা হয়েছে এবং যা ক্রিয়েটর অ্যাপ্লিকেশনটি আগেই গণনা করে রেখেছে। এর ফলে ফলাফল দ্রুত, সুনির্দিষ্ট (deterministic) এবং বিভিন্ন ধরণের PDF জেনারেটরের অগোছালো পরিবেশেও নির্ভুল হয়।

আসল শিক্ষা

ফন্ট নামের বিপরীতে Regex ব্যবহার করা একটি ফাঁদ। এটি একটি শর্টকাট মনে হতে পারে কারণ এটি আপনার পরীক্ষা করা একটি ফাইলের ক্ষেত্রে কাজ করে, কিন্তু দ্বিতীয় কোনো এক্সপোর্টের সামান্য চাপে এটি ব্যর্থ হয়ে যায়। আসল তথ্য ইতিমধ্যেই PDF-এর ভেতরে ডিসক্রিপ্টর, ম্যাট্রিক্স এবং ভেক্টর পাথে বিদ্যমান। আপনার পাইপলাইন এই তথ্যগুলোর ওপর ভিত্তি করে তৈরি করুন। ফন্ট অবজেক্টকে জিজ্ঞাসা করুন সেটি বোল্ড কি না। শেয়ার (shear) শনাক্ত করতে ট্রান্সফর্ম ম্যাট্রিক্স পরীক্ষা করুন। আন্ডারলাইন খুঁজতে আঁকা সেগমেন্টগুলো দেখুন। আপনি যদি ডকুমেন্টের উপরিভাগের লেবেলগুলো স্ক্র্যাপ করার পরিবর্তে এর নিজস্ব ইঞ্জিনিয়ারিং কুয়েরি করেন, তবে আপনি এমন স্টাইল পাবেন যা এক PDF জেনারেটর থেকে অন্যটিতেও টিকে থাকে।