• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SkylerHu / py-img-processor / 21635625330

03 Feb 2026 03:10PM UTC coverage: 98.858% (-0.5%) from 99.375%
21635625330

Pull #13

github

web-flow
Merge 3ba7c09ff into 3602b34bd
Pull Request #13: feat: 支持动图

335 of 342 branches covered (97.95%)

Branch coverage included in aggregate %.

21 of 24 new or added lines in 3 files covered. (87.5%)

1 existing line in 1 file now uncovered.

964 of 972 relevant lines covered (99.18%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.39
/imgprocessor/processor.py
1
#!/usr/bin/env python
2
# coding=utf-8
3
import typing
1✔
4
import tempfile
1✔
5
import colorsys
1✔
6

7
from PIL import Image, ImageOps, ImageFile
1✔
8

9
from imgprocessor import enums, settings
1✔
10
from imgprocessor.parsers import BaseParser, ProcessParams
1✔
11
from imgprocessor.parsers.base import trans_uri_to_im, has_transparency
1✔
12

13

14
class ProcessorCtr(object):
1✔
15

16
    @classmethod
1✔
17
    def handle_img_actions(cls, ori_im: ImageFile.ImageFile, actions: list[BaseParser]) -> ImageFile.ImageFile:
1✔
18
        im = ori_im
1✔
19
        # 解决旋转问题
20
        im = ImageOps.exif_transpose(im)
1✔
21
        for parser in actions:
1✔
22
            im = parser.do_action(im)
1✔
23
        return im
1✔
24

25
    @classmethod
1✔
26
    def save_img_to_file(
1✔
27
        cls,
28
        im: ImageFile.ImageFile,
29
        out_path: typing.Optional[str] = None,
30
        **kwargs: typing.Any,
31
    ) -> typing.Optional[typing.ByteString]:
32
        fmt = kwargs.get("format") or im.format
1✔
33

34
        if fmt:
1!
35
            if fmt.upper() == enums.ImageFormat.JPEG.value and im.mode not in ["GBA", "L"]:
1✔
36
                im = im.convert("RGB")
1✔
37
            elif fmt.upper() == enums.ImageFormat.WEBP.value and im.mode == "P" and has_transparency(im):
1✔
38
                im = im.convert("RGBA")
1✔
39

40
        if not kwargs.get("quality"):
1✔
41
            if fmt and fmt.upper() == enums.ImageFormat.JPEG.value and im.format == enums.ImageFormat.JPEG.value:
1!
UNCOV
42
                kwargs["quality"] = "keep"
×
43
            else:
44
                kwargs["quality"] = settings.PROCESSOR_DEFAULT_QUALITY
1✔
45

46
        if out_path:
1✔
47
            # icc_profile 是为解决色域的问题
48
            im.save(out_path, **kwargs)
1✔
49
            return None
1✔
50

51
        # 没有传递保存的路径,返回文件内容
52
        suffix = fmt or "png"
1✔
53
        with tempfile.NamedTemporaryFile(suffix=f".{suffix}", dir=settings.PROCESSOR_TEMP_DIR) as fp:
1✔
54
            im.save(fp.name, **kwargs)
1✔
55
            fp.seek(0)
1✔
56
            content = fp.read()
1✔
57
        return content
1✔
58

59

60
def process_image(
1✔
61
    input_uri: str,
62
    params: typing.Union[ProcessParams, dict, str],
63
    out_path: typing.Optional[str] = None,
64
    **kwargs: typing.Any,
65
) -> typing.Optional[typing.ByteString]:
66
    """处理图像
67

68
    Args:
69
        input_uri: 输入图像路径
70
        params: 图像处理参数
71
        out_path: 输出图像保存路径
72

73
    Raises:
74
        ProcessLimitException: 超过处理限制会抛出异常
75

76
    Returns:
77
        默认输出直接存储无返回,仅当out_path为空时会返回处理后图像的二进制内容
78
    """
79
    # 初始化输入
80
    params_obj: ProcessParams = ProcessParams.init(params)
1✔
81
    with trans_uri_to_im(input_uri) as ori_im:
1✔
82
        # 处理图像
83
        im = ProcessorCtr.handle_img_actions(ori_im, params_obj.actions)
1✔
84
        # 输出、保存
85
        _kwargs = params_obj.save_parser.compute(ori_im, im)
1✔
86
        _kwargs.update(kwargs)
1✔
87
        ret = ProcessorCtr.save_img_to_file(im, out_path=out_path, **_kwargs)
1✔
88
    return ret
1✔
89

90

91
def process_image_obj(
1✔
92
    ori_im: ImageFile.ImageFile,
93
    params: typing.Union[ProcessParams, dict, str],
94
    out_path: typing.Optional[str] = None,
95
    **kwargs: typing.Any,
96
) -> typing.Optional[typing.ByteString]:
97
    """处理图像
98

99
    Args:
100
        ori_im: 输入图像为Image对象
101
        params: 图像处理参数
102
        out_path: 输出图像保存路径
103

104
    Returns:
105
        默认输出直接存储无返回,仅当out_path为空时会返回处理后图像的二进制内容
106
    """
107
    params_obj: ProcessParams = ProcessParams.init(params)
1✔
108
    im = ProcessorCtr.handle_img_actions(ori_im, params_obj.actions)
1✔
109
    _kwargs = params_obj.save_parser.compute(ori_im, im)
1✔
110
    _kwargs.update(kwargs)
1✔
111
    ret = ProcessorCtr.save_img_to_file(im, out_path=out_path, **_kwargs)
1✔
112
    return ret
1✔
113

114

115
def extract_main_color(img_path: str, delta_h: float = 0.3) -> str:
1✔
116
    """获取图像主色调
117

118
    Args:
119
        img_path: 输入图像的路径
120
        delta_h: 像素色相和平均色相做减法的绝对值小于该值,才用于计算主色调,取值范围[0,1]
121

122
    Returns:
123
        颜色值,eg: FFFFFF
124
    """
125
    r, g, b = 0, 0, 0
1✔
126
    with Image.open(img_path) as im:
1✔
127
        if im.mode != "RGB":
1✔
128
            im = im.convert("RGB")
1✔
129
        # 转换成HSV即 色相(Hue)、饱和度(Saturation)、明度(alue),取值范围[0,1]
130
        # 取H计算平均色相
131
        all_h = [colorsys.rgb_to_hsv(*im.getpixel((x, y)))[0] for x in range(im.size[0]) for y in range(im.size[1])]
1✔
132
        avg_h = sum(all_h) / (im.size[0] * im.size[1])
1✔
133
        # 取与平均色相相近的像素色值rgb用于计算,像素值取值范围[0,255]
134
        beyond = list(
1✔
135
            filter(
136
                lambda x: abs(colorsys.rgb_to_hsv(*x)[0] - avg_h) < delta_h,
137
                [im.getpixel((x, y)) for x in range(im.size[0]) for y in range(im.size[1])],
138
            )
139
        )
140
    if len(beyond):
1✔
141
        r = int(sum(e[0] for e in beyond) / len(beyond))
1✔
142
        g = int(sum(e[1] for e in beyond) / len(beyond))
1✔
143
        b = int(sum(e[2] for e in beyond) / len(beyond))
1✔
144

145
    color = "{}{}{}".format(hex(r)[2:].zfill(2), hex(g)[2:].zfill(2), hex(b)[2:].zfill(2))
1✔
146
    return color.upper()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc