「工具」Semi-Utils 开发文档

简介

Semi Utils 是一个基于命令行界面(CLI)的图像处理程序,主要由以下四个模块组成:

  1. 菜单组件 MenuComponent:用于生成CLI中的菜单,接受用户的输入并执行对应的回调函数。它是整个应用的核心组件之一,用于实现应用的交互逻辑。
  2. 配置对象 Config:用于修改配置文件,与菜单组件配合使用,共同组成应用的CLI交互。它包含了应用的各种配置选项和默认值,用户可以通过菜单组件来修改这些选项。
  3. 图片容器 ImageContainer:用于读取并保存图片的exif等信息,操作原图片、生成的图片等。它提供了一组操作图片的API,可以用于实现各种图像处理需求。
  4. 处理器组件 ProcessorComponent:用于处理图片,不同的处理器可以组合使用,通过处理器链逐个处理图片。它包含了一系列处理图片的操作,例如水印、边框、阴影等,用户可以选择不同的处理器组合来实现不同的图像处理需求。

项目结构如下:

 1.
 2├── exiftool	# exiftool 路径
 3├── fonts	# 字体路径
 4├── logos	# logo 文件路径
 5├── input	# 输入文件夹
 6├── output	# 输出文件夹
 7├── entity	# 主要对象
 8   ├── config.py	# 配置对象,用于操作 config.yaml
 9   ├── image_container.py	# 图片容器,用于读取并保存图片的exif等信息
10   ├── image_processor.py	# 用于处理图片,不同的处理器可以组合使用
11   └── menu.py	# 用于生成CLI中的菜单,接受用户的输入并执行对应的回调函数
12├── enums
13   └── constant.py	# 常量类
14├── init.py	# 加载配置文件,初始化和定义用户菜单
15├── main.py	# 启动和配置应用程序,并负责调用其他模块中的相关代码来完成应用程序的功能
16├── utils.py	# 工具类,获取图片列表、根据文字生成图片等
17├── config.yaml	# 配置文件
18├── install.sh	# 应用程序的安装脚本,下载 exiftool 并安装 python 依赖
19├── main.spec	# pyinstaller 打包相关配置
20├── requirements.txt	# 指定所需要的第三方包及其版本的配置文件
21├── README.md
22└── LICENSE
 1graph TD;
 2  subgraph 处理图片程序主循环
 3    初始化状态为0-->检测程序状态;
 4    检测程序状态-->状态0菜单处理;
 5    状态0菜单处理-->处理用户输入;
 6    处理用户输入-->|"y"或回车|状态100处理图片;
 7    处理用户输入-->|"x"|退出程序;
 8    处理用户输入-->|"r"|返回上一层菜单;
 9    处理用户输入-->|"n(1 <= n <= 子菜单数量)"|进入子菜单;
10    状态100处理图片-->进行数据处理;
11    进行数据处理-->结束状态100;
12    退出程序-->结束程序;
13    返回上一层菜单-->进入状态0;
14    进入子菜单-->处理用户输入;
15  end;

菜单组件

主要由四个类组成,分别是:

  • 接口类 MenuComponent
  • 主菜单 Menu
  • 子菜单 SubMenu
  • 菜单项 MenuItem
classDiagram class MenuComponent { +__init__() +add(component) +get_parent() +set_parent(component) +remove(component) +display() +display_item() +is_leaf() } class Menu { +__init__(name) +get_parent() +set_parent(component) +add(component) +remove(component) +display() -name: str -components: list } class SubMenu { +__init__(name) +get_parent() +set_parent(component) +add(component) +remove(component) +set_value_getter(config, getter) +get_value() +set_compare_method(method) +check_active() +get_active_item() +display() -name: str -components: list -source: Any -getter: callable -is_active: int | None -compare_method: callable } class MenuItem { +__init__(name) +get_active_item() +add(component) +get_value() +remove(component) +display() +set_procedure(procedure, \*\*kwargs) +run() +is_leaf() -_name: str -_procedure: callable -_procedure_args: dict|None -_value: str|None } MenuComponent <|-- Menu MenuComponent <|-- SubMenu MenuComponent <|-- MenuItem

接口类 MenuComponent

