1+ import argparse
2+ import os
3+ import zipfile
4+ import shutil
5+ import tempfile
6+ import libbbf
7+ import re
8+ import sys
9+
10+
11+ class PagePlan :
12+ def __init__ (self , path , filename , order = 0 ):
13+ self .path = path
14+ self .filename = filename
15+ self .order = order # 0=unspecified, >0=start, <0=end
16+
17+ def compare_pages (a ):
18+ if a .order > 0 :
19+ return (0 , a .order )
20+ elif a .order == 0 :
21+ return (1 , a .filename )
22+ else :
23+ return (2 , a .order )
24+
25+ def trim_quotes (s ):
26+ if not s : return ""
27+ if len (s ) >= 2 and s .startswith ('"' ) and s .endswith ('"' ):
28+ return s [1 :- 1 ]
29+ return s
30+
31+ def main ():
32+ parser = argparse .ArgumentParser (description = "Mux CBZ/images to BBF (bbfenc compatible)" )
33+ parser .add_argument ("inputs" , nargs = "+" , help = "Input files (.cbz, images) or directories" )
34+ parser .add_argument ("--output" , "-o" , help = "Output .bbf file" , default = "out.bbf" )
35+
36+ # Matching bbfenc options
37+ parser .add_argument ("--order" , help = "Text file defining page order (filename:index)" )
38+ parser .add_argument ("--sections" , help = "Text file defining sections" )
39+ parser .add_argument ("--section" , action = "append" , help = "Add section 'Name:Target[:Parent]'" )
40+ parser .add_argument ("--meta" , action = "append" , help = "Add metadata 'Key:Value'" )
41+
42+ args = parser .parse_args ()
43+
44+ # 1. Parse Order File
45+ order_map = {}
46+ if args .order and os .path .exists (args .order ):
47+ with open (args .order , 'r' , encoding = 'utf-8' ) as f :
48+ for line in f :
49+ line = line .strip ()
50+ if not line : continue
51+ if ':' in line :
52+ fname , val = line .rsplit (':' , 1 )
53+ order_map [trim_quotes (fname )] = int (val )
54+ else :
55+ order_map [trim_quotes (line )] = 0
56+
57+ manifest = []
58+
59+ # We need to extract CBZs to temp to get individual file paths for the Builder
60+ # bbfenc processes directories and images. Since Python zipfile needs extraction
61+ # to pass a path to the C++ fstream, we extract everything to a temp dir.
62+ temp_dir = tempfile .mkdtemp (prefix = "bbfmux_" )
63+
64+ try :
65+ print ("Gathering inputs..." )
66+ for inp in args .inputs :
67+ inp = trim_quotes (inp )
68+
69+ if os .path .isdir (inp ):
70+ # Directory input
71+ for root , dirs , files in os .walk (inp ):
72+ for f in files :
73+ if f .lower ().endswith (('.png' , '.avif' , '.jpg' , '.jpeg' )):
74+ full_path = os .path .join (root , f )
75+ p = PagePlan (full_path , f )
76+ if f in order_map : p .order = order_map [f ]
77+ manifest .append (p )
78+
79+ elif zipfile .is_zipfile (inp ):
80+ # CBZ input
81+ print (f"Extracting { os .path .basename (inp )} ..." )
82+ with zipfile .ZipFile (inp , 'r' ) as zf :
83+ # Extract all valid images
84+ for name in zf .namelist ():
85+ if name .lower ().endswith (('.png' , '.avif' , '.jpg' , '.jpeg' )):
86+
87+ extracted_path = zf .extract (name , temp_dir )
88+ fname = os .path .basename (name )
89+
90+ p = PagePlan (extracted_path , fname )
91+
92+ if fname in order_map : p .order = order_map [fname ]
93+
94+ # Also check if the ZIP name itself has an order (less likely for pages)
95+ zip_name = os .path .basename (inp )
96+ if zip_name in order_map and len (manifest ) == 0 :
97+ # TODO: I'll figure this out LATER!
98+ pass
99+
100+ manifest .append (p )
101+ else :
102+ #Single image file
103+ fname = os .path .basename (inp )
104+ p = PagePlan (inp , fname )
105+ if fname in order_map : p .order = order_map [fname ]
106+ manifest .append (p )
107+
108+ #Sort Manifest
109+ print (f"Sorting { len (manifest )} pages..." )
110+ manifest .sort (key = compare_pages )
111+
112+ file_to_page = {}
113+ #allow --section="Vol 1":"chapter1.cbx" to work
114+ input_file_start_map = {} # TODO: DO THIS LATER!
115+
116+ for idx , p in enumerate (manifest ):
117+ file_to_page [p .filename ] = idx
118+ # For now, we rely on exact filename matching as per bbfenc.
119+
120+ # Structure: Name:Target[:Parent]
121+ sec_reqs = []
122+
123+ # From file
124+ if args .sections and os .path .exists (args .sections ):
125+ with open (args .sections , 'r' , encoding = 'utf-8' ) as f :
126+ for line in f :
127+ if not line .strip (): continue
128+ parts = [trim_quotes (x ) for x in line .strip ().split (':' )]
129+ if len (parts ) >= 2 :
130+ sec_reqs .append ({
131+ 'name' : parts [0 ],
132+ 'target' : parts [1 ],
133+ 'parent' : parts [2 ] if len (parts ) > 2 else None
134+ })
135+
136+ # From args
137+ if args .section :
138+ for s in args .section :
139+ # Naive split on colon, might break if titles have colons,
140+ # but bbfenc does simplistic parsing too.
141+ parts = [trim_quotes (x ) for x in s .split (':' )]
142+ if len (parts ) >= 2 :
143+ sec_reqs .append ({
144+ 'name' : parts [0 ],
145+ 'target' : parts [1 ],
146+ 'parent' : parts [2 ] if len (parts ) > 2 else None
147+ })
148+
149+ #Initialize Builder
150+ builder = libbbf .BBFBuilder (args .output )
151+
152+ # Write Pages
153+ print ("Writing pages to BBF..." )
154+ for p in manifest :
155+ ext = os .path .splitext (p .filename )[1 ].lower ()
156+ ftype = 2 # PNG default
157+ if ext == '.avif' : ftype = 1
158+ elif ext in ['.jpg' , '.jpeg' ]: ftype = 3
159+
160+ if not builder .add_page (p .path , ftype ):
161+ print (f"Failed to add page: { p .path } " )
162+ sys .exit (1 )
163+
164+ # Write Sections
165+ section_name_to_idx = {}
166+ # We need to process sections in order to resolve parents correctly if they refer to
167+ # sections defined earlier in the list.
168+
169+ for i , req in enumerate (sec_reqs ):
170+ target = req ['target' ]
171+ name = req ['name' ]
172+ parent_name = req ['parent' ]
173+
174+ page_index = 0
175+
176+ # Is target a number?
177+ if target .lstrip ('-' ).isdigit ():
178+ val = int (target )
179+ page_index = max (0 , val - 1 ) # 1-based to 0-based
180+ else :
181+ # It's a filename
182+ if target in file_to_page :
183+ page_index = file_to_page [target ]
184+ else :
185+ print (f"Warning: Section target '{ target } ' not found in manifest. Defaulting to Pg 1." )
186+
187+ parent_idx = 0xFFFFFFFF
188+ if parent_name and parent_name in section_name_to_idx :
189+ parent_idx = section_name_to_idx [parent_name ]
190+
191+ builder .add_section (name , page_index , parent_idx )
192+ section_name_to_idx [name ] = i # bbfenc uses index in the vector
193+
194+ # Write Metadata
195+ if args .meta :
196+ for m in args .meta :
197+ if ':' in m :
198+ k , v = m .split (':' , 1 )
199+ builder .add_metadata (trim_quotes (k ), trim_quotes (v ))
200+
201+ print ("Finalizing..." )
202+ builder .finalize ()
203+ print (f"Created { args .output } " )
204+
205+ finally :
206+ shutil .rmtree (temp_dir )
207+
208+ if __name__ == "__main__" :
209+ main ()
0 commit comments