|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
有这样的一个添加水印的工具,源代码地址:https://github.com/leslievan/semi-utils
需求:想把核心的image_container和image_processor还有config这三个文件里的类,封装成接口,这样方便做小程序或者web端,目前这个是一个cli命令行程序
实现:用户先选择模板,然后选择需要展示的参数,即左上角、左下角、右上角、右下角显示的内容,最后上传图片,后台处理图片,返回生成水印图片的地址,提供用户下载。
问了chatGPT,也问了各大AI,但组成的类还是有问题,建议用flask框架,下面附上核心类:
1.image_container.py
- import logging
- import re
- from datetime import datetime
- from enum import Enum
- from pathlib import Path
- from PIL import Image
- from PIL.Image import Transpose
- from dateutil import parser
- from entity.config import ElementConfig
- from enums.constant import *
- from utils import calculate_pixel_count
- from utils import extract_attribute
- from utils import get_exif
- logger = logging.getLogger(__name__)
- class ExifId(Enum):
- CAMERA_MODEL = 'CameraModelName'
- CAMERA_MAKE = 'Make'
- LENS_MODEL = ['LensModel', 'Lens']
- LENS_MAKE = 'LensMake'
- DATETIME = 'DateTimeOriginal'
- FOCAL_LENGTH = 'FocalLength'
- FOCAL_LENGTH_IN_35MM_FILM = 'FocalLengthIn35mmFormat'
- F_NUMBER = 'FNumber'
- ISO = 'ISO'
- EXPOSURE_TIME = 'ExposureTime'
- SHUTTER_SPEED_VALUE = 'ShutterSpeedValue'
- ORIENTATION = 'Orientation'
- PATTERN = re.compile(r"(\d+)\.") # 匹配小数
- def get_datetime(exif) -> datetime:
- dt = datetime.now()
- try:
- dt = parser.parse(extract_attribute(exif, ExifId.DATETIME.value,
- default_value=str(datetime.now())))
- except ValueError as e:
- logger.info(f'Error: 时间格式错误:{extract_attribute(exif, ExifId.DATETIME.value)}')
- return dt
- def get_focal_length(exif):
- focal_length = DEFAULT_VALUE
- focal_length_in_35mm_film = DEFAULT_VALUE
- try:
- focal_lengths = PATTERN.findall(extract_attribute(exif, ExifId.FOCAL_LENGTH.value))
- try:
- focal_length = focal_lengths[0] if focal_length else DEFAULT_VALUE
- except IndexError as e:
- logger.info(
- f'ValueError: 不存在焦距:{focal_lengths} : {e}')
- try:
- focal_length_in_35mm_film: str = focal_lengths[1] if focal_length else DEFAULT_VALUE
- except IndexError as e:
- logger.info(f'ValueError: 不存在 35mm 焦距:{focal_lengths} : {e}')
- except Exception as e:
- logger.info(f'KeyError: 焦距转换错误:{extract_attribute(exif, ExifId.FOCAL_LENGTH.value)} : {e}')
- return focal_length, focal_length_in_35mm_film
- class ImageContainer(object):
- def __init__(self, path: Path):
- self.path: Path = path
- self.target_path: Path | None = None
- self.img: Image.Image = Image.open(path)
- self.exif: dict = get_exif(path)
- # 图像信息
- self.original_width = self.img.width
- self.original_height = self.img.height
- self._param_dict = dict()
- self.model: str = extract_attribute(self.exif, ExifId.CAMERA_MODEL.value)
- self.make: str = extract_attribute(self.exif, ExifId.CAMERA_MAKE.value)
- self.lens_model: str = extract_attribute(self.exif, *ExifId.LENS_MODEL.value)
- self.lens_make: str = extract_attribute(self.exif, ExifId.LENS_MAKE.value)
- self.date: datetime = get_datetime(self.exif)
- self.focal_length, self.focal_length_in_35mm_film = get_focal_length(self.exif)
- self.f_number: str = extract_attribute(self.exif, ExifId.F_NUMBER.value, default_value=DEFAULT_VALUE)
- self.exposure_time: str = extract_attribute(self.exif, ExifId.EXPOSURE_TIME.value, default_value=DEFAULT_VALUE,
- suffix='s')
- self.iso: str = extract_attribute(self.exif, ExifId.ISO.value, default_value=DEFAULT_VALUE)
- # 是否使用等效焦距
- self.use_equivalent_focal_length: bool = False
- # 修正图像方向
- self.orientation = self.exif[ExifId.ORIENTATION.value] if ExifId.ORIENTATION.value in self.exif else 1
- if self.orientation == "Rotate 0":
- pass
- elif self.orientation == "Rotate 90 CW":
- self.img = self.img.transpose(Transpose.ROTATE_270)
- elif self.orientation == "Rotate 180":
- self.img = self.img.transpose(Transpose.ROTATE_180)
- elif self.orientation == "Rotate 270 CW":
- self.img = self.img.transpose(Transpose.ROTATE_90)
- else:
- pass
- # 水印设置
- self.custom = '无'
- self.logo = None
- # 水印图片
- self.watermark_img = None
- self._param_dict[MODEL_VALUE] = self.model
- self._param_dict[PARAM_VALUE] = self.get_param_str()
- self._param_dict[MAKE_VALUE] = self.make
- self._param_dict[DATETIME_VALUE] = self._parse_datetime()
- self._param_dict[DATE_VALUE] = self._parse_date()
- self._param_dict[LENS_VALUE] = self.lens_model
- self._param_dict[FILENAME_VALUE] = self.path.name
- self._param_dict[TOTAL_PIXEL_VALUE] = calculate_pixel_count(self.original_width, self.original_height)
- self._param_dict[CAMERA_MAKE_CAMERA_MODEL_VALUE] = ' '.join(
- [self._param_dict[MAKE_VALUE], self._param_dict[MODEL_VALUE]])
- self._param_dict[LENS_MAKE_LENS_MODEL_VALUE] = ' '.join(
- [self.lens_make, self._param_dict[LENS_VALUE]])
- self._param_dict[CAMERA_MODEL_LENS_MODEL_VALUE] = ' '.join(
- [self._param_dict[MODEL_VALUE], self._param_dict[LENS_VALUE]])
- self._param_dict[DATE_FILENAME_VALUE] = ' '.join(
- [self._param_dict[DATE_VALUE], self._param_dict[FILENAME_VALUE]])
- self._param_dict[DATETIME_FILENAME_VALUE] = ' '.join(
- [self._param_dict[DATETIME_VALUE], self._param_dict[FILENAME_VALUE]])
- def get_height(self):
- return self.get_watermark_img().height
- def get_width(self):
- return self.get_watermark_img().width
- def get_model(self):
- return self.model
- def get_make(self):
- return self.make
- def get_ratio(self):
- return self.img.width / self.img.height
- def get_img(self):
- return self.img
- def _parse_datetime(self) -> str:
- """
- 解析日期,转换为指定的格式
- :return: 指定格式的日期字符串,转换失败返回原始的时间字符串
- """
- return datetime.strftime(self.date, '%Y-%m-%d %H:%M')
- def _parse_date(self) -> str:
- """
- 解析日期,转换为指定的格式
- :return: 指定格式的日期字符串,转换失败返回原始的时间字符串
- """
- return datetime.strftime(self.date, '%Y-%m-%d')
- def get_attribute_str(self, element: ElementConfig) -> str:
- """
- 通过 element 获取属性值
- :param element: element 对象有 name 和 value 两个字段,通过 name 和 value 获取属性值
- :return: 属性值字符串
- """
- if element.get_name() in self._param_dict:
- return self._param_dict[element.get_name()]
- if element is None or element.get_name() == '':
- return ''
- if element.get_name() == CUSTOM_VALUE:
- self.custom = element.get_value()
- return self.custom
- elif element.get_name() in self._param_dict:
- return self._param_dict[element.get_name()]
- else:
- return ''
- def get_param_str(self) -> str:
- """
- 组合拍摄参数,输出一个字符串
- :return: 拍摄参数字符串
- """
- focal_length = self.focal_length_in_35mm_film if self.use_equivalent_focal_length else self.focal_length
- return ' '.join([str(focal_length) + 'mm', 'f/' + self.f_number, self.exposure_time,
- 'ISO' + str(self.iso)])
- def get_original_height(self):
- return self.original_height
- def get_original_width(self):
- return self.original_width
- def get_original_ratio(self):
- return self.original_width / self.original_height
- def get_logo(self):
- return self.logo
- def set_logo(self, logo) -> None:
- self.logo = logo
- def is_use_equivalent_focal_length(self, flag: bool) -> None:
- self.use_equivalent_focal_length = flag
- def get_watermark_img(self) -> Image.Image:
- if self.watermark_img is None:
- self.watermark_img = self.img.copy()
- return self.watermark_img
- def update_watermark_img(self, watermark_img) -> None:
- if self.watermark_img == watermark_img:
- return
- original_watermark_img = self.watermark_img
- self.watermark_img = watermark_img
- if original_watermark_img is not None:
- original_watermark_img.close()
- def close(self):
- self.img.close()
- self.watermark_img.close()
- def save(self, target_path, quality=100):
- if self.orientation == "Rotate 0":
- pass
- elif self.orientation == "Rotate 90 CW":
- self.watermark_img = self.watermark_img.transpose(Transpose.ROTATE_90)
- elif self.orientation == "Rotate 180":
- self.watermark_img = self.watermark_img.transpose(Transpose.ROTATE_180)
- elif self.orientation == "Rotate 270 CW":
- self.watermark_img = self.watermark_img.transpose(Transpose.ROTATE_270)
- else:
- pass
- if self.watermark_img.mode != 'RGB':
- self.watermark_img = self.watermark_img.convert('RGB')
- if 'exif' in self.img.info:
- self.watermark_img.save(target_path, quality=quality, encoding='utf-8',
- exif=self.img.info['exif'] if 'exif' in self.img.info else '')
- else:
- self.watermark_img.save(target_path, quality=quality, encoding='utf-8')
复制代码
这是一个相当复杂的Python类,它处理图像处理和元数据(EXIF)提取。这个类的主要功能是处理图像的EXIF数据,包括相机型号、制造商、镜头型号、日期、焦距、ISO等,并根据这些数据对图像进行操作,如旋转和添加水印。
这个类的主要方法包括:
- `__init__`:初始化方法,打开图像,提取EXIF数据,处理图像方向,并初始化一些属性。
- `get_attribute_str`:根据给定的元素配置对象,返回对应的属性值。
- `get_param_str`:返回一个包含拍摄参数的字符串。
- `get_watermark_img`:返回带有水印的图像。如果水印图像不存在,则复制原始图像。
- `update_watermark_img`:更新水印图像。
- `save`:保存带有水印的图像,根据原始图像的方向进行旋转,然后保存。
这个类的操作主要集中在图像的打开、处理和保存,以及EXIF数据的提取和使用。这是一个很好的例子,展示了如何使用Python的PIL库进行图像处理,以及如何处理图像的EXIF数据。
我看到你的代码中没有包含添加水印的具体逻辑,你可能需要实现一个方法来处理这部分。你可以使用PIL库的`ImageDraw`模块在图像上绘制文本,或者使用`Image.paste`方法将一个水印图像粘贴到原始图像上。你可能还需要考虑水印的位置、大小和透明度等因素。
如果你需要更具体的帮助,例如如何在这个类中实现添加水印的方法,请告诉我,我会很乐意帮助你。
2.image_processor.py
- import string
- from PIL import Image
- from PIL import ImageFilter
- from PIL import ImageOps
- from entity.config import Config
- from entity.image_container import ImageContainer
- from enums.constant import GRAY
- from enums.constant import TRANSPARENT
- from utils import append_image_by_side
- from utils import concatenate_image
- from utils import merge_images
- from utils import padding_image
- from utils import resize_image_with_height
- from utils import resize_image_with_width
- from utils import square_image
- from utils import text_to_image
- printable = set(string.printable)
- NORMAL_HEIGHT = 1000
- SMALL_HORIZONTAL_GAP = Image.new('RGBA', (50, 20), color=TRANSPARENT)
- MIDDLE_HORIZONTAL_GAP = Image.new('RGBA', (100, 20), color=TRANSPARENT)
- LARGE_HORIZONTAL_GAP = Image.new('RGBA', (200, 20), color=TRANSPARENT)
- SMALL_VERTICAL_GAP = Image.new('RGBA', (20, 50), color=TRANSPARENT)
- MIDDLE_VERTICAL_GAP = Image.new('RGBA', (20, 100), color=TRANSPARENT)
- LARGE_VERTICAL_GAP = Image.new('RGBA', (20, 200), color=TRANSPARENT)
- LINE_GRAY = Image.new('RGBA', (20, 1000), color=GRAY)
- LINE_TRANSPARENT = Image.new('RGBA', (20, 1000), color=TRANSPARENT)
- class ProcessorComponent:
- """
- 图片处理器组件
- """
- LAYOUT_ID = None
- LAYOUT_NAME = None
- def __init__(self, config: Config):
- self.config = config
- def process(self, container: ImageContainer) -> None:
- """
- 处理图片容器中的 watermark_img,将处理后的图片放回容器中
- """
- raise NotImplementedError
- def add(self, component):
- raise NotImplementedError
- class ProcessorChain(ProcessorComponent):
- def __init__(self):
- super().__init__(None)
- self.components = []
- def add(self, component) -> None:
- self.components.append(component)
- def process(self, container: ImageContainer) -> None:
- for component in self.components:
- component.process(container)
- class EmptyProcessor(ProcessorComponent):
- LAYOUT_ID = 'empty'
- def process(self, container: ImageContainer) -> None:
- pass
- class ShadowProcessor(ProcessorComponent):
- LAYOUT_ID = 'shadow'
- def process(self, container: ImageContainer) -> None:
- # 加载图像
- image = container.get_watermark_img()
- max_pixel = max(image.width, image.height)
- # 计算阴影边框大小
- radius = int(max_pixel / 512)
- # 创建阴影效果
- shadow = Image.new('RGB', image.size, color='#6B696A')
- shadow = ImageOps.expand(shadow, border=(radius * 2, radius * 2, radius * 2, radius * 2), fill=(255, 255, 255))
- # 模糊阴影
- shadow = shadow.filter(ImageFilter.GaussianBlur(radius=radius))
- # 将原始图像放置在阴影图像上方
- shadow.paste(image, (radius, radius))
- container.update_watermark_img(shadow)
- class SquareProcessor(ProcessorComponent):
- LAYOUT_ID = 'square'
- LAYOUT_NAME = '1:1填充'
- def process(self, container: ImageContainer) -> None:
- image = container.get_watermark_img()
- container.update_watermark_img(square_image(image, auto_close=False))
- class WatermarkProcessor(ProcessorComponent):
- LAYOUT_ID = 'watermark'
- def __init__(self, config: Config):
- super().__init__(config)
- # 默认值
- self.logo_position = 'left'
- self.logo_enable = True
- self.bg_color = '#ffffff'
- self.line_color = GRAY
- self.font_color_lt = '#212121'
- self.bold_font_lt = True
- self.font_color_lb = '#424242'
- self.bold_font_lb = False
- self.font_color_rt = '#212121'
- self.bold_font_rt = True
- self.font_color_rb = '#424242'
- self.bold_font_rb = False
- def is_logo_left(self):
- return self.logo_position == 'left'
- def process(self, container: ImageContainer) -> None:
- """
- 生成一个默认布局的水印图片
- :param container: 图片对象
- :return: 添加水印后的图片对象
- """
- config = self.config
- config.bg_color = self.bg_color
- # 下方水印的占比
- ratio = (.04 if container.get_ratio() >= 1 else .09) + 0.02 * config.get_font_padding_level()
- # 水印中上下边缘空白部分的占比
- padding_ratio = (.52 if container.get_ratio() >= 1 else .7) - 0.04 * config.get_font_padding_level()
- # 创建一个空白的水印图片
- watermark = Image.new('RGBA', (int(NORMAL_HEIGHT / ratio), NORMAL_HEIGHT), color=self.bg_color)
- with Image.new('RGBA', (10, 100), color=self.bg_color) as empty_padding:
- # 填充左边的文字内容
- left_top = text_to_image(container.get_attribute_str(config.get_left_top()),
- config.get_font(),
- config.get_bold_font(),
- is_bold=self.bold_font_lt,
- fill=self.font_color_lt)
- left_bottom = text_to_image(container.get_attribute_str(config.get_left_bottom()),
- config.get_font(),
- config.get_bold_font(),
- is_bold=self.bold_font_lb,
- fill=self.font_color_lb)
- left = concatenate_image([left_top, empty_padding, left_bottom])
- # 填充右边的文字内容
- right_top = text_to_image(container.get_attribute_str(config.get_right_top()),
- config.get_font(),
- config.get_bold_font(),
- is_bold=self.bold_font_rt,
- fill=self.font_color_rt)
- right_bottom = text_to_image(container.get_attribute_str(config.get_right_bottom()),
- config.get_font(),
- config.get_bold_font(),
- is_bold=self.bold_font_rb,
- fill=self.font_color_rb)
- right = concatenate_image([right_top, empty_padding, right_bottom])
- # 将左右两边的文字内容等比例缩放到相同的高度
- max_height = max(left.height, right.height)
- left = padding_image(left, int(max_height * padding_ratio), 'tb')
- right = padding_image(right, int(max_height * padding_ratio), 't')
- right = padding_image(right, left.height - right.height, 'b')
- logo = config.load_logo(container.make)
- if self.logo_enable:
- if self.is_logo_left():
- # 如果 logo 在左边
- append_image_by_side(watermark, [logo, left], is_start=logo is None)
- append_image_by_side(watermark, [right], side='right')
- else:
- # 如果 logo 在右边
- if logo is not None:
- # 如果 logo 不为空,等比例缩小 logo
- logo = padding_image(logo, int(padding_ratio * logo.height))
- # 插入一根线条用于分割 logo 和文字
- line = padding_image(LINE_GRAY, int(padding_ratio * LINE_GRAY.height * .8))
- else:
- line = LINE_TRANSPARENT.copy()
- append_image_by_side(watermark, [left], is_start=True)
- append_image_by_side(watermark, [logo, line, right], side='right')
- line.close()
- else:
- append_image_by_side(watermark, [left], is_start=True)
- append_image_by_side(watermark, [right], side='right')
- left.close()
- right.close()
- # 缩放水印的大小
- watermark = resize_image_with_width(watermark, container.get_width())
- # 将水印图片放置在原始图片的下方
- bg = ImageOps.expand(container.get_watermark_img().convert('RGBA'),
- border=(0, 0, 0, watermark.height),
- fill=self.bg_color)
- fg = ImageOps.expand(watermark, border=(0, container.get_height(), 0, 0), fill=TRANSPARENT)
- result = Image.alpha_composite(bg, fg)
- watermark.close()
- # 更新图片对象
- result = ImageOps.exif_transpose(result).convert('RGB')
- container.update_watermark_img(result)
- class WatermarkRightLogoProcessor(WatermarkProcessor):
- LAYOUT_ID = 'watermark_right_logo'
- LAYOUT_NAME = 'normal(Logo 居右)'
- def __init__(self, config: Config):
- super().__init__(config)
- self.logo_position = 'right'
- class WatermarkLeftLogoProcessor(WatermarkProcessor):
- LAYOUT_ID = 'watermark_left_logo'
- LAYOUT_NAME = 'normal'
- def __init__(self, config: Config):
- super().__init__(config)
- self.logo_position = 'left'
- class DarkWatermarkRightLogoProcessor(WatermarkRightLogoProcessor):
- LAYOUT_ID = 'dark_watermark_right_logo'
- LAYOUT_NAME = 'normal(黑红配色,Logo 居右)'
- def __init__(self, config: Config):
- super().__init__(config)
- self.bg_color = '#212121'
- self.line_color = GRAY
- self.font_color_lt = '#D32F2F'
- self.bold_font_lt = True
- self.font_color_lb = '#d4d1cc'
- self.bold_font_lb = False
- self.font_color_rt = '#D32F2F'
- self.bold_font_rt = True
- self.font_color_rb = '#d4d1cc'
- self.bold_font_rb = False
- class DarkWatermarkLeftLogoProcessor(WatermarkLeftLogoProcessor):
- LAYOUT_ID = 'dark_watermark_left_logo'
- LAYOUT_NAME = 'normal(黑红配色)'
- def __init__(self, config: Config):
- super().__init__(config)
- self.bg_color = '#212121'
- self.line_color = GRAY
- self.font_color_lt = '#D32F2F'
- self.bold_font_lt = True
- self.font_color_lb = '#d4d1cc'
- self.bold_font_lb = False
- self.font_color_rt = '#D32F2F'
- self.bold_font_rt = True
- self.font_color_rb = '#d4d1cc'
- self.bold_font_rb = False
- class CustomWatermarkProcessor(WatermarkProcessor):
- LAYOUT_ID = 'custom_watermark'
- LAYOUT_NAME = 'normal(自定义配置)'
- def __init__(self, config: Config):
- super().__init__(config)
- # 读取配置文件
- self.logo_position = self.config.is_logo_left()
- self.logo_enable = self.config.has_logo_enabled()
- self.bg_color = self.config.get_background_color()
- self.font_color_lt = self.config.get_left_top().get_color()
- self.bold_font_lt = self.config.get_left_top().is_bold()
- self.font_color_lb = self.config.get_left_bottom().get_color()
- self.bold_font_lb = self.config.get_left_bottom().is_bold()
- self.font_color_rt = self.config.get_right_top().get_color()
- self.bold_font_rt = self.config.get_right_top().is_bold()
- self.font_color_rb = self.config.get_right_bottom().get_color()
- self.bold_font_rb = self.config.get_right_bottom().is_bold()
- class MarginProcessor(ProcessorComponent):
- LAYOUT_ID = 'margin'
- def process(self, container: ImageContainer) -> None:
- config = self.config
- padding_size = int(config.get_white_margin_width() * min(container.get_width(), container.get_height()) / 100)
- padding_img = padding_image(container.get_watermark_img(), padding_size, 'tlr', color=config.bg_color)
- container.update_watermark_img(padding_img)
- class SimpleProcessor(ProcessorComponent):
- LAYOUT_ID = 'simple'
- LAYOUT_NAME = '简洁'
- def process(self, container: ImageContainer) -> None:
- ratio = .16 if container.get_ratio() >= 1 else .1
- padding_ratio = .5 if container.get_ratio() >= 1 else .5
- first_text = text_to_image('Shot on',
- self.config.get_alternative_font(),
- self.config.get_alternative_bold_font(),
- is_bold=False,
- fill='#212121')
- model = text_to_image(container.get_model().replace(r'/', ' ').replace(r'_', ' '),
- self.config.get_alternative_font(),
- self.config.get_alternative_bold_font(),
- is_bold=True,
- fill='#D32F2F')
- make = text_to_image(container.get_make().split(' ')[0],
- self.config.get_alternative_font(),
- self.config.get_alternative_bold_font(),
- is_bold=True,
- fill='#212121')
- first_line = merge_images([first_text, MIDDLE_HORIZONTAL_GAP, model, MIDDLE_HORIZONTAL_GAP, make], 0, 1)
- second_line_text = container.get_param_str()
- second_line = text_to_image(second_line_text,
- self.config.get_alternative_font(),
- self.config.get_alternative_bold_font(),
- is_bold=False,
- fill='#9E9E9E')
- image = merge_images([first_line, MIDDLE_VERTICAL_GAP, second_line], 1, 0)
- height = container.get_height() * ratio * padding_ratio
- image = resize_image_with_height(image, int(height))
- horizontal_padding = int((container.get_width() - image.width) / 2)
- vertical_padding = int((container.get_height() * ratio - image.height) / 2)
- watermark = ImageOps.expand(image, (horizontal_padding, vertical_padding), fill=TRANSPARENT)
- bg = Image.new('RGBA', watermark.size, color='white')
- bg = Image.alpha_composite(bg, watermark)
- watermark_img = merge_images([container.get_watermark_img(), bg], 1, 1)
- container.update_watermark_img(watermark_img)
- class PaddingToOriginalRatioProcessor(ProcessorComponent):
- LAYOUT_ID = 'padding_to_original_ratio'
- def process(self, container: ImageContainer) -> None:
- original_ratio = container.get_original_ratio()
- ratio = container.get_ratio()
- if original_ratio > ratio:
- # 如果原始比例大于当前比例,说明宽度大于高度,需要填充高度
- padding_size = int(container.get_width() / original_ratio - container.get_height())
- padding_img = ImageOps.expand(container.get_watermark_img(), (0, padding_size), fill='white')
- else:
- # 如果原始比例小于当前比例,说明高度大于宽度,需要填充宽度
- padding_size = int(container.get_height() * original_ratio - container.get_width())
- padding_img = ImageOps.expand(container.get_watermark_img(), (padding_size, 0), fill='white')
- container.update_watermark_img(padding_img)
- PADDING_PERCENT_IN_BACKGROUND = 0.18
- GAUSSIAN_KERNEL_RADIUS = 35
- class BackgroundBlurProcessor(ProcessorComponent):
- LAYOUT_ID = 'background_blur'
- LAYOUT_NAME = '背景模糊'
- def process(self, container: ImageContainer) -> None:
- background = container.get_watermark_img()
- background = background.filter(ImageFilter.GaussianBlur(radius=GAUSSIAN_KERNEL_RADIUS))
- fg = Image.new('RGB', background.size, color=(255, 255, 255))
- background = Image.blend(background, fg, 0.1)
- background = background.resize((int(container.get_width() * (1 + PADDING_PERCENT_IN_BACKGROUND)),
- int(container.get_height() * (1 + PADDING_PERCENT_IN_BACKGROUND))))
- background.paste(container.get_watermark_img(),
- (int(container.get_width() * PADDING_PERCENT_IN_BACKGROUND / 2),
- int(container.get_height() * PADDING_PERCENT_IN_BACKGROUND / 2)))
- container.update_watermark_img(background)
- class BackgroundBlurWithWhiteBorderProcessor(ProcessorComponent):
- LAYOUT_ID = 'background_blur_with_white_border'
- LAYOUT_NAME = '背景模糊+白框'
- def process(self, container: ImageContainer) -> None:
- padding_size = int(
- self.config.get_white_margin_width() * min(container.get_width(), container.get_height()) / 256)
- padding_img = padding_image(container.get_watermark_img(), padding_size, 'tblr', color='white')
- background = container.get_img()
- background = background.filter(ImageFilter.GaussianBlur(radius=GAUSSIAN_KERNEL_RADIUS))
- background = background.resize((int(padding_img.width * (1 + PADDING_PERCENT_IN_BACKGROUND)),
- int(padding_img.height * (1 + PADDING_PERCENT_IN_BACKGROUND))))
- fg = Image.new('RGB', background.size, color=(255, 255, 255))
- background = Image.blend(background, fg, 0.1)
- background.paste(padding_img, (int(padding_img.width * PADDING_PERCENT_IN_BACKGROUND / 2),
- int(padding_img.height * PADDING_PERCENT_IN_BACKGROUND / 2)))
- container.update_watermark_img(background)
- class PureWhiteMarginProcessor(ProcessorComponent):
- LAYOUT_ID = 'pure_white_margin'
- LAYOUT_NAME = '白色边框'
- def process(self, container: ImageContainer) -> None:
- config = self.config
- padding_size = int(config.get_white_margin_width() * min(container.get_width(), container.get_height()) / 100)
- padding_img = padding_image(container.get_watermark_img(), padding_size, 'tlrb', color=config.bg_color)
- container.update_watermark_img(padding_img)
复制代码
这段代码是用于处理图像的处理器组件,包括不同的处理器类和处理器链。
其中,`ProcessorComponent` 是一个抽象基类,定义了处理器组件的基本接口和方法。每个具体的处理器类都继承自 `ProcessorComponent`,并实现了 `process` 方法,用于对图像进行特定的处理操作。
以下是一些具体的处理器类和它们的作用:
- `ShadowProcessor`:为图像添加阴影效果。
- `SquareProcessor`:将图像调整为正方形。
- `WatermarkProcessor`:根据配置信息生成水印,并将水印添加到图像中。
- `MarginProcessor`:在图像周围添加白色边框。
- `SimpleProcessor`:生成简洁风格的水印。
- `PaddingToOriginalRatioProcessor`:根据原始图像的比例,调整图像的宽高比。
- `BackgroundBlurProcessor`:对图像的背景进行高斯模糊处理。
- `BackgroundBlurWithWhiteBorderProcessor`:在背景模糊的基础上添加白色边框。
- `PureWhiteMarginProcessor`:在图像周围添加纯白色边框。
这些处理器类通过继承和重写 `process` 方法,实现了不同的图像处理操作。在处理过程中,它们可以访问图像容器对象 `ImageContainer`,并对其中的水印图像进行处理。
此外,还有一个 `ProcessorChain` 类,用于将多个处理器组合成一个处理器链,依次对图像进行处理。
3.config.py
- import os
- import yaml
- from PIL import Image
- from PIL import ImageFont
- from enums.constant import CUSTOM_VALUE
- from enums.constant import LOCATION_LEFT_BOTTOM
- from enums.constant import LOCATION_LEFT_TOP
- from enums.constant import LOCATION_RIGHT_BOTTOM
- from enums.constant import LOCATION_RIGHT_TOP
- class ElementConfig(object):
- """
- 布局中元素的配置对象
- """
- def __init__(self, element):
- self.element = element
- def get_name(self):
- return self.element['name']
- def is_bold(self):
- return self.element['is_bold']
- def get_value(self):
- return self.element['value'] if 'value' in self.element else None
- def get_color(self):
- if 'color' in self.element:
- return self.element['color']
- else:
- return '#212121'
- # 字体大小,影响字体的清晰度
- FONT_SIZE = 240
- BOLD_FONT_SIZE = 260
- class Config(object):
- """
- 配置对象
- """
- def __init__(self, path):
- self._path = path
- with open(self._path, 'r', encoding='utf-8') as f:
- self._data = yaml.safe_load(f)
- self._logos = {}
- self._left_top = ElementConfig(self._data['layout']['elements'][LOCATION_LEFT_TOP])
- self._left_bottom = ElementConfig(self._data['layout']['elements'][LOCATION_LEFT_BOTTOM])
- self._right_top = ElementConfig(self._data['layout']['elements'][LOCATION_RIGHT_TOP])
- self._right_bottom = ElementConfig(self._data['layout']['elements'][LOCATION_RIGHT_BOTTOM])
- self._makes = self._data['logo']['makes']
- self.bg_color = self._data['layout']['background_color'] \
- if 'background_color' in self._data['layout'] \
- else '#ffffff'
- def get(self, key):
- if key in self._data:
- return self._data[key]
- else:
- return None
- def get_or_default(self, key, default):
- if key in self._data:
- return self._data[key]
- else:
- return default
- def set(self, key, value):
- self._data[key] = value
- def load_logo(self, make) -> Image.Image:
- """
- 根据厂商获取 logo
- :param make: 厂商
- :return: logo
- """
- # 已经读到内存中的 logo
- if make in self._logos:
- return self._logos[make]
- # 未读取到内存中的 logo
- for m in self._makes.values():
- if m['id'] == '':
- pass
- if m['id'].lower() in make.lower():
- logo = Image.open(m['path'])
- self._logos[make] = logo
- return logo
- logo_path = self._data['logo']['default']['path']
- logo = Image.open(logo_path)
- self._logos[make] = logo
- return logo
- def get_data(self) -> dict:
- return self._data
- def get_input_dir(self):
- return self._data['base']['input_dir']
- def get_output_dir(self):
- output_dir = self._data['base']['output_dir']
- if not os.path.exists(output_dir):
- os.makedirs(output_dir)
- return output_dir
- def get_quality(self):
- return self._data['base']['quality']
- def get_alternative_font(self):
- return ImageFont.truetype(self._data['base']['alternative_font'], self.get_font_size())
- def get_alternative_bold_font(self):
- return ImageFont.truetype(self._data['base']['alternative_bold_font'], self.get_bold_font_size())
- def get_font(self):
- return ImageFont.truetype(self._data['base']['font'], self.get_font_size())
- def get_bold_font(self):
- return ImageFont.truetype(self._data['base']['bold_font'], self.get_bold_font_size())
- def get_font_size(self):
- font_size = self._data['base']['font_size']
- if font_size == 1:
- return 240
- elif font_size == 2:
- return 250
- elif font_size == 3:
- return 300
- else:
- return 240
- def get_bold_font_size(self):
- font_size = self._data['base']['bold_font_size']
- if font_size == 1:
- return 260
- elif font_size == 2:
- return 290
- elif font_size == 3:
- return 320
- else:
- return 260
- def get_font_padding_level(self):
- bold_font_size = self._data['base']['bold_font_size'] if 1 <= self._data['base']['bold_font_size'] <= 3 else 1
- font_size = self._data['base']['font_size'] if 1 <= self._data['base']['font_size'] <= 3 else 1
- return bold_font_size + font_size
- def save(self):
- with open(self._path, 'w') as f:
- yaml.dump(self._data, f, encoding='utf-8')
- def enable_shadow(self):
- self._data['global']['shadow']['enable'] = True
- def disable_shadow(self):
- self._data['global']['shadow']['enable'] = False
- def has_shadow_enabled(self):
- return self._data['global']['shadow']['enable']
- def has_white_margin_enabled(self):
- return self._data['global']['white_margin']['enable']
- def enable_white_margin(self):
- self._data['global']['white_margin']['enable'] = True
- def disable_white_margin(self):
- self._data['global']['white_margin']['enable'] = False
- def get_white_margin_width(self) -> int:
- white_margin_width = self._data['global']['white_margin']['width']
- if white_margin_width > 30:
- white_margin_width = 30
- if white_margin_width < 0:
- white_margin_width = 0
- self._data['global']['white_margin']['width'] = white_margin_width
- return white_margin_width
- def enable_equivalent_focal_length(self):
- self._data['global']['focal_length']['use_equivalent_focal_length'] = True
- def disable_equivalent_focal_length(self):
- self._data['global']['focal_length']['use_equivalent_focal_length'] = False
- def use_equivalent_focal_length(self):
- return self._data['global']['focal_length']['use_equivalent_focal_length']
- def enable_padding_with_original_ratio(self):
- self._data['global']['padding_with_original_ratio']['enable'] = True
- def disable_padding_with_original_ratio(self):
- self._data['global']['padding_with_original_ratio']['enable'] = False
- def has_padding_with_original_ratio_enabled(self):
- return self._data['global']['padding_with_original_ratio']['enable']
- def set_layout(self, layout):
- self._data['layout']['type'] = layout
- def get_background_color(self) -> str:
- return self._data['layout']['background_color'] if 'background_color' in self._data['layout'] else '#ffffff'
- def enable_logo(self):
- self._data['layout']['logo_enable'] = True
- def disable_logo(self):
- self._data['layout']['logo_enable'] = False
- def has_logo_enabled(self):
- return self._data['layout']['logo_enable']
- def is_logo_left(self):
- if self._data['layout']['logo_position'] == 'left':
- return True
- def set_logo_left(self):
- self._data['layout']['logo_position'] = 'left'
- def set_logo_right(self):
- self._data['layout']['logo_position'] = 'right'
- def get_layout_type(self) -> str:
- return self._data['layout']['type']
- def get_left_top(self) -> ElementConfig:
- return self._left_top
- def get_left_bottom(self) -> ElementConfig:
- return self._left_bottom
- def get_right_top(self) -> ElementConfig:
- return self._right_top
- def get_right_bottom(self) -> ElementConfig:
- return self._right_bottom
- def get_custom_value(self, location):
- if 'value' in self._data['layout']['elements'][location]:
- return self._data['layout']['elements'][location]['value']
- else:
- return ''
- def set_custom(self, location):
- self._data['layout']['elements'][location]['name'] = 'Custom'
- user_input = input('输入自定义字段的值(上次使用的值为:{})\n'.format(self.get_custom_value(location)))
- self._data['layout']['elements'][location]['value'] = user_input
- def set_element_name(self, location, name):
- if CUSTOM_VALUE == name:
- self.set_custom(location)
- else:
- self._data['layout']['elements'][location]['name'] = name
- def set_default_logo_path(self, logo_path):
- self._data["logo"]['default']['path'] = logo_path
- self.save()
复制代码
这段代码定义了两个类:`ElementConfig` 和 `Config`。
`ElementConfig` 类是布局中元素的配置对象,用于封装元素的各种属性,如名称、是否加粗、颜色等。它提供了一些方法来获取这些属性的值。
`Config` 类是配置对象,用于加载和保存配置文件,并提供了一些方法来获取和设置配置项的值。在初始化时,它会读取指定路径的配置文件(YAML 格式),并将配置数据存储在 `_data` 属性中。它还包含了一些与配置相关的方法,如获取输入/输出目录、获取字体信息、加载 logo、设置布局等。
以下是一些主要的方法:
- `load_logo(make)`:根据厂商名称获取对应的 logo 图像。
- `get_input_dir()`:获取输入目录。
- `get_output_dir()`:获取输出目录。
- `get_quality()`:获取图像的质量。
- `get_alternative_font()` 和 `get_alternative_bold_font()`:获取备用字体和加粗备用字体。
- `get_font()` 和 `get_bold_font()`:获取字体和加粗字体。
- `get_font_size()` 和 `get_bold_font_size()`:获取字体大小和加粗字体大小。
- `get_font_padding_level()`:获取字体填充级别。
- `enable_shadow()` 和 `disable_shadow()`:启用或禁用阴影效果。
- `has_shadow_enabled()`:检查阴影效果是否已启用。
- `has_white_margin_enabled()`:检查白色边框是否已启用。
- `enable_white_margin()` 和 `disable_white_margin()`:启用或禁用白色边框。
- `get_white_margin_width()`:获取白色边框的宽度。
- `enable_equivalent_focal_length()` 和 `disable_equivalent_focal_length()`:启用或禁用等效焦距。
- `use_equivalent_focal_length()`:检查是否启用了等效焦距。
- `enable_padding_with_original_ratio()` 和 `disable_padding_with_original_ratio()`:启用或禁用按原始比例填充。
- `has_padding_with_original_ratio_enabled()`:检查是否启用了按原始比例填充。
- `set_layout(layout)`:设置布局类型。
- `get_background_color()`:获取背景颜色。
- `enable_logo()` 和 `disable_logo()`:启用或禁用 logo。
- `has_logo_enabled()`:检查是否启用了 logo。
- `is_logo_left()`:检查 logo 是否位于左侧。
- `set_logo_left()` 和 `set_logo_right()`:将 logo 定位在左侧或右侧。
- `get_left_top()`、`get_left_bottom()`、`get_right_top()` 和 `get_right_bottom()`:获取四个位置的元素配置对象。
- `get_custom_value(location)`:获取指定位置的自定义值。
- `set_custom(location)`:设置指定位置的自定义值。
- `set_element_name(location, name)`:设置指定位置的元素名称。
- `set_default_logo_path(logo_path)`:设置默认 logo 图像的路径。
这些方法提供了对配置文件中各项配置的访问和修改功能,可以根据需要使用它们来获取和设置配置信息。
这是定义的常量:constant.py
- MODEL_NAME = '相机型号(eg. Nikon Z7)'
- MODEL_VALUE = 'Model'
- MAKE_NAME = '相机厂商(eg. Nikon)'
- MAKE_VALUE = 'Make'
- LENS_NAME = '镜头型号(eg. Nikkor 24-70 f/2.8)'
- LENS_VALUE = 'LensModel'
- PARAM_NAME = '拍摄参数(eg. 50mm f/1.8 1/1000s ISO 100)'
- PARAM_VALUE = 'Param'
- DATETIME_NAME = '拍摄时间(eg. 2023-01-01 12:00)'
- DATETIME_VALUE = 'Datetime'
- DATE_NAME = '拍摄日期(eg. 2023-01-01)'
- DATE_VALUE = 'Date'
- CUSTOM_NAME = '自定义'
- CUSTOM_VALUE = 'Custom'
- NONE_NAME = ' '
- NONE_VALUE = 'None'
- LENS_MAKE_LENS_MODEL_NAME = '镜头厂商 + 镜头型号(eg. Nikon Nikkor 24-70 f/2.8)'
- LENS_MAKE_LENS_MODEL_VALUE = 'LensMake_LensModel'
- CAMERA_MODEL_LENS_MODEL_NAME = '相机型号 + 镜头型号(eg. Nikon Z7 Nikkor 24-70 f/2.8)'
- CAMERA_MODEL_LENS_MODEL_VALUE = 'CameraModel_LensModel'
- TOTAL_PIXEL_NAME = '总像素(MP)'
- TOTAL_PIXEL_VALUE = 'TotalPixel'
- CAMERA_MAKE_CAMERA_MODEL_NAME = '相机厂商 + 相机型号(eg. DJI FC123)'
- CAMERA_MAKE_CAMERA_MODEL_VALUE = 'CameraMake_CameraModel'
- FILENAME_NAME = '文件名'
- FILENAME_VALUE = 'Filename'
- DATE_FILENAME_NAME = '日期 + 文件名(eg. 2023-01-01 DXO_0001.jpg)'
- DATE_FILENAME_VALUE = 'Date_Filename'
- DATETIME_FILENAME_NAME = '日期时间 + 文件名(eg. 2023-01-01 12:00 DXO_0001.jpg)'
- DATETIME_FILENAME_VALUE = 'Datetime_Filename'
- LOCATION_LEFT_TOP = 'left_top'
- LOCATION_LEFT_BOTTOM = 'left_bottom'
- LOCATION_RIGHT_TOP = 'right_top'
- LOCATION_RIGHT_BOTTOM = 'right_bottom'
- TRANSPARENT = (0, 0, 0, 0)
- DEBUG = False
- GRAY = '#CBCBC9'
- DEFAULT_VALUE = '--'
复制代码
@FishC |
|