classDiagram class MenuComponent { +__init__() +get_parent() +set_parent(component) +add(component) +remove(component) +display() +display_item() +is_leaf() }

它是一个基类,提供了基础的接口,用于定义菜单组件的行为。该类声明了一些方法,包括添加、获取和移除子项、设置和获取父项、显示菜单、显示菜单条目、判断是否为叶子节点等。

主菜单 Menu

classDiagram class Menu { +__init__(name) +get_parent() +set_parent(component) +add(component) +remove(component) +display() -name: str -components: list }

Menu 类是一个具体的菜单组件类,继承自 MenuComponent 类,表示主菜单。该类具有添加、移除和显示子菜单、显示菜单等基本行为。其中,add 方法用于添加子菜单组件,remove 方法用于移除子菜单组件,display 方法用于打印子菜单条目。

子菜单 SubMenu

classDiagram class SubMenu { +__init__(name) +get_parent() +set_parent(component) +add(component) +remove(component) +set_value_getter(config, getter) +get_value() +set_compare_method(method) +check_active() +get_active_item() +display() -name: str -components: list -source: Any -getter: callable -is_active: int | None -compare_method: callable }

SubMenu 类是一个具体的菜单组件类,继承自 MenuComponent 类,表示子菜单。该类具有添加、移除和显示菜单项、设置和获取子菜单当前值、检查并更新当前选中项等基本行为。其中,addremove 方法用于添加或移除菜单项,display 方法用于打印完整的子菜单选项。同时,该类还提供若干个具体的处理方法,用于获取或更新子菜单当前值,判断当前选中项等。

菜单项 MenuItem

classDiagram class MenuItem { +__init__(name) +get_active_item() +add(component) +get_value() +remove(component) +display() +set_procedure(procedure, \*\*kwargs) +run() +is_leaf() -_name: str -_procedure: callable -_procedure_args: dict|None -_value: str|None }

MenuItem 类是一个具体的菜单组件类,继承自 MenuComponent 类,表示菜单项。该类主要有设置和获取菜单项信息、获取菜单项值、执行菜单项的处理方法等基本行为。其中,set_procedure 方法用于设置菜单项的处理方法,run 方法用于执行菜单项的处理方法。同时,该类还实现了 is_leaf 方法,用于判断是否为叶子节点。


下面是一个简单的菜单示例,演示了如何创建一个简单的菜单,包含一个主菜单和一个子菜单,子菜单下有两个菜单项。通过注释可以清晰地了解每一步的作用和意义。

 1# 创建一个主菜单,名称为“当前设置”,空格用于缩进,使其在菜单中居中显示。
 2root_menu = Menu('    当前设置')
 3
 4# 在主菜单中创建一个名为“logo”的子菜单
 5logo_menu = SubMenu('logo')
 6# 两个参数结合,表示读取 config['logo']['enable'] 作为子菜单当前选中的值
 7logo_menu.set_value_getter(config, lambda x: x['logo']['enable'])
 8# 设置子菜单当前选中的值和菜单项的值的比较方法
 9logo_menu.set_compare_method(lambda x, y: x == y)
10root_menu.add(logo_menu)	# 将子菜单添加到主菜单中
11
12# 在子菜单中创建名为“启用”的菜单项,值为 True,并设置其对应的处理方法。
13logo_enable_menu = MenuItem('启用')
14logo_enable_menu._value = True	# 用于和子菜单当前选中的值进行比较,如果相等则表示选中启用
15logo_enable_menu.set_procedure(config.enable_logo)	# 修改配置文件,启用 logo
16logo_menu.add(logo_enable_menu)	# 将菜单项添加到子菜单中
17
18# 在子菜单中创建名为“不启用”的菜单项,值为 False,并设置其对应的处理方法。
19logo_disable_menu = MenuItem('不启用')
20logo_disable_menu._value = False	# 用于和子菜单当前选中的值进行比较,如果相等则表示选中不启用
21logo_disable_menu.set_procedure(config.disable_logo)	# 修改配置文件,禁用 logo
22logo_menu.add(logo_disable_menu)	# 将菜单项添加到子菜单中

菜单组件在 init.py 中被实例化,菜单组件的定义在 entity/menu.py

