惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - *感悟人生*

PDFtoEXCEL批量处理高保真同步格式 JS 逆向与前端安全加固实战指南 邮件群发系统 注册授权--续 独立授权模块 --可以为你的程序或者工具加上一把锁 音频转换合并切割工具 修改文件重命名(1、默认去掉预览和备份,2、默认当前文件路径) AI 平台 SQL语句解析 扣代码中,遇到this 应该怎么处理 AST解OB混淆,适用大部分的混淆 Akamai的形成与风控分析:聚焦Akamai 3.0 执行py3o的重启脚本(包含手动执行,以及自动执行的脚本) reduce与map+filter的复杂计算场景 py3o中汇总的计算:sum reduce map 三种形式来处理对比 py3o中数字金额转大写 加密解密基本概念 MJ提示词自动批处理GUI版 uv 在 Python 开发中的常用命令详解 表单和载荷的区别,以及python和js在处理json时的空格问题。 R函数处理异步迭代,在爬虫中的作用。
批量处理苹果电脑的HEIF格式转成JPG|PNG
*感悟人生* · 2026-03-09 · via 博客园 - *感悟人生*
# -*- coding: utf-8 -*-
"""
HEIF 转文件格式 - 批量转换工具
三松强哥出品,必属精品。2027年5月前可用,过期后静默禁用,不提示。
"""

import os
import sys
from datetime import date
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from pathlib import Path
from typing import List, Tuple

# 过期日期:2027年5月1日起不可用(不提示,全部禁用)
EXPIRY_DATE = date(2027, 5, 1)


def _get_app_base_dir() -> str:
    """程序所在目录:脚本运行时为脚本目录,打包 exe 后为 exe 所在目录。"""
    if getattr(sys, "frozen", False):
        base = os.path.dirname(sys.executable)
    else:
        base = os.path.dirname(os.path.abspath(__file__))
    return base if base else os.getcwd()


def _get_watermark_path() -> Path:
    """用于防改时间的记录文件路径(AppData)。"""
    appdata = os.environ.get("APPDATA", "")
    if not appdata:
        appdata = os.path.expanduser("~")
    folder = Path(appdata) / "HEIFtoPIC_SSQG"
    folder.mkdir(parents=True, exist_ok=True)
    return folder / "dt"


def _is_expired() -> bool:
    """
    是否已过期。2027年5月及之后视为过期;若检测到本地时间被回拨(曾记录过更大日期),也视为过期。
    过期不弹窗、不提示,仅返回 True,由调用方禁用界面。
    """
    today = date.today()
    if today >= EXPIRY_DATE:
        return True
    path = _get_watermark_path()
    max_seen = None
    try:
        if path.exists():
            raw = path.read_text().strip()
            if raw:
                max_seen = date.fromisoformat(raw)
    except Exception:
        pass
    if max_seen is not None and today < max_seen:
        return True
    new_max = max(today, max_seen) if max_seen else today
    try:
        path.write_text(new_max.isoformat())
    except Exception:
        pass
    return False

# 延迟导入,便于在无 GUI 时做模块检查
try:
    from PIL import Image
    from pillow_heif import register_heif_opener
except ImportError as e:
    print("请先安装依赖: pip install -r requirements.txt")
    raise SystemExit(1) from e

# 注册 HEIF 解码器,使 Pillow 能打开 .heic/.heif
register_heif_opener()

# 支持的 HEIF 输入扩展名
HEIF_EXTENSIONS = (".heic", ".heif")
# 目标格式及对应扩展名
TARGET_FORMATS = [
    ("JPEG", ".jpg"),
    ("PNG", ".png"),
]


def find_heif_files(folder: str) -> List[Path]:
    """递归扫描文件夹及所有子目录中的 HEIF/HEIC 文件。"""
    folder_path = Path(folder).resolve()
    if not folder_path.is_dir():
        return []
    files = []
    for f in folder_path.rglob("*"):
        if f.is_file() and f.suffix.lower() in HEIF_EXTENSIONS:
            files.append(f)
    return sorted(files)


