简易的照片监控打印程序

背景:

由于近些天需要辅助照片打印,而摄影师和打印彼此独立进行,不能使用像“拍马传”这种联网传照片,摄影打印一体化的方式。因此拿到摄影师照片的方式只能是拿他的摄像机的存储卡。随后进行存储卡的轮换。打印时再借助拍马,将需要打印的照片复制到打印文件夹中,拍马便会自动打印对应的图片,并将文件夹中的图片删除。

但是上述做法存在几个问题:

①轮换存储卡时,你可能无法记得之前拷贝过哪些文件,为了保证拍出来的照片都保存下来,只能是对存储卡中拍摄出来的照片不加分辨的全部拷贝,然后放到一个文件夹中;结果导致的是文件夹中的拷贝照片越积越多,给照片筛选造成困难;

②摄影师为了保证有合格照片不可避免地进行连拍;连拍造成的照片冗余,对筛选照片同样造成困难;这种冗余不是照片拷贝时可以避免的,因此属于不可避免的冗余;

③无法准确回忆已经打印过的照片;因为打印文件夹中的文件会被自动移除,为了能保证拍出来的合格照片都被打印出来,我们需要记住打印到什么位置了。我们可能本来是计划严格按照时间顺序进行照片打印的,但是有时会因为有人催促或其他因素导致打印顺序发生错乱;此时我们难以准确记忆我们究竟打印到哪一张照片以及打印了多少张;

解决之道:

针对上述问题,仔细思考便能发现解决之道:

针对①,可以直接通过程序判断两张图片的修改时间是否相同;相同可以认定两张图片就是一模一样的了,此时我们就可以直接将这张重复图片给删除掉;

针对②,连拍对应的照片本质属性就是修改时间上间隔极小;我们可以将图片间隔时间小于特定值的归位一类,如重命名成"1-1", "1-2", 如此在整理时便可以直接凭借文件名前缀快速进行照片筛选;

针对③,我们可以利用程序监测打印文件夹;严格的记录曾经拖动到打印文件夹中的文件以及次数。当然需要解决一个问题就是,在②时我们利用了重命名的机制,而记录照片打印情况,就是用"文件名: 次数"的形式。重命名可能导致这个Key发生改变,因此在进行重命名时,对于已经重命名了的文件,我们不能对它们进行重命名。

如下就是针对上面诸多情况编写的简易GUI程序,没有添加过多功能,只能保证基础使用:

# -*- coding: utf-8 -*-
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from threading import Thread
from tkinter import filedialog, messagebox
from tkinter.ttk import Treeview, Combobox, Entry
from time import sleep
import tkinter as tk
import json
import os
import re


FONT = ("宋体", 14, "bold")
CURRENT_PATH = os.path.dirname(os.path.abspath(__file__))


