pyimagecuda 0.0.4__cp312-cp312-win_amd64.whl → 0.1.0__cp312-cp312-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/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import ctypes
2
2
  import sys
3
3
 
4
- __version__ = "0.0.4"
4
+ __version__ = "0.1.0"
5
5
 
6
6
  def _check_nvidia_driver():
7
7
  try:
@@ -37,7 +37,7 @@ except ImportError as e:
37
37
  if _INTERNAL_LOADED:
38
38
  try:
39
39
  from .image import Image, ImageU8
40
- from .io import upload, download, copy, save, load, convert_float_to_u8, convert_u8_to_float
40
+ from .io import upload, download, copy, save, load, convert_float_to_u8, convert_u8_to_float, from_numpy, to_numpy, save_u8
41
41
  from .fill import Fill
42
42
  from .resize import Resize
43
43
  from .blend import Blend
@@ -45,6 +45,9 @@ if _INTERNAL_LOADED:
45
45
  from .effect import Effect
46
46
  from .adjust import Adjust
47
47
  from .transform import Transform
48
+ from .text import Text
49
+ from .gl_interop import GLResource
50
+ from .pyimagecuda_internal import cuda_sync # type: ignore
48
51
  except ImportError as e:
49
52
  print(f"Warning: Error importing Python wrappers: {e}")
50
53
 
pyimagecuda/adjust.py CHANGED
@@ -19,6 +19,9 @@ class Adjust:
19
19
 
20
20
  Docs & Examples: https://offerrall.github.io/pyimagecuda/adjust/#brightness
21
21
  """
22
+ if abs(factor) < 1e-6:
23
+ return
24
+
22
25
  adjust_brightness_f32(
23
26
  image._buffer._handle,
24
27
  image.width,
@@ -36,6 +39,9 @@ class Adjust:
36
39
 
37
40
  Docs & Examples: https://offerrall.github.io/pyimagecuda/adjust/#contrast
38
41
  """
