1+ import struct
2+ import zlib
3+ import sys
4+ import os
5+
6+
7+ # ---------------------------------------------------------------------------
8+ # PNG extraction
9+ # ---------------------------------------------------------------------------
10+
11+ _PNG_SIGNATURE = b"\x89 PNG\r \n \x1a \n "
12+
13+
14+ def _extract_from_png (data : bytes ) -> str | None :
15+ #Return the GraphML string embedded in a PNG file, or None
16+ if data [:8 ] != _PNG_SIGNATURE :
17+ return None
18+
19+ pos = 8
20+ while pos + 12 <= len (data ):
21+ length = struct .unpack (">I" , data [pos : pos + 4 ])[0 ]
22+ chunk_type = data [pos + 4 : pos + 8 ]
23+ chunk_data = data [pos + 8 : pos + 8 + length ]
24+
25+ if chunk_type == b"tEXt" :
26+ try :
27+ null_pos = chunk_data .index (b"\x00 " )
28+ except ValueError :
29+ pos += 12 + length
30+ continue
31+ keyword = chunk_data [:null_pos ].decode ("latin-1" )
32+ if keyword .lower () == "graphml" :
33+ text = chunk_data [null_pos + 1 :].decode ("latin-1" )
34+ return text
35+
36+ elif chunk_type == b"zTXt" :
37+ try :
38+ null_pos = chunk_data .index (b"\x00 " )
39+ except ValueError :
40+ pos += 12 + length
41+ continue
42+ keyword = chunk_data [:null_pos ].decode ("latin-1" )
43+ if keyword .lower () == "graphml" :
44+ # compression method byte follows null, then deflate data
45+ compressed = chunk_data [null_pos + 2 :]
46+ try :
47+ text = zlib .decompress (compressed ).decode ("utf-8" )
48+ return text
49+ except Exception :
50+ pass
51+
52+ elif chunk_type == b"iTXt" :
53+ # Keyword, null, compression flag, compression method, lang,
54+ # translated keyword, null, text (may be compressed)
55+ try :
56+ null_pos = chunk_data .index (b"\x00 " )
57+ keyword = chunk_data [:null_pos ].decode ("latin-1" )
58+ if keyword .lower () == "graphml" :
59+ rest = chunk_data [null_pos + 1 :]
60+ comp_flag = rest [0 ]
61+ comp_method = rest [1 ]
62+ rest = rest [2 :]
63+ # skip language tag
64+ second_null = rest .index (b"\x00 " )
65+ rest = rest [second_null + 1 :]
66+ # skip translated keyword
67+ third_null = rest .index (b"\x00 " )
68+ text_bytes = rest [third_null + 1 :]
69+ if comp_flag == 1 :
70+ text_bytes = zlib .decompress (text_bytes )
71+ return text_bytes .decode ("utf-8" )
72+ except Exception :
73+ pass
74+
75+ elif chunk_type == b"IEND" :
76+ break
77+
78+ pos += 12 + length
79+
80+ return None
81+
82+
83+ # ---------------------------------------------------------------------------
84+ # JPEG extraction
85+ # ---------------------------------------------------------------------------
86+
87+ _JPEG_SOI = b"\xff \xd8 "
88+
89+
90+ def _extract_from_jpeg (data : bytes ) -> str | None :
91+ #Return the GraphML string embedded in a JPEG file, or None.
92+ if data [:2 ] != _JPEG_SOI :
93+ return None
94+
95+ # Strategy 1: look for raw <?xml ... </graphml> span anywhere in the file.
96+ xml_start = data .find (b"<?xml" )
97+ if xml_start == - 1 :
98+ # Also try without XML declaration
99+ xml_start = data .find (b"<graphml" )
100+ if xml_start == - 1 :
101+ return None
102+
103+ graphml_end = data .find (b"</graphml>" , xml_start )
104+ if graphml_end == - 1 :
105+ return None
106+
107+ xml_bytes = data [xml_start : graphml_end + len (b"</graphml>" )]
108+ try :
109+ return xml_bytes .decode ("utf-8" )
110+ except UnicodeDecodeError :
111+ try :
112+ return xml_bytes .decode ("latin-1" )
113+ except Exception :
114+ return None
115+
116+
117+ # ---------------------------------------------------------------------------
118+ # Public API
119+ # ---------------------------------------------------------------------------
120+
121+
122+ def extract_graphml (image_path : str ) -> str | None :
123+ try :
124+ with open (image_path , "rb" ) as fh :
125+ data = fh .read ()
126+ except OSError as exc :
127+ print (f"[extract_graphml] Cannot open '{ image_path } ': { exc } " , file = sys .stderr )
128+ return None
129+
130+ # Detect by magic bytes
131+ if data [:8 ] == _PNG_SIGNATURE :
132+ return _extract_from_png (data )
133+
134+ if data [:2 ] == _JPEG_SOI :
135+ return _extract_from_jpeg (data )
136+
137+ print (
138+ f"[extract_graphml] '{ image_path } ' is neither PNG nor JPEG." ,
139+ file = sys .stderr ,
140+ )
141+ return None
142+
143+
144+ def extract_graphml_to_file (image_path : str , output_path : str | None = None ) -> str | None :
145+ graphml = extract_graphml (image_path )
146+ if graphml is None :
147+ print (
148+ f"[extract_graphml] No embedded GraphML found in '{ image_path } '." ,
149+ file = sys .stderr ,
150+ )
151+ return None
152+
153+ if output_path is None :
154+ base , _ = os .path .splitext (image_path )
155+ # Handle double-extension names like "foo.graphml.png" -> "foo.graphml"
156+ if base .endswith (".graphml" ):
157+ output_path = base
158+ else :
159+ output_path = base + ".graphml"
160+
161+ try :
162+ with open (output_path , "w" , encoding = "utf-8" ) as fh :
163+ fh .write (graphml )
164+ print (f"[extract_graphml] Extracted GraphML written to '{ output_path } '." )
165+ return output_path
166+ except OSError as exc :
167+ print (
168+ f"[extract_graphml] Cannot write to '{ output_path } ': { exc } " ,
169+ file = sys .stderr ,
170+ )
171+ return None
172+
173+
174+ # ---------------------------------------------------------------------------
175+ # CLI entry point
176+ # ---------------------------------------------------------------------------
177+
178+ if __name__ == "__main__" :
179+ if len (sys .argv ) < 2 :
180+ print ("Usage: python extract_graphml.py <image.png|jpg> [output.graphml]" )
181+ sys .exit (1 )
182+
183+ in_path = sys .argv [1 ]
184+ out_path = sys .argv [2 ] if len (sys .argv ) >= 3 else None
185+
186+ result = extract_graphml_to_file (in_path , out_path )
187+ if result is None :
188+ sys .exit (1 )
0 commit comments