pyimagecuda 0.1.0__cp311-cp311-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/io.py ADDED
@@ -0,0 +1,259 @@
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
+
6
+ try:
7
+ import numpy as np
8
+ except ImportError:
9
+ np = None
10
+
11
+ def upload(image: ImageBase, data: bytes | bytearray | memoryview) -> None:
12
+ """
13
+ Uploads the image data from a bytes-like object to the GPU.
14
+
15
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#direct-uploaddownload
16
+ """
17
+ bytes_per_pixel = 4 if isinstance(image, ImageU8) else 16
18
+ expected = image.width * image.height * bytes_per_pixel
19
+ actual = data.nbytes if isinstance(data, memoryview) else len(data)
20
+
21
+ if actual != expected:
22
+ raise ValueError(f"Expected {expected} bytes, got {actual}")
23
+
24
+ upload_to_buffer(image._buffer._handle, data, image.width, image.height)
25
+
26
+
27
+ def download(image: ImageBase) -> bytes:
28
+ """
29
+ Downloads the image data from the GPU to a bytes object.
30
+
31
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#direct-uploaddownload
32
+ """
33
+ return download_from_buffer(image._buffer._handle, image.width, image.height)
34
+
35
+
36
+ def copy(dst: ImageBase, src: ImageBase) -> None:
37
+ """
38
+ Copies image data from the source image to the destination image.
39
+
40
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#copy-between-buffers
41
+ """
42
+ dst.resize(src.width, src.height)
43
+ copy_buffer(dst._buffer._handle, src._buffer._handle, src.width, src.height)
44
+
45
+
46
+ def convert_float_to_u8(dst: ImageU8, src: Image) -> None:
47
+ """
48
+ Converts a floating-point image to an 8-bit unsigned integer image.
49
+
50
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
51
+ """
52
+ dst.resize(src.width, src.height)
53
+ convert_f32_to_u8(dst._buffer._handle, src._buffer._handle, src.width, src.height)
54
+
55
+
56
+ def convert_u8_to_float(dst: Image, src: ImageU8) -> None:
57
+ """
58
+ Converts an 8-bit unsigned integer image to a floating-point image.
59
+
60
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#manual-conversions
61
+ """
62
+ dst.resize(src.width, src.height)
63
+ convert_u8_to_f32(dst._buffer._handle, src._buffer._handle, src.width, src.height)
64
+
65
+
66
+ def load(
67
+ filepath: str,
68
+ f32_buffer: Image | None = None,
69
+ u8_buffer: ImageU8 | None = None
70
+ ) -> Image | None:
71
+ """
72
+ Loads an image from a file (returns new image or writes to buffer).
73
+
74
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#loading-images
75
+ """
76
+ vips_img = pyvips.Image.new_from_file(filepath, access='sequential')
77
+
78
+ if vips_img.bands == 1:
79
+ vips_img = vips_img.bandjoin([vips_img, vips_img, vips_img])
80
+ vips_img = vips_img.bandjoin(255)
81
+ elif vips_img.bands == 3:
82
+ vips_img = vips_img.bandjoin(255)
83
+ elif vips_img.bands == 4:
84
+ pass
85
+ else:
86
+ raise ValueError(
87
+ f"Unsupported image format: {vips_img.bands} channels. "
88
+ f"Only grayscale (1), RGB (3), and RGBA (4) are supported."
89
+ )
90
+
91
+ width = vips_img.width
92
+ height = vips_img.height
93
+
94
+ should_return = False
95
+
96
+ if f32_buffer is None:
97
+ f32_buffer = Image(width, height)
98
+ should_return = True
99
+ else:
100
+ f32_buffer.resize(width, height)
101
+ should_return = False
102
+
103
+ if u8_buffer is None:
104
+ u8_buffer = ImageU8(width, height)
105
+ owns_u8 = True
106
+ else:
107
+ u8_buffer.resize(width, height)
108
+ owns_u8 = False
109
+
110
+ vips_img = vips_img.cast('uchar')
111
+ pixel_data = vips_img.write_to_memory()
112
+
113
+ upload(u8_buffer, pixel_data)
114
+
115
+ convert_u8_to_float(f32_buffer, u8_buffer)
116
+
117
+ if owns_u8:
118
+ u8_buffer.free()
119
+
120
+ return f32_buffer if should_return else None
121
+
122
+
123
+ def _save_internal(u8_image: ImageU8, filepath: str, quality: int | None = None) -> None:
124
+ pixel_data = download(u8_image)
125
+
126
+ vips_img = pyvips.Image.new_from_memory(
127
+ pixel_data,
128
+ u8_image.width,
129
+ u8_image.height,
130
+ bands=4,
131
+ format='uchar'
132
+ )
133
+
134
+ vips_img = vips_img.copy(interpretation='srgb')
135
+
136
+ save_kwargs = {}
137
+ if quality is not None:
138
+ if filepath.lower().endswith(('.jpg', '.jpeg')):
139
+ save_kwargs['Q'] = quality
140
+ elif filepath.lower().endswith('.webp'):
141
+ save_kwargs['Q'] = quality
142
+ elif filepath.lower().endswith(('.heic', '.heif')):
143
+ save_kwargs['Q'] = quality
144
+
145
+ vips_img.write_to_file(filepath, **save_kwargs)
146
+
147
+
148
+ def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality: int | None = None) -> None:
149
+ """
150
+ Saves the floating-point image to a file (using an 8-bit buffer for conversion).
151
+
152
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
153
+ """
154
+ if u8_buffer is None:
155
+ u8_buffer = ImageU8(image.width, image.height)
156
+ owns_buffer = True
157
+ else:
158
+ u8_buffer.resize(image.width, image.height)
159
+ owns_buffer = False
160
+
161
+ convert_float_to_u8(u8_buffer, image)
162
+ _save_internal(u8_buffer, filepath, quality)
163
+
164
+ if owns_buffer:
165
+ u8_buffer.free()
166
+
167
+
168
+ def save_u8(image: ImageU8, filepath: str, quality: int | None = None) -> None:
169
+ """
170
+ Saves an 8-bit unsigned integer image directly to a file.
171
+
172
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
173
+ """
174
+ _save_internal(image, filepath, quality)
175
+
176
+
177
+ def from_numpy(array, f32_buffer: Image | None = None, u8_buffer: ImageU8 | None = None) -> Image:
178
+ """
179
+ Creates a PyImageCUDA Image from a NumPy array (e.g. from OpenCV, Pillow, Matplotlib).
180
+
181
+ - Handles uint8 (0-255) -> float32 (0.0-1.0) conversion automatically on GPU.
182
+ - Handles Grayscale/RGB -> RGBA expansion automatically.
183
+ - Optimized: Uploads uint8 data (4x smaller) if possible, then converts on GPU.
184
+
185
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#numpy-integration
186
+ """
187
+ if np is None:
188
+ raise ImportError("NumPy is not installed. Run `pip install numpy` to use this feature.")
189
+
190
+ if not isinstance(array, np.ndarray):
191
+ raise TypeError(f"Expected numpy.ndarray, got {type(array)}")
192
+
193
+ target_dtype = array.dtype
194
+
195
+ if array.ndim == 2:
196
+ h, w = array.shape
197
+ alpha_val = 255 if target_dtype == np.uint8 else 1.0
198
+ alpha_channel = np.full((h, w), alpha_val, dtype=target_dtype)
199
+ array = np.dstack((array, array, array, alpha_channel))
200
+
201
+ elif array.ndim == 3:
202
+ h, w, c = array.shape
203
+ if c == 3:
204
+ alpha_val = 255 if target_dtype == np.uint8 else 1.0
205
+ alpha_channel = np.full((h, w), alpha_val, dtype=target_dtype)
206
+ array = np.dstack((array, alpha_channel))
207
+ elif c != 4:
208
+ raise ValueError(f"Unsupported channel count: {c}. PyImageCUDA requires 1, 3, or 4 channels.")
209
+ else:
210
+ raise ValueError(f"Unsupported array shape: {array.shape}. Expected (H, W), (H, W, 3) or (H, W, 4).")
211
+
212
+ if not array.flags['C_CONTIGUOUS']:
213
+ array = np.ascontiguousarray(array)
214
+
215
+ height, width = array.shape[:2]
216
+
217
+ should_return = False
218
+ if f32_buffer is None:
219
+ f32_buffer = Image(width, height)
220
+ should_return = True
221
+ else:
222
+ f32_buffer.resize(width, height)
223
+
224
+ if array.dtype == np.uint8:
225
+ owns_u8 = False
226
+ if u8_buffer is None:
227
+ u8_buffer = ImageU8(width, height)
228
+ owns_u8 = True
229
+ else:
230
+ u8_buffer.resize(width, height)
231
+
232
+ upload(u8_buffer, array.tobytes())
233
+ convert_u8_to_float(f32_buffer, u8_buffer)
234
+
235
+ if owns_u8:
236
+ u8_buffer.free()
237
+
238
+ elif array.dtype == np.float32:
239
+ upload(f32_buffer, array.tobytes())
240
+ else:
241
+ array = array.astype(np.float32)
242
+ upload(f32_buffer, array.tobytes())
243
+
244
+ return f32_buffer if should_return else None
245
+
246
+
247
+ def to_numpy(image: Image) -> 'np.ndarray': # type: ignore
248
+ """
249
+ Downloads a PyImageCUDA Image to a NumPy array.
250
+
251
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#numpy-integration
252
+ """
253
+ if np is None:
254
+ raise ImportError("NumPy is not installed. Run `pip install numpy` to use this feature.")
255
+
256
+ raw_bytes = download(image)
257
+ array = np.frombuffer(raw_bytes, dtype=np.float32)
258
+
259
+ return array.reshape((image.height, image.width, 4))
pyimagecuda/resize.py ADDED
@@ -0,0 +1,108 @@
1
+ from .image import Image
2
+ from .pyimagecuda_internal import resize_f32 #type: ignore
3
+ from .io import copy
4
+
5
+
6
+ def _resize_internal(
7
+ src: Image,
8
+ width: int | None,
9
+ height: int | None,
10
+ method: int,
11
+ dst_buffer: Image | None = None
12
+ ) -> Image | None:
13
+
14
+ if width is None and height is None:
15
+ raise ValueError("At least one of width or height must be specified")
16
+ elif width is None:
17
+ width = int(src.width * (height / src.height))
18
+ elif height is None:
19
+ height = int(src.height * (width / src.width))
20
+
21
+ # No resize needed, just copy
22
+ if width == src.width and height == src.height:
23
+ if dst_buffer is None:
24
+ dst_buffer = Image(width, height)
25
+ copy(dst_buffer, src)
26
+ return dst_buffer
27
+ else:
28
+ copy(dst_buffer, src)
29
+ return None
30
+
31
+ # Resize normal
32
+ if dst_buffer is None:
33
+ dst_buffer = Image(width, height)
34
+ return_buffer = True
35
+ else:
36
+ dst_buffer.resize(width, height)
37
+ return_buffer = False
38
+
39
+ resize_f32(
40
+ src._buffer._handle,
41
+ dst_buffer._buffer._handle,
42
+ src.width,
43
+ src.height,
44
+ width,
45
+ height,
46
+ method
47
+ )
48
+
49
+ return dst_buffer if return_buffer else None
50
+
51
+
52
+ class Resize:
53
+ @staticmethod
54
+ def nearest(
55
+ src: Image,
56
+ width: int | None = None,
57
+ height: int | None = None,
58
+ dst_buffer: Image | None = None
59
+ ) -> Image | None:
60
+ """
61
+ Resizes the image using nearest neighbor interpolation (returns new image or writes to buffer).
62
+
63
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/resize/#nearest
64
+ """
65
+ return _resize_internal(src, width, height, 0, dst_buffer)
66
+
67
+ @staticmethod
68
+ def bilinear(
69
+ src: Image,
70
+ width: int | None = None,
71
+ height: int | None = None,
72
+ dst_buffer: Image | None = None
73
+ ) -> Image | None:
74
+ """
75
+ Resizes the image using bilinear interpolation (returns new image or writes to buffer).
76
+
77
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/resize/#bilinear
78
+ """
79
+
80
+ return _resize_internal(src, width, height, 1, dst_buffer)
81
+
82
+ @staticmethod
83
+ def bicubic(
84
+ src: Image,
85
+ width: int | None = None,
86
+ height: int | None = None,
87
+ dst_buffer: Image | None = None
88
+ ) -> Image | None:
89
+ """
90
+ Resizes the image using bicubic interpolation (returns new image or writes to buffer).
91
+
92
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/resize/#bicubic
93
+ """
94
+ return _resize_internal(src, width, height, 2, dst_buffer)
95
+
96
+ @staticmethod
97
+ def lanczos(
98
+ src: Image,
99
+ width: int | None = None,
100
+ height: int | None = None,
101
+ dst_buffer: Image | None = None
102
+ ) -> Image | None:
103
+ """
104
+ Resizes the image using Lanczos interpolation (returns new image or writes to buffer).
105
+
106
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/resize/#lanczos
107
+ """
108
+ return _resize_internal(src, width, height, 3, dst_buffer)
pyimagecuda/text.py ADDED
@@ -0,0 +1,106 @@
1
+ from typing import Literal
2
+ import pyvips
3
+
4
+ from .image import Image, ImageU8
5
+ from .io import upload, convert_u8_to_float
6
+
7
+ class Text:
8
+ @staticmethod
9
+ def create(
10
+ text: str,
11
+ font: str = "Sans",
12
+ size: float = 12.0,
13
+ color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
14
+ bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
15
+ align: Literal['left', 'centre', 'right'] = 'left',
16
+ justify: bool = False,
17
+ spacing: int = 0,
18
+ letter_spacing: float = 0.0,
19
+ dst_buffer: Image | None = None,
20
+ u8_buffer: ImageU8 | None = None
21
+ ) -> Image | None:
22
+ """
23
+ Renders text into an image with specified font, size, color, alignment, and spacing.
24
+
25
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/text/#text-rendering
26
+ """
27
+ full_font_string = f"{font} {size}"
28
+
29
+ text_opts = {
30
+ 'font': full_font_string,
31
+ 'dpi': 72,
32
+ 'align': 0 if align == 'left' else (1 if align == 'centre' else 2),
33
+ 'justify': justify,
34
+ 'spacing': spacing
35
+ }
36
+
37
+ final_text = text
38
+ if letter_spacing != 0:
39
+ pango_spacing = int(letter_spacing * 1024)
40
+ final_text = f'<span letter_spacing="{pango_spacing}">{text}</span>'
41
+ text_opts['rgba'] = True
42
+
43
+ if '<' in text and '>' in text:
44
+ text_opts['rgba'] = True
45
+
46
+ try:
47
+ if text_opts.get('rgba'):
48
+ mask_rgba = pyvips.Image.text(final_text, **text_opts)
49
+ if mask_rgba.bands == 4:
50
+ mask = mask_rgba[3]
51
+ else:
52
+ mask = mask_rgba.colourspace('b-w')[0]
53
+ else:
54
+ mask = pyvips.Image.text(final_text, **text_opts)
55
+
56
+ except pyvips.Error as e:
57
+ raise RuntimeError(f"Failed to render text: {e}")
58
+
59
+ fg_r, fg_g, fg_b, fg_a = [int(c * 255) for c in color]
60
+ bg_r, bg_g, bg_b, bg_a = [int(c * 255) for c in bg_color]
61
+
62
+ fg_rgb = mask.new_from_image([fg_r, fg_g, fg_b])
63
+
64
+ if fg_a < 255:
65
+ fg_alpha = (mask * (fg_a / 255.0)).cast('uchar')
66
+ else:
67
+ fg_alpha = mask
68
+
69
+ fg_layer = fg_rgb.bandjoin(fg_alpha)
70
+ fg_layer = fg_layer.copy(interpretation='srgb')
71
+
72
+ if bg_a > 0:
73
+ bg_layer = mask.new_from_image([bg_r, bg_g, bg_b, bg_a])
74
+ bg_layer = bg_layer.copy(interpretation='srgb')
75
+ final_vips = bg_layer.composite(fg_layer, 'over')
76
+ else:
77
+ final_vips = fg_layer
78
+
79
+ final_vips = final_vips.cast('uchar')
80
+
81
+ w, h = final_vips.width, final_vips.height
82
+ raw_bytes = final_vips.write_to_memory()
83
+
84
+ if dst_buffer is None:
85
+ result = Image(w, h)
86
+ return_result = True
87
+ else:
88
+ dst_buffer.resize(w, h)
89
+ result = dst_buffer
90
+ return_result = False
91
+
92
+ if u8_buffer is None:
93
+ u8_temp = ImageU8(w, h)
94
+ free_u8 = True
95
+ else:
96
+ u8_buffer.resize(w, h)
97
+ u8_temp = u8_buffer
98
+ free_u8 = False
99
+
100
+ upload(u8_temp, raw_bytes)
101
+ convert_u8_to_float(result, u8_temp)
102
+
103
+ if free_u8:
104
+ u8_temp.free()
105
+
106
+ return result if return_result else None