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

SkylerHu / py-img-processor / 21596253450

02 Feb 2026 03:30PM UTC coverage: 99.375% (-0.2%) from 99.532%
21596253450

push

github

web-flow
fix: 调整

- fix: 调整`blur`取值范围,从`[1,50]`调整为`[1,512]`
- fix: 修复`mode=P`的图片保存`JPEG`报错的问题
    - JPEG 仅支持真彩色(RGB)或灰度图(L)
- fix: 修复 `ImageFormat.WEBP` 的值,`WebP`统一调整为大写`WEBP`

325 of 328 branches covered (99.09%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 3 files covered. (100.0%)

1 existing line in 1 file now uncovered.

948 of 953 relevant lines covered (99.48%)

0.99 hits per line

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

97.37
/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
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 and fmt.upper() == enums.ImageFormat.JPEG.value and im.mode not in ["GBA", "L"]:
1✔
35
            im = im.convert("RGB")
1✔
36

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

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

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

56

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

65
    Args:
66
        input_uri: 输入图像路径
67
        params: 图像处理参数
68
        out_path: 输出图像保存路径
69

70
    Raises:
71
        ProcessLimitException: 超过处理限制会抛出异常
72

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

87

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

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

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

111

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

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

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

142
    color = "{}{}{}".format(hex(r)[2:].zfill(2), hex(g)[2:].zfill(2), hex(b)[2:].zfill(2))
1✔
143
    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