"""图片处理流水线 处理流程:双页拆分 → 裁白边 → 缩放 → 灰度 → 对比度增强 各步骤均可通过 config 开关控制。 """ from pathlib import Path from PIL import Image, ImageEnhance, ImageOps from kobo_manga.config import DeviceConfig, ProcessingConfig class ImageProcessor: """图片处理器,将漫画原图优化为适合 e-ink 设备阅读的格式。""" def __init__(self, processing: ProcessingConfig, device: DeviceConfig): self.processing = processing self.device = device def process_image(self, img: Image.Image) -> list[Image.Image]: """执行完整处理流水线。 返回列表是因为双页拆分可能产生 2 张图片。 """ images = [img] if self.processing.split_double_page: images = self._split_double_pages(images) result = [] for im in images: if self.processing.crop_whitespace: im = self._crop_whitespace(im) if self.processing.resize: im = self._resize(im) if self.processing.grayscale and not self.device.color: im = self._grayscale(im) if self.processing.enhance_contrast: im = self._enhance_contrast(im) result.append(im) return result def process_chapter( self, image_paths: list[Path], output_dir: Path ) -> list[Path]: """处理整个章节的图片,返回输出文件路径列表。 逐张处理以控制内存占用。 """ output_dir.mkdir(parents=True, exist_ok=True) output_paths = [] page_num = 1 for path in image_paths: img = Image.open(path) processed = self.process_image(img) img.close() for p_img in processed: out_path = output_dir / f"page_{page_num:03d}.jpg" # 灰度图保存为 L 模式 JPEG if p_img.mode == "L": p_img.save(out_path, "JPEG", quality=85) else: p_img.save(out_path, "JPEG", quality=85) p_img.close() output_paths.append(out_path) page_num += 1 return output_paths # ── 流水线各步骤 ────────────────────────────────────── def _split_double_pages( self, images: list[Image.Image] ) -> list[Image.Image]: """检测并拆分双页。宽高比 > 1.2 视为双页。 日漫从右往左读,所以右半页在前。 """ result = [] for img in images: w, h = img.size if w > h * 1.2: mid = w // 2 right = img.crop((mid, 0, w, h)) left = img.crop((0, 0, mid, h)) result.extend([right, left]) else: result.append(img) return result def _crop_whitespace(self, img: Image.Image) -> Image.Image: """裁剪图片周围的白色/浅色边框。""" # 转灰度检测边界 if img.mode == "RGBA": gray = img.convert("RGB").convert("L") else: gray = img.convert("L") # 反转后白边变黑(0),getbbox() 找非零区域 inverted = ImageOps.invert(gray) bbox = inverted.getbbox() if bbox is None: # 全白页,不裁剪 return img # 安全检查:裁剪后面积不应小于原图 50% orig_area = img.size[0] * img.size[1] crop_w = bbox[2] - bbox[0] crop_h = bbox[3] - bbox[1] if crop_w * crop_h < orig_area * 0.5: return img # 加 2px margin margin = 2 x0 = max(0, bbox[0] - margin) y0 = max(0, bbox[1] - margin) x1 = min(img.size[0], bbox[2] + margin) y1 = min(img.size[1], bbox[3] + margin) return img.crop((x0, y0, x1, y1)) def _resize(self, img: Image.Image) -> Image.Image: """缩放到设备分辨率内,保持宽高比,只缩小不放大。""" w, h = img.size target_w = self.device.width target_h = self.device.height # 已经小于等于目标尺寸,不处理 if w <= target_w and h <= target_h: return img scale = min(target_w / w, target_h / h) new_w = int(w * scale) new_h = int(h * scale) return img.resize((new_w, new_h), Image.Resampling.LANCZOS) def _grayscale(self, img: Image.Image) -> Image.Image: """转灰度。""" return img.convert("L") def _enhance_contrast(self, img: Image.Image) -> Image.Image: """增强对比度。""" enhancer = ImageEnhance.Contrast(img) return enhancer.enhance(self.processing.contrast_factor)