配置对象

主要由两个类组成,分别是:

  • 配置对象 Config,用于修改配置文件 config.yaml
  • 元素配置对象 ElementConfig,用于表示四个角的文字内容
classDiagram ElementConfig <-- Config class ElementConfig{ -element +get_name() +is_bold() +get_value() } class Config{ -_path -_data -_logos -_left_top -_left_bottom -_right_top -_right_bottom -_makes +__init__(path) +save() +get_input_dir() +get_output_dir() +get_quality() +...() }

这两个配置类都是通过读取 yaml 文件来获取一些配置信息。

ElementConfig 保存的是布局中元素的配置信息,它包含了获取元素名称、是否加粗和元素的值等方法。每个元素在 yaml 文件中都可以通过其名称指定,而获取这个元素的具体配置信息,可以通过该类的实例方法来实现。

Config 则是整个配置对象的入口,它包含了获取输入输出路径、生成图片的期望质量等方法。在实例化 Config 类时,首先会读取 yaml 文件中的所有配置信息,并将其保存到一个字典(self._data)中。然后,Config 类会根据这个字典中的信息,创建一些 ElementConfig 类的实例来保存布局中元素的配置信息,并创建一个列表 self._makes 来保存 logo 制作的信息。同时该类还提供了 save 方法,用于保存配置信息到所对应的 yaml 文件中。除此之外,还有许多用于读写配置的方法,比如 enable_logo ,用于将 logo 标志设置为 Trueset_normal_with_right_logo_layout,用于将布局设置为 normal_with_right_logo

配置对象是单例的,在 init.py 中被实例化,配置对象的定义在 entity/config.py

示例

下面是一个简单的配置对象示例:

1config_path = './config.yaml'
2# 通过路径实例化一个配置对象
3config = Config(config_path)
4# 分别获取当前的布局类型,然后设置布局类型为 normal
5current_layout = config.get_layout_type()
6config.set_normal_layout()
7# 保存修改后的配置
8config.save()

图片容器

ImageContainer 类组成。

classDiagram class ImageContainer{ -path: Path -target_path: Path|None -img: Image -exif: dict -model: str -make: str -lens_model: str -lens_make: str -date: datetime -focal_length: str -focal_length_in_35mm_film: str -use_equivalent_focal_length: bool -f_number: str -exposure_time: str -iso: str -orientation -custom: str -logo: object -original_width -original_height -watermark_img: Image.Image +__init__(path: Path) +get_height() +get_width() +get_model() +get_make() +get_ratio() +get_img() +_parse_datetime() -> str +_parse_date() -> str +get_attribute_str(element: ElementConfig) -> str +get_param_str() -> str +get_original_height() +get_original_width() +get_original_ratio() +get_logo() +set_logo(logo: object) -> None +is_use_equivalent_focal_length(flag: bool) -> None +get_watermark_img() -> Image.Image +update_watermark_img(watermark_img: Image.Image) -> None +close() }

ImageContainer 是一个图片处理的类,用于从图片文件中读取 EXIF 信息,获取图片的基本属性,如拍摄时间、机器型号、镜头型号等,并且可以对这些属性进行一些处理,生成对应的字符串形式的属性值。其中,ImageContainer 通过对 Image 对象的操作,获取图片的基本信息。get_attribute_str 方法根据传入的 ElementConfig 对象中的属性名,返回对应的属性值,该方法用于将图片属性值组合成字符串。

此外,该对象还提供了其他一些方法,如设置和获取水印、获取图片高度和宽度、修改焦距等。在实际使用中,可以根据需要调用这些方法来处理图片和获取属性值,从而实现对图片的处理和生成水印。

图片容器在 main.py 中被实例化,图片容器的定义在 entity/image_container.py

示例

下面是一个简单的图片容器示例:

1source_path = 'images/example.jpg'
2# 通过路径实例化一个图片容器对象
3container = ImageContainer(source_path)
4# 分别获取照片的相机机型和相机厂商
5model = container.get_model()
6make = container.get_make()
7# 关闭图片容器
8container.close()

处理器组件

