How to Detect WARNING and CAUTION Boxes in PDFs Using Only Vector Geometry
TLDR
Keyword text matching (searching for"WARNING") incorrectly flags prose that mentions warnings rather than callout containers. Ground-truth callout box extraction tracks graphics state stacks (q/Q), records fillColor active during rectangle paint operations (re, f), filters out neutral background fills (near-black PDF defaults and near-white page surfaces), and pairs enclosed text nodes to emit semantic HTML <aside class="pdf-box pdf-box--warning"> elements.
| Extraction Step | Graphics State Mechanism | Classification Criteria | Output Element |
|---|---|---|---|
| Color Stack Tracking | q (Save) / Q (Restore) | Captures active fillColor on re | filledRect record |
| Neutral Fill Filtering | c > 0.92 (White) / c < 0.08 (Black) | Filters PDF defaults & page backgrounds | Chromatic callout fills |
| Role Assignment | Containment matching + Text Regex | Encloses text matching /warning/i | <aside class="pdf-box"> |
Technical architecture & implementation
1. Graphics state stack color tracking
Maintain a color state stack acrossq (save) and Q (restore) operator executions inside ctmAdapter:
// ctmAdapter: Color state stack tracking
const colorStateStack = [{ fill: [0, 0, 0], stroke: [0, 0, 0] }];
let currentColorState = { fill: [0, 0, 0], stroke: [0, 0, 0] };
export function processColorOperator(op, args) { if (op === OPS.save) { colorStateStack.push({ fill: currentColorState.fill.slice(), stroke: currentColorState.stroke.slice() }); } else if (op === OPS.restore) { const restored = colorStateStack.pop(); if (restored) { currentColorState = { fill: restored.fill.slice(), stroke: restored.stroke.slice() }; } } }
2. Neutral fill color filtering & role classification
Filter out neutral fills (near-white page surfaces and near-black PDF defaults) before classifying callout roles:// Filter neutral fill colors and assign callout roles based on enclosed text
export function classifySemanticBoxRegion(filledRect, enclosedTextItems) {
const fc = filledRect.fillColor;
// Filter out neutral fill colors: near-white (page) and near-black (PDF default) const isNeutralColor = !fc || fc.every(c => c > 0.92) || fc.every(c => c < 0.08);
if (isNeutralColor) return null;
const combinedText = enclosedTextItems.map(item => item.str).join(' '); let role = 'generic';
if (/warning|danger/i.test(combinedText)) role = 'warning'; else if (/caution/i.test(combinedText)) role = 'caution'; else if (/note|notice|important/i.test(combinedText)) role = 'note'; else if (/tip|hint/i.test(combinedText)) role = 'tip';
return { type: 'BOX', role, fillColorRgb: rgb(${Math.round(fc[0]255)}, ${Math.round(fc[1]255)}, ${Math.round(fc[2]*255)}), textItems: enclosedTextItems }; }
<!-- Assembled HTML Callout Element -->
<aside class="pdf-box pdf-box--warning" style="background: rgb(255, 245, 245)">
<p><strong>WARNING</strong></p>
<p>Do not operate equipment without proper ventilation.</p>
</aside>
Rule of thumb: Track graphics state stacks (q/Q) to capture active fill colors, filtering out neutral page/default fills before classifying callout roles.