class WinGUI(tk.Tk):
    def __init__(self):
        super().__init__()
        self.selected_option: tk.IntVar = tk.IntVar()
        self.__win()
        self.rename_tip_label: tk.Label = self.__set_rename_entry_tip()
        self.monitor_tip_label: tk.Label = self.__set_monitor_entry_tip()
        self.rename_entry: tk.Entry = self.__set_rename_entry_dir()
        self.monitor_entry: tk.Entry = self.__set_monitor_entry_dir()
        self.ask_dir_btn1: tk.Button = self.__set_rename_btn_dir()
        self.ask_dir_btn2: tk.Button = self.__set_monitor_btn_dir()
        self.link_dir_btn1: tk.Button = self.__set_btn_link_dir1()
        self.link_dir_btn2: tk.Button = self.__set_btn_link_dir2()
        self.suffix_label: tk.Label = self.__set_label_suffix()
        self.suffix_combobox: Combobox = self.__set_suffix_combobox()
        self.interval_label: tk.Label = self.__set_label_interval()
        self.time_interval_entry: tk.Entry = self.__set_entry_time_interval()
        self.monitor_treeview: MonitorTreeview = self.__set_monitor_treeview()
        self.rename_btn: tk.Button = self.__set_rename_btn()
        self.monitor_btn: tk.Button = self.__set_monitor_btn()

    def __win(self):
        self.title("照片打印监测")
        # 设置窗口大小、居中
        width, height = 390, 230
        screenwidth = self.winfo_screenwidth()
        screenheight = self.winfo_screenheight()
        geometry = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2)
        self.geometry(geometry)
        self.attributes('-topmost', 1)
        self.resizable(False, False)

    def __set_rename_entry_tip(self):
        rename_entry_tip_label = tk.Label(self, text="命名区:", font=FONT)
        rename_entry_tip_label.place(x=10, y=3)
        return rename_entry_tip_label
    
    def __set_monitor_entry_tip(self):
        monitor_entry_tip = tk.Label(self, text="监测区:", font=FONT)
        monitor_entry_tip.place(x=10, y=38)
        return monitor_entry_tip
    
    def __set_rename_entry_dir(self):
        rename_entry = Entry(self)
        rename_entry.place(x=83, y=0, height=30, width=196)
        return rename_entry
    
    def __set_monitor_entry_dir(self):
        monitor_entry = Entry(self)
        monitor_entry.place(x=83, y=35, height=30, width=196)
        return monitor_entry

    def __set_rename_btn_dir(self):
        button_dir = tk.Button(self, text="浏览", bg="wheat", font=('宋体', 14, "bold"))
        button_dir.place(x=274, y=0, height=30, width=59)
        return button_dir
    
    def __set_monitor_btn_dir(self):
        button_dir = tk.Button(self, text="浏览", bg="wheat", font=('宋体', 14, "bold"))
        button_dir.place(x=274, y=35, height=30, width=59)
        return button_dir

    def __set_btn_link_dir1(self):
        link_btn = tk.Button(self, text='>>', bg='gold', font=('宋体', 15))
        link_btn.place(x=332, y=0, height=30, width=30)
        return link_btn
    
    def __set_btn_link_dir2(self):
        link_btn = tk.Button(self, text='>>', bg='gold', font=('宋体', 15))
        link_btn.place(x=332, y=35, height=30, width=30)
        return link_btn

    def __set_label_suffix(self):
        label_suffix = tk.Label(self, text="后缀名:", font=FONT)
        label_suffix.place(x=10, y=112, height=30)
        return label_suffix

    def __set_label_interval(self):
        label_interval = tk.Label(self, text='合并域:', font=FONT)
        label_interval.place(x=10, y=70, height=35)
        return label_interval

    def __set_suffix_combobox(self):
        combobox = Combobox(self, values=['.jpg', '.png', '.jpeg'])
        combobox.insert(0, '.jpg')
        combobox.state(['readonly'])
        combobox.place(x=83, y=110, width=78, height=30)
        return combobox

    def __set_entry_time_interval(self):
        entry_time_interval = Entry(self, width=5)
        entry_time_interval.place(x=83, y=73, width=78, height=30)
        return entry_time_interval

    def __set_monitor_treeview(self):
        columns = ("主编号", "子编号", "总次数")
        column_width = (60, 105, 60)
        treeview = MonitorTreeview(self, columns=columns, show='headings')
        for column, width in zip(columns, column_width):
            treeview.column(column, width=width, anchor="center")
            treeview.heading(column, text=column)
        treeview.place(x=166, y=69, height=162, width=225)
        return treeview

    def __set_rename_btn(self):
        button_rename = tk.Button(self, text="命名", bg='#A6D2FF', font=('宋体', 18, 'bold'))
        button_rename.place(x=10, y=165, height=45, width=76)
        return button_rename

    def __set_monitor_btn(self):
        button_monitor = tk.Button(self, text="监测", bg='#27CC58', font=('宋体', 18, 'bold'))
        button_monitor.place(x=86, y=165, height=45, width=74)
        return button_monitor
    

class MonitorTreeview(Treeview):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.monitor_info: dict | None = None
        self.undo_stack: list = []
        self.toplevel_alive: bool = False
        
    def bind_event(self, monitor_info):
        self.monitor_info = monitor_info
        self.bind("<Double-1>", self.__modify)
        self.bind("<Delete>", self.__delete)
        self.bind("<Control-z>", self.__undo)

    def refresh(self):
        sorted_keys = sorted(self.monitor_info.keys(), key=lambda x: int(x) if x.isdigit() else ord(x[0]))
        self.delete(*self.get_children())
        for key in sorted_keys:
            if str(key).isdigit():
                data = (key, self.monitor_info[key], sum(self.monitor_info[key].values()))
            else:
                data = (key, 0, self.monitor_info[key])
            self.insert('', tk.END, values=data)

    def __modify(self, event):
        def save():
            try:
                data: dict = eval(text_widget.get("1.0", tk.END).strip())
                is_valid_format = all(
                isinstance(k, str) and k.isdigit() and isinstance(v, int) 
                for k, v in data.items()
            )
            except Exception:
                is_valid_format = False
            
            if is_valid_format:
                self.undo_stack.append((values[0], self.monitor_info[values[0]]))
                self.monitor_info[values[0]] = data
                self.refresh()
            else:
                messagebox.showerror("错误", "格式错误, 保存失败!")
            self.__destroy_toplevel(child_window)

        selection = self.selection()
        if len(selection) == 0:
            return
        
        if self.toplevel_alive:
            return
        
        values = self.item(selection[0], 'values')
        if values[1] == '0':
            return
        
        self.toplevel_alive = True
        child_window = tk.Toplevel()
        child_window.protocol('WM_DELETE_WINDOW', lambda: self.__destroy_toplevel(child_window))
        child_window.attributes('-topmost', 1)
        child_window.title(f"主编号: {values[0]}")
        child_window.geometry(f"220x100+{event.x_root-110}+{event.y_root-50}")
        text_widget = tk.Text(child_window, font=("Calibri", 14))
        text_widget.place(width=160, height=100)
        save_btn = tk.Button(child_window, text="保存", command=save, bg="#26C755", font=FONT)
        save_btn.place(x=160, y=30, width=60, height=30)
        text_widget.insert(tk.END, json.dumps(eval(values[1]), indent=2))

    def __destroy_toplevel(self, top_window: tk.Toplevel):
        self.toplevel_alive = False
        top_window.destroy()

    def __delete(self, _):
        selection = self.selection()
        if len(selection) == 0:
            return
        for item in selection:
            values = self.item(item, 'values')
            self.undo_stack.append((values[0], self.monitor_info[values[0]]))
            self.monitor_info.pop(values[0])
        self.delete(*selection)

    def __undo(self, _):
        if self.toplevel_alive or len(self.undo_stack) == 0:
            return
        key, value = self.undo_stack.pop()
        self.monitor_info[key] = value
        self.refresh()
    

