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