py-img-processor 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of py-img-processor might be problematic. Click here for more details.

@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+ import typing
4
+
5
+ from PIL import Image, ImageOps
6
+ from imgprocessor import enums, settings
7
+ from imgprocessor.exceptions import ParamValidateException, ProcessLimitException
8
+ from .base import BaseParser, pre_processing
9
+
10
+
11
+ class ResizeParser(BaseParser):
12
+
13
+ KEY = enums.OpAction.RESIZE
14
+ ARGS = {
15
+ "m": {"type": enums.ArgType.STRING, "default": enums.ResizeMode.LFIT, "choices": enums.ResizeMode},
16
+ "w": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
17
+ "h": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
18
+ "l": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
19
+ "s": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
20
+ "limit": {"type": enums.ArgType.INTEGER, "default": 1, "choices": [0, 1]},
21
+ "color": {
22
+ "type": enums.ArgType.STRING,
23
+ "default": "FFFFFF",
24
+ "regex": r"^([0-9a-fA-F]{6}|[0-9a-fA-F]{8}|[0-9a-fA-F]{3,4})$",
25
+ },
26
+ "p": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": 1000},
27
+ }
28
+
29
+ def __init__(
30
+ self,
31
+ m: str = enums.ResizeMode.LFIT, # type: ignore
32
+ w: int = 0,
33
+ h: int = 0,
34
+ l: int = 0, # noqa: E741
35
+ s: int = 0,
36
+ limit: int = 1,
37
+ color: str = "FFFFFF",
38
+ p: int = 0,
39
+ **kwargs: typing.Any,
40
+ ) -> None:
41
+ self.m = m
42
+ self.w = w
43
+ self.h = h
44
+ self.l = l # noqa: E741
45
+ self.s = s
46
+ self.limit = limit
47
+ self.color = color
48
+ self.p = p
49
+
50
+ def compute(self, src_w: int, src_h: int) -> tuple:
51
+ """计算出`Image.resize`需要的参数"""
52
+ if self.w or self.h:
53
+ if self.m in [enums.ResizeMode.FIXED, enums.ResizeMode.PAD, enums.ResizeMode.FIT]:
54
+ # 有可能改变原图宽高比
55
+ if not (self.w and self.h):
56
+ raise ParamValidateException(f"当m={self.m}的模式下,参数w和h都必不可少且不能为0")
57
+ # w,h按指定的即可,无需计算
58
+ w, h = self.w, self.h
59
+ elif self.m == enums.ResizeMode.MFIT:
60
+ # 等比缩放
61
+ if self.w and self.h:
62
+ # 指定w与h的矩形外的最小图像
63
+ if self.w / self.h > src_w / src_h:
64
+ w, h = self.w, int(self.w * src_h / src_w)
65
+ else:
66
+ w, h = int(self.h * src_w / src_h), self.h
67
+ elif self.w:
68
+ w, h = self.w, int(self.w * src_h / src_w)
69
+ else:
70
+ w, h = int(self.h * src_w / src_h), self.h
71
+ else:
72
+ # 默认 enums.ResizeMode.LFIT
73
+ # 等比缩放
74
+ if self.w and self.h:
75
+ # 指定w与h的矩形内的最大图像
76
+ if self.w / self.h > src_w / src_h:
77
+ w, h = int(self.h * src_w / src_h), self.h
78
+ else:
79
+ w, h = self.w, int(self.w * src_h / src_w)
80
+ elif self.w:
81
+ w, h = self.w, int(self.w * src_h / src_w)
82
+ else:
83
+ w, h = int(self.h * src_w / src_h), self.h
84
+ elif self.l:
85
+ # 按最长边缩放
86
+ if src_w > src_h:
87
+ w, h = self.l, int(src_h * self.l / src_w)
88
+ else:
89
+ w, h = int(src_w * self.l / src_h), self.l
90
+ elif self.s:
91
+ # 按最短边缩放
92
+ if src_w > src_h:
93
+ w, h = int(src_w * self.s / src_h), self.s
94
+ else:
95
+ w, h = self.s, int(src_h * self.s / src_w)
96
+ elif self.p:
97
+ # 按照比例缩放
98
+ w, h = int(src_w * self.p / 100), int(src_h * self.p / 100)
99
+ else:
100
+ # 缺少参数
101
+ raise ParamValidateException("resize操作缺少合法参数")
102
+
103
+ if self.limit and (w > src_w or h > src_h):
104
+ # 超过原图大小,默认不处理
105
+ w, h = (src_w, src_h)
106
+ elif w * h > settings.PROCESSOR_MAX_PIXEL:
107
+ raise ProcessLimitException(f"缩放的目标图像总像素不可超过{settings.PROCESSOR_MAX_PIXEL}像素")
108
+ return (w, h)
109
+
110
+ def do_action(self, im: Image) -> Image:
111
+ im = pre_processing(im)
112
+ size = self.compute(*im.size)
113
+ if size == im.size:
114
+ # 大小没有变化直接返回
115
+ return im
116
+ if self.m == enums.ResizeMode.PAD:
117
+ out = ImageOps.pad(im, size, color=f"#{self.color}")
118
+ elif self.m == enums.ResizeMode.FIT:
119
+ out = ImageOps.fit(im, size)
120
+ else:
121
+ out = im.resize(size, resample=Image.LANCZOS)
122
+ return out
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+ import typing
4
+
5
+ from PIL import Image
6
+
7
+ from imgprocessor import enums
8
+ from .base import BaseParser, pre_processing
9
+
10
+
11
+ class RotateParser(BaseParser):
12
+
13
+ KEY = enums.OpAction.ROTATE
14
+ ARGS = {
15
+ # 顺时针旋转的度数
16
+ "value": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 360},
17
+ }
18
+
19
+ def __init__(
20
+ self,
21
+ value: int = 0,
22
+ **kwargs: typing.Any,
23
+ ) -> None:
24
+ self.value = value
25
+
26
+ def do_action(self, im: Image) -> Image:
27
+ im = pre_processing(im)
28
+ if 0 < self.value < 360:
29
+ # 函数提供的是逆时针旋转
30
+ im = im.rotate(360 - self.value, expand=True)
31
+ return im
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+ import typing
4
+
5
+ from PIL import Image, ImageFont, ImageDraw
6
+
7
+ from imgprocessor import enums, settings, utils
8
+ from imgprocessor.exceptions import ParamValidateException
9
+ from .base import BaseParser, pre_processing, compute_splice_two_im, compute_by_geography
10
+
11
+
12
+ class WatermarkParser(BaseParser):
13
+
14
+ KEY = enums.OpAction.WATERMARK
15
+ ARGS = {
16
+ # 水印本身的不透明度,100表示完全不透明
17
+ "t": {"type": enums.ArgType.INTEGER, "default": 100, "min": 0, "max": 100},
18
+ "g": {"type": enums.ArgType.STRING, "choices": enums.Geography},
19
+ "x": {"type": enums.ArgType.INTEGER, "default": 10, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
20
+ "y": {"type": enums.ArgType.INTEGER, "default": 10, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
21
+ # percent field, eg: xy
22
+ "pf": {"type": enums.ArgType.STRING, "default": ""},
23
+ # 是否将图片水印或文字水印铺满原图; 值为1开启
24
+ "fill": {"type": enums.ArgType.INTEGER, "default": 0, "choices": [0, 1]},
25
+ "padx": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 4096},
26
+ "pady": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 4096},
27
+ # 图片水印路径
28
+ "image": {"type": enums.ArgType.STRING, "base64_encode": True},
29
+ # 水印的原始设计参照尺寸,会根据原图大小缩放水印
30
+ "design": {"type": enums.ArgType.INTEGER, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
31
+ # 文字
32
+ "text": {"type": enums.ArgType.STRING, "base64_encode": True, "max_length": 64},
33
+ "font": {"type": enums.ArgType.STRING, "base64_encode": True},
34
+ "color": {"type": enums.ArgType.STRING, "default": "000000", "regex": "^([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$"},
35
+ "size": {"type": enums.ArgType.INTEGER, "default": 40, "min": 1, "max": 1000},
36
+ # 文字水印的阴影透明度, 0表示没有阴影
37
+ "shadow": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 100},
38
+ # 顺时针旋转角度
39
+ "rotate": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 360},
40
+ # 图文混合水印参数
41
+ # 文字和图片水印的前后顺序; 0表示图片水印在前;1表示文字水印在前
42
+ "order": {"type": enums.ArgType.INTEGER, "default": enums.PositionOrder.BEFORE, "choices": enums.PositionOrder},
43
+ # 文字水印和图片水印的对齐方式; 0表示文字水印和图片水印上对齐; 1表示文字水印和图片水印中对齐; 2: 表示文字水印和图片水印下对齐
44
+ "align": {"type": enums.ArgType.INTEGER, "default": enums.PositionAlign.BOTTOM, "choices": enums.PositionAlign},
45
+ # 文字水印和图片水印间的间距
46
+ "interval": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 1000},
47
+ }
48
+
49
+ def __init__(
50
+ self,
51
+ t: int = 100,
52
+ g: typing.Optional[str] = None,
53
+ x: int = 10,
54
+ y: int = 10,
55
+ pf: str = "",
56
+ fill: int = 0,
57
+ padx: int = 0,
58
+ pady: int = 0,
59
+ image: typing.Optional[str] = None,
60
+ design: typing.Optional[int] = None,
61
+ text: typing.Optional[str] = None,
62
+ font: typing.Optional[str] = None,
63
+ color: str = "000000",
64
+ size: int = 40,
65
+ shadow: int = 0,
66
+ rotate: int = 0,
67
+ order: int = 0,
68
+ align: int = 2,
69
+ interval: int = 0,
70
+ **kwargs: typing.Any,
71
+ ) -> None:
72
+ self.t = t
73
+ self.g = g
74
+ self.x = x
75
+ self.y = y
76
+ self.pf = pf
77
+ self.fill = fill
78
+ self.padx = padx
79
+ self.pady = pady
80
+ self.image = image
81
+ self.design = design
82
+ self.text = text
83
+ self.font = font
84
+ self.color = color
85
+ self.size = size
86
+ self.shadow = shadow
87
+ self.rotate = rotate
88
+ self.order = order
89
+ self.align = align
90
+ self.interval = interval
91
+
92
+ def validate(self) -> None:
93
+ super().validate()
94
+ if not self.image and not self.text:
95
+ raise ParamValidateException("image或者text参数必须传递一个")
96
+
97
+ def get_watermark_im(self) -> Image:
98
+ """初始化水印对象"""
99
+ w1, h1, w2, h2 = 0, 0, 0, 0
100
+ icon = None
101
+ if self.image:
102
+ icon = Image.open(self.image)
103
+ icon = pre_processing(icon, use_alpha=True)
104
+ if not self.text:
105
+ # 没有文字,直接返回
106
+ return icon
107
+ w1, h1 = icon.size
108
+
109
+ try:
110
+ _font_path = self.font or settings.PROCESSOR_TEXT_FONT
111
+ font = ImageFont.truetype(_font_path, self.size)
112
+ except OSError:
113
+ raise ParamValidateException(f"未找到字体 {_font_path}")
114
+
115
+ if utils.get_pil_version() >= utils.Version("10.0.0"):
116
+ _, _, w2, h2 = font.getbbox(self.text)
117
+ else:
118
+ w2, h2 = font.getsize(self.text)
119
+
120
+ w, h, x1, y1, x2, y2 = compute_splice_two_im(
121
+ w1,
122
+ h1,
123
+ w2,
124
+ h2,
125
+ align=self.align,
126
+ order=self.order,
127
+ interval=self.interval,
128
+ )
129
+
130
+ mark = Image.new("RGBA", (w, h))
131
+ draw = ImageDraw.Draw(mark, mode="RGBA")
132
+
133
+ # 阴影要单独处理透明度,放在文字之前处理
134
+ if self.shadow:
135
+ offset = max(int(self.size / 20), 2)
136
+ shadow_color = "#000000"
137
+ # 左上到右下的阴影,只保留这一个
138
+ draw.text((x2 + offset, y2 + offset), self.text, font=font, fill=shadow_color)
139
+ # draw.text((x2 - offset, y2 + offset), self.text, font=font, fill=shadow_color)
140
+ # draw.text((x2 + offset, y2 - offset), self.text, font=font, fill=shadow_color)
141
+ # draw.text((x2 - offset, y2 - offset), self.text, font=font, fill=shadow_color)
142
+ _, _, _, alpha_channel = mark.split()
143
+ alpha_channel = alpha_channel.point(lambda i: min(int(255 * self.shadow / 100), i))
144
+ mark.putalpha(alpha_channel)
145
+
146
+ # 处理文字
147
+ draw.text((x2, y2), self.text, font=font, fill=f"#{self.color}")
148
+
149
+ if icon:
150
+ # icon放在文字之后粘贴,是因为文字要做一些其他处理
151
+ mark.paste(icon, (x1, y1), icon)
152
+
153
+ return mark
154
+
155
+ def do_action(self, im: Image) -> Image:
156
+ im = pre_processing(im, use_alpha=True)
157
+ src_w, src_h = im.size
158
+
159
+ pf = self.pf or ""
160
+ if self.g:
161
+ # g在的时候pf不生效
162
+ pf = ""
163
+
164
+ mark = self.get_watermark_im()
165
+ w, h = mark.size
166
+
167
+ if self.design:
168
+ # 处理缩放
169
+ rate = min(src_w, src_h) / self.design
170
+ if rate != 1:
171
+ w, h = int(w * rate), int(h * rate)
172
+ mark = mark.resize((w, h), resample=Image.LANCZOS)
173
+
174
+ if 0 < self.rotate < 360:
175
+ # 处理旋转
176
+ mark = mark.rotate(360 - self.rotate, expand=True)
177
+ # 旋转会改变大小
178
+ w, h = mark.size
179
+
180
+ if w > src_w or h > src_h:
181
+ # 水印大小超过原图了, 原图矩形内的最大图像
182
+ if w / h > src_w / src_h:
183
+ w, h = src_w, int(src_w * h / w)
184
+ self.x = 0
185
+ else:
186
+ w, h = int(src_h * w / h), src_h
187
+ self.y = 0
188
+ mark = mark.resize((w, h), resample=Image.LANCZOS)
189
+
190
+ if self.t < 100:
191
+ # 处理透明度
192
+ _, _, _, alpha_channel = mark.split()
193
+ alpha_channel = alpha_channel.point(lambda i: min(int(255 * self.t / 100), i))
194
+ mark.putalpha(alpha_channel)
195
+
196
+ # 计算位置,粘贴水印
197
+ x, y = compute_by_geography(src_w, src_h, self.x, self.y, w, h, self.g, pf)
198
+ im.paste(mark, (x, y), mark)
199
+
200
+ if self.fill:
201
+ # 铺满整个图片
202
+ # 寻找平铺最左上角的原点
203
+ wx, wy = x, y
204
+ while wx > 0:
205
+ wx = wx - w - self.padx
206
+ while wy > 0:
207
+ wy = wy - h - self.pady
208
+ # 往右下角方向平铺
209
+ ux = wx
210
+ while ux <= src_w:
211
+ uy = wy
212
+ while uy <= src_h:
213
+ if (ux, uy) != (x, y):
214
+ im.paste(mark, (ux, uy), mark)
215
+ uy = uy + h + self.pady
216
+ ux = ux + w + self.padx
217
+
218
+ return im
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+ import typing
4
+ import os
5
+ import tempfile
6
+ import colorsys
7
+
8
+ from PIL import Image, ImageOps
9
+
10
+ from imgprocessor import settings, enums
11
+ from imgprocessor.exceptions import ProcessLimitException
12
+ from imgprocessor.parsers import BaseParser, ProcessParams
13
+
14
+
15
+ def handle_img_actions(ori_im: Image, actions: list[BaseParser]) -> Image:
16
+ src_w, src_h = ori_im.size
17
+ if src_w > settings.PROCESSOR_MAX_W_H or src_h > settings.PROCESSOR_MAX_W_H:
18
+ raise ProcessLimitException(
19
+ f"图像宽和高单边像素不能超过{settings.PROCESSOR_MAX_W_H}像素,输入图像({src_w}, {src_h})"
20
+ )
21
+ if src_w * src_h > settings.PROCESSOR_MAX_PIXEL:
22
+ raise ProcessLimitException(f"图像总像素不可超过{settings.PROCESSOR_MAX_PIXEL}像素,输入图像({src_w}, {src_h})")
23
+
24
+ im = ori_im
25
+ im = ImageOps.exif_transpose(im)
26
+
27
+ for parser in actions:
28
+ im = parser.do_action(im)
29
+
30
+ return im
31
+
32
+
33
+ def save_img_to_file(
34
+ im: Image,
35
+ out_path: typing.Optional[str] = None,
36
+ **kwargs: typing.Any,
37
+ ) -> typing.Optional[typing.ByteString]:
38
+ fmt = kwargs.get("format") or im.format
39
+ kwargs["format"] = fmt
40
+
41
+ if fmt.upper() == enums.ImageFormat.JPEG and im.mode == "RGBA":
42
+ im = im.convert("RGB")
43
+
44
+ if not kwargs.get("quality"):
45
+ if fmt.upper() == enums.ImageFormat.JPEG and im.format == enums.ImageFormat.JPEG:
46
+ kwargs["quality"] = "keep"
47
+ else:
48
+ kwargs["quality"] = settings.PROCESSOR_DEFAULT_QUALITY
49
+
50
+ if out_path:
51
+ # icc_profile 是为解决色域的问题
52
+ im.save(out_path, **kwargs)
53
+ return None
54
+
55
+ # 没有传递保存的路径,返回文件内容
56
+ suffix = fmt or "png"
57
+ with tempfile.NamedTemporaryFile(suffix=f".{suffix}") as fp:
58
+ im.save(fp.name, **kwargs)
59
+ fp.seek(0)
60
+ content = fp.read()
61
+ return content
62
+
63
+
64
+ def process_image_by_path(
65
+ input_path: str, out_path: str, params: typing.Union[ProcessParams, dict, str]
66
+ ) -> typing.Optional[typing.ByteString]:
67
+ """处理图像
68
+
69
+ Args:
70
+ input_path: 输入图像文件路径
71
+ out_path: 输出图像保存路径
72
+ params: 图像处理参数
73
+
74
+ Raises:
75
+ ProcessLimitException: 超过处理限制会抛出异常
76
+
77
+ Returns:
78
+ 默认输出直接存储无返回,仅当out_path为空时会返回处理后图像的二进制内容
79
+ """
80
+ size = os.path.getsize(input_path)
81
+ if size > settings.PROCESSOR_MAX_FILE_SIZE * 1024 * 1024:
82
+ raise ProcessLimitException(f"图像文件大小不得超过{settings.PROCESSOR_MAX_FILE_SIZE}MB")
83
+ if isinstance(params, dict):
84
+ params = ProcessParams(**params)
85
+ elif isinstance(params, str):
86
+ params = ProcessParams.parse_str(params)
87
+ params = typing.cast(ProcessParams, params)
88
+
89
+ ori_im = Image.open(input_path)
90
+ # 处理图像
91
+ im = handle_img_actions(ori_im, params.actions)
92
+
93
+ kwargs = params.save_parser.compute(ori_im, im)
94
+ return save_img_to_file(im, out_path=out_path, **kwargs)
95
+
96
+
97
+ def extract_main_color(img_path: str, delta_h: float = 0.3) -> str:
98
+ """获取图像主色调
99
+
100
+ Args:
101
+ img_path: 输入图像的路径
102
+ delta_h: 像素色相和平均色相做减法的绝对值小于改值,才用于计算主色调,取值范围[0,1]
103
+
104
+ Returns:
105
+ 颜色值,eg: FFFFFF
106
+ """
107
+ r, g, b = 0, 0, 0
108
+ im = Image.open(img_path)
109
+ if im.mode != "RGB":
110
+ im = im.convert("RGB")
111
+ # 转换成HSV即 色相(Hue)、饱和度(Saturation)、明度(alue),取值范围[0,1]
112
+ # 取H计算平均色相
113
+ 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])]
114
+ avg_h = sum(all_h) / (im.size[0] * im.size[1])
115
+ # 取与平均色相相近的像素色值rgb用于计算,像素值取值范围[0,255]
116
+ beyond = list(
117
+ filter(
118
+ lambda x: abs(colorsys.rgb_to_hsv(*x)[0] - avg_h) < delta_h,
119
+ [im.getpixel((x, y)) for x in range(im.size[0]) for y in range(im.size[1])],
120
+ )
121
+ )
122
+ if len(beyond):
123
+ r = int(sum(e[0] for e in beyond) / len(beyond))
124
+ g = int(sum(e[1] for e in beyond) / len(beyond))
125
+ b = int(sum(e[2] for e in beyond) / len(beyond))
126
+
127
+ color = "{}{}{}".format(hex(r)[2:].zfill(2), hex(g)[2:].zfill(2), hex(b)[2:].zfill(2))
128
+ return color.upper()
imgprocessor/utils.py ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+ import base64
4
+ import PIL
5
+
6
+ try:
7
+ # python3.12之后被移除
8
+ from distutils.version import StrictVersion as Version
9
+ except Exception:
10
+ from packaging.version import Version # type: ignore[no-redef]
11
+
12
+
13
+ def get_pil_version() -> Version:
14
+ return Version(PIL.__version__)
15
+
16
+
17
+ def base64url_encode(value: str) -> str:
18
+ """
19
+ 对内容进行URL安全的Base64编码,需要将结果中的部分编码替换:
20
+
21
+ - 将结果中的加号 `+` 替换成短划线 `-`;
22
+ - 将结果中的正斜线 `/` 替换成下划线 `_`;
23
+ - 将结果中尾部的所有等号 `=` 省略。
24
+
25
+ Args:
26
+ value: 输入字符串
27
+
28
+ Returns:
29
+ 返回编码后字符串
30
+
31
+ """
32
+ s = base64.urlsafe_b64encode(value.encode()).decode()
33
+ s = s.strip("=")
34
+ return s
35
+
36
+
37
+ def base64url_decode(value: str) -> str:
38
+ """
39
+ 对URL安全编码进行解码
40
+
41
+ Args:
42
+ value: 输入编码字符串
43
+
44
+ Returns:
45
+ 解码后字符串
46
+
47
+ """
48
+ # 补全后面等号
49
+ padding = 4 - (len(value) % 4)
50
+ value = value + ("=" * padding)
51
+ # 解码
52
+ s = base64.urlsafe_b64decode(value.encode()).decode()
53
+ s = s.strip("=")
54
+ return s
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Skyler Hu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.