当前位置:首页>python>图片格式转换(python开发)

图片格式转换(python开发)

  • 2026-09-04 01:51:18
图片格式转换(python开发)

此工具是女朋友做图片设计用到图片转换而用ai开发出来的工具,仅供参考。软件我会放到下载里面。 方便的可以自己用。 效果图:

产物位于 `dist\图片格式转换工具.exe`,约 18 MB。---## ❓ 常见问题**Q:双击 exe 没反应?**A:首次启动单文件 exe 需要解压内置资源到临时目录,可能需要数秒。请稍等;如仍未启动,检查是否被杀毒软件拦截。**Q:AVIF 转换失败?**A:源码运行需要 Pillow 11+(内置 libavif)。运行 `python -c "from PIL import features; print(features.version('avif'))"` 确认;exe 版本已内嵌,无需关心。**Q:转换后图片颜色变了 / 出现白底?**A:JPEG / BMP / GIF 等格式不支持透明通道(alpha),程序会自动把透明区域合成到白底。如需保留透明,请选择 PNG / WebP / AVIF / TIFF 等支持透明的格式。**Q:为什么有些格式不在下拉列表里?**A:下拉列表只显示当前 Pillow **可写入**的格式。某些格式(如 EPS、PSD、HEIC 等)只能读取不能写入,因此不会出现在输出选项中。**Q:能不能保留原图的 EXIF 信息?**A:当前版本未保留 EXIF(聚焦格式转换本身)。如需保留,可自行在 `convert_one` 中用 `pillow-heif` 或 `piexif` 补充。---## 📄 依赖| 包         | 用途                  | 说明                    || ---------- | --------------------- | ----------------------- || Pillow     | 图片编解码            | 11+ 推荐(含 AVIF 支持)|| tkinter   | GUI 界面              | Python 标准库自带       || PyInstaller| 打包为 exe(可选)    | 仅打包时需要            |---## ⚠️ 免责声明本工具仅供个人/学习使用。批量处理前请先备份原文件,作者不对因使用本工具导致的任何数据损失负责。
# -*- coding: utf-8 -*-"""图片格式转换工具功能:1. AVIF -> JPG/JPEG 专用快速转换(支持批量、文件夹递归、质量调整)2. 通用图片格式互转(所有 Pillow 支持的读取/写入格式之间互转)依赖:Pillow >= 9.1(需带 AVIF 支持,Pillow 11+ 默认内置 libavif)"""from __future__ import annotationsimport osimport sysimport threadingimport tracebackfrom pathlib import Pathfrom typing import Iterablefrom PIL import Image, ImageFile# 允许 Pillow 处理截断的图片ImageFile.LOAD_TRUNCATED_IMAGES = Truetry:    import tkinter as tk    from tkinter import filedialog, messagebox, ttkexcept ImportError as e:  # pragma: no cover    raise SystemExit("未找到 tkinter,请安装 Python 的 Tk 包。") from e# ---------------------------------------------------------------------------# 格式探测# ---------------------------------------------------------------------------def _norm_ext(ext: str) -> str:    """统一扩展名表示,去掉前导点并小写。"""    return ext.lstrip(".").lower()def discover_formats() -> dict:    """    探测当前 Pillow 支持的扩展名。    返回 dict:        {            "read":   {ext_lower: format_name, ...},   # 可读取的扩展名 -> 格式名            "write":  {ext_lower: format_name, ...},  # 可写入的扩展名 -> 格式名        }    说明:        Pillow 内部用格式名(如 "JPEG")注册 OPEN/SAVE 函数,        而 registered_extensions() 返回 {ext: ImageFormat},        ImageFormat 的 .name 就是格式名。    """    read_map: dict[str, str] = {}    write_map: dict[str, str] = {}    for ext, fmt in Image.registered_extensions().items():        key = _norm_ext(ext)        # registered_extensions 的 fmt 可能是 ImageFormat 对象或字符串        if isinstance(fmt, str):            fmt_name = fmt        else:            fmt_name = getattr(fmt, "name", str(fmt))        # 用格式名查 OPEN/SAVE(Pillow 内部字典以格式名为键)        if fmt_name in Image.OPEN:            read_map[key] = fmt_name        if fmt_name in Image.SAVE:            write_map[key] = fmt_name    # 同义扩展名补全(registered_extensions 通常已包含,这里兜底)    synonyms = {        "jpg": "jpeg",        "avifs": "avif",        "tif": "tiff",        "tff": "tiff",        "jfif": "jpeg",    }    for short, long_ in synonyms.items():        if long_ in read_map and short not in read_map:            read_map[short] = read_map[long_]        if long_ in write_map and short not in write_map:            write_map[short] = write_map[long_]    return {"read": read_map, "write": write_map}# 一些格式在保存时需要特殊处理(模式转换、扩展名匹配等)_FORMAT_SAVE_HINTS: dict[str, dict] = {    # JPEG/JPG 不支持 alpha 通道    "jpeg": {"modes": ("RGB",)},    "jpg": {"modes": ("RGB",)},    # BMP 仅支持有限模式    "bmp": {"modes": ("RGB", "RGBA", "P", "L")},    # GIF 限制调色板    "gif": {"modes": ("P", "L")},    # EPS / PDF 等向量/复合格式,转过去需要 RGBA->RGB 等处理    "pdf": {"modes": ("RGB",)},    "eps": {"save_kwargs": {"scale": 1.0}},    # ICO / ICNS 尺寸集合有限制    "ico": {"sizes": ((16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256))},    "icns": {"sizes": ((16, 16), (32, 32), (64, 64), (128, 128), (256, 256), (512, 512))},    # WebP 保留透明    "webp": {},    "avif": {},    "png": {},    "tiff": {},}def _save_image(img: Image.Image, target: Path, fmt_ext: str, quality: int | None = None) -> None:    """根据目标扩展名调用 Image.save,处理模式转换与默认参数。"""    fmt_ext = _norm_ext(fmt_ext)    target.parent.mkdir(parents=True, exist_ok=True)    hint = _FORMAT_SAVE_HINTS.get(fmt_ext, {})    save_kwargs: dict = {}    # 目标格式名(PIL 接受 jpeg/png/webp 等格式名)    fmt_name_map = discover_formats()["write"]    fmt_id = fmt_name_map.get(fmt_ext) or fmt_ext.upper()    # 模式转换    allowed_modes = hint.get("modes")    if allowed_modes:        if img.mode not in allowed_modes:            if "RGB" in allowed_modes and img.mode in ("RGBA", "LA", "P"):                bg = Image.new("RGB", img.size, (255, 255, 255))                if img.mode == "P":                    img = img.convert("RGBA")                if img.mode in ("RGBA", "LA"):                    bg.paste(img, mask=img.split()[-1])                else:                    bg.paste(img)                img = bg            else:                img = img.convert(allowed_modes[0])    # JPEG 系列质量    if fmt_ext in ("jpeg", "jpg"):        save_kwargs.update({"quality": quality if quality is not None else 95,                            "subsampling": 0, "optimize": True})        if img.mode in ("RGBA", "LA", "P"):            bg = Image.new("RGB", img.size, (255, 255, 255))            if img.mode == "P":                img = img.convert("RGBA")            if img.mode == "RGBA":                bg.paste(img, mask=img.split()[-1])            else:                bg.paste(img)            img = bg    elif fmt_ext == "webp":        save_kwargs.update({"quality": quality if quality is not None else 90, "method": 6})    elif fmt_ext == "avif":        # quality 0-100,Pillow 把它转给 libavif        save_kwargs.update({"quality": quality if quality is not None else 80})    elif fmt_ext == "png":        save_kwargs.update({"optimize": True})    elif fmt_ext == "gif":        save_kwargs.update({"optimize": True, "disposal": 2})    # ICO/ICNS 尺寸约束    sizes = hint.get("sizes")    if sizes:        save_kwargs["sizes"] = sizes    save_kwargs.update(hint.get("save_kwargs", {}))    # 用 format=fmt_id 显式指定,避免仅靠扩展名歧义    img.save(target, format=fmt_id, **save_kwargs)# ---------------------------------------------------------------------------# 转换核心# ---------------------------------------------------------------------------def _iter_files(paths: Iterable[str], recursive: bool, exts: set[str]) -> list[Path]:    """从路径列表展开为文件列表,按扩展名过滤。"""    out: list[Path] = []    for p in paths:        path = Path(p)        if path.is_file():            if _norm_ext(path.suffix) in exts:                out.append(path)        elif path.is_dir():            glob = "**/*" if recursive else "*"            for f in path.glob(glob):                if f.is_file() and _norm_ext(f.suffix) in exts:                    out.append(f)    # 去重保序    seen = set()    deduped = []    for f in out:        rp = f.resolve()        if rp not in seen:            seen.add(rp)            deduped.append(f)    return dedupeddef convert_one(src: Path, dst_ext: str, out_dir: Path | None,               quality: int | None, keep_dir: bool, src_root: Path | None,               overwrite: bool) -> tuple[bool, str]:    """转换单个文件。返回 (success, message)。"""    try:        with Image.open(src) as img:            # 处理多帧(GIF/APNG/动画 WebP):取首帧            try:                img.seek(0)            except (EOFError, AttributeError):                pass            # 计算输出路径            if out_dir is None:                target_dir = src.parent            else:                if keep_dir and src_root is not None:                    rel = src.relative_to(src_root).parent                    target_dir = out_dir / rel                else:                    target_dir = out_dir            target = target_dir / f"{src.stem}.{_norm_ext(dst_ext)}"            if target.exists() and not overwrite:                return False, f"已存在(跳过): {target}"            _save_image(img, target, dst_ext, quality)        return True, str(target)    except Exception as e:        return False, f"失败 {src.name}: {e}\n{traceback.format_exc()}"# ---------------------------------------------------------------------------# GUI# ---------------------------------------------------------------------------class ConverterApp(tk.Tk):    def __init__(self):        super().__init__()        self.title("图片格式转换工具  (AVIF→JPG/JPEG · 通用互转)")        self.geometry("900x680")        self.minsize(780, 600)        self.fmts = discover_formats()        self._build_ui()    # ------------------------------------------------------------------ UI    def _build_ui(self):        nb = ttk.Notebook(self)        nb.pack(fill="both", expand=True, padx=8, pady=8)        self.avif_tab = ttk.Frame(nb)        self.generic_tab = ttk.Frame(nb)        nb.add(self.avif_tab, text="AVIF → JPG/JPEG")        nb.add(self.generic_tab, text="通用格式互转")        self._build_avif_tab()        self._build_generic_tab()        self.status_var = tk.StringVar(value="就绪。")        bar = ttk.Frame(self)        bar.pack(fill="x", side="bottom")        ttk.Label(bar, textvariable=self.status_var, anchor="w").pack(            fill="x", padx=8, pady=4)    # -------- AVIF 专用 --------    def _build_avif_tab(self):        t = self.avif_tab        t.columnconfigure(1, weight=1)        # 输入        ttk.Label(t, text="输入 AVIF 文件/文件夹:").grid(            row=0, column=0, sticky="w", padx=8, pady=8)        self.avif_input_var = tk.StringVar()        ttk.Entry(t, textvariable=self.avif_input_var).grid(            row=0, column=1, sticky="we", padx=8, pady=8)        ttk.Button(t, text="添加文件…",                   command=lambda: self._pick_files(self.avif_input_var)).grid(            row=0, column=2, padx=4, pady=8)        ttk.Button(t, text="选文件夹…",                   command=lambda: self._pick_dir(self.avif_input_var)).grid(            row=0, column=3, padx=4, pady=8)        # 输出目录        ttk.Label(t, text="输出目录(留空=源目录):").grid(            row=1, column=0, sticky="w", padx=8, pady=8)        self.avif_out_var = tk.StringVar()        ttk.Entry(t, textvariable=self.avif_out_var).grid(            row=1, column=1, sticky="we", padx=8, pady=8)        ttk.Button(t, text="选目录…",                   command=lambda: self._pick_dir(self.avif_out_var, is_dir=True)).grid(            row=1, column=2, columnspan=2, sticky="we", padx=4, pady=8)        # 目标格式        ttk.Label(t, text="目标格式:").grid(            row=2, column=0, sticky="w", padx=8, pady=8)        self.avif_tgt_var = tk.StringVar(value="jpg")        ft = ttk.Frame(t)        ft.grid(row=2, column=1, sticky="w", padx=8, pady=8)        ttk.Radiobutton(ft, text="JPG (.jpg)", value="jpg",                        variable=self.avif_tgt_var).pack(side="left")        ttk.Radiobutton(ft, text="JPEG (.jpeg)", value="jpeg",                        variable=self.avif_tgt_var).pack(side="left", padx=12)        # 选项        opts = ttk.Frame(t)        opts.grid(row=3, column=0, columnspan=4, sticky="w", padx=8, pady=4)        self.avif_recursive = tk.BooleanVar(value=True)        self.avif_keepdir = tk.BooleanVar(value=True)        self.avif_overwrite = tk.BooleanVar(value=False)        ttk.Checkbutton(opts, text="文件夹递归子目录",                        variable=self.avif_recursive).pack(side="left")        ttk.Checkbutton(opts, text="保留相对目录结构",                        variable=self.avif_keepdir).pack(side="left", padx=12)        ttk.Checkbutton(opts, text="覆盖已存在文件",                        variable=self.avif_overwrite).pack(side="left")        # 质量        qf = ttk.Frame(t)        qf.grid(row=4, column=0, columnspan=4, sticky="we", padx=8, pady=4)        ttk.Label(qf, text="JPEG 质量:").pack(side="left")        self.avif_quality = tk.IntVar(value=95)        self.avif_qscale = ttk.Scale(qf, from_=10, to=100,                                     variable=self.avif_quality,                                     orient="horizontal",                                     command=lambda v: self.avif_quality.set(int(float(v))))        self.avif_qscale.pack(side="left", fill="x", expand=True, padx=8)        self.avif_qlbl = ttk.Label(qf, textvariable=self.avif_quality, width=4)        self.avif_qlbl.pack(side="left")        # 按钮        bf = ttk.Frame(t)        bf.grid(row=5, column=0, columnspan=4, sticky="we", padx=8, pady=8)        bf.columnconfigure(0, weight=1)        self.avif_btn = ttk.Button(bf, text="开始转换 AVIF → JPG/JPEG",                                  command=self._run_avif)        self.avif_btn.pack(fill="x")        # 日志        ttk.Label(t, text="日志:").grid(row=6, column=0, sticky="nw", padx=8, pady=4)        log_frame = ttk.Frame(t)        log_frame.grid(row=6, column=1, columnspan=3, sticky="nsew",                       padx=8, pady=4)        t.rowconfigure(6, weight=1)        log_frame.columnconfigure(0, weight=1)        log_frame.rowconfigure(0, weight=1)        self.avif_log = tk.Text(log_frame, height=12, wrap="none",                                font=("Consolas", 9))        self.avif_log.grid(row=0, column=0, sticky="nsew")        sb = ttk.Scrollbar(log_frame, command=self.avif_log.yview)        sb.grid(row=0, column=1, sticky="ns")        self.avif_log.configure(yscrollcommand=sb.set)    # -------- 通用互转 --------    def _build_generic_tab(self):        t = self.generic_tab        t.columnconfigure(1, weight=1)        # 输入        ttk.Label(t, text="输入文件/文件夹:").grid(            row=0, column=0, sticky="w", padx=8, pady=8)        self.gen_input_var = tk.StringVar()        ttk.Entry(t, textvariable=self.gen_input_var).grid(            row=0, column=1, sticky="we", padx=8, pady=8)        ttk.Button(t, text="添加文件…",                   command=lambda: self._pick_files(self.gen_input_var)).grid(            row=0, column=2, padx=4, pady=8)        ttk.Button(t, text="选文件夹…",                   command=lambda: self._pick_dir(self.gen_input_var)).grid(            row=0, column=3, padx=4, pady=8)        # 输出目录        ttk.Label(t, text="输出目录(留空=源目录):").grid(            row=1, column=0, sticky="w", padx=8, pady=8)        self.gen_out_var = tk.StringVar()        ttk.Entry(t, textvariable=self.gen_out_var).grid(            row=1, column=1, sticky="we", padx=8, pady=8)        ttk.Button(t, text="选目录…",                   command=lambda: self._pick_dir(self.gen_out_var, is_dir=True)).grid(            row=1, column=2, columnspan=2, sticky="we", padx=4, pady=8)        # 目标格式下拉(仅显示可写入的扩展名)        ttk.Label(t, text="目标格式:").grid(            row=2, column=0, sticky="w", padx=8, pady=8)        write_exts = sorted(self.fmts["write"].keys())        self.gen_tgt_var = tk.StringVar(value="png" if "png" in write_exts else (write_exts[0] if write_exts else "png"))        self.gen_tgt_combo = ttk.Combobox(            t, textvariable=self.gen_tgt_var, values=write_exts,            state="readonly", width=12)        self.gen_tgt_combo.grid(row=2, column=1, sticky="w", padx=8, pady=8)        ttk.Label(t, text=f"(共 {len(write_exts)} 种可写格式)").grid(            row=2, column=2, columnspan=2, sticky="w", padx=8)        # 源格式说明        ttk.Label(            t,            text=f"支持的读取格式: {', '.join(sorted(self.fmts['read'].keys()))}",            wraplength=760, justify="left", foreground="#666").grid(            row=3, column=0, columnspan=4, sticky="we", padx=8, pady=4)        # 选项        opts = ttk.Frame(t)        opts.grid(row=4, column=0, columnspan=4, sticky="w", padx=8, pady=4)        self.gen_recursive = tk.BooleanVar(value=True)        self.gen_keepdir = tk.BooleanVar(value=True)        self.gen_overwrite = tk.BooleanVar(value=False)        self.gen_skip_same = tk.BooleanVar(value=True)        ttk.Checkbutton(opts, text="文件夹递归子目录",                        variable=self.gen_recursive).pack(side="left")        ttk.Checkbutton(opts, text="保留相对目录结构",                        variable=self.gen_keepdir).pack(side="left", padx=12)        ttk.Checkbutton(opts, text="覆盖已存在文件",                        variable=self.gen_overwrite).pack(side="left")        ttk.Checkbutton(opts, text="源=目标扩展名则跳过",                        variable=self.gen_skip_same).pack(side="left", padx=12)        # 质量        qf = ttk.Frame(t)        qf.grid(row=5, column=0, columnspan=4, sticky="we", padx=8, pady=4)        ttk.Label(qf, text="质量(对 JPG/WEBP/AVIF 有效):").pack(side="left")        self.gen_quality = tk.IntVar(value=95)        self.gen_qscale = ttk.Scale(qf, from_=10, to=100,                                    variable=self.gen_quality,                                    orient="horizontal",                                    command=lambda v: self.gen_quality.set(int(float(v))))        self.gen_qscale.pack(side="left", fill="x", expand=True, padx=8)        self.gen_qlbl = ttk.Label(qf, textvariable=self.gen_quality, width=4)        self.gen_qlbl.pack(side="left")        # 按钮        bf = ttk.Frame(t)        bf.grid(row=6, column=0, columnspan=4, sticky="we", padx=8, pady=8)        bf.columnconfigure(0, weight=1)        self.gen_btn = ttk.Button(bf, text="开始通用转换",                                  command=self._run_generic)        self.gen_btn.pack(fill="x")        # 日志        ttk.Label(t, text="日志:").grid(row=7, column=0, sticky="nw", padx=8, pady=4)        log_frame = ttk.Frame(t)        log_frame.grid(row=7, column=1, columnspan=3, sticky="nsew",                       padx=8, pady=4)        t.rowconfigure(7, weight=1)        log_frame.columnconfigure(0, weight=1)        log_frame.rowconfigure(0, weight=1)        self.gen_log = tk.Text(log_frame, height=12, wrap="none",                               font=("Consolas", 9))        self.gen_log.grid(row=0, column=0, sticky="nsew")        sb = ttk.Scrollbar(log_frame, command=self.gen_log.yview)        sb.grid(row=0, column=1, sticky="ns")        self.gen_log.configure(yscrollcommand=sb.set)    # -------------------------------------------------------------- 工具    def _pick_files(self, var: tk.StringVar):        files = filedialog.askopenfilenames(            title="选择图片文件",            filetypes=[("图片", "." + " *.".join(sorted(self.fmts["read"].keys()))),                       ("所有文件", "*.*")])        if files:            # 多文件用换行分隔显示            existing = var.get().strip()            new = "\n".join(files)            var.set(new if not existing else existing + "\n" + new)    def _pick_dir(self, var: tk.StringVar, is_dir: bool = True):        if is_dir:            d = filedialog.askdirectory(title="选择文件夹")            if d:                var.set(d)        else:            f = filedialog.askopenfilename(title="选择文件")            if f:                var.set(f)    def _log(self, widget: tk.Text, msg: str):        # 线程安全:通过 after 调度到主线程        self.after(0, self._do_log, widget, msg)    def _do_log(self, widget: tk.Text, msg: str):        widget.insert("end", msg + "\n")        widget.see("end")    def _set_busy(self, busy: bool):        self.after(0, self._do_set_busy, busy)    def _do_set_busy(self, busy: bool):        for btn in (self.avif_btn, self.gen_btn):            btn.config(state="disabled" if busy else "normal")        self.status_var.set("处理中…" if busy else "就绪。")    def _set_status(self, text: str):        self.after(0, self.status_var.set, text)    # -------------------------------------------------------------- 运行    def _parse_paths(self, text: str) -> list[str]:        # 支持换行或分号分隔        parts = [p.strip() for p in text.replace(";", "\n").split("\n") if p.strip()]        return parts    def _run_avif(self):        inputs = self._parse_paths(self.avif_input_var.get())        if not inputs:            messagebox.showwarning("提示", "请先选择 AVIF 文件或文件夹。")            return        tgt = self.avif_tgt_var.get()        out_dir = self.avif_out_var.get().strip()        out_dir_p = Path(out_dir) if out_dir else None        recursive = self.avif_recursive.get()        keep_dir = self.avif_keepdir.get()        overwrite = self.avif_overwrite.get()        quality = int(self.avif_quality.get())        # 源扩展名集合:avif 及其变体        src_exts = {"avif", "avifs", "heif", "heic"} & self.fmts["read"].keys()        if not src_exts:            messagebox.showerror("错误", "当前 Pillow 不支持读取 AVIF。")            return        files = _iter_files(inputs, recursive, src_exts)        self.avif_log.delete("1.0", "end")        if not files:            self._log(self.avif_log, "未找到任何 AVIF 文件。")            return        src_root = self._common_root([Path(p) for p in inputs])        self._set_busy(True)        threading.Thread(            target=self._worker,            args=(files, tgt, out_dir_p, quality, keep_dir, overwrite,                  self.avif_log, src_root),            daemon=True).start()    def _run_generic(self):        inputs = self._parse_paths(self.gen_input_var.get())        if not inputs:            messagebox.showwarning("提示", "请先选择输入文件或文件夹。")            return        tgt = _norm_ext(self.gen_tgt_var.get())        out_dir = self.gen_out_var.get().strip()        out_dir_p = Path(out_dir) if out_dir else None        recursive = self.gen_recursive.get()        keep_dir = self.gen_keepdir.get()        overwrite = self.gen_overwrite.get()        skip_same = self.gen_skip_same.get()        quality = int(self.gen_quality.get())        src_exts = set(self.fmts["read"].keys())        files = _iter_files(inputs, recursive, src_exts)        self.gen_log.delete("1.0", "end")        if not files:            self._log(self.gen_log, "未找到任何可识别的图片文件。")            return        src_root = self._common_root([Path(p) for p in inputs])        self._set_busy(True)        threading.Thread(            target=self._worker,            args=(files, tgt, out_dir_p, quality, keep_dir, overwrite,                  self.gen_log, src_root, skip_same),            daemon=True).start()    def _worker(self, files, tgt, out_dir, quality, keep_dir, overwrite,                log_widget, src_root=None, skip_same=False):        try:            total = len(files)            ok = 0            fail = 0            for i, f in enumerate(files, 1):                self._set_status(f"[{i}/{total}] {f.name}")                # 跳过同扩展名                if skip_same and _norm_ext(f.suffix) == _norm_ext(tgt):                    self._log(log_widget, f"[跳过-同格式] {f}")                    continue                success, msg = convert_one(                    f, tgt, out_dir, quality, keep_dir, src_root, overwrite)                if success:                    ok += 1                    self._log(log_widget, f"[OK] {f.name} -> {msg}")                else:                    fail += 1                    self._log(log_widget, f"[FAIL] {msg}")            self._log(log_widget,                      f"\n=== 完成:成功 {ok},失败 {fail},共 {total} ===")        except Exception as e:            self._log(log_widget, f"严重错误: {e}\n{traceback.format_exc()}")        finally:            self._set_busy(False)    @staticmethod    def _common_root(paths: list[Path]) -> Path | None:        if not paths:            return None        if len(paths) == 1:            p = paths[0]            return p if p.is_dir() else p.parent        # 计算公共父目录        common = list(paths[0].resolve().parts)        for p in paths[1:]:            parts = list(p.resolve().parts)            new_common = []            for a, b in zip(common, parts):                if a == b:                    new_common.append(a)                else:                    break            common = new_common            if not common:                break        if common:            return Path(*common)        return Nonedef main():    app = ConverterApp()    app.mainloop()if __name__ == "__main__":    main()

最新文章

随机文章