|
""" |
|
Kumiko Manga/Comics Panel Extractor (WebUI) |
|
Copyright (C) 2025 avan |
|
|
|
This program is a web interface for the Kumiko library. |
|
The core logic is based on Kumiko, the Comics Cutter. |
|
Copyright (C) 2018 njean42 |
|
|
|
This program is free software: you can redistribute it and/or modify |
|
it under the terms of the GNU Affero General Public License as |
|
published by the Free Software Foundation, either version 3 of the |
|
License, or (at your option) any later version. |
|
|
|
This program is distributed in the hope that it will be useful, |
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
GNU Affero General Public License for more details. |
|
|
|
You should have received a copy of the GNU Affero General Public License |
|
along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
""" |
|
|
|
import gradio as gr |
|
import os |
|
import tempfile |
|
import shutil |
|
import numpy as np |
|
import cv2 as cv |
|
|
|
|
|
import kumikolib |
|
import lib.page |
|
from manga_panel_processor import remove_border |
|
|
|
|
|
|
|
|
|
|
|
|
|
def imread_unicode(filename, flags=cv.IMREAD_COLOR): |
|
""" |
|
Replaces cv.imread to support non-ASCII paths. |
|
""" |
|
try: |
|
with open(filename, 'rb') as f: |
|
n = np.frombuffer(f.read(), np.uint8) |
|
img = cv.imdecode(n, flags) |
|
return img |
|
except Exception as e: |
|
print(f"Error reading file {filename}: {e}") |
|
return None |
|
|
|
def imwrite_unicode(filename, img): |
|
""" |
|
Replaces cv.imwrite to support non-ASCII paths. |
|
""" |
|
try: |
|
ext = os.path.splitext(filename)[1] |
|
if not ext: |
|
ext = ".jpg" |
|
result, n = cv.imencode(ext, img) |
|
if result: |
|
with open(filename, 'wb') as f: |
|
f.write(n) |
|
return True |
|
else: |
|
return False |
|
except Exception as e: |
|
print(f"Error writing to file {filename}: {e}") |
|
return False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lib.page.cv.imread = imread_unicode |
|
|
|
|
|
kumikolib.cv.imwrite = imwrite_unicode |
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_manga_images(files, output_structure, use_rtl, remove_borders, progress=gr.Progress(track_tqdm=True)): |
|
""" |
|
The main processing logic for the Gradio interface. |
|
Receives uploaded files and settings, processes them, and returns a path to a ZIP file. |
|
""" |
|
if not files: |
|
raise gr.Error("Please upload at least one image file.") |
|
|
|
|
|
|
|
|
|
panel_output_dir = tempfile.mkdtemp(prefix="kumiko_panels_") |
|
zip_output_dir = tempfile.mkdtemp(prefix="kumiko_zip_") |
|
|
|
try: |
|
|
|
image_paths = [file.name for file in files] |
|
|
|
progress(0, desc="Initializing Kumiko...") |
|
|
|
|
|
k = kumikolib.Kumiko({ |
|
'debug': False, |
|
'progress': False, |
|
'rtl': use_rtl, |
|
'panel_expansion': True, |
|
}) |
|
|
|
|
|
total_files = len(image_paths) |
|
for i, path in enumerate(image_paths): |
|
progress((i + 1) / total_files, desc=f"Analyzing: {os.path.basename(path)}") |
|
try: |
|
k.parse_image(path) |
|
except lib.page.NotAnImageException as e: |
|
print(f"Warning: Skipping file {os.path.basename(path)} because it is not a valid image. Error: {e}") |
|
continue |
|
|
|
|
|
|
|
progress(0.8, desc="Saving all panels...") |
|
nb_written_panels = 0 |
|
for page in k.page_list: |
|
original_filename_base = os.path.splitext(os.path.basename(page.filename))[0] |
|
|
|
for i, panel in enumerate(page.panels): |
|
x, y, width, height = panel.to_xywh() |
|
panel_img = page.img[y:y + height, x:x + width] |
|
|
|
|
|
if remove_borders: |
|
panel_img = remove_border(panel_img) |
|
|
|
output_filepath = "" |
|
|
|
if output_structure == "Group panels in folders": |
|
|
|
image_specific_dir = os.path.join(panel_output_dir, original_filename_base) |
|
os.makedirs(image_specific_dir, exist_ok=True) |
|
output_filename = f"panel_{i}.jpg" |
|
output_filepath = os.path.join(image_specific_dir, output_filename) |
|
else: |
|
|
|
output_filename = f"{original_filename_base}_panel_{i}.jpg" |
|
output_filepath = os.path.join(panel_output_dir, output_filename) |
|
|
|
|
|
if imwrite_unicode(output_filepath, panel_img): |
|
nb_written_panels += 1 |
|
else: |
|
print(f"\n[ERROR] Failed to write panel image to {output_filepath}\n") |
|
|
|
|
|
progress(0.9, desc="Creating ZIP archive...") |
|
if nb_written_panels == 0: |
|
raise gr.Error("Analysis complete, but no croppable panels were detected.") |
|
|
|
zip_filename_base = os.path.join(zip_output_dir, "kumiko_output") |
|
zip_filepath = shutil.make_archive(zip_filename_base, 'zip', panel_output_dir) |
|
|
|
progress(1, desc="Done!") |
|
|
|
return zip_filepath |
|
|
|
except Exception as e: |
|
|
|
raise gr.Error(f"An error occurred during processing: {e}") |
|
finally: |
|
|
|
shutil.rmtree(panel_output_dir) |
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(theme=gr.themes.Soft()) as demo: |
|
gr.Markdown( |
|
""" |
|
# Kumiko Manga/Comics Panel Extractor (WebUI) |
|
Upload your manga or comic book images. This tool will automatically analyze the panels on each page, |
|
crop them into individual image files, and package them into a single ZIP file for you to download. |
|
|
|
This application is licensed under the **GNU Affero General Public License v3.0**. |
|
The core panel detection is powered by the **kumikolib** library, created by **njean42** ([Original Project](https://github.com/njean42/kumiko)). |
|
""" |
|
) |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
image_input = gr.Files( |
|
label="Upload Manga Images", |
|
file_count="multiple", |
|
file_types=["image"], |
|
) |
|
|
|
with gr.Accordion("Advanced Settings", open=True): |
|
|
|
output_structure_choice = gr.Radio( |
|
label="ZIP File Structure", |
|
choices=["Group panels in folders", "Create a flat directory"], |
|
value="Group panels in folders", |
|
info="Choose how to organize panels in the output ZIP file." |
|
) |
|
|
|
|
|
rtl_checkbox = gr.Checkbox( |
|
label="Right-to-Left (RTL) Reading Order", |
|
value=True, |
|
info="Check this for manga that is read from right to left." |
|
) |
|
|
|
|
|
remove_borders_checkbox = gr.Checkbox( |
|
label="Attempt to remove panel borders", |
|
value=False, |
|
info="Crops the image to the content area. May not be perfect for all images." |
|
) |
|
|
|
process_button = gr.Button("Start Analysis & Cropping", variant="primary") |
|
|
|
with gr.Column(scale=1): |
|
output_zip = gr.File( |
|
label="Download Cropped Panels (ZIP)", |
|
) |
|
|
|
process_button.click( |
|
fn=process_manga_images, |
|
inputs=[image_input, output_structure_choice, rtl_checkbox, remove_borders_checkbox], |
|
outputs=output_zip, |
|
api_name="process" |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
demo.launch(inbrowser=True) |