pyimagecuda 0.0.5__cp310-cp310-win_amd64.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.
pyimagecuda/fill.py ADDED
@@ -0,0 +1,263 @@
1
+ from typing import Literal
2
+ from .image import Image
3
+ from .pyimagecuda_internal import (fill_color_f32, #type: ignore
4
+ fill_gradient_f32,
5
+ fill_circle_f32,
6
+ fill_checkerboard_f32,
7
+ fill_grid_f32,
8
+ fill_stripes_f32,
9
+ fill_dots_f32,
10
+ fill_noise_f32,
11
+ fill_perlin_f32,
12
+ fill_ngon_f32
13
+ )
14
+
15
+
16
+ class Fill:
17
+
18
+ @staticmethod
19
+ def color(image: Image, rgba: tuple[float, float, float, float]) -> None:
20
+ """
21
+ Fills the image with a solid color (in-place).
22
+
23
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#solid-colors
24
+ """
25
+ fill_color_f32(image._buffer._handle, rgba, image.width, image.height)
26
+
27
+ @staticmethod
28
+ def gradient(image: Image,
29
+ rgba1: tuple[float, float, float, float],
30
+ rgba2: tuple[float, float, float, float],
31
+ direction: Literal['horizontal', 'vertical', 'diagonal', 'radial'] = 'horizontal',
32
+ seamless: bool = False) -> None:
33
+ """
34
+ Fills the image with a gradient (in-place).
35
+
36
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#gradients
37
+ """
38
+ direction_map = {
39
+ 'horizontal': 0,
40
+ 'vertical': 1,
41
+ 'diagonal': 2,
42
+ 'radial': 3
43
+ }
44
+
45
+ dir_int = direction_map.get(direction)
46
+ if dir_int is None:
47
+ raise ValueError(f"Invalid direction: {direction}. Must be one of {list(direction_map.keys())}")
48
+
49
+ fill_gradient_f32(
50
+ image._buffer._handle,
51
+ rgba1,
52
+ rgba2,
53
+ image.width,
54
+ image.height,
55
+ dir_int,
56
+ seamless
57
+ )
58
+
59
+ @staticmethod
60
+ def checkerboard(
61
+ image: Image,
62
+ size: int = 20,
63
+ color1: tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1.0),
64
+ color2: tuple[float, float, float, float] = (0.5, 0.5, 0.5, 1.0),
65
+ offset_x: int = 0,
66
+ offset_y: int = 0
67
+ ) -> None:
68
+ """
69
+ Fills buffer with a checkerboard pattern.
70
+
71
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#checkerboard
72
+ """
73
+ if size <= 0:
74
+ raise ValueError("Checkerboard size must be positive")
75
+
76
+ fill_checkerboard_f32(
77
+ image._buffer._handle,
78
+ image.width, image.height,
79
+ int(size),
80
+ int(offset_x), int(offset_y),
81
+ color1, color2
82
+ )
83
+
84
+ @staticmethod
85
+ def grid(
86
+ image: Image,
87
+ spacing: int = 50,
88
+ line_width: int = 1,
89
+ color: tuple[float, float, float, float] = (0.5, 0.5, 0.5, 1.0),
90
+ bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
91
+ offset_x: int = 0,
92
+ offset_y: int = 0
93
+ ) -> None:
94
+ """
95
+ Fills buffer with a grid pattern.
96
+
97
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#grid
98
+ """
99
+ if spacing <= 0:
100
+ raise ValueError("Grid spacing must be positive")
101
+ if line_width <= 0:
102
+ raise ValueError("Line width must be positive")
103
+
104
+ fill_grid_f32(
105
+ image._buffer._handle,
106
+ image.width, image.height,
107
+ int(spacing), int(line_width),
108
+ int(offset_x), int(offset_y),
109
+ color, bg_color
110
+ )
111
+
112
+ @staticmethod
113
+ def stripes(
114
+ image: Image,
115
+ angle: float = 45.0,
116
+ spacing: int = 40,
117
+ width: int = 20,
118
+ color1: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
119
+ color2: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
120
+ offset: int = 0
121
+ ) -> None:
122
+ """
123
+ Fills buffer with alternating stripes with Anti-Aliasing.
124
+
125
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#stripes
126
+ """
127
+ if spacing <= 0:
128
+ raise ValueError("Stripes spacing must be positive")
129
+
130
+ fill_stripes_f32(
131
+ image._buffer._handle,
132
+ image.width, image.height,
133
+ float(angle), int(spacing), int(width), int(offset),
134
+ color1, color2
135
+ )
136
+
137
+ @staticmethod
138
+ def dots(
139
+ image: Image,
140
+ spacing: int = 40,
141
+ radius: float = 10.0,
142
+ color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
143
+ bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
144
+ offset_x: int = 0,
145
+ offset_y: int = 0,
146
+ softness: float = 0.0
147
+ ) -> None:
148
+ """
149
+ Fills buffer with a Polka Dot pattern.
150
+ - softness: 0.0 = Hard edge, 1.0 = Soft glow.
151
+
152
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#dots
153
+ """
154
+ if spacing <= 0:
155
+ raise ValueError("Spacing must be positive")
156
+
157
+ fill_dots_f32(
158
+ image._buffer._handle,
159
+ image.width, image.height,
160
+ int(spacing), float(radius),
161
+ int(offset_x), int(offset_y), float(softness),
162
+ color, bg_color
163
+ )
164
+
165
+ @staticmethod
166
+ def circle(
167
+ image: Image,
168
+ color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
169
+ bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
170
+ softness: float = 0.0
171
+ ) -> None:
172
+ """
173
+ Fills the buffer with a centered circle fitted to the image size.
174
+ - softness: Edge softness. 0.0 = Hard edge (with AA), >0.0 = Soft gradient.
175
+
176
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#circle
177
+ """
178
+
179
+ fill_circle_f32(
180
+ image._buffer._handle,
181
+ image.width, image.height,
182
+ float(softness),
183
+ color, bg_color
184
+ )
185
+
186
+ @staticmethod
187
+ def noise(
188
+ image: Image,
189
+ seed: float = 0.0,
190
+ monochrome: bool = True
191
+ ) -> None:
192
+ """
193
+ Fills the buffer with random White Noise.
194
+ - seed: Random seed. Change this to animate the noise.
195
+ - monochrome: True for grayscale noise, False for RGB noise.
196
+
197
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#noise
198
+ """
199
+
200
+ fill_noise_f32(
201
+ image._buffer._handle,
202
+ image.width, image.height,
203
+ float(seed),
204
+ int(monochrome)
205
+ )
206
+
207
+ @staticmethod
208
+ def perlin(
209
+ image: Image,
210
+ scale: float = 50.0,
211
+ seed: float = 0.0,
212
+ octaves: int = 1,
213
+ persistence: float = 0.5,
214
+ lacunarity: float = 2.0,
215
+ offset_x: float = 0.0,
216
+ offset_y: float = 0.0,
217
+ color1: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
218
+ color2: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)
219
+ ) -> None:
220
+ """
221
+ Fills buffer with Perlin Noise (Gradient Noise).
222
+ - scale: "Zoom" level. Higher values = bigger features (zoomed in).
223
+ - octaves: Detail layers. 1 = smooth, 6 = rocky/detailed.
224
+ - persistence: How much each octave contributes (0.0 to 1.0).
225
+ - lacunarity: Detail frequency multiplier (usually 2.0).
226
+
227
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#perlin-noise
228
+ """
229
+ if scale <= 0: scale = 0.001
230
+
231
+ fill_perlin_f32(
232
+ image._buffer._handle,
233
+ image.width, image.height,
234
+ float(scale), float(seed),
235
+ int(octaves), float(persistence), float(lacunarity),
236
+ float(offset_x), float(offset_y),
237
+ color1, color2
238
+ )
239
+
240
+ @staticmethod
241
+ def ngon(
242
+ image: Image,
243
+ sides: int = 3,
244
+ color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
245
+ bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
246
+ rotation: float = 0.0,
247
+ softness: float = 0.0
248
+ ) -> None:
249
+ """
250
+ Fills buffer with a Regular Polygon (Triangle, Pentagon, Hexagon...).
251
+ - softness: Edge softness (0.0 = Hard AA, >0.0 = Glow).
252
+
253
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/fill/#ngon
254
+ """
255
+ if sides < 3:
256
+ raise ValueError("Polygon must have at least 3 sides")
257
+
258
+ fill_ngon_f32(
259
+ image._buffer._handle,
260
+ image.width, image.height,
261
+ int(sides), float(rotation), float(softness),
262
+ color, bg_color
263
+ )
pyimagecuda/filter.py ADDED
@@ -0,0 +1,168 @@
1
+ from .image import Image
2
+ from .utils import ensure_capacity
3
+ from .pyimagecuda_internal import (gaussian_blur_separable_f32, #type: ignore
4
+ sharpen_f32,
5
+ sepia_f32,
6
+ invert_f32,
7
+ threshold_f32,
8
+ solarize_f32,
9
+ filter_sobel_f32,
10
+ filter_emboss_f32)
11
+
12
+
13
+ class Filter:
14
+
15
+ @staticmethod
16
+ def gaussian_blur(
17
+ src: Image,
18
+ radius: int = 3,
19
+ sigma: float | None = None,
20
+ dst_buffer: Image | None = None,
21
+ temp_buffer: Image | None = None
22
+ ) -> Image | None:
23
+ """
24
+ Applies a Gaussian blur to the image (returns new image or writes to buffer).
25
+
26
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#gaussian-blur
27
+ """
28
+
29
+ if sigma is None:
30
+ sigma = radius / 3.0
31
+
32
+ if dst_buffer is None:
33
+ dst_buffer = Image(src.width, src.height)
34
+ return_dst = True
35
+ else:
36
+ ensure_capacity(dst_buffer, src.width, src.height)
37
+ return_dst = False
38
+
39
+ if temp_buffer is None:
40
+ temp_buffer = Image(src.width, src.height)
41
+ owns_temp = True
42
+ else:
43
+ ensure_capacity(temp_buffer, src.width, src.height)
44
+ owns_temp = False
45
+
46
+ gaussian_blur_separable_f32(
47
+ src._buffer._handle,
48
+ temp_buffer._buffer._handle,
49
+ dst_buffer._buffer._handle,
50
+ src.width,
51
+ src.height,
52
+ radius,
53
+ float(sigma)
54
+ )
55
+
56
+ if owns_temp:
57
+ temp_buffer.free()
58
+
59
+ return dst_buffer if return_dst else None
60
+
61
+ @staticmethod
62
+ def sharpen(
63
+ src: Image,
64
+ strength: float = 1.0,
65
+ dst_buffer: Image | None = None
66
+ ) -> Image | None:
67
+ """
68
+ Sharpens the image (returns new image or writes to buffer).
69
+
70
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sharpen
71
+ """
72
+
73
+ if dst_buffer is None:
74
+ dst_buffer = Image(src.width, src.height)
75
+ return_buffer = True
76
+ else:
77
+ ensure_capacity(dst_buffer, src.width, src.height)
78
+ return_buffer = False
79
+
80
+ sharpen_f32(
81
+ src._buffer._handle,
82
+ dst_buffer._buffer._handle,
83
+ src.width,
84
+ src.height,
85
+ float(strength)
86
+ )
87
+
88
+ return dst_buffer if return_buffer else None
89
+
90
+ @staticmethod
91
+ def sepia(image: Image, intensity: float = 1.0) -> None:
92
+ """
93
+ Applies Sepia tone (in-place).
94
+
95
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sepia
96
+ """
97
+
98
+ sepia_f32(image._buffer._handle, image.width, image.height, float(intensity))
99
+
100
+ @staticmethod
101
+ def invert(image: Image) -> None:
102
+ """
103
+ Inverts colors (Negative effect) in-place.
104
+
105
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#invert
106
+ """
107
+ invert_f32(image._buffer._handle, image.width, image.height)
108
+
109
+ @staticmethod
110
+ def threshold(image: Image, value: float = 0.5) -> None:
111
+ """
112
+ Converts to pure Black & White based on luminance threshold.
113
+ value: 0.0 to 1.0. Pixels brighter than value become white, others black.
114
+
115
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#threshold
116
+ """
117
+ threshold_f32(image._buffer._handle, image.width, image.height, float(value))
118
+
119
+ @staticmethod
120
+ def solarize(image: Image, threshold: float = 0.5) -> None:
121
+ """
122
+ Inverts only pixels brighter than threshold. Creates a psychedelic/retro look.
123
+
124
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#solarize
125
+ """
126
+ solarize_f32(image._buffer._handle, image.width, image.height, float(threshold))
127
+
128
+ @staticmethod
129
+ def sobel(src: Image, dst_buffer: Image | None = None) -> Image | None:
130
+ """
131
+ Detects edges using Sobel operator. Returns a black & white image with edges.
132
+
133
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sobel
134
+ """
135
+ if dst_buffer is None:
136
+ dst_buffer = Image(src.width, src.height)
137
+ return_buffer = True
138
+ else:
139
+ ensure_capacity(dst_buffer, src.width, src.height)
140
+ return_buffer = False
141
+
142
+ filter_sobel_f32(
143
+ src._buffer._handle,
144
+ dst_buffer._buffer._handle,
145
+ src.width, src.height
146
+ )
147
+ return dst_buffer if return_buffer else None
148
+
149
+ @staticmethod
150
+ def emboss(src: Image, strength: float = 1.0, dst_buffer: Image | None = None) -> Image | None:
151
+ """
152
+ Applies Emboss (Relief) effect.
153
+
154
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#emboss
155
+ """
156
+ if dst_buffer is None:
157
+ dst_buffer = Image(src.width, src.height)
158
+ return_buffer = True
159
+ else:
160
+ ensure_capacity(dst_buffer, src.width, src.height)
161
+ return_buffer = False
162
+
163
+ filter_emboss_f32(
164
+ src._buffer._handle,
165
+ dst_buffer._buffer._handle,
166
+ src.width, src.height, float(strength)
167
+ )
168
+ return dst_buffer if return_buffer else None
pyimagecuda/image.py ADDED
@@ -0,0 +1,95 @@
1
+ from .pyimagecuda_internal import create_buffer_f32, free_buffer, create_buffer_u8 #type: ignore
2
+
3
+
4
+ class Buffer:
5
+
6
+ def __init__(self, width: int, height: int, is_u8: bool = False):
7
+ create_func = create_buffer_u8 if is_u8 else create_buffer_f32
8
+ self._handle = create_func(width, height)
9
+ self.capacity_width = width
10
+ self.capacity_height = height
11
+
12
+ def free(self) -> None:
13
+ free_buffer(self._handle)
14
+
15
+
16
+ class ImageBase:
17
+
18
+ def __init__(self, width: int, height: int, is_u8: bool = False):
19
+ self._buffer = Buffer(width, height, is_u8)
20
+ self._width = width
21
+ self._height = height
22
+
23
+ @property
24
+ def width(self) -> int:
25
+ return self._width
26
+
27
+ @width.setter
28
+ def width(self, value: int) -> None:
29
+ value = int(value)
30
+ if value <= 0:
31
+ raise ValueError(f"Width must be positive, got {value}")
32
+
33
+ if value > self._buffer.capacity_width:
34
+ raise ValueError(
35
+ f"Width {value} exceeds buffer capacity "
36
+ f"{self._buffer.capacity_width}"
37
+ )
38
+
39
+ self._width = value
40
+
41
+ @property
42
+ def height(self) -> int:
43
+ return self._height
44
+
45
+ @height.setter
46
+ def height(self, value: int) -> None:
47
+ value = int(value)
48
+ if value <= 0:
49
+ raise ValueError(f"Height must be positive, got {value}")
50
+
51
+ if value > self._buffer.capacity_height:
52
+ raise ValueError(
53
+ f"Height {value} exceeds buffer capacity "
54
+ f"{self._buffer.capacity_height}"
55
+ )
56
+
57
+ self._height = value
58
+
59
+ def free(self) -> None:
60
+ self._buffer.free()
61
+
62
+ def __enter__(self):
63
+ return self
64
+
65
+ def __exit__(self, exc_type, exc_value, traceback):
66
+ self.free()
67
+ return False
68
+
69
+ def get_max_capacity(self) -> tuple[int, int]:
70
+ return (self._buffer.capacity_width, self._buffer.capacity_height)
71
+
72
+ def __repr__(self) -> str:
73
+ return f"{self.__class__.__name__}({self.width}×{self.height})"
74
+
75
+
76
+ class Image(ImageBase):
77
+
78
+ def __init__(self, width: int, height: int):
79
+ """
80
+ Creates a floating-point image with the given width and height.
81
+
82
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/image/#image-float32-precision
83
+ """
84
+ super().__init__(width, height, is_u8=False)
85
+
86
+
87
+ class ImageU8(ImageBase):
88
+
89
+ def __init__(self, width: int, height: int):
90
+ """
91
+ Creates an 8-bit unsigned integer image with the given width and height.
92
+
93
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/image/#imageu8-8-bit-precision
94
+ """
95
+ super().__init__(width, height, is_u8=True)
pyimagecuda/io.py ADDED
@@ -0,0 +1,158 @@
1
+ import pyvips
2
+
3
+ from .pyimagecuda_internal import upload_to_buffer, convert_f32_to_u8, convert_u8_to_f32, download_from_buffer, copy_buffer #type: ignore
4
+ from .image import Image, ImageU8, ImageBase
5
+ from .utils import ensure_capacity
6
+
7
+
8
+ def upload(image: ImageBase, data: bytes | bytearray | memoryview) -> None:
9
+ """
10
+ Uploads the image data from a bytes-like object to the GPU.
11
+
12
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#direct-uploaddownload
13
+ """
14
+ bytes_per_pixel = 4 if isinstance(image, ImageU8) else 16
15
+ expected = image.width * image.height * bytes_per_pixel
16
+ actual = data.nbytes if isinstance(data, memoryview) else len(data)
17
+
18
+ if actual != expected:
19
+ raise ValueError(f"Expected {expected} bytes, got {actual}")
20
+
21
+ upload_to_buffer(image._buffer._handle, data, image.width, image.height)
22
+
23
+
24
+ def download(image: ImageBase) -> bytes:
25
+ """
26
+ Downloads the image data from the GPU to a bytes object.
27
+
28
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#direct-uploaddownload
29
+ """
30
+ return download_from_buffer(image._buffer._handle, image.width, image.height)
31
+
32
+
33
+ def copy(dst: ImageBase, src: ImageBase) -> None:
34
+ """
35
+ Copies image data from the source image to the destination image.
36
+
37
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#copy-between-buffers
38
+ """
39
+ ensure_capacity(dst, src.width, src.height)
40
+ copy_buffer(dst._buffer._handle, src._buffer._handle, src.width, src.height)
41
+
42
+
43
+ def convert_float_to_u8(dst: ImageU8, src: Image) -> None:
44
+ """
45
+ Converts a floating-point image to an 8-bit unsigned integer image.
46
+
47
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
48
+ """
49
+ ensure_capacity(dst, src.width, src.height)
50
+ convert_f32_to_u8(dst._buffer._handle, src._buffer._handle, src.width, src.height)
51
+
52
+
53
+ def convert_u8_to_float(dst: Image, src: ImageU8) -> None:
54
+ """
55
+ Converts an 8-bit unsigned integer image to a floating-point image.
56
+
57
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
58
+ """
59
+ ensure_capacity(dst, src.width, src.height)
60
+ convert_u8_to_f32(dst._buffer._handle, src._buffer._handle, src.width, src.height)
61
+
62
+
63
+ def load(
64
+ filepath: str,
65
+ f32_buffer: Image | None = None,
66
+ u8_buffer: ImageU8 | None = None
67
+ ) -> Image | None:
68
+ """
69
+ Loads an image from a file (returns new image or writes to buffer).
70
+
71
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#loading-images
72
+ """
73
+ vips_img = pyvips.Image.new_from_file(filepath, access='sequential')
74
+
75
+ if vips_img.bands == 1:
76
+ vips_img = vips_img.bandjoin([vips_img, vips_img, vips_img])
77
+ vips_img = vips_img.bandjoin(255)
78
+ elif vips_img.bands == 3:
79
+ vips_img = vips_img.bandjoin(255)
80
+ elif vips_img.bands == 4:
81
+ pass
82
+ else:
83
+ raise ValueError(
84
+ f"Unsupported image format: {vips_img.bands} channels. "
85
+ f"Only grayscale (1), RGB (3), and RGBA (4) are supported."
86
+ )
87
+
88
+ width = vips_img.width
89
+ height = vips_img.height
90
+
91
+ should_return = False
92
+
93
+ if f32_buffer is None:
94
+ f32_buffer = Image(width, height)
95
+ should_return = True
96
+ else:
97
+ ensure_capacity(f32_buffer, width, height)
98
+ should_return = False
99
+
100
+ if u8_buffer is None:
101
+ u8_buffer = ImageU8(width, height)
102
+ owns_u8 = True
103
+ else:
104
+ ensure_capacity(u8_buffer, width, height)
105
+ owns_u8 = False
106
+
107
+ vips_img = vips_img.cast('uchar')
108
+ pixel_data = vips_img.write_to_memory()
109
+
110
+ upload(u8_buffer, pixel_data)
111
+
112
+ convert_u8_to_float(f32_buffer, u8_buffer)
113
+
114
+ if owns_u8:
115
+ u8_buffer.free()
116
+
117
+ return f32_buffer if should_return else None
118
+
119
+
120
+ def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality: int | None = None) -> None:
121
+ """
122
+ Saves the floating-point image to a file (using an 8-bit buffer for conversion).
123
+
124
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
125
+ """
126
+ if u8_buffer is None:
127
+ u8_buffer = ImageU8(image.width, image.height)
128
+ owns_buffer = True
129
+ else:
130
+ ensure_capacity(u8_buffer, image.width, image.height)
131
+ owns_buffer = False
132
+
133
+ convert_float_to_u8(u8_buffer, image)
134
+ pixel_data = download(u8_buffer)
135
+
136
+ vips_img = pyvips.Image.new_from_memory(
137
+ pixel_data,
138
+ image.width,
139
+ image.height,
140
+ bands=4,
141
+ format='uchar'
142
+ )
143
+
144
+ vips_img = vips_img.copy(interpretation='srgb')
145
+
146
+ save_kwargs = {}
147
+ if quality is not None:
148
+ if filepath.lower().endswith(('.jpg', '.jpeg')):
149
+ save_kwargs['Q'] = quality
150
+ elif filepath.lower().endswith('.webp'):
151
+ save_kwargs['Q'] = quality
152
+ elif filepath.lower().endswith(('.heic', '.heif')):
153
+ save_kwargs['Q'] = quality
154
+
155
+ vips_img.write_to_file(filepath, **save_kwargs)
156
+
157
+ if owns_buffer:
158
+ u8_buffer.free()