class Control(WinGUI):
    def __init__(self):
        super().__init__()
        self.rename_initial_dir: str = ''
        self.monitor_initial_dir: str = ''
        self.extension: str = '.jpg'
        self.monitor_state: enumerate[0, 1] = 0
        self.monitor_info: dict  = self.load_disk_info()
        self.id_table: dict = {
            0: [self.rename_entry, self.rename_initial_dir],
            1: [self.monitor_entry, self.monitor_initial_dir],
        }
        self.__event_bind()

    def __event_bind(self) -> None:
        self.protocol('WM_DELETE_WINDOW', lambda: self.close_save())
        self.monitor_treeview.bind_event(self.monitor_info)
        self.ask_dir_btn1.config(command=lambda: self.choose_directory(0))
        self.ask_dir_btn2.config(command=lambda: self.choose_directory(1))
        self.link_dir_btn1.config(command=lambda: self.link_to_dir(0))
        self.link_dir_btn2.config(command=lambda: self.link_to_dir(1))
        self.rename_btn.config(command=self.start_rename)
        self.monitor_btn.config(command=self.start_refresh_monitor)

    def link_to_dir(self, entry_id) -> None:
        directory = self.id_table[entry_id][0].get()
        if not os.path.isdir(directory):
            return
        self.after(3, lambda: os.startfile(directory))

    @staticmethod
    def get_files(directory: str) -> list:
        with os.scandir(directory) as entries:
            return [entry.name for entry in entries if entry.is_file()]

    def choose_directory(self, entry_id) -> None:
        entry, initial_dir = self.id_table[entry_id]
        folder_selected = filedialog.askdirectory(initialdir=initial_dir)
        if not folder_selected:
            return
        self.id_table[entry_id][1] = folder_selected
        entry.delete(0, tk.END)
        entry.insert(0, folder_selected)

    def get_same_suffix_interval(self) -> float:
        try:
            time_interval = float(self.time_interval_entry.get())
            if time_interval < 0:
                raise ValueError
            return time_interval
        except ValueError:
            self.time_interval_entry.delete(0, tk.END)
            self.time_interval_entry.insert(0, '5')
            return 5.0

    def prefix_suffix_rename(self, work_path: str) -> None:
        # 初始化变量
        os.chdir(work_path)
        folder_files, prefix = self.get_required_info()
        suffix: int = 0
        prev_time: float = 0.0
        time_interval = self.get_same_suffix_interval()
        for old_name in folder_files:
            current_time = os.path.getmtime(old_name)
            if (current_time - prev_time) > time_interval:
                prefix += 1
                suffix = 1
            else:
                suffix += 1
            new_name = f"{prefix}-{suffix}{self.extension}"
            prev_time = current_time
            try:
                os.rename(old_name, new_name)
            except (PermissionError, OSError):
                messagebox.showerror(f'命名时出现错误,\n请尝试重新命名')
                os.chdir(CURRENT_PATH)
                return

        os.chdir(CURRENT_PATH)
        messagebox.showinfo("提示", "重命名成功!")

    def get_required_info(self) -> tuple:
        # return: required_rename_files + have_renamed_max_num
        files: list = self.get_files(self.rename_entry.get())
        prev_time: float = 0.0
        renamed_max_num: int = 0
        required_rename_files: list = []
        for file in files:
            current_time = os.path.getmtime(file)
            if current_time == prev_time:
                # 对重复文件进行删除
                os.remove(file)
            elif re.match('\d+-\d+', file):
                renamed_max_num = max(renamed_max_num, int(file.split('-')[0]))
            elif re.match('.*\.(jpg|png|jpeg)', file, re.I):
                required_rename_files.append(file)
            prev_time = current_time
        required_rename_files.sort(key=lambda x: os.path.getmtime(x))
        return required_rename_files, renamed_max_num

    def get_proper_director(self, enter_id) -> str:
        director = self.id_table[enter_id][0].get()
        if director == '':
            messagebox.showinfo('提示', '请先选择文件夹!')
            return
        elif not os.path.isdir(director):
            messagebox.showerror('错误', '文件夹路径不存在,\n请重新选择文件夹!')
            return
        else:
            return director

    def start_rename(self) -> None:
        directory = self.get_proper_director(0)
        if directory is None:
            return
        self.extension = self.suffix_combobox.get()
        Thread(target=self.prefix_suffix_rename, args=(directory, ), daemon=True).start()

    def start_refresh_monitor(self) -> None:
        if self.monitor_state == 1:
            Thread(target=self.monitor_treeview.refresh, daemon=True).start()
        else:
            directory = self.get_proper_director(1)
            if directory is None:
                return
            self.monitor_btn.config(text="刷新")
            Thread(target=self.__monitor, args=(directory, ), daemon=True).start()

    def __monitor(self, directory):
        self.monitor_state = 1
        event_handler = MonitorHandler(directory, self.monitor_info)
        observer = Observer()
        observer.schedule(event_handler, directory, recursive=True)
        observer.start()
        try:
            while self.monitor_state:
                sleep(3)
        except KeyboardInterrupt:
            observer.stop()
        observer.join()
        
    def close_save(self):
        with open('basic_data.json', 'w', encoding='utf8') as f:
            json.dump({
                'monitor_dir': self.monitor_entry.get(),
                'rename_dir': self.rename_entry.get(),
                'time_interval': self.time_interval_entry.get(),
                'monitor_info': self.monitor_info
                }, f, ensure_ascii=False, indent=4
            )
        self.monitor_state = 0
        self.destroy()

    @staticmethod
    def compare_key(key):
        try:
            prefix, suffix = key.split('-')
            return int(prefix), int(suffix)
        except ValueError:
            return

    def load_disk_info(self) -> dict:
        # 直接载入数据了
        if not os.path.exists("basic_data.json"):
            self.monitor_info = dict()
            return
        with open("basic_data.json", "r", encoding='utf8') as f:
            disk_info = json.load(f)
        self.monitor_entry.insert(0, disk_info['monitor_dir'])
        self.rename_entry.insert(0, disk_info['rename_dir'])
        self.time_interval_entry.insert(0, disk_info['time_interval'])
        return disk_info['monitor_info']


