1+ #!/usr/bin/env python3
2+ """
3+ Classified Information Overlay Control Extractor
4+
5+ Extracts control and enhancement information from the Classified Information Overlay PDF.
6+ Outputs a JSON file with all controls, enhancements, and their attributes.
7+
8+ Usage:f
9+ python classified_information_overlay_extractor.py <pdf_file>
10+ python classified_information_overlay_extractor.py <pdf_file> --debug-page N
11+ python classified_information_overlay_extractor.py <pdf_file> --search-9
12+ """
13+
14+ import fitz # PyMuPDF for PDF parsing
15+ import json
16+ import re
17+ import sys
18+ from typing import Dict , List
19+
20+ class ClassifiedControlExtractor :
21+ """
22+ Extracts controls and enhancements from a classified overlay PDF.
23+ """
24+ def __init__ (self , pdf_path : str ):
25+ self .pdf_path = pdf_path
26+ self .controls = {} # All extracted controls
27+ self .current_control = None # Current control/enhancement being processed
28+ self .current_base_control = None # Current base control (e.g., "AC-3")
29+ self .current_attribute = None # Current attribute being appended to
30+
31+ def extract_controls (self ) -> Dict :
32+ """
33+ Extract all controls and enhancements from the PDF.
34+ """
35+ try :
36+ doc = fitz .open (self .pdf_path )
37+ for page_num in range (len (doc )):
38+ page = doc [page_num ]
39+ self ._process_page (page , page_num + 1 )
40+ doc .close ()
41+ return self .controls
42+ except Exception as e :
43+ print (f"Error processing PDF: { e } " )
44+ return {}
45+
46+ def _process_page (self , page , page_num : int ):
47+ """
48+ Process a single PDF page: extract formatted text and parse controls/attributes.
49+ """
50+ try :
51+ text_dict = page .get_text ("dict" )
52+ formatted_text = self ._extract_formatted_text (text_dict )
53+ self ._find_controls_and_attributes (formatted_text , page_num )
54+ except Exception as e :
55+ pass
56+
57+ def _extract_formatted_text (self , text_dict : dict ) -> List [dict ]:
58+ """
59+ Extract lines of text with formatting (bold, font, etc.) from a PDF text dict.
60+ Skips footers and page numbers.
61+ """
62+ formatted_text = []
63+ for block in text_dict .get ("blocks" , []):
64+ if block .get ("type" ) == 0 : # Text block
65+ for line in block .get ("lines" , []):
66+ line_text = ""
67+ line_formats = []
68+ for span in line .get ("spans" , []):
69+ text = span .get ("text" , "" )
70+ flags = span .get ("flags" , 0 )
71+ line_text += text
72+ line_formats .append ({
73+ "text" : text ,
74+ "bold" : bool (flags & 16 ),
75+ "font" : span .get ("font" , "" ),
76+ "flags" : flags
77+ })
78+ if line_text .strip ():
79+ # Skip footers and page numbers
80+ stripped = line_text .strip ()
81+ if (stripped == "Classified Information Overlay" or
82+ stripped .isdigit () or
83+ stripped == "May 9, 2014" ):
84+ continue
85+ formatted_text .append ({
86+ "text" : stripped ,
87+ "formats" : line_formats
88+ })
89+ return formatted_text
90+
91+ def _flexible_enhancement_match (self , line_text : str ):
92+ """
93+ Match enhancement lines like 'Control Enhancement: 4, 5, 6' with flexible patterns.
94+ """
95+ patterns = [
96+ r'^Control Enhancement:\s*(\d+(?:,\s*\d+)*)$' ,
97+ r'^Control\s+Enhancement:\s*(\d+(?:,\s*\d+)*)$' ,
98+ r'^Control\s*Enhancement\s*:\s*(\d+(?:,\s*\d+)*)$' ,
99+ r'^Control Enhancement\s*:\s*(\d+(?:,\s*\d+)*).*$' ,
100+ r'^Control Enhancement\s*:\s*(\d+(?:,\s*\d+)*)\s*$'
101+ ]
102+ for pattern in patterns :
103+ match = re .match (pattern , line_text , re .IGNORECASE )
104+ if match :
105+ return match
106+ return None
107+
108+ def _find_controls_and_attributes (self , formatted_text : List [dict ], page_num : int ):
109+ """
110+ Parse formatted lines to find controls, enhancements, and their attributes.
111+ """
112+ for line_data in formatted_text :
113+ line_text = line_data ["text" ]
114+ formats = line_data ["formats" ]
115+ is_bold = any (fmt .get ("bold" , False ) for fmt in formats )
116+ # Base control: e.g., "AC-3, ACCESS ENFORCEMENT"
117+ base_control_match = re .match (r'^([A-Z]{2}-\d{1,2}),?\s*(.+)$' , line_text )
118+ enhancement_match = self ._flexible_enhancement_match (line_text )
119+ if base_control_match and is_bold :
120+ control_id = base_control_match .group (1 )
121+ control_name = base_control_match .group (2 ).strip ()
122+ self .current_control = control_id
123+ self .current_base_control = control_id
124+ self .current_attribute = None
125+ self .controls [control_id ] = {
126+ "name" : control_name ,
127+ "attributes" : {},
128+ "page" : page_num
129+ }
130+ continue
131+ elif enhancement_match :
132+ if self .current_base_control :
133+ enhancement_numbers = enhancement_match .group (1 ).split (',' )
134+ for enhancement_num in enhancement_numbers :
135+ enhancement_num = enhancement_num .strip ()
136+ enhancement_id = f"{ self .current_base_control } ({ enhancement_num } )"
137+ base_name = self .controls .get (self .current_base_control , {}).get ("name" , "" )
138+ self .controls [enhancement_id ] = {
139+ "name" : base_name ,
140+ "attributes" : {},
141+ "page" : page_num
142+ }
143+ self .current_control = f"{ self .current_base_control } ({ enhancement_numbers [- 1 ].strip ()} )"
144+ self .current_attribute = None
145+ continue
146+ # Attribute line: e.g., "Justification to Select: ..."
147+ if self .current_control and ':' in line_text :
148+ attribute_result = self ._extract_attribute_from_line (line_text )
149+ if attribute_result :
150+ attr_name , attr_content = attribute_result
151+ # Append or set attribute
152+ attrs = self .controls [self .current_control ]["attributes" ]
153+ if attr_name in attrs :
154+ attrs [attr_name ] += " " + attr_content .strip ()
155+ else :
156+ attrs [attr_name ] = attr_content .strip ()
157+ self .current_attribute = attr_name
158+ continue
159+ # Continuation of previous attribute
160+ elif self .current_control and self .current_attribute and line_text .strip ():
161+ if not (base_control_match or enhancement_match or self ._extract_attribute_from_line (line_text )):
162+ attrs = self .controls [self .current_control ]["attributes" ]
163+ attrs [self .current_attribute ] += " " + line_text .strip ()
164+
165+ def _extract_attribute_from_line (self , line_text : str ):
166+ """
167+ If line starts with a known attribute, return (attribute_name, content).
168+ """
169+ known_attributes = [
170+ "Justification to Select" ,
171+ "Supplemental Guidance" ,
172+ "Parameter Value(s)" ,
173+ "Parameter Value" ,
174+ "Regulatory/Statutory Reference(s)" ,
175+ "Control Extension" ,
176+ "Control Extension(s)" ,
177+ "Control Extension and Parameter Value(s)"
178+ ]
179+ for attr in known_attributes :
180+ if line_text .startswith (attr + ":" ):
181+ content = line_text [len (attr ) + 1 :].strip ()
182+ return attr , content
183+ return None
184+
185+ def save_to_json (self , output_file : str ):
186+ """
187+ Save extracted controls to a JSON file.
188+ """
189+ try :
190+ with open (output_file , 'w' , encoding = 'utf-8' ) as f :
191+ json .dump (self .controls , f , indent = 2 , ensure_ascii = False )
192+ print (f"\n Saved { len (self .controls )} controls to { output_file } " )
193+ except Exception as e :
194+ print (f"Error saving to JSON: { e } " )
195+
196+ def print_summary (self ):
197+ """
198+ Print a summary of extracted controls and enhancements.
199+ """
200+ print (f"\n === EXTRACTION SUMMARY ===" )
201+ print (f"Total controls extracted: { len (self .controls )} " )
202+ if not self .controls :
203+ print ("No controls found!" )
204+ return
205+ families = {}
206+ enhancements = 0
207+ base_controls = 0
208+ for control_id in self .controls .keys ():
209+ family = control_id .split ('-' )[0 ]
210+ families [family ] = families .get (family , 0 ) + 1
211+ if '(' in control_id :
212+ enhancements += 1
213+ else :
214+ base_controls += 1
215+ print (f"Base controls: { base_controls } " )
216+ print (f"Control enhancements: { enhancements } " )
217+ print ("\n Controls by family:" )
218+ for family , count in sorted (families .items ()):
219+ print (f" { family } : { count } controls" )
220+ print ("\n First 10 controls found:" )
221+ for i , (control_id , control_info ) in enumerate (list (self .controls .items ())[:10 ]):
222+ print (f" { control_id } : { control_info ['name' ]} " )
223+ if control_info ['attributes' ]:
224+ print (f" Attributes: { list (control_info ['attributes' ].keys ())} " )
225+ else :
226+ print (f" Attributes: None found" )
227+
228+ def main ():
229+ if len (sys .argv ) < 2 :
230+ print ("Usage: python classified_information_overlay_extractor.py <pdf_file>" )
231+ sys .exit (1 )
232+ pdf_file = sys .argv [1 ]
233+ output_file = "extracted_classified_information_overlay.json"
234+ print ("Classified Information Overlay Control Extractor\n " + "=" * 50 )
235+ extractor = ClassifiedControlExtractor (pdf_file )
236+ controls = extractor .extract_controls ()
237+ if controls :
238+ extractor .save_to_json (output_file )
239+ extractor .print_summary ()
240+ else :
241+ print ("No controls were extracted." )
242+
243+ if __name__ == "__main__" :
244+ main ()
0 commit comments