paths.py 5.78 KB
"""应用数据路径 + 图片格式校验工具。

跨平台数据目录探测(macOS .app 包外存储 / Windows APPDATA / 开发环境同目录),
PNG/JPEG 格式回正(Pillow 重写防止伪装 MIME)。
"""
import io
import logging
import os
import platform
import shutil
import sys
from pathlib import Path


def _migrate_data_from_app_bundle(target_path: Path):
    """将 .app 内部的旧数据迁移到外部目录(仅 macOS 打包环境)"""
    if not (getattr(sys, 'frozen', False) and platform.system() == "Darwin"):
        return

    old_path = Path(sys.executable).parent / "images"
    if not old_path.exists() or old_path == target_path:
        return

    old_files = list(old_path.rglob("*"))
    if not old_files:
        return

    try:
        target_path.mkdir(parents=True, exist_ok=True)
        migrated = 0
        for src_file in old_path.rglob("*"):
            if src_file.is_file():
                rel_path = src_file.relative_to(old_path)
                dst_file = target_path / rel_path
                if not dst_file.exists():
                    dst_file.parent.mkdir(parents=True, exist_ok=True)
                    shutil.copy2(str(src_file), str(dst_file))
                    migrated += 1
        print(f"已从 .app 内部迁移 {migrated} 个文件到: {target_path}")
    except Exception as e:
        print(f"数据迁移失败(不影响使用): {e}")


def get_app_data_path() -> Path:
    """获取应用数据存储路径 - 智能选择"""

    def get_candidate_paths():
        system = platform.system()
        candidates = []

        if getattr(sys, 'frozen', False) and system == "Darwin":
            candidates.append(Path.home() / "Library/Application Support/ZB100ImageGenerator/images")
        elif getattr(sys, 'frozen', False):
            candidates.append(Path(sys.executable).parent / "images")
        else:
            # 开发环境:保持和老路径一致 —— 项目根目录下的 images/
            # __file__ 在 core/,需要往上一级
            candidates.append(Path(__file__).resolve().parent.parent / "images")

        if system == "Darwin":
            candidates.append(Path.home() / "Library/Application Support/ZB100ImageGenerator/images")
            candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images")
        elif system == "Windows":
            candidates.append(Path(os.environ.get("APPDATA", "")) / "ZB100ImageGenerator/images")
            candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images")
        else:
            candidates.append(Path.home() / ".config/zb100imagegenerator/images")
            candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images")

        return candidates

    def test_path_write_access(path: Path) -> bool:
        try:
            path.mkdir(parents=True, exist_ok=True)
            test_file = path / ".write_test"
            test_file.write_text("test")
            test_file.unlink()
            return True
        except (PermissionError, OSError) as e:
            print(f"路径 {path} 无写入权限: {e}")
            return False
        except Exception as e:
            print(f"路径 {path} 测试失败: {e}")
            return False

    candidates = get_candidate_paths()

    for path in candidates:
        if test_path_write_access(path):
            _migrate_data_from_app_bundle(path)
            print(f"使用图片存储路径: {path}")
            return path

    fallback_path = get_candidate_paths()[0]
    try:
        fallback_path.mkdir(parents=True, exist_ok=True)
        print(f"使用备选路径: {fallback_path}")
        return fallback_path
    except Exception as e:
        print(f"警告: 无法创建存储路径,将在当前目录操作: {e}")
        return Path.cwd() / "images"


def save_png_with_validation(file_path: str, image_bytes: bytes) -> bool:
    """使用 Pillow 验证并重写 PNG/JPEG,确保 MIME 与扩展名一致。

    返回 True 表示 Pillow 处理成功;False 表示 Pillow 不可用或处理失败,
    调用方应回退到原始 write_bytes。
    """
    try:
        from PIL import Image

        with Image.open(io.BytesIO(image_bytes)) as img:
            file_format = img.format
            if file_format == 'JPEG':
                logger = logging.getLogger(__name__)
                logger.info(f"检测到伪装PNG的JPEG文件,实际格式: {file_format}")

            save_format = 'PNG' if file_path.lower().endswith('.png') else 'JPEG'

            if file_format and file_format != save_format:
                logger = logging.getLogger(__name__)
                logger.info(f"执行格式转换: {file_format} -> {save_format}")

                if save_format == 'PNG':
                    if img.mode not in ['RGBA', 'RGB', 'L']:
                        if img.mode == 'P':
                            img = img.convert('RGBA')
                        elif img.mode == 'LA':
                            img = img.convert('RGBA')
                        else:
                            img = img.convert('RGBA')
                elif save_format == 'JPEG':
                    if img.mode in ['RGBA', 'P']:
                        img = img.convert('RGB')
                    elif img.mode == 'L':
                        img = img.convert('RGB')

            img.save(file_path, save_format, optimize=True)

        logger = logging.getLogger(__name__)
        logger.info(f"图片格式验证成功: {file_path}, 保存格式: {save_format}")
        return True

    except ImportError:
        logger = logging.getLogger(__name__)
        logger.warning("Pillow库不可用,使用原始保存方法")
        return False

    except Exception as e:
        logger = logging.getLogger(__name__)
        logger.warning(f"Pillow处理失败,使用原始保存方法: {e}")
        return False