libbbf 0.2.4__cp312-cp312-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.2
2
+ Name: libbbf
3
+ Version: 0.2.4
4
+ Summary: Bound Book Format (BBF) tools and bindings
5
+ Home-page: https://github.com/ef1500/libbbf
6
+ Author: EF1500
7
+ Author-email: rosemilovelockofficial@proton.me
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ Dynamic: author
11
+ Dynamic: author-email
12
+ Dynamic: description
13
+ Dynamic: description-content-type
14
+ Dynamic: home-page
15
+ Dynamic: requires-python
16
+ Dynamic: summary
17
+
18
+ # libbbf-python: Bound Book Format (BBF) Tools & Python Bindings
19
+
20
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/libbbf.svg)](https://pypi.org/project/libbbf/)
21
+ [![PyPI](https://img.shields.io/pypi/v/libbbf.svg)](https://pypi.org/project/libbbf/)
22
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
23
+
24
+ ## Project Overview
25
+
26
+ `libbbf-python` provides Python bindings and command-line tools for the Bound Book Format (BBF). The Bound Book Format is an archive format designed for digital books and comics. This package allows Python developers to programmatically create and read `.bbf` files, and provides convenient command-line utilities to convert between common archive formats (like CBZ/CBX) and BBF.
27
+
28
+ The core of `libbbf-python` is a `pybind11` wrapper around the high-performance C++ `libbbf` library, ensuring speed and native integration.
29
+
30
+ ## Features
31
+
32
+ * **`libbbf` Python Bindings**: Access the full `BBFBuilder` (for creating BBF files) and `BBFReader` (for reading and extracting BBF files) functionality directly in Python.
33
+ * **`cbx2bbf` Command-Line Tool**: Convert one or more CBZ/CBX archives (or directories of images) into a single `.bbf` file. Supports advanced features like custom page ordering, section markers (chapters, volumes), and metadata.
34
+ * **`bbf2cbx` Command-Line Tool**: Extract the contents of a `.bbf` file back into a `.cbz` archive or an image directory. Supports integrity verification, section-specific extraction, and range extraction using keywords.
35
+ * **High Performance**: Leverages C++ for core operations (file I/O, hashing, memory mapping) for optimal speed.
36
+ * **Integrity Checks**: Built-in XXH3 hashing for robust data verification during creation and extraction.
37
+
38
+ ## Installation
39
+
40
+ You can install `libbbf-python` directly from PyPI:
41
+
42
+ ```bash
43
+ pip install libbbf
44
+ ```
45
+
46
+ **Note on C++ Compiler:**
47
+ For platforms where pre-built binary wheels are not available (e.g., specific Linux distributions or older Python versions), `pip` will attempt to build the package from source. This requires a C++ compiler (like GCC/Clang on Linux/macOS or MSVC on Windows) to be installed on your system.
48
+
49
+ ## Command-Line Tools Usage
50
+
51
+ Once installed, two command-line tools will be available globally: `cbx2bbf` and `bbf2cbx`.
52
+
53
+ ### `cbx2bbf`: Create BBF Files from CBZ/Images
54
+
55
+ **Usage:**
56
+ ```cbx2bbf <inputs...> [options] --output <output.bbf>
57
+ ```
58
+
59
+ **Inputs:**
60
+ Can be individual image files (`.png`, `.avif`, `.jpg`), directories containing images, or `.cbz`/`.cbx` archives.
61
+
62
+ **Options:**
63
+ * `--output <output.bbf>`, `-o <output.bbf>`: **Required.** Specifies the output BBF filename.
64
+ * `--order <path.txt>`: Use a text file to define page order. Format: `filename:index` (e.g., `cover.png:1`, `credits.png:-1` for last page).
65
+ * `--sections <path.txt>`: Use a text file to define multiple sections. Format: `Name:Target[:Parent]`.
66
+ * `--section "Name:Target[:Parent]"`: Add a single section marker. Target can be a 1-based page index or a filename. Example: `--section "Chapter 1:001.png"` or `--section "Volume 1:my_comic.cbx:Series"`.
67
+ * `--meta "Key:Value"`: Add archival metadata (e.g., `--meta "Title:Akira"`).
68
+
69
+ **Examples:**
70
+
71
+ 1. **Basic Conversion:**
72
+ ```bash
73
+ cbx2bbf my_comic.cbz -o output.bbf
74
+ ```
75
+
76
+ 2. **Converting Multiple CBZs with Sections and Metadata:**
77
+ ```bash
78
+ cbx2bbf vol1.cbz vol2.cbz \
79
+ --section "Volume 1:vol1.cbz" \
80
+ --section "Volume 2:vol2.cbz" \
81
+ --meta "Title:My Awesome Series" \
82
+ --meta "Author:Me" \
83
+ -o series.bbf
84
+ ```
85
+ *(Note: `vol1.cbz` and `vol2.cbz` as targets will automatically map to the first page of those archives after sorting)*
86
+
87
+ 3. **Converting a Directory of Images with a Custom Order File:**
88
+ ```bash
89
+ # pages.txt content:
90
+ # cover.png:1
91
+ # page001.png:2
92
+ # ...
93
+ # credits.png:-1
94
+
95
+ cbx2bbf ./my_pages_folder/ --order pages.txt -o my_book.bbf
96
+ ```
97
+
98
+ ### `bbf2cbx`: Extract BBF Files
99
+
100
+ **Usage:**
101
+ ```
102
+ bbf2cbx <input.bbf> [options] --output <output.cbz_or_dir>
103
+ ```
104
+
105
+ **Options:**
106
+ * `--output <output_path>`, `-o <output_path>`: **Required.** Output `.cbz` file or directory name.
107
+ * `--dir`: Extract to a directory instead of a `.cbz` archive.
108
+ * `--section "Name"`: Extract only a specific section defined in the BBF.
109
+ * `--rangekey "String"`: When extracting a section, find the end of extraction by matching this string against the *next* section's title. Useful for extracting "Vol 1" until "Vol 2" begins.
110
+ * `--verify`: Perform a full XXH3 integrity check on all assets and the directory hash before extraction.
111
+ * `--info`: Display book structure and metadata without extracting.
112
+
113
+ **Examples:**
114
+
115
+ 1. **Extract entire BBF to CBZ with Verification:**
116
+ ```bash
117
+ bbf2cbx my_book.bbf -o my_book_extracted.cbz --verify
118
+ ```
119
+
120
+ 2. **Extract a specific section to a folder:**
121
+ ```bash
122
+ bbf2cbx series.bbf --section "Volume 1" --dir -o ./output/vol1/
123
+ ```
124
+
125
+ 3. **Extract a range using `rangekey`:**
126
+ ```bash
127
+ bbf2cbx series.bbf --section "Volume 1" --rangekey "Volume 2" -o vol1_only.cbz
128
+ ```
129
+
130
+ 4. **Display BBF Information:**
131
+ ```bash
132
+ bbf2cbx series.bbf --info
133
+ ```
134
+
135
+ ## Contributing
136
+
137
+ Contributions, issues, and feature requests are welcome! For libbbf, free to check the [issues page](https://github.com/ef1500/libbbf/issues) (or create one if it doesn't exist yet).
138
+
139
+ ## License
140
+
141
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
142
+
143
+ ---
@@ -0,0 +1,9 @@
1
+ libbbf.cp312-win32.pyd,sha256=NMLCloFzmYQc4OkOJtHVBjlBOJ2p5gX6fZo_-T6iP0M,240640
2
+ libbbf_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ libbbf_tools/bbf2cbx.py,sha256=HbPmcVOUSTgVnkPpXEJCTRYesIhcYnn0rA91oGC9XKI,4314
4
+ libbbf_tools/cbx2bbf.py,sha256=n9qsPojUqnm_2yFyBWB4q5_4J71_lZjVdz9L8LIYLTQ,8107
5
+ libbbf-0.2.4.dist-info/METADATA,sha256=WBPIrNkDde5UEM3JEJ8uWA-0TYRIrH8Fcfa5Re8NDR4,6123
6
+ libbbf-0.2.4.dist-info/WHEEL,sha256=3-JGBxlfJ7cpG1EEEM2PrRJYpl2Lix3zyWKiPlGiWWY,97
7
+ libbbf-0.2.4.dist-info/entry_points.txt,sha256=7qX-_x7iM2ygeZmkpL_bLEuJY1TlifTetaprHlK39hA,90
8
+ libbbf-0.2.4.dist-info/top_level.txt,sha256=DM4mwVaAZp8aBb8KOwCvyYCev1_RMhsj3lXjBWDnbJc,20
9
+ libbbf-0.2.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (76.1.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-win32
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ bbf2cbx = libbbf_tools.bbf2cbx:main
3
+ cbx2bbf = libbbf_tools.cbx2bbf:main
@@ -0,0 +1,2 @@
1
+ libbbf
2
+ libbbf_tools
libbbf.cp312-win32.pyd ADDED
Binary file
File without changes
@@ -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()
@@ -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()