def convert_one(
    src_path: Path,
    out_folder: str,
    target_ext: str,
    target_format: str,
    input_root: str | Path | None = None,
) -> Tuple[bool, str]:
    """
    转换单个 HEIF 文件。
    若提供 input_root,则按相对路径在 out_folder 下保持相同目录结构;否则输出到 out_folder 根目录。
    返回 (是否成功, 消息)。
    """
    try:
        out_base = Path(out_folder).resolve()
        if input_root is not None:
            input_root = Path(input_root).resolve()
            rel = src_path.resolve().relative_to(input_root)
            out_path = out_base / rel.parent / (rel.stem + target_ext)
            out_path.parent.mkdir(parents=True, exist_ok=True)
        else:
            out_path = out_base / (src_path.stem + target_ext)
        img = Image.open(src_path)
        if img.mode in ("RGBA", "P") and target_format == "JPEG":
            img = img.convert("RGB")
        img.save(out_path, format=target_format, quality=95)
        if input_root is not None:
            return True, f"成功: {rel} -> {out_path.relative_to(out_base)}"
        return True, f"成功: {src_path.name} -> {out_path.name}"
    except Exception as e:
        return False, f"失败: {src_path} - {type(e).__name__}: {e}"


class HeifConverterApp:
    """主窗口:三松强哥出品,必属精品。过期后静默禁用,不提示。"""

    def __init__(self):
        self.root = tk.Tk()
        self.root.title("HEIF 转文件格式 批量转换工具 - 三松强哥出品,必属精品")
        self.root.geometry("620x460")
        self.root.resizable(True, True)
        self.root.configure(bg="#f0f0f0")

        self.expired = _is_expired()
        self.failed_list: List[Path] = []
        self._last_input_folder: str | None = None
        self._default_dir = _get_app_base_dir()

        self._build_ui()
        if self.expired:
            self._disable_all()

    def _build_ui(self):
        main = ttk.Frame(self.root, padding="12 8")
        main.pack(fill=tk.BOTH, expand=True)

        # ---------- 品牌字样 ----------
        brand = ttk.Label(main, text="三松强哥出品,必属精品", font=("Microsoft YaHei", 10, "bold"))
        brand.pack(anchor=tk.W, pady=(0, 8))

        # ---------- 选择要转换的图片文件夹(默认当前/程序所在目录) ----------
        row1 = ttk.Frame(main)
        row1.pack(fill=tk.X, pady=(0, 6))
        ttk.Label(row1, text="选择要转换的图片文件夹:").pack(side=tk.LEFT, padx=(0, 8))
        self.input_var = tk.StringVar(value=self._default_dir)
        self.entry_input = ttk.Entry(row1, textvariable=self.input_var, width=50)
        self.entry_input.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8))
        self.btn_browse_in = ttk.Button(row1, text="浏览", command=self._browse_input)
        self.btn_browse_in.pack(side=tk.LEFT)

        # ---------- 选择转换后的图片文件夹(默认当前/程序所在目录) ----------
        row2 = ttk.Frame(main)
        row2.pack(fill=tk.X, pady=(0, 6))
        ttk.Label(row2, text="选择转换后的图片文件夹:").pack(side=tk.LEFT, padx=(0, 8))
        self.output_var = tk.StringVar(value=self._default_dir)
        self.entry_output = ttk.Entry(row2, textvariable=self.output_var, width=50)
        self.entry_output.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8))
        self.btn_browse_out = ttk.Button(row2, text="浏览", command=self._browse_output)
        self.btn_browse_out.pack(side=tk.LEFT)

        # ---------- 目标格式 ----------
        row3 = ttk.Frame(main)
        row3.pack(fill=tk.X, pady=(0, 8))
        ttk.Label(row3, text="目标格式:").pack(side=tk.LEFT, padx=(0, 8))
        self.format_var = tk.StringVar(value="JPEG")
        self.combo = ttk.Combobox(
            row3,
            textvariable=self.format_var,
            values=[f[0] for f in TARGET_FORMATS],
            state="readonly",
            width=12,
        )
        self.combo.pack(side=tk.LEFT)

        # ---------- 状态/日志区域 ----------
        log_frame = ttk.LabelFrame(main, text="转换状态与日志", padding=4)
        log_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 8))
        self.log_text = scrolledtext.ScrolledText(
            log_frame,
            height=12,
            wrap=tk.WORD,
            font=("Consolas", 9),
            state=tk.DISABLED,
        )
        self.log_text.pack(fill=tk.BOTH, expand=True)

        # ---------- 按钮 ----------
        btn_frame = ttk.Frame(main)
        btn_frame.pack(fill=tk.X)
        self.btn_start = ttk.Button(btn_frame, text="开始转换", command=self._start_convert)
        self.btn_start.pack(side=tk.LEFT, padx=(0, 12))
        self.btn_retry = ttk.Button(btn_frame, text="重试转换失败文件", command=self._retry_failed)
        self.btn_retry.pack(side=tk.LEFT)

    def _disable_all(self):
        """过期时禁用所有可操作控件,不弹任何提示。"""
        self.entry_input.configure(state=tk.DISABLED)
        self.entry_output.configure(state=tk.DISABLED)
        self.combo.configure(state=tk.DISABLED)
        self.btn_browse_in.configure(state=tk.DISABLED)
        self.btn_browse_out.configure(state=tk.DISABLED)
        self.btn_start.configure(state=tk.DISABLED)
        self.btn_retry.configure(state=tk.DISABLED)

    def _log(self, msg: str):
        """在日志区追加一行并滚动到底。"""
        self.log_text.configure(state=tk.NORMAL)
        self.log_text.insert(tk.END, msg + "\n")
        self.log_text.see(tk.END)
        self.log_text.configure(state=tk.DISABLED)
        self.root.update_idletasks()

    def _browse_input(self):
        path = filedialog.askdirectory(title="选择要转换的图片文件夹")
        if path:
            self.input_var.set(path)

    def _browse_output(self):
        path = filedialog.askdirectory(title="选择转换后的图片文件夹")
        if path:
            self.output_var.set(path)

    def _get_target_format_and_ext(self) -> Tuple[str, str]:
        name = self.format_var.get().strip().upper()
        for fmt, ext in TARGET_FORMATS:
            if fmt == name:
                return fmt, ext
        return "JPEG", ".jpg"

    def _start_convert(self):
        inp = self.input_var.get().strip()
        out = self.output_var.get().strip()
        if not inp:
            messagebox.showwarning("提示", "请先选择要转换的图片文件夹。")
            return
        if not out:
            messagebox.showwarning("提示", "请先选择转换后的图片保存文件夹。")
            return
        if not os.path.isdir(inp):
            messagebox.showerror("错误", f"输入文件夹不存在:{inp}")
            return
        if not os.path.isdir(out):
            messagebox.showerror("错误", f"输出文件夹不存在:{out}")
            return

        target_format, target_ext = self._get_target_format_and_ext()
        files = find_heif_files(inp)
        if not files:
            self._log("未在输入文件夹中发现 HEIF/HEIC 文件。")
            messagebox.showinfo("提示", "该文件夹中没有找到 .heic 或 .heif 文件。")
            return

        self.failed_list.clear()
        self._last_input_folder = inp
        self._log(f"开始转换(含子目录),共 {len(files)} 个文件,目标格式: {target_format}")
        ok_count = 0
        for src in files:
            success, msg = convert_one(src, out, target_ext, target_format, input_root=inp)
            self._log(msg)
            if success:
                ok_count += 1
            else:
                self.failed_list.append(src)
        self._log(f"完成。成功: {ok_count},失败: {len(self.failed_list)}")
        if self.failed_list:
            messagebox.showinfo(
                "转换结束",
                f"成功 {ok_count} 个,失败 {len(self.failed_list)} 个。可点击「重试转换失败文件」重试。",
            )
        else:
            messagebox.showinfo("转换结束", f"全部完成,共转换 {ok_count} 个文件。")

    def _retry_failed(self):
        if not self.failed_list:
            self._log("当前没有失败记录,请先执行一次「开始转换」。")
            messagebox.showinfo("提示", "没有需要重试的失败文件。")
            return

        out = self.output_var.get().strip()
        if not out or not os.path.isdir(out):
            messagebox.showwarning("提示", "请先选择有效的「转换后的图片文件夹」。")
            return

        target_format, target_ext = self._get_target_format_and_ext()
        input_root = getattr(self, "_last_input_folder", None)
        self._log("--- 重试转换失败文件 ---")
        still_failed = []
        for src in self.failed_list:
            success, msg = convert_one(src, out, target_ext, target_format, input_root=input_root)
            self._log(msg)
            if not success:
                still_failed.append(src)
        self.failed_list = still_failed
        self._log(f"重试完成。仍失败: {len(still_failed)} 个")
        if not still_failed:
            messagebox.showinfo("重试完成", "失败文件已全部转换成功。")

    def run(self):
        self.root.mainloop()


def main():
    app = HeifConverterApp()
    app.run()


if __name__ == "__main__":
    main()