鱼C论坛

 找回密码
 立即注册
查看: 678|回复: 7

封装接口问题

[复制链接]
发表于 2023-12-12 16:44:27 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

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

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2023-12-12 16:45:12 | 显示全部楼层
首先,封装接口的目的是使得代码更具可复用性和可扩展性,方便在不同平台或环境中使用。对于这个添加水印的工具,你可以将核心的功能封装成一个类,并提供相应的接口方法。

根据你的需求,可以将image_container.py文件中的ImageContainer类封装成接口类。以下是一个简单的示例:
from abc import ABC, abstractmethodclass WatermarkToolInterface(ABC):    @abstractmethod    def select_template(self, template_name):        pass        @abstractmethod    def select_params(self, params):        pass        @abstractmethod    def upload_image(self, image_path):        pass        @abstractmethod    def generate_watermark(self):        pass        @abstractmethod    def get_watermark_image_url(self):        pass
然后,你可以创建一个新的类实现WatermarkToolInterface接口,并在其中使用image_container.py中的ImageContainer类来处理图片和生成水印。以下是一个简单的示例实现:
from entity.image_container import ImageContainerfrom entity.config import ElementConfigclass WatermarkTool(WatermarkToolInterface):    def __init__(self):        self.image_container = None            def select_template(self, template_name):        # 根据模板名称选择模板的处理逻辑        # 如果需要,可以在这里设置image_container的一些属性            def select_params(self, params):        # 根据用户选择的参数设置image_container的一些属性            def upload_image(self, image_path):        self.image_container = ImageContainer(image_path)            def generate_watermark(self):        # 生成水印图片的逻辑            def get_watermark_image_url(self):        # 返回生成水印图片的地址
在这个示例中,你可以根据需要扩展WatermarkTool类的其他方法来完成你的需求。例如,你可能需要一个保存生成的水印图片的方法,或者一个返回图片处理相关参数的方法等。

最后,你可以使用Flask框架来创建一个Web应用,将WatermarkTool类作为后端处理的核心部分,用于接收用户的请求、处理上传的图片、生成水印,并返回生成的水印图片的地址供用户下载。

希望以上信息能对你有所帮助!

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-12-12 19:09:18 | 显示全部楼层
没有人吗?只有AI了
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-12-12 19:10:16 | 显示全部楼层
如默 发表于 2023-12-12 19:09
没有人吗?只有AI了

非常抱歉让您等待了这么久,我现在来回答您的问题。

首先,在封装接口方面,您可以按照以下步骤进行操作:

1. 创建一个新的Python文件,命名为`watermark_tool.py`。
2. 在`watermark_tool.py`中导入所需的模块和类:
from entity.image_container import ImageContainerfrom entity.config import ElementConfig
3. 定义一个WatermarkTool类,并将其定义为一个继承自object的对象:
class WatermarkTool(object):    def __init__(self):        self.image_container = None
4. 在WatermarkTool类中,编写各种方法来处理用户的请求。根据您的需求,可以添加以下方法:

- `select_template(self, template_name)`:根据用户选择的模板名称,设置相应的模板处理逻辑。
- `select_params(self, params)`:根据用户选择的参数,设置相应的属性,例如左上角、左下角、右上角、右下角显示的内容。
- `upload_image(self, image_path)`:根据用户上传的图片路径,创建一个ImageContainer对象。
- `generate_watermark(self)`:生成水印图片的逻辑,可以使用ImageContainer对象来实现。
- `get_watermark_image_url(self)`:获取生成的水印图片的地址。

下面是一个示例代码:
class WatermarkTool(object):    def __init__(self):        self.image_container = None    def select_template(self, template_name):        # 根据模板名称选择模板的处理逻辑        # 如果需要,可以在这里设置image_container的一些属性    def select_params(self, params):        # 根据用户选择的参数设置image_container的一些属性    def upload_image(self, image_path):        self.image_container = ImageContainer(image_path)    def generate_watermark(self):        # 生成水印图片的逻辑    def get_watermark_image_url(self):        # 返回生成水印图片的地址
请根据您的实际需求完善这些方法。

最后,您可以使用Flask框架来创建一个Web应用,将WatermarkTool类作为后端处理的核心部分。在Flask应用中,您可以定义路由和视图函数来处理用户的请求,并调用WatermarkTool类中相应的方法来完成相关操作。例如,您可以创建一个上传图片的路由,用户上传图片后,视图函数调用WatermarkTool对象的`upload_image()`方法,然后调用`generate_watermark()`方法生成水印图片,最后调用`get_watermark_image_url()`方法获取生成的水印图片地址并返回给用户。

希望以上解答能够帮助到您!如果您有任何疑问,请随时提问。

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-12-13 09:56:13 | 显示全部楼层

回帖奖励 +10 鱼币

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2023-12-13 09:56:37 | 显示全部楼层
鱼C有你更精彩
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-12-13 17:15:08 | 显示全部楼层

回帖奖励 +10 鱼币

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2023-12-19 10:15:00 | 显示全部楼层

为什么都不回答问题啊,伤心
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-9-21 20:23

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表