主要由以下几个类组成:

  • ProcessorComponent 类:处理器组件的基类。
  • ProcessorChain 类:将多个处理器组件串联起来进行连续处理的类。
  • EmptyProcessor 类:一个空处理器组件,不做任何处理,只返回原始图片。
  • ShadowProcessor 类:添加阴影处理器组件。
  • SquareProcessor 类:将水印图片转化为正方形处理器组件。
  • WatermarkProcessor 类:添加水印处理器组件。
  • MarginProcessor 类:添加白边处理器组件。
  • SimpleProcessor 类:添加简洁布局处理器组件。
  • PaddingToOriginalRatioProcessor 类:填充长宽比处理器组件。
classDiagram class ProcessorComponent { LAYOUT_ID: string +process(container: ImageContainer): void +add(component: ProcessorComponent): void } class ProcessorChain { -components: ProcessorComponent[] +add(component: ProcessorComponent): void +process(container: ImageContainer): void } class EmptyProcessor { LAYOUT_ID: string -config: Config +process(container: ImageContainer): void } class ShadowProcessor { LAYOUT_ID: string -config: Config +process(container: ImageContainer): void } class SquareProcessor { LAYOUT_ID: string -config: Config +process(container: ImageContainer): void } class WatermarkProcessor { LAYOUT_ID: string -config: Config +process(container: ImageContainer): void } class MarginProcessor { LAYOUT_ID: string -config: Config +process(container: ImageContainer): void } class SimpleProcessor { LAYOUT_ID: string -config: Config +process(container: ImageContainer): void } class PaddingToOriginalRatioProcessor { LAYOUT_ID: string -config: Config +process(container: ImageContainer): void } ProcessorComponent <|-- WatermarkProcessor ProcessorComponent <|-- MarginProcessor ProcessorComponent <|-- SimpleProcessor ProcessorComponent <|-- PaddingToOriginalRatioProcessor ProcessorComponent <|-- SquareProcessor ProcessorComponent <|-- ShadowProcessor ProcessorComponent <|-- EmptyProcessor ProcessorComponent <-- ProcessorChain

图片处理器组件,定义了多个处理图片的方法。这些方法分别对应不同的图片处理功能,如添加阴影、填充白边、生成水印图片等。这些处理方法都继承自 ProcessorComponent 类,并重写了父类的 process 方法。

ProcessorChain 类通过添加多个处理器组件,实现了对图片的多个处理步骤的连续处理。同时,ProcessorChain 也继承自 ProcessorComponent,重写了 process 方法,将容器传递给各个处理器组件进行连续处理。

因为图片处理功能比较多,每个处理器组件都有其自己特定的处理功能,通过 ProcessorComponent 类的 LAYOUT_ID 属性将处理器组件进行分类,方便在程序中进行调用。例如,对于 WatermarkProcessor 类,使用了 watermark 作为其 LAYOUT_ID

用户可以根据自己的需求自定义处理器组件,处理器组件在 init.py 中被实例化,在 main.py 中被调用,处理器组件的定义在 entity/image_processor.py

示例

下面是一个简单的处理器组件示例:

 1class PaddingToOriginalRatioProcessor(ProcessorComponent):
 2    LAYOUT_ID = 'padding_to_original_ratio'
 3
 4    def __init__(self, config: Config):
 5        self.config = config
 6
 7    def process(self, container: ImageContainer) -> None:
 8        original_ratio = container.get_original_ratio()
 9        ratio = container.get_ratio()
10        if original_ratio > ratio:
11            # 如果原始比例大于当前比例,说明宽度大于高度,需要填充高度
12            padding_size = int(container.get_width() / original_ratio - container.get_height())
13            padding_img = ImageOps.expand(container.get_watermark_img(), (0, padding_size), fill='white')
14        else:
15            # 如果原始比例小于当前比例,说明高度大于宽度,需要填充宽度
16            padding_size = int(container.get_height() * original_ratio - container.get_width())
17            padding_img = ImageOps.expand(container.get_watermark_img(), (padding_size, 0), fill='white')
18        container.update_watermark_img(padding_img)

这个示例实现了填充图片容器中水印图片以保持原始宽高比例的功能。具体来说,它会根据原始比例和当前比例的大小关系,决定填充高度或宽度,并在图片上添加白色填充以保持原始宽高比例。