diff --git a/pyproject.toml b/pyproject.toml index 3ae6860..2ab4698 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,13 @@ version = "0.1.0" description = "Process images for Waveshare E6 display" readme = "README.md" requires-python = ">=3.13" -dependencies = ["pillow>=12.1.1"] +dependencies = ["numpy>=2.0", "pillow>=12.1.1"] [dependency-groups] dev = ["ruff"] [tool.ruff] -line-length = 150 +line-length = 180 target-version = "py313" [tool.ruff.lint] diff --git a/src/new_convert.py b/src/new_convert.py index 122a7b8..acc215b 100644 --- a/src/new_convert.py +++ b/src/new_convert.py @@ -6,127 +6,199 @@ import argparse from pathlib import Path +import numpy as np from PIL import Image, ImageEnhance, ImageOps -# E Ink Spectra 6 six basic ink colors: Black, White, Red, Green, Blue, Yellow -SPECTRA6_PALETTE = [ - 0, 0, 0, - 255, 255, 255, - 255, 0, 0, - 0, 255, 0, - 0, 0, 255, - 255, 255, 0 -] -# Pad the palette to 256 colors (required by PIL), fill the rest with 0 -SPECTRA6_PALETTE += [0] * (256 * 3 - len(SPECTRA6_PALETTE)) +class EPaperConverter: + """Converts photos for Waveshare E-Paper (E Ink Spectra 6) display output.""" + # E Ink Spectra 6 palette: [R, G, B] triplets + SPECTRA6_COLORS = [ + [0, 0, 0], # Black + [255, 255, 255], # White + [255, 0, 0], # Red + [0, 255, 0], # Green + [0, 0, 255], # Blue + [255, 255, 0], # Yellow + ] -def create_spectra_palette(): - """Create a reference image with the Spectra 6 palette.""" - pal_img = Image.new('P', (1, 1)) - pal_img.putpalette(SPECTRA6_PALETTE) - return pal_img + BW_COLORS = [ + [0, 0, 0], # Black + [255, 255, 255], # White + ] + # Optimization presets: {enhancement_attr: multiplier} + ENHANCE_PRESETS = { + 'normal': {'brightness': 1.1, 'color': 1.5, 'contrast': 1.2, 'sharpness': 1.0}, + 'portrait': {'brightness': 1.2, 'color': 1.2, 'contrast': 1.05, 'sharpness': 1.0}, + 'landscape': {'brightness': 1.0, 'color': 1.8, 'contrast': 1.4, 'sharpness': 2.0}, + } -def apply_epaper_gamma_correction(img, gamma=1.2): - """ - Apply E-paper specific Gamma correction. - Since e-paper is non-emissive, dark areas tend to blend together. - This function brightens the midtones to rescue dark details. - """ - # Convert Gamma value to a lookup table for the PIL point function - inv_gamma = 1.0 / gamma - table = [int((i / 255.0) ** inv_gamma * 255) for i in range(256)] + # Error diffusion kernels: list of (dy, dx, weight) tuples + DITHER_KERNELS = { + 'atkinson': [ + # * 1/8 1/8 + # 1/8 1/8 1/8 + # 1/8 + # Only diffuses 75% of error → preserves highlights, cleaner look + (0, 1, 1 / 8), (0, 2, 1 / 8), + (1, -1, 1 / 8), (1, 0, 1 / 8), (1, 1, 1 / 8), + (2, 0, 1 / 8), + ], + 'stucki': [ + # * 8/42 4/42 + # 2/42 4/42 8/42 4/42 2/42 + # 1/42 2/42 4/42 2/42 1/42 + # Largest diffusion area → smoothest gradients + (0, 1, 8 / 42), (0, 2, 4 / 42), + (1, -2, 2 / 42), (1, -1, 4 / 42), (1, 0, 8 / 42), (1, 1, 4 / 42), (1, 2, 2 / 42), + (2, -2, 1 / 42), (2, -1, 2 / 42), (2, 0, 4 / 42), (2, 1, 2 / 42), (2, 2, 1 / 42), + ], + } - # Separate RGB channels and apply correction - if img.mode == 'RGB': + def __init__(self, width=800, height=480, color='rgb', mode='normal', dither='floyd-steinberg'): + self.width = width + self.height = height + self.color = color + self.mode = mode + self.dither = dither + + # -- Image enhancement --------------------------------------------------- + + def _apply_gamma_correction(self, img, gamma=1.3): + """Brighten midtones via gamma correction to compensate for e-paper's non-emissive display.""" + inv_gamma = 1.0 / gamma + lut = [int((i / 255.0) ** inv_gamma * 255) for i in range(256)] r, g, b = img.split() - r = r.point(table) - g = g.point(table) - b = b.point(table) - return Image.merge('RGB', (r, g, b)) + return Image.merge('RGB', (r.point(lut), g.point(lut), b.point(lut))) - return img.point(table) + def _optimize(self, img): + """Apply gamma correction and preset-based enhancement for e-paper output.""" + img = self._apply_gamma_correction(img) + preset = self.ENHANCE_PRESETS[self.mode] + for attr, factor in preset.items(): + if factor != 1.0: + enhancer_cls = getattr(ImageEnhance, attr.capitalize()) + img = enhancer_cls(img).enhance(factor) + return img + + # -- Dithering ----------------------------------------------------------- + + def _build_pil_palette(self, colors): + """Build a PIL palette image from a list of [R, G, B] triplets.""" + flat = [v for rgb in colors for v in rgb] + flat += [0] * (256 * 3 - len(flat)) + pal_img = Image.new('P', (1, 1)) + pal_img.putpalette(flat) + return pal_img + + def _error_diffusion_dither(self, img, palette_colors): + """Apply custom error diffusion dithering (atkinson / stucki) using NumPy.""" + kernel = self.DITHER_KERNELS[self.dither] + palette = np.array(palette_colors, dtype=np.float64) + pixels = np.array(img, dtype=np.float64) + h, w, _ = pixels.shape + + for y in range(h): + for x in range(w): + old_pixel = pixels[y, x].copy() + distances = np.sum((palette - old_pixel) ** 2, axis=1) + new_pixel = palette[np.argmin(distances)] + pixels[y, x] = new_pixel + + error = old_pixel - new_pixel + for dy, dx, weight in kernel: + ny, nx = y + dy, x + dx + if 0 <= ny < h and 0 <= nx < w: + pixels[ny, nx] += error * weight + + return Image.fromarray(np.clip(pixels, 0, 255).astype(np.uint8), 'RGB') + + def _dither_rgb(self, img): + """Quantize an RGB image to the Spectra 6 palette.""" + if self.dither == 'floyd-steinberg': + pal_img = self._build_pil_palette(self.SPECTRA6_COLORS) + return img.quantize(palette=pal_img, dither=Image.FLOYDSTEINBERG).convert('RGB') + return self._error_diffusion_dither(img, self.SPECTRA6_COLORS) + + def _dither_bw(self, img): + """Convert an image to black and white with dithering.""" + img = ImageEnhance.Contrast(img).enhance(1.5) + if self.dither == 'floyd-steinberg': + return img.convert('1').convert('RGB') + return self._error_diffusion_dither(img, self.BW_COLORS) + + # -- Public API ---------------------------------------------------------- + + def convert(self, img_path, output_path=None): + """ + Main conversion pipeline: load → resize → enhance → dither → save. + + Args: + img_path: Path to the source image. + output_path: Optional output path. Auto-generated if None. + + Returns: + Path to the saved output file. + """ + img_path = Path(img_path) + if not img_path.exists(): + raise FileNotFoundError(f'Image file not found: {img_path}') + + print(f'Processing: {img_path}') + img = Image.open(img_path).convert('RGB') + + # Resize to fit the e-paper frame + img = ImageOps.fit(img, (self.width, self.height), Image.Resampling.LANCZOS) + + # Enhance and dither + print(f'Color: {self.color} | Mode: {self.mode} | Dither: {self.dither}') + if self.color == 'rgb': + img = self._optimize(img) + final_img = self._dither_rgb(img) + else: + final_img = self._dither_bw(img) + + # Save as 24-bit RGB BMP (required by Waveshare e-paper frames) + if output_path is None: + output_path = img_path.with_name(f'{img_path.stem}_{self.color}_{self.mode}_{self.dither}.bmp') + else: + output_path = Path(output_path) + + final_img.save(output_path) + print(f'Saved: {output_path}') + return output_path -def optimize_image(img, mode): - """Apply extreme color compensation based on the selected mode to offset physical e-paper attenuation.""" +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- - # Apply base Gamma brightening for all color images before conversion - img = apply_epaper_gamma_correction(img, gamma=1.3) - if mode == 'normal': - # Normal mode: Boost saturation and contrast globally - img = ImageEnhance.Color(img).enhance(1.5) # Significantly increase vibrance - img = ImageEnhance.Contrast(img).enhance(1.2) # Strengthen light/dark contrast - img = ImageEnhance.Brightness(img).enhance(1.1) # Slightly brighten overall - - elif mode == 'portrait': - # Portrait mode: Protect skin tones. Brighten faces, but limit saturation. - img = ImageEnhance.Brightness(img).enhance(1.2) # Faces must be bright enough - img = ImageEnhance.Color(img).enhance(1.2) # Moderately increase color - img = ImageEnhance.Contrast(img).enhance(1.05) # Maintain soft transitions - - elif mode == 'landscape': - # Landscape mode: Maximize greens, blues, and 3D depth - img = ImageEnhance.Color(img).enhance(1.8) # Extreme vibrance for mountains and skies - img = ImageEnhance.Contrast(img).enhance(1.4) # Strong contrast for ridges and clouds - img = ImageEnhance.Sharpness(img).enhance(2.0) # Sharpen edge details - - return img +def build_parser(): + """Build and return the argument parser.""" + parser = argparse.ArgumentParser(description='Spectra 6 E-Paper Image Conversion Engine') + parser.add_argument('--img_path', type=str, required=True, help='Path to the original photo') + parser.add_argument('--color', type=str, choices=['rgb', 'bw'], required=True, help="'rgb' (Spectra 6) or 'bw' (pure black and white)") + parser.add_argument('--mode', type=str, choices=['normal', 'portrait', 'landscape'], default='normal', help='Optimization preset (default: normal)') + parser.add_argument('--dither', type=str, choices=['floyd-steinberg', 'atkinson', 'stucki'], default='floyd-steinberg', help='Dithering algorithm (default: floyd-steinberg)') + parser.add_argument('--width', type=int, default=800, help='E-paper width (default: 800)') + parser.add_argument('--height', type=int, default=480, help='E-paper height (default: 480)') + parser.add_argument('--out', type=str, default=None, help='Custom output filename') + return parser def main(): - parser = argparse.ArgumentParser(description='Spectra 6 E-Paper Image Conversion Engine') - parser.add_argument('--img_path', type=str, required=True, help='Path to the original photo (e.g., my_photo.jpg)') - parser.add_argument( - '--color', type=str, choices=['rgb', 'bw'], required=True, help="Color mode: 'rgb' (Full color Spectra 6) or 'bw' (Pure black and white)" + args = build_parser().parse_args() + converter = EPaperConverter( + width=args.width, + height=args.height, + color=args.color, + mode=args.mode, + dither=args.dither, ) - parser.add_argument( - '--mode', - type=str, - choices=['normal', 'portrait', 'landscape'], - default='normal', - help="Optimization mode: 'normal', 'portrait', 'landscape'. Ignored if color is 'bw'.", - ) - parser.add_argument('--width', type=int, default=800, help='E-paper width (default: 800)') - parser.add_argument('--height', type=int, default=480, help='E-paper height (default: 480)') - parser.add_argument('--out', type=str, default=None, help='Custom output filename (default: auto-generated)') - - args = parser.parse_args() - - img_path = Path(args.img_path) - if not img_path.exists(): - raise FileNotFoundError(f'Error: Image file not found: {img_path}') - - print(f'Processing: {img_path}') - img = Image.open(img_path).convert('RGB') - - # 1. Smart crop and resize to fill the frame - img = ImageOps.fit(img, (args.width, args.height), Image.Resampling.LANCZOS) - - # Generate automatic filename if not provided - output_name = Path(args.out) if args.out else img_path.with_name(f'{img_path.stem}_{args.color}_{args.mode}.bmp') - - # 2. Core conversion logic - if args.color == 'rgb': - print(f'Applying color optimization mode: {args.mode}') - img_optimized = optimize_image(img, args.mode) - pal_img = create_spectra_palette() - # Convert and apply Floyd-Steinberg dithering, then back to 24-bit RGB - # Waveshare e-paper frames only recognize 24-bit RGB BMP files - final_img = img_optimized.quantize(palette=pal_img, dither=Image.FLOYDSTEINBERG).convert('RGB') - - elif args.color == 'bw': - print(f'Applying pure black and white mode (ignoring optimization mode: {args.mode})') - # Black and white mode only needs basic contrast stretching - img_bw = ImageEnhance.Contrast(img).enhance(1.5) - final_img = img_bw.convert('1').convert('RGB') - - # 3. Save file (24-bit RGB BMP, required by Waveshare e-paper frames) - final_img.save(output_name) - print(f'Success! Image saved as: {output_name}') + converter.convert(args.img_path, output_path=args.out) if __name__ == '__main__': diff --git a/uv.lock b/uv.lock index dd76e02..be0f56b 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,56 @@ version = 1 revision = 3 requires-python = ">=3.13" +[[package]] +name = "numpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, +] + [[package]] name = "pillow" version = "12.1.1" @@ -90,6 +140,7 @@ name = "waveshare-e6-image-processor" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "numpy" }, { name = "pillow" }, ] @@ -99,7 +150,10 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "pillow", specifier = ">=12.1.1" }] +requires-dist = [ + { name = "numpy", specifier = ">=2.0" }, + { name = "pillow", specifier = ">=12.1.1" }, +] [package.metadata.requires-dev] dev = [{ name = "ruff" }]