class MonitorHandler(FileSystemEventHandler):
    def __init__(self, folder, monitor_info):
        # monitor_info形如: {1: {1: 1, 2: 2}}
        self.folder = folder
        self.monitor_info = monitor_info

    def on_created(self, event):
        if event.is_directory:
            return
        file_name = os.path.basename(event.src_path)
        result = re.match(r'(\d+)-(\d+)\.(jpg|png|jpeg)', file_name, re.I)
        if result:
            prefix, suffix, _ = result.groups()
            if prefix not in self.monitor_info:
                self.monitor_info[prefix] = {}
            if suffix not in self.monitor_info[prefix]:
                self.monitor_info[prefix][suffix] = 0
            self.monitor_info[prefix][suffix] += 1
        else:
            if file_name not in self.monitor_info:
                self.monitor_info[file_name] = 0
            self.monitor_info[file_name] += 1


if __name__ == '__main__':
    gui = Control()
    gui.mainloop()

基础界面:

程序使用方法:

①命名区就是指从存储卡中拷贝出来的照片存储的文件夹;

②监测区就是拍马打印照片时监测的文件夹;

③合并域就是指两张照片修改时间间隔小于多少秒时,重命名的前缀相同,归位一类。;

④后缀名就是指重命名后图片的后缀,限定了只能选择图片文件"jpg|png|jpeg";

按下"命名"按钮,程序会自动将命名区中的重复图片删除,同时获取没有重命名过的图片以及已经命名图片的最大序号,基于最大序号对这些没有重命名过的图片进行重命名

按下"监测"按钮,按钮上的文字会变成"刷新",同时程序开始对监测区进行监测;任何移动到多监测区的文件都会被记录;监测数据会显示在右侧表格中,但并不会实时刷新,需要手动按原来的"监测"按钮(后面变成"刷新"按钮)进行数据刷新;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值