#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
エロ漫画品質自動チェッカー v1.0
DLsite/FANZA出品前の技術仕様チェックを自動化
使用方法: python quality_checker.py <zipファイルパス>
依存: Pillow (pip install Pillow)
"""
import sys, os, zipfile, re
from pathlib import Path
try:
from PIL import Image
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
print("[WARNING] Pillow未インストール: pip install Pillow")
# ===== 設定 =====
CONFIG = {
"min_dpi": 300, # T001: 推奨300dpi以上
"min_dpi_strict": 600, # T001: 厳密600dpi以上
"jpeg_quality": 90, # T002: JPEG品質90%以上
"max_file_mb": 10, # T006: 1ページ最大10MB
"mosaic_min_px": 4, # T018: モザイク最小4px
"required_width": 560, # M001: パッケージ画像幅
"required_height": 420, # M001: パッケージ画像高さ
"min_sample": 5, # M019: サンプル最低5枚
"max_total_mb": 500, # ZIPファイル最大500MB
}
VALID_EXTENSIONS = {'.jpg', '.jpeg', '.png'}
FORBIDDEN_FILES = {'.ds_store', 'thumbs.db', 'desktop.ini'}
FILENAME_PATTERN = re.compile(r'^\d{3,4}\.(jpg|jpeg|png)$', re.IGNORECASE)
FULLWIDTH_PATTERN = re.compile(r'[^\x00-\x7F]')
class QualityChecker:
def __init__(self, zip_path: str):
self.zip_path = Path(zip_path)
self.results = []
self.errors = []
self.warnings = []
self.score = 100
def check(self, item_id, condition, msg_ok, msg_fail, severity="ERROR"):
if condition:
self.results.append(("PASS", item_id, msg_ok))
else:
self.results.append(("FAIL", item_id, msg_fail))
if severity == "ERROR":
self.errors.append(f"[{item_id}] {msg_fail}")
self.score -= 5
else:
self.warnings.append(f"[{item_id}] {msg_fail}")
self.score -= 2
def run(self):
if not self.zip_path.exists():
print(f"[ERROR] ファイルが存在しません: {self.zip_path}")
return
print(f"\n{'='*60}")
print(f" エロ漫画品質チェッカー v1.0")
print(f" 対象: {self.zip_path.name}")
print(f"{'='*60}\n")
# T006系: ZIPファイルサイズ
zip_mb = self.zip_path.stat().st_size / 1024 / 1024
self.check("T-ZIP", zip_mb <= CONFIG["max_total_mb"],
f"ZIPサイズ {zip_mb:.1f}MB (正常範囲内)",
f"ZIPサイズ {zip_mb:.1f}MB が {CONFIG['max_total_mb']}MB超過",
"WARN")
with zipfile.ZipFile(self.zip_path, 'r') as zf:
names = zf.namelist()
images = []
bad_names = []
for name in names:
fn = Path(name).name.lower()
# T016: 隠しファイルチェック
if fn in FORBIDDEN_FILES:
self.check(f"T016-{fn}", False,
"", f"隠しファイル検出: {name}", "ERROR")
ext = Path(name).suffix.lower()
if ext not in VALID_EXTENSIONS:
if ext: # 拡張子ありファイルのみ
bad_names.append(name)
continue
images.append(name)
# T009: ファイル名連番チェック
bad_numbering = [n for n in images
if not FILENAME_PATTERN.match(Path(n).name)]
self.check("T009",
len(bad_numbering) == 0,
f"全{len(images)}ファイル連番命名OK",
f"連番命名エラー: {bad_numbering[:5]}", "ERROR")
# T010: フォルダ混入チェック
has_folder = any('/' in n for n in names if n.endswith('/'))
self.check("T010", not has_folder,
"ZIP内フォルダなし (正常)",
"ZIP内にフォルダが存在します。フラット構造にしてください", "ERROR")
# T011: 全角・スペースチェック
fullwidth_names = [n for n in names
if FULLWIDTH_PATTERN.search(Path(n).name)]
self.check("T011", len(fullwidth_names) == 0,
"全ファイル名: 半角英数字のみ (正常)",
f"全角/スペース含むファイル名: {fullwidth_names[:3]}", "ERROR")
# T013: 拡張子小文字統一
upper_ext = [n for n in images
if Path(n).suffix != Path(n).suffix.lower()]
self.check("T013", len(upper_ext) == 0,
"全拡張子: 小文字統一 (正常)",
f"大文字拡張子: {upper_ext[:3]}", "WARN")
# 画像解像度チェック (Pillow必須)
if PIL_AVAILABLE and images:
print(f" 画像チェック中 ({len(images)}枚)...")
low_res = []
cmyk_files = []
large_files = []
for img_name in images[:20]: # 最初20枚をサンプルチェック
try:
with zf.open(img_name) as f:
img_data = f.read()
file_mb = len(img_data) / 1024 / 1024
# T006: ファイルサイズ
if file_mb > CONFIG["max_file_mb"]:
large_files.append(f"{img_name} ({file_mb:.1f}MB)")
# PIL解析
import io
img = Image.open(io.BytesIO(img_data))
dpi = img.info.get('dpi', (72, 72))
dpi_val = dpi[0] if isinstance(dpi, tuple) else dpi
# T001: 解像度
if dpi_val < CONFIG["min_dpi"]:
low_res.append(f"{img_name} ({dpi_val:.0f}dpi)")
# T005/T008: カラーモード
if img.mode == 'CMYK':
cmyk_files.append(img_name)
except Exception as e:
self.warnings.append(f"画像読み込みエラー {img_name}: {e}")
self.check("T001", len(low_res) == 0,
f"解像度チェック: 全サンプル {CONFIG['min_dpi']}dpi以上",
f"解像度不足: {low_res[:3]}", "ERROR")
self.check("T008", len(cmyk_files) == 0,
"カラーモード: 全てRGB (正常)",
f"CMYKファイル検出: {cmyk_files[:3]}", "ERROR")
self.check("T006", len(large_files) == 0,
f"ファイルサイズ: 全て{CONFIG['max_file_mb']}MB以下",
f"サイズ超過ファイル: {large_files[:3]}", "WARN")
# ページ数チェック
self.check("PAGE", len(images) >= 8,
f"ページ数: {len(images)}P (十分)",
f"ページ数が{len(images)}P と少なすぎます (最低8P推奨)", "WARN")
# 結果表示
self._print_results()
def _print_results(self):
print("\n" + "="*60)
print(" チェック結果")
print("="*60)
pass_count = sum(1 for r in self.results if r[0] == "PASS")
fail_count = sum(1 for r in self.results if r[0] == "FAIL")
for status, item_id, msg in self.results:
icon = "✓" if status == "PASS" else "✗"
print(f" {icon} [{item_id}] {msg}")
print(f"\n{'='*60}")
print(f" PASS: {pass_count} FAIL: {fail_count}")
print(f" 技術スコア: {max(0, self.score)}/100")
if self.errors:
print(f"\n [ERRORS - 要修正]")
for e in self.errors:
print(f" {e}")
if self.warnings:
print(f"\n [WARNINGS - 推奨修正]")
for w in self.warnings:
print(f" {w}")
status = "出品可能" if not self.errors else "要修正後に再チェック"
print(f"\n 最終判定: {status}")
print("="*60 + "\n")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("使用方法: python quality_checker.py <zipファイルパス>")
sys.exit(1)
checker = QualityChecker(sys.argv[1])
checker.run()