Skip to content

Commit da8dd6c

Browse files
committed
revert. add python code instead :-(
1 parent a9646a2 commit da8dd6c

File tree

14 files changed

+1447
-293
lines changed

14 files changed

+1447
-293
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

LICENSE

Lines changed: 0 additions & 21 deletions
This file was deleted.

MANIFEST.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include src/*.h
2+
include src/*.cpp
3+
include src/*.c

libbbf_tools/__init__.py

Whitespace-only changes.

libbbf_tools/bbf2cbx.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import argparse
2+
import zipfile
3+
import libbbf
4+
import sys
5+
import os
6+
7+
def find_section_end(sections, page_count, current_idx, range_key):
8+
start_page = sections[current_idx]['startPage']
9+
10+
# Iterate through subsequent sections
11+
for j in range(current_idx + 1, len(sections)):
12+
next_sec = sections[j]
13+
title = next_sec['title']
14+
15+
if not range_key:
16+
if next_sec['startPage'] > start_page:
17+
return next_sec['startPage']
18+
else:
19+
if range_key in title:
20+
return next_sec['startPage']
21+
22+
return page_count
23+
24+
def main():
25+
parser = argparse.ArgumentParser(description="Extract BBF to CBZ/Folder")
26+
parser.add_argument("input_bbf", help="Input .bbf file")
27+
parser.add_argument("--output", "-o", help="Output .cbz file (or directory if --dir)")
28+
parser.add_argument("--extract", action="store_true", help="Enable extraction mode (legacy flag support)")
29+
parser.add_argument("--dir", action="store_true", help="Extract to directory instead of CBZ")
30+
parser.add_argument("--section", help="Extract only specific section by name")
31+
parser.add_argument("--rangekey", help="String to match for end of range (e.g. 'Vol 2')")
32+
parser.add_argument("--verify", action="store_true", help="Verify integrity before extraction")
33+
parser.add_argument("--info", action="store_true", help="Show info and exit")
34+
35+
args = parser.parse_args()
36+
37+
reader = libbbf.BBFReader(args.input_bbf)
38+
if not reader.is_valid:
39+
print("Error: Invalid or corrupt BBF file.")
40+
sys.exit(1)
41+
42+
# Info Mode
43+
if args.info:
44+
print(f"BBF Version: 1")
45+
print(f"Pages: {reader.get_page_count()}")
46+
print(f"Assets: {reader.get_asset_count()}")
47+
print("\n[Sections]")
48+
secs = reader.get_sections()
49+
if not secs: print(" None.")
50+
for s in secs:
51+
print(f" - {s['title']:<20} (Start: {s['startPage']+1})")
52+
print("\n[Metadata]")
53+
for k, v in reader.get_metadata():
54+
print(f" - {k}: {v}")
55+
return
56+
57+
# Verify Mode
58+
if args.verify:
59+
print("Verifying assets (XXH3)...")
60+
if reader.verify():
61+
print("Integrity OK.")
62+
else:
63+
print("Integrity Check FAILED.")
64+
sys.exit(1)
65+
66+
# Extraction Mode
67+
if not args.output:
68+
print("Error: Output filename required (-o)")
69+
sys.exit(1)
70+
71+
sections = reader.get_sections()
72+
total_pages = reader.get_page_count()
73+
74+
start_idx = 0
75+
end_idx = total_pages
76+
77+
# Calculate Range
78+
if args.section:
79+
found_sec_idx = -1
80+
for i, s in enumerate(sections):
81+
if s['title'] == args.section:
82+
found_sec_idx = i
83+
break
84+
85+
if found_sec_idx == -1:
86+
print(f"Error: Section '{args.section}' not found.")
87+
sys.exit(1)
88+
89+
start_idx = sections[found_sec_idx]['startPage']
90+
end_idx = find_section_end(sections, total_pages, found_sec_idx, args.rangekey)
91+
92+
print(f"Extracting Section: '{args.section}' (Pages {start_idx+1}-{end_idx})")
93+
94+
# Perform Extraction
95+
is_zip = not args.dir
96+
97+
if is_zip:
98+
zf = zipfile.ZipFile(args.output, 'w', zipfile.ZIP_DEFLATED)
99+
else:
100+
if not os.path.exists(args.output):
101+
os.makedirs(args.output)
102+
103+
pad_len = len(str(total_pages))
104+
105+
count = 0
106+
for i in range(start_idx, end_idx):
107+
info = reader.get_page_info(i)
108+
data = reader.get_page_data(i)
109+
110+
ext = ".png"
111+
if info['type'] == 1: ext = ".avif"
112+
elif info['type'] == 3: ext = ".jpg"
113+
114+
fname = f"p{str(i+1).zfill(pad_len)}{ext}"
115+
116+
if is_zip:
117+
zf.writestr(fname, data)
118+
else:
119+
with open(os.path.join(args.output, fname), 'wb') as f:
120+
f.write(data)
121+
122+
count += 1
123+
if count % 10 == 0:
124+
print(f"\rExtracted {count} pages...", end="")
125+
126+
if is_zip: zf.close()
127+
print(f"\nDone. Extracted {count} pages.")
128+
129+
if __name__ == "__main__":
130+
main()

libbbf_tools/cbx2bbf.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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()

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["setuptools>=42", "wheel", "pybind11>=2.6.0"]
3+
build-backend = "setuptools.build_meta"

0 commit comments

Comments
 (0)