|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
有这样的一个添加水印的工具,源代码地址:https://github.com/leslievan/semi-utils
需求:想把核心的image_container和image_processor还有config这三个文件里的类,封装成接口,这样方便做小程序或者web端,目前这个是一个cli命令行程序
实现:用户先选择模板,然后选择需要展示的参数,即左上角、左下角、右上角、右下角显示的内容,最后上传图片,后台处理图片,返回生成水印图片的地址,提供用户下载。
问了chatGPT,也问了各大AI,但组成的类还是有问题,建议用flask框架,下面附上核心类:
1.image_container.pyimport 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.pyimport 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.pyimport 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.pyMODEL_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 |
|