42
+ if abs(factor - 1.0) < 1e-6:
43
+ return
44
+
39
45
  adjust_contrast_f32(
40
46
  image._buffer._handle,
41
47
  image.width,
@@ -54,6 +60,9 @@ class Adjust:
54
60
 
55
61
  Docs & Examples: https://offerrall.github.io/pyimagecuda/adjust/#saturation
56
62
  """
63
+ if abs(factor - 1.0) < 1e-6:
64
+ return
65
+
57
66
  adjust_saturation_f32(
58
67
  image._buffer._handle,
59
68
  image.width,
@@ -75,6 +84,9 @@ class Adjust:
75
84
  """
76
85
  if gamma <= 0:
77
86
  raise ValueError("Gamma must be positive")
87
+
88
+ if abs(gamma - 1.0) < 1e-6:
89
+ return
78
90
 
79
91
  adjust_gamma_f32(
80
92
  image._buffer._handle,
@@ -92,6 +104,8 @@ class Adjust:
92
104
 
93
105
  Docs & Examples: https://offerrall.github.io/pyimagecuda/adjust/#opacity
94
106
  """
107
+ if abs(factor - 1.0) < 1e-6:
108
+ return
95
109
 
96
110
  adjust_opacity_f32(
97
111
  image._buffer._handle,
pyimagecuda/blend.py CHANGED
@@ -45,6 +45,9 @@ class Blend:
45
45
 
46
46
  Docs & Examples: https://offerrall.github.io/pyimagecuda/blend/#normal
47
47
  """
48
+ if abs(opacity) < 1e-6:
49
+ return
50
+
48
51
  pos_x, pos_y = _calculate_position(
49
52
  base.width, base.height,
50
53
  overlay.width, overlay.height,
@@ -75,6 +78,9 @@ class Blend:
75
78
 
76
79
  Docs & Examples: https://offerrall.github.io/pyimagecuda/blend/#multiply
77
80
  """
81
+ if abs(opacity) < 1e-6:
82
+ return
83
+
78
84
  pos_x, pos_y = _calculate_position(
79
85
  base.width, base.height,
80
86
  overlay.width, overlay.height,
@@ -105,6 +111,9 @@ class Blend:
105
111
 
106
112
  Docs & Examples: https://offerrall.github.io/pyimagecuda/blend/#screen
107
113
  """
114
+ if abs(opacity) < 1e-6:
115
+ return
116
+
108
117
  pos_x, pos_y = _calculate_position(
109
118
  base.width, base.height,
110
119
  overlay.width, overlay.height,
@@ -135,6 +144,9 @@ class Blend:
135
144
 
136
145
  Docs & Examples: https://offerrall.github.io/pyimagecuda/blend/#add
137
146
  """
147
+ if abs(opacity) < 1e-6:
148
+ return
149
+
138
150
  pos_x, pos_y = _calculate_position(
139
151
  base.width, base.height,
140
152
  overlay.width, overlay.height,
@@ -165,6 +177,9 @@ class Blend:
165
177
 
166
178
  Docs & Examples: https://offerrall.github.io/pyimagecuda/blend/#overlay
167
179
  """
180
+ if abs(opacity) < 1e-6:
181
+ return
182
+
168
183
  pos_x, pos_y = _calculate_position(
169
184
  base.width, base.height,
170
185
  overlay.width, overlay.height,
@@ -195,6 +210,9 @@ class Blend:
195
210
 
196
211
  Docs & Examples: https://offerrall.github.io/pyimagecuda/blend/#soft-light
197
212
  """
213
+ if abs(opacity) < 1e-6:
214
+ return
215
+
198
216
  pos_x, pos_y = _calculate_position(
199
217
  base.width, base.height,
200
218
  overlay.width, overlay.height,
@@ -225,6 +243,9 @@ class Blend:
225
243
 
226
244
  Docs & Examples: https://offerrall.github.io/pyimagecuda/blend/#hard-light
227
245
  """
246
+ if abs(opacity) < 1e-6:
247
+ return
248
+
228
249
  pos_x, pos_y = _calculate_position(
229
250
  base.width, base.height,
230
251
  overlay.width, overlay.height,
pyimagecuda/effect.py CHANGED
@@ -4,7 +4,6 @@ from .image import Image
4
4
  from .filter import Filter
5
5
  from .blend import Blend
6
6
  from .fill import Fill
7
- from .utils import ensure_capacity
8
7
  from .pyimagecuda_internal import ( #type: ignore
9
8
  rounded_corners_f32,
10
9
  extract_alpha_f32,
@@ -25,14 +24,19 @@ class Effect:
25
24
  Docs & Examples: https://offerrall.github.io/pyimagecuda/effect/#rounded_corners
26
25
  """
27
26
  max_radius = min(image.width, image.height) / 2.0
28
- if radius < 0: raise ValueError("Radius must be non-negative")
29
- if radius > max_radius: radius = max_radius
27
+ if radius < 0:
28
+ raise ValueError("Radius must be non-negative")
29
+ if radius == 0:
30
+ return
31
+ if radius > max_radius:
32
+ radius = max_radius
30
33
 
31
34
  rounded_corners_f32(image._buffer._handle, image.width, image.height, float(radius))
32
35
 
33
36
  @staticmethod
34
37
  def drop_shadow(
35
- image: Image, offset_x: int = 10,
38
+ image: Image,
39
+ offset_x: int = 10,
36
40
  offset_y: int = 10,
37
41
  blur: int = 20,
38
42
  color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.5),
@@ -51,30 +55,46 @@ class Effect:
51
55
  pad_r = blur + max(0, offset_x)
52
56
  pad_t = blur + max(0, -offset_y)
53
57
  pad_b = blur + max(0, offset_y)
54
- res_w, res_h = image.width + pad_l + pad_r, image.height + pad_t + pad_b
55
- img_x, img_y = pad_l, pad_t
58
+ res_w = image.width + pad_l + pad_r
59
+ res_h = image.height + pad_t + pad_b
60
+ img_x = pad_l
61
+ img_y = pad_t
56
62
  else:
57
- res_w, res_h = image.width, image.height
58
- img_x, img_y = 0, 0
63
+ res_w = image.width
64
+ res_h = image.height
65
+ img_x = 0
66
+ img_y = 0
59
67
 
60
68
  if dst_buffer is None:
61
- result = Image(res_w, res_h); ret = True
69
+ result = Image(res_w, res_h)
70
+ ret = True
62
71
  else:
63
- ensure_capacity(dst_buffer, res_w, res_h); result = dst_buffer; ret = False
72
+ dst_buffer.resize(res_w, res_h)
73
+ result = dst_buffer
74
+ ret = False
64
75
 
65
76
  if shadow_buffer is None:
66
- shadow = Image(res_w, res_h); own_shadow = True
77
+ shadow = Image(res_w, res_h)
78
+ own_shadow = True
67
79
  else:
68
- ensure_capacity(shadow_buffer, res_w, res_h); shadow = shadow_buffer; own_shadow = False
80
+ shadow_buffer.resize(res_w, res_h)
81
+ shadow = shadow_buffer
82
+ own_shadow = False
69
83
 
70
84
  if expand:
71
- if temp_buffer is None: temp = Image(res_w, res_h)
72
- else: ensure_capacity(temp_buffer, res_w, res_h); temp = temp_buffer
73
- Fill.color(shadow, (0,0,0,0))
74
- temp.width, temp.height = image.width, image.height
85
+ if temp_buffer is None:
86
+ temp = Image(res_w, res_h)
87
+ else:
88
+ temp_buffer.resize(res_w, res_h)
89
+ temp = temp_buffer
90
+
91
+ Fill.color(shadow, (0, 0, 0, 0))
92
+ temp.resize(image.width, image.height)
75
93
  extract_alpha_f32(image._buffer._handle, temp._buffer._handle, image.width, image.height)
76
94
  Blend.normal(shadow, temp, anchor='top-left', offset_x=pad_l, offset_y=pad_t)
77
- if temp_buffer is None: temp.free()
95
+
96
+ if temp_buffer is None:
97
+ temp.free()
78
98
  else:
79
99
  extract_alpha_f32(image._buffer._handle, shadow._buffer._handle, image.width, image.height)
80
100
 
@@ -82,11 +102,13 @@ class Effect:
82
102
  Filter.gaussian_blur(shadow, radius=blur, sigma=blur/3.0, dst_buffer=shadow)
83
103
 
84
104
  colorize_alpha_mask_f32(shadow._buffer._handle, shadow.width, shadow.height, color)
85
- Fill.color(result, (0,0,0,0))
105
+ Fill.color(result, (0, 0, 0, 0))
86
106
  Blend.normal(result, shadow, anchor='top-left', offset_x=offset_x, offset_y=offset_y)
87
107
  Blend.normal(result, image, anchor='top-left', offset_x=img_x, offset_y=img_y)
88
108
 
89
- if own_shadow: shadow.free()
109
+ if own_shadow:
110
+ shadow.free()
111
+
90
112
  return result if ret else None
91
113
 
92
114
  @staticmethod
@@ -104,38 +126,66 @@ class Effect:
104
126
 
105
127
  Docs & Examples: https://offerrall.github.io/pyimagecuda/effect/#stroke
106
128
  """
107
- if width < 1 or width > 1000: raise ValueError("Invalid stroke width")
108
- if position not in ('outside', 'inside'): raise ValueError("Invalid position")
129
+ if width < 1 or width > 1000:
130
+ raise ValueError("Invalid stroke width")
131
+ if position not in ('outside', 'inside'):
132
+ raise ValueError("Invalid position")
109
133
 
110
134
  if position == 'outside' and expand:
111
- res_w, res_h = image.width + width * 2, image.height + width * 2
112
- off_x, off_y = width, width
135
+ res_w = image.width + width * 2
136
+ res_h = image.height + width * 2
137
+ off_x = width
138
+ off_y = width
113
139
  else:
114
- res_w, res_h = image.width, image.height
115
- off_x, off_y = 0, 0
140
+ res_w = image.width
141
+ res_h = image.height
142
+ off_x = 0
143
+ off_y = 0
116
144
 
117
145
  if dst_buffer is None:
118
- result = Image(res_w, res_h); ret = True
146
+ result = Image(res_w, res_h)
147
+ ret = True
119
148
  else:
120
- ensure_capacity(dst_buffer, res_w, res_h); result = dst_buffer; ret = False
149
+ dst_buffer.resize(res_w, res_h)
150
+ result = dst_buffer
151
+ ret = False
121
152
 
122
153
  if distance_buffer is None:
123
- distance = Image(res_w, res_h); own_dist = True
154
+ distance = Image(res_w, res_h)
155
+ own_dist = True
124
156
  else:
125
- ensure_capacity(distance_buffer, res_w, res_h); distance = distance_buffer; own_dist = False
157
+ distance_buffer.resize(res_w, res_h)
158
+ distance = distance_buffer
159
+ own_dist = False
126
160
 
127
161
  compute_distance_field_f32(
128
- image._buffer._handle, distance._buffer._handle,
129
- image.width, image.height, res_w, res_h, off_x, off_y
162
+ image._buffer._handle,
163
+ distance._buffer._handle,
164
+ image.width,
165
+ image.height,
166
+ res_w,
167
+ res_h,
168
+ off_x,
169
+ off_y
130
170
  )
131
171
 
132
172
  generate_stroke_composite_f32(
133
- image._buffer._handle, distance._buffer._handle, result._buffer._handle,
134
- image.width, image.height, res_w, res_h, off_x, off_y,
135
- float(width), color, 0 if position == 'outside' else 1
173
+ image._buffer._handle,
174
+ distance._buffer._handle,
175
+ result._buffer._handle,
176
+ image.width,
177
+ image.height,
178
+ res_w,
179
+ res_h,
180
+ off_x,
181
+ off_y,
182
+ float(width),
183
+ color,
184
+ 0 if position == 'outside' else 1
136
185
  )
137
186
 
138
- if own_dist: distance.free()
187
+ if own_dist:
188
+ distance.free()
139
189
 
140
190
  return result if ret else None
141
191
 
@@ -151,8 +201,10 @@ class Effect:
151
201
 
152
202
  Docs & Examples: https://offerrall.github.io/pyimagecuda/effect/#vignette
153
203
  """
154
- if radius < 0: radius = 0.0
155
- if softness < 0: softness = 0.0
204
+ if radius < 0:
205
+ radius = 0.0
206
+ if softness < 0:
207
+ softness = 0.0
156
208
 
157
209
  effect_vignette_f32(
158
210
  image._buffer._handle,
pyimagecuda/filter.py CHANGED
@@ -1,13 +1,15 @@
1
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)
2
+ from .io import copy
3
+ from .pyimagecuda_internal import ( #type: ignore
4
+ gaussian_blur_separable_f32,
5
+ sharpen_f32,
6
+ sepia_f32,
7
+ invert_f32,
8
+ threshold_f32,
9
+ solarize_f32,
10
+ filter_sobel_f32,
11
+ filter_emboss_f32
12
+ )
11
13
 
12
14
 
13
15
  class Filter:
@@ -26,6 +28,15 @@ class Filter:
26
28
  Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#gaussian-blur
27
29
  """
28
30
 
31
+ if radius == 0 or (sigma is not None and sigma <= 0.001):
32
+ if dst_buffer is None:
33
+ dst_buffer = Image(src.width, src.height)
34
+ copy(dst_buffer, src)
35
+ return dst_buffer
36
+ else:
37
+ copy(dst_buffer, src)
38
+ return None
39
+
29
40
  if sigma is None:
30
41
  sigma = radius / 3.0
31
42
 
@@ -33,14 +44,14 @@ class Filter:
33
44
  dst_buffer = Image(src.width, src.height)
34
45
  return_dst = True
35
46
  else:
36
- ensure_capacity(dst_buffer, src.width, src.height)
47
+ dst_buffer.resize(src.width, src.height)
37
48
  return_dst = False
38
49
 
39
50
  if temp_buffer is None:
40
51
  temp_buffer = Image(src.width, src.height)
41
52
  owns_temp = True
42
53
  else:
43
- ensure_capacity(temp_buffer, src.width, src.height)
54
+ temp_buffer.resize(src.width, src.height)
44
55
  owns_temp = False
45
56
 
46
57
  gaussian_blur_separable_f32(
@@ -70,11 +81,20 @@ class Filter:
70
81
  Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sharpen
71
82
  """
72
83
 
84
+ if abs(strength) < 1e-6:
85
+ if dst_buffer is None:
86
+ dst_buffer = Image(src.width, src.height)
87
+ copy(dst_buffer, src)
88
+ return dst_buffer
89
+ else:
90
+ copy(dst_buffer, src)
91
+ return None
92
+
73
93
  if dst_buffer is None:
74
94
  dst_buffer = Image(src.width, src.height)
75
95
  return_buffer = True
76
96
  else:
77
- ensure_capacity(dst_buffer, src.width, src.height)
97
+ dst_buffer.resize(src.width, src.height)
78
98
  return_buffer = False
79
99
 
80
100
  sharpen_f32(
@@ -94,6 +114,8 @@ class Filter:
94
114
 
95
115
  Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#sepia
96
116
  """
117
+ if abs(intensity) < 1e-6:
118
+ return
97
119
 
98
120
  sepia_f32(image._buffer._handle, image.width, image.height, float(intensity))
99
121
 
@@ -136,7 +158,7 @@ class Filter:
136
158
  dst_buffer = Image(src.width, src.height)
137
159
  return_buffer = True
138
160
  else:
139
- ensure_capacity(dst_buffer, src.width, src.height)
161
+ dst_buffer.resize(src.width, src.height)
140
162
  return_buffer = False
141
163
 
142
164
  filter_sobel_f32(
@@ -153,11 +175,20 @@ class Filter:
153
175
 
154
176
  Docs & Examples: https://offerrall.github.io/pyimagecuda/filter/#emboss
155
177
  """
178
+ if abs(strength) < 1e-6:
179
+ if dst_buffer is None:
180
+ dst_buffer = Image(src.width, src.height)
181
+ copy(dst_buffer, src)
182
+ return dst_buffer
183
+ else:
184
+ copy(dst_buffer, src)
185
+ return None
186
+
156
187
  if dst_buffer is None:
157
188
  dst_buffer = Image(src.width, src.height)
158
189
  return_buffer = True
159
190
  else:
160
- ensure_capacity(dst_buffer, src.width, src.height)
191
+ dst_buffer.resize(src.width, src.height)
161
192
  return_buffer = False
162
193
 
163
194
  filter_emboss_f32(
@@ -0,0 +1,84 @@
1
+ from .pyimagecuda_internal import ( # type: ignore
2
+ register_gl_pbo,
3
+ unregister_gl_resource,
4
+ copy_to_gl_pbo
5
+ )
6
+ from .image import ImageU8
7
+
8
+
9
+ class GLResource:
10
+ """
11
+ CUDA-OpenGL interop resource for direct GPU-to-GPU transfers.
12
+
13
+ Represents a registered OpenGL PBO that can receive ImageU8 data
14
+ directly without CPU roundtrips.
15
+
16
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
17
+ """
18
+
19
+ def __init__(self, pbo_id: int):
20
+ """
21
+ Parameters:
22
+ pbo_id: Valid OpenGL PBO ID. The PBO must exist and remain
23
+ valid for the lifetime of this GLResource.
24
+
25
+ Raises:
26
+ ValueError: If pbo_id is invalid
27
+ RuntimeError: If CUDA registration fails
28
+
29
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
30
+ """
31
+ if not isinstance(pbo_id, int) or pbo_id <= 0:
32
+ raise ValueError(f"Invalid PBO ID: {pbo_id}")
33
+
34
+ self._handle = None
35
+ self.pbo_id = pbo_id
36
+ self._handle = register_gl_pbo(pbo_id)
37
+
38
+ def copy_from(self, image: ImageU8, sync: bool = True) -> None:
39
+ """
40
+ Copies ImageU8 data directly to the registered PBO (GPU→GPU).
41
+
42
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
43
+ """
44
+ if not isinstance(image, ImageU8):
45
+ raise TypeError(f"Expected ImageU8, got {type(image).__name__}")
46
+
47
+ if self._handle is None:
48
+ raise RuntimeError("GLResource has been freed")
49
+
50
+ copy_to_gl_pbo(
51
+ image._buffer._handle,
52
+ self._handle,
53
+ image.width,
54
+ image.height,
55
+ sync
56
+ )
57
+
58
+ def free(self) -> None:
59
+ """
60
+ Unregisters the OpenGL resource from CUDA.
61
+
62
+ Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
63
+ """
64
+ if self._handle is not None:
65
+ unregister_gl_resource(self._handle)
66
+ self._handle = None
67
+
68
+ def __enter__(self):
69
+ return self
70
+
71
+ def __exit__(self, exc_type, exc_value, traceback):
72
+ self.free()
73
+ return False
74
+
75
+ def __del__(self):
76
+ if hasattr(self, '_handle') and self._handle is not None:
77
+ try:
78
+ self.free()
79
+ except Exception:
80
+ pass
81
+
82
+ def __repr__(self) -> str:
83
+ status = "active" if getattr(self, '_handle', None) is not None else "freed"
84
+ return f"GLResource(pbo_id={self.pbo_id}, status={status})"
pyimagecuda/image.py CHANGED
@@ -1,13 +1,11 @@
1
1
  from .pyimagecuda_internal import create_buffer_f32, free_buffer, create_buffer_u8 #type: ignore
2
2
 
3
-
4
3
  class Buffer:
5
4
 
6
5
  def __init__(self, width: int, height: int, is_u8: bool = False):
7
6
  create_func = create_buffer_u8 if is_u8 else create_buffer_f32
8
7
  self._handle = create_func(width, height)
9
- self.capacity_width = width
10
- self.capacity_height = height
8
+ self.capacity_pixels = width * height
11
9
 
12
10
  def free(self) -> None:
13
11
  free_buffer(self._handle)
@@ -29,11 +27,11 @@ class ImageBase:
29
27
  value = int(value)
30
28
  if value <= 0:
31
29
  raise ValueError(f"Width must be positive, got {value}")
32
-
33
- if value > self._buffer.capacity_width:
30
+
31
+ if value * self._height > self._buffer.capacity_pixels:
34
32
  raise ValueError(
35
- f"Width {value} exceeds buffer capacity "
36
- f"{self._buffer.capacity_width}"
33
+ f"Dimensions {value}×{self._height} exceed buffer capacity "
34
+ f"({self._buffer.capacity_pixels:,} pixels)"
37
35
  )
38
36
 
39
37
  self._width = value
@@ -48,14 +46,46 @@ class ImageBase:
48
46
  if value <= 0:
49
47
  raise ValueError(f"Height must be positive, got {value}")
50
48
 
51
- if value > self._buffer.capacity_height:
49
+ if self._width * value > self._buffer.capacity_pixels:
52
50
  raise ValueError(
53
- f"Height {value} exceeds buffer capacity "
54
- f"{self._buffer.capacity_height}"
51
+ f"Dimensions {self._width}×{value} exceed buffer capacity "
52
+ f"({self._buffer.capacity_pixels:,} pixels)"
55
53
  )
56
54
 
57
55
  self._height = value
58
56
 
57
+ def resize(self, width: int, height: int) -> None:
58
+ """
59
+ Sets both width and height atomically with proper validation.
60
+
61
+ Recommended when changing aspect ratio to avoid temporary invalid states.
62
+
63
+ Example:
64
+ img = Image(1920, 1080)
65
+ img.resize(3840, 540) # Changes aspect ratio safely
66
+
67
+ Note: This not change image data, only dimensions.
68
+ """
69
+ width = int(width)
70
+ height = int(height)
71
+
72
+ if width <= 0 or height <= 0:
73
+ raise ValueError(f"Dimensions must be positive, got {width}×{height}")
74
+
75
+ total_pixels = width * height
76
+ if total_pixels > self._buffer.capacity_pixels:
77
+ raise ValueError(
78
+ f"Dimensions {width}×{height} ({total_pixels:,} pixels) "
79
+ f"exceed buffer capacity ({self._buffer.capacity_pixels:,} pixels)"
80
+ )
81
+
82
+ self._width = width
83
+ self._height = height
84
+
85
+ def get_max_capacity(self) -> int:
86
+ """Returns the total pixel capacity of the buffer."""
87
+ return self._buffer.capacity_pixels
88
+
59
89
  def free(self) -> None:
60
90
  self._buffer.free()
61
91
 
@@ -66,13 +96,9 @@ class ImageBase:
66
96
  self.free()
67
97
  return False
68
98
 
69
- def get_max_capacity(self) -> tuple[int, int]:
70
- return (self._buffer.capacity_width, self._buffer.capacity_height)
71
-
72
99
  def __repr__(self) -> str:
73
100
  return f"{self.__class__.__name__}({self.width}×{self.height})"
74
101
 
75
-
76
102
  class Image(ImageBase):
77
103
 
78
104
  def __init__(self, width: int, height: int):