From 341e3431069860676daeddadc70e8d7eb61410fc Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 16 Jan 2024 09:22:25 +0100 Subject: [PATCH 1/4] palette based dithering --- frameos/src/frameos/runner.nim | 5 +- frameos/src/frameos/utils/dither.nim | 85 +++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/frameos/src/frameos/runner.nim b/frameos/src/frameos/runner.nim index f436f936..a22e2c16 100644 --- a/frameos/src/frameos/runner.nim +++ b/frameos/src/frameos/runner.nim @@ -5,7 +5,8 @@ import scenes/default as defaultScene import frameos/channels import frameos/types import frameos/config -from frameos/utils/image import rotateDegrees, renderError, scaleAndDrawImage +import frameos/utils/image +import frameos/utils/dither import drivers/drivers as drivers @@ -36,6 +37,8 @@ proc setLastImage(image: Image) = proc getLastPng*(): string = var copy: seq[ColorRGBX] withLock pngLock: + # TODO: optimize this locked time + pngImage.dither(desaturatedPalette) copy = pngImage.data return encodePng(pngImage.width, pngImage.height, 4, copy[0].addr, copy.len * 4) diff --git a/frameos/src/frameos/utils/dither.nim b/frameos/src/frameos/utils/dither.nim index ae3cca4b..43006845 100644 --- a/frameos/src/frameos/utils/dither.nim +++ b/frameos/src/frameos/utils/dither.nim @@ -1,4 +1,4 @@ -import math, pixie +import math, pixie, chroma proc toGrayscaleFloat*(image: Image, grayscale: var seq[float], multiple: float = 1.0) = let @@ -26,3 +26,86 @@ proc floydSteinberg*(pixels: var seq[float], width, height: int) = for i in 0..<4: if (x + v[i] >= 0) and (x + v[i] < width) and (y + u[i] < height): pixels[(y + u[i]) * width + (x + v[i])] += error * distribution[i] + + +let desaturatedPalette* = @[ + parseHtmlColor("#000000"), + parseHtmlColor("#FFFFFF"), + parseHtmlColor("#00FF00"), + parseHtmlColor("#0000FF"), + parseHtmlColor("#FF0000"), + parseHtmlColor("#FFFF00"), + parseHtmlColor("#FF8C00"), + parseHtmlColor("#FFFFFF"), +] + +let saturatedPalette* = @[ + parseHtmlColor("#393039"), + parseHtmlColor("#FFFFFF"), + parseHtmlColor("#3A5B46"), + parseHtmlColor("#3D3B5E"), + parseHtmlColor("#9C484B"), + parseHtmlColor("#D0BE47"), + parseHtmlColor("#B16A49"), + parseHtmlColor("#FFFFFF"), +] + +proc closestPalette*(palette: seq[Color], r, g, b: int): (int, int, int, int) = + var index: int = 0 + var min = 99999999999 + for i in 0.. 255: return 255 + else: return value.uint8 + +proc dither*(image: var Image, palette: seq[Color]) = + let + width = image.width + height = image.height + + let + distribution = [7, 3, 5, 1] + u = [0, 1, 1, 1] # + y + v = [1, -1, 0, 1] # + x + + for y in 0..= 0) and (x + v[i] < width) and (y + u[i] < height): + let errorIndex = (y + u[i]) * width + (x + v[i]) + image.data[errorIndex].r = clip8(image.data[errorIndex].r.int + (errorR * distribution[i] div 16)) + image.data[errorIndex].g = clip8(image.data[errorIndex].g.int + (errorG * distribution[i] div 16)) + image.data[errorIndex].b = clip8(image.data[errorIndex].b.int + (errorB * distribution[i] div 16)) From 76c4a4feacdba07d1c0d0ff9c1d2fbbb53129804 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 16 Jan 2024 20:38:52 +0100 Subject: [PATCH 2/4] dither cleanup --- frameos/src/frameos/runner.nim | 3 - frameos/src/frameos/utils/dither.nim | 83 ++++++++++++---------------- 2 files changed, 35 insertions(+), 51 deletions(-) diff --git a/frameos/src/frameos/runner.nim b/frameos/src/frameos/runner.nim index a22e2c16..b6e79a2b 100644 --- a/frameos/src/frameos/runner.nim +++ b/frameos/src/frameos/runner.nim @@ -6,7 +6,6 @@ import frameos/channels import frameos/types import frameos/config import frameos/utils/image -import frameos/utils/dither import drivers/drivers as drivers @@ -37,8 +36,6 @@ proc setLastImage(image: Image) = proc getLastPng*(): string = var copy: seq[ColorRGBX] withLock pngLock: - # TODO: optimize this locked time - pngImage.dither(desaturatedPalette) copy = pngImage.data return encodePng(pngImage.width, pngImage.height, 4, copy[0].addr, copy.len * 4) diff --git a/frameos/src/frameos/utils/dither.nim b/frameos/src/frameos/utils/dither.nim index 43006845..e19220cf 100644 --- a/frameos/src/frameos/utils/dither.nim +++ b/frameos/src/frameos/utils/dither.nim @@ -13,8 +13,8 @@ proc toGrayscaleFloat*(image: Image, grayscale: var seq[float], multiple: float proc floydSteinberg*(pixels: var seq[float], width, height: int) = let distribution = [7.0 / 16.0, 3.0 / 16.0, 5.0 / 16.0, 1.0 / 16.0] - u = [0, 1, 1, 1] # + y - v = [1, -1, 0, 1] # + x + dx = [0, 1, 1, 1] + dy = [1, -1, 0, 1] for y in 0..= 0) and (x + v[i] < width) and (y + u[i] < height): - pixels[(y + u[i]) * width + (x + v[i])] += error * distribution[i] - - -let desaturatedPalette* = @[ - parseHtmlColor("#000000"), - parseHtmlColor("#FFFFFF"), - parseHtmlColor("#00FF00"), - parseHtmlColor("#0000FF"), - parseHtmlColor("#FF0000"), - parseHtmlColor("#FFFF00"), - parseHtmlColor("#FF8C00"), - parseHtmlColor("#FFFFFF"), + if (x + dx[i] >= 0) and (x + dx[i] < width) and (y + dy[i] < height): + pixels[(y + dy[i]) * width + (x + dx[i])] += error * distribution[i] + +const desaturatedPalette* = @[ + (0, 0, 0), + (255, 255, 255), + (0, 255, 0), + (0, 0, 255), + (255, 0, 0), + (255, 255, 0), + (255, 140, 0), + (255, 255, 255) ] -let saturatedPalette* = @[ - parseHtmlColor("#393039"), - parseHtmlColor("#FFFFFF"), - parseHtmlColor("#3A5B46"), - parseHtmlColor("#3D3B5E"), - parseHtmlColor("#9C484B"), - parseHtmlColor("#D0BE47"), - parseHtmlColor("#B16A49"), - parseHtmlColor("#FFFFFF"), +const saturatedPalette* = @[ + (57, 48, 57), + (255, 255, 255), + (58, 91, 70), + (61, 59, 94), + (156, 72, 75), + (208, 190, 71), + (177, 106, 73), + (255, 255, 255), ] -proc closestPalette*(palette: seq[Color], r, g, b: int): (int, int, int, int) = +proc closestPalette*(palette: seq[(int, int, int)], r, g, b: int): (int, int, int, int) = + # TODO: optimize with a lookup table var index: int = 0 var min = 99999999999 for i in 0.. 255: return 255 else: return value.uint8 -proc dither*(image: var Image, palette: seq[Color]) = +proc ditherPalette*(image: var Image, palette: seq[(int, int, int)]) = let width = image.width height = image.height - - let distribution = [7, 3, 5, 1] - u = [0, 1, 1, 1] # + y - v = [1, -1, 0, 1] # + x + dy = [0, 1, 1, 1] + dx = [1, -1, 0, 1] for y in 0..= 0) and (x + v[i] < width) and (y + u[i] < height): - let errorIndex = (y + u[i]) * width + (x + v[i]) + if (x + dx[i] >= 0) and (x + dx[i] < width) and (y + dy[i] < height): + let errorIndex = (y + dy[i]) * width + (x + dx[i]) image.data[errorIndex].r = clip8(image.data[errorIndex].r.int + (errorR * distribution[i] div 16)) image.data[errorIndex].g = clip8(image.data[errorIndex].g.int + (errorG * distribution[i] div 16)) image.data[errorIndex].b = clip8(image.data[errorIndex].b.int + (errorB * distribution[i] div 16)) From c286b7af5a199b566b66a1ed76b66acfcac5f8d3 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 16 Jan 2024 22:34:58 +0100 Subject: [PATCH 3/4] bwr --- backend/app/drivers/waveshare.py | 12 ++++++------ backend/list_devices.py | 2 +- frameos/src/drivers/waveshare/driver.nim | 2 +- frameos/src/drivers/waveshare/types.nim | 2 +- frameos/src/frameos/server.nim | 1 - 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/backend/app/drivers/waveshare.py b/backend/app/drivers/waveshare.py index 6c615c0a..b2a2cf57 100644 --- a/backend/app/drivers/waveshare.py +++ b/backend/app/drivers/waveshare.py @@ -19,7 +19,7 @@ class WaveshareVariant: display_function: Optional[str] = None display_arguments: Optional[List[str]] = None init_returns_zero: bool = False - color_option: Literal["Unknown", "Black", "BlackRed", "FourGray", "SevenColor", "BlackWhiteYellowRed"] = "Unknown" + color_option: Literal["Unknown", "Black", "BlackWhiteRed", "FourGray", "SevenColor", "BlackWhiteYellowRed"] = "Unknown" # Colors if we can't autodetect VARIANT_COLORS = { @@ -59,7 +59,7 @@ class WaveshareVariant: "EPD_5in65f": "SevenColor", } -# TODO: BlackRed support +# TODO: BlackWhiteRed support # TODO: BlackWhiteYellowRed support https://www.waveshare.com/wiki/4.37inch_e-Paper_Module_(G)_Manual#Working_With_Raspberry_Pi # TODO: SevenColor support @@ -157,7 +157,7 @@ def convert_waveshare_source(variant_key: str) -> WaveshareVariant: if variant.display_arguments == ["Black"]: variant.color_option = "Black" elif variant.display_arguments == ["Black", "Red"]: - variant.color_option = "BlackRed" + variant.color_option = "BlackWhiteRed" elif variant.display_arguments == ["BlackWhiteYellowRed"]: variant.color_option = "BlackWhiteYellowRed" elif variant.display_arguments == ["FourGray"]: @@ -205,9 +205,9 @@ def write_waveshare_driver_nim(drivers: Dict[str, Driver]) -> str: waveshareDisplay.{variant.sleep_function}() proc renderImage*(image: seq[uint8]) = - {f'waveshareDisplay.{variant.display_function}(addr image[0])' if variant.color_option != 'BlackRed' else 'discard'} + {f'waveshareDisplay.{variant.display_function}(addr image[0])' if variant.color_option != 'BlackWhiteRed' else 'discard'} -proc renderImageBlackRed*(image1: seq[uint8], image2: seq[uint8]) = - {f'waveshareDisplay.{variant.display_function}(addr image1[0], addr image2[0])' if variant.color_option == 'BlackRed' else 'discard'} +proc renderImageBlackWhiteRed*(image1: seq[uint8], image2: seq[uint8]) = + {f'waveshareDisplay.{variant.display_function}(addr image1[0], addr image2[0])' if variant.color_option == 'BlackWhiteRed' else 'discard'} """ diff --git a/backend/list_devices.py b/backend/list_devices.py index b57c0648..81d2036c 100644 --- a/backend/list_devices.py +++ b/backend/list_devices.py @@ -23,7 +23,7 @@ for v in variants: color = { "Black": "Black/White", - "BlackRed": "Black/White/Red", + "BlackWhiteRed": "Black/White/Red", "BlackWhiteYellowRed": "Black/White/Yellow/Red (not implemented)", "FourGray": "4 Grayscale (not implemented!)", "SevenColor": "7 Color (not implemented!)", diff --git a/frameos/src/drivers/waveshare/driver.nim b/frameos/src/drivers/waveshare/driver.nim index 7d3f116f..d44094e1 100644 --- a/frameos/src/drivers/waveshare/driver.nim +++ b/frameos/src/drivers/waveshare/driver.nim @@ -30,5 +30,5 @@ proc renderImage*(image: seq[uint8]) = # This file is autogenerated for your frame on deploy waveshareDisplay.EPD_2in13_V3_Display_Base(addr image[0]) -proc renderImageBlackRed*(image1: seq[uint8], image2: seq[uint8]) = +proc renderImageBlackWhiteRed*(image1: seq[uint8], image2: seq[uint8]) = discard diff --git a/frameos/src/drivers/waveshare/types.nim b/frameos/src/drivers/waveshare/types.nim index c9cc6363..331c78fe 100644 --- a/frameos/src/drivers/waveshare/types.nim +++ b/frameos/src/drivers/waveshare/types.nim @@ -1,6 +1,6 @@ type ColorOption* = enum Black = "Black" - BlackRed = "BlackRed" + BlackWhiteRed = "BlackWhiteRed" SevenColor = "SevenColor" FourGray = "FourGray" BlackWhiteYellowRed = "BlackWhiteYellowRed" diff --git a/frameos/src/frameos/server.nim b/frameos/src/frameos/server.nim index 84d8f030..1fea339f 100644 --- a/frameos/src/frameos/server.nim +++ b/frameos/src/frameos/server.nim @@ -14,7 +14,6 @@ import strutils import drivers/drivers as drivers import frameos/types import frameos/channels -import frameos/utils/image from frameos/runner import getLastPng, triggerRender var globalFrameConfig: FrameConfig From fef93cac7ac33ec38311d26852b6eefe71b42a4e Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 16 Jan 2024 22:45:42 +0100 Subject: [PATCH 4/4] 4 and 7 color modes --- frameos/src/drivers/waveshare/waveshare.nim | 95 ++++++++++----- frameos/src/frameos/utils/dither.nim | 121 +++++++++++++------- 2 files changed, 146 insertions(+), 70 deletions(-) diff --git a/frameos/src/drivers/waveshare/waveshare.nim b/frameos/src/drivers/waveshare/waveshare.nim index 3fdb48a8..3ed88392 100644 --- a/frameos/src/drivers/waveshare/waveshare.nim +++ b/frameos/src/drivers/waveshare/waveshare.nim @@ -16,6 +16,8 @@ type Driver* = ref object of FrameOSDriver var lastFloatImageLock: Lock lastFloatImage: seq[float] + lastPixelsLock: Lock + lastPixels: seq[uint8] proc setLastFloatImage*(image: seq[float]) = withLock lastFloatImageLock: @@ -25,6 +27,14 @@ proc getLastFloatImage*(): seq[float] = withLock lastFloatImageLock: result = lastFloatImage +proc setLastPixels*(image: seq[uint8]) = + withLock lastPixelsLock: + lastPixels = image + +proc getLastPixels*(): seq[uint8] = + withLock lastPixelsLock: + result = lastPixels + proc init*(frameOS: FrameOS): Driver = let logger = frameOS.logger let width = waveshareDriver.width @@ -89,8 +99,8 @@ proc renderFourGray*(self: Driver, image: Image) = blackImage[index div 4] = blackImage[index div 4] or ((bw and 0b11) shl (6 - (index mod 4) * 2)) waveshareDriver.renderImage(blackImage) - -proc renderBlackRed*(self: Driver, image: Image) = +proc renderBlackWhiteRed*(self: Driver, image: Image) = + let pixels = ditherPaletteIndexed(image, @[(0, 0, 0), (255, 0, 0), (255, 255, 255)]) let rowWidth = ceil(image.width.float / 8).int var blackImage = newSeq[uint8](rowWidth * image.height) var redImage = newSeq[uint8](rowWidth * image.height) @@ -98,21 +108,23 @@ proc renderBlackRed*(self: Driver, image: Image) = for y in 0.. 100 and pixel.g > 50 and pixel.b > 50: 1 else: 0 - blackImage[index div 8] = blackImage[index div 8] or (bw shl (7 - (index mod 8))) - redImage[index div 8] = redImage[index div 8] or (red shl (7 - (index mod 8))) - - waveshareDriver.renderImageBlackRed(blackImage, redImage) + let index = y * rowWidth + x div 8 + let oneByte = pixels[inputIndex div 2] + let pixel = if inputIndex mod 2 == 0: oneByte shr 4 else: oneByte and 0x0F + let bw: uint8 = if pixel == 0: 1 else: 0 + let red: uint8 = if pixel == 1: 1 else: 0 + blackImage[index] = blackImage[index] or (bw shl (7 - (x mod 8))) + redImage[index] = redImage[index] or (red shl (7 - (x mod 8))) -proc renderSevenColor*(self: Driver, image: Image) = - raise newException(Exception, "7 color mode not yet supported") + waveshareDriver.renderImageBlackWhiteRed(blackImage, redImage) proc renderBlackWhiteYellowRed*(self: Driver, image: Image) = - raise newException(Exception, "Black White Yellow Red mode not yet supported") + let pixels = ditherPaletteIndexed(image, saturated4ColorPalette) + waveshareDriver.renderImage(pixels) + +proc renderSevenColor*(self: Driver, image: Image) = + let pixels = ditherPaletteIndexed(image, saturated7ColorPalette) + waveshareDriver.renderImage(pixels) proc render*(self: Driver, image: Image) = # Refresh at least every 12h to preserve display @@ -129,8 +141,8 @@ proc render*(self: Driver, image: Image) = case waveshareDriver.colorOption: of ColorOption.Black: self.renderBlack(image) - of ColorOption.BlackRed: - self.renderBlackRed(image) + of ColorOption.BlackWhiteRed: + self.renderBlackWhiteRed(image) of ColorOption.SevenColor: self.renderSevenColor(image) of ColorOption.FourGray: @@ -142,31 +154,58 @@ proc render*(self: Driver, image: Image) = # Convert the rendered pixels to a PNG image. For accurate colors on the web. proc toPng*(rotate: int = 0): string = - let pixels = getLastFloatImage() var outputImage = newImage(width, height) case waveshareDriver.colorOption: of ColorOption.Black: + let pixels = getLastFloatImage() for y in 0 ..< height: for x in 0 ..< width: let index = y * width + x - outputImage.data[index].r = (pixels[index] * 255).uint8 - outputImage.data[index].g = (pixels[index] * 255).uint8 - outputImage.data[index].b = (pixels[index] * 255).uint8 + let pixel = (pixels[index] * 255).uint8 + outputImage.data[index].r = pixel + outputImage.data[index].g = pixel + outputImage.data[index].b = pixel outputImage.data[index].a = 255 of ColorOption.FourGray: + let pixels = getLastFloatImage() for y in 0 ..< height: for x in 0 ..< width: let index = y * width + x - outputImage.data[index].r = (pixels[index] * 85).uint8 - outputImage.data[index].g = (pixels[index] * 85).uint8 - outputImage.data[index].b = (pixels[index] * 85).uint8 + let pixel = (pixels[index] * 85).uint8 + outputImage.data[index].r = pixel + outputImage.data[index].g = pixel + outputImage.data[index].b = pixel + outputImage.data[index].a = 255 + of ColorOption.BlackWhiteRed: + let pixels = getLastPixels() + for y in 0 ..< height: + for x in 0 ..< width: + let index = y * width + x + let pixel = (pixels[index div 4] shr ((3 - (index mod 4)) * 2)) and 0x03 + outputImage.data[index].r = if pixel == 0: 0 else: 255 + outputImage.data[index].g = if pixel == 2: 255 else: 1 + outputImage.data[index].b = if pixel == 2: 255 else: 1 outputImage.data[index].a = 255 - of ColorOption.BlackRed: - discard - of ColorOption.SevenColor: - discard of ColorOption.BlackWhiteYellowRed: - discard + let pixels = getLastPixels() + for y in 0 ..< height: + for x in 0 ..< width: + let index = y * width + x + let pixel = (pixels[index div 4] shr ((3 - (index mod 4)) * 2)) and 0x03 + outputImage.data[index].r = saturated4ColorPalette[pixel][0].uint8 + outputImage.data[index].g = saturated4ColorPalette[pixel][1].uint8 + outputImage.data[index].b = saturated4ColorPalette[pixel][2].uint8 + outputImage.data[index].a = 255 + of ColorOption.SevenColor: + let pixels = getLastPixels() + for y in 0 ..< height: + for x in 0 ..< width: + let index = y * width + x + let pixel = pixels[index] + outputImage.data[index].r = saturated7ColorPalette[pixel][0].uint8 + outputImage.data[index].g = saturated7ColorPalette[pixel][1].uint8 + outputImage.data[index].b = saturated7ColorPalette[pixel][2].uint8 + outputImage.data[index].a = 255 if rotate != 0: return outputImage.rotateDegrees(rotate).encodeImage(PngFormat) diff --git a/frameos/src/frameos/utils/dither.nim b/frameos/src/frameos/utils/dither.nim index e19220cf..0f9c6597 100644 --- a/frameos/src/frameos/utils/dither.nim +++ b/frameos/src/frameos/utils/dither.nim @@ -1,4 +1,53 @@ -import math, pixie, chroma +import math, pixie + +const desaturated4ColorPalette* = @[ + (0, 0, 0), + (255, 255, 255), + (255, 255, 0), + (255, 0, 0), +] + +const saturated4ColorPalette* = @[ + (57, 48, 57), + (255, 255, 255), + (208, 190, 71), + (156, 72, 75), +] + +const desaturated7ColorPalette* = @[ + (0, 0, 0), + (255, 255, 255), + (0, 255, 0), + (0, 0, 255), + (255, 0, 0), + (255, 255, 0), + (255, 140, 0) +] + +const saturated7ColorPalette* = @[ + (57, 48, 57), + (255, 255, 255), + (58, 91, 70), + (61, 59, 94), + (156, 72, 75), + (208, 190, 71), + (177, 106, 73), +] + +proc clip8(value: int): uint8 {.inline.} = + if value < 0: return 0 + elif value > 255: return 255 + else: return value.uint8 + +proc nextPowerOfTwo(bits: int): int = + var n = bits - 1 + n = n or n shr 1 + n = n or n shr 2 + n = n or n shr 4 + n = n or n shr 8 + n = n or n shr 16 + n += 1 + return n proc toGrayscaleFloat*(image: Image, grayscale: var seq[float], multiple: float = 1.0) = let @@ -13,8 +62,8 @@ proc toGrayscaleFloat*(image: Image, grayscale: var seq[float], multiple: float proc floydSteinberg*(pixels: var seq[float], width, height: int) = let distribution = [7.0 / 16.0, 3.0 / 16.0, 5.0 / 16.0, 1.0 / 16.0] - dx = [0, 1, 1, 1] - dy = [1, -1, 0, 1] + dy = [0, 1, 1, 1] + dx = [1, -1, 0, 1] for y in 0..= 0) and (x + dx[i] < width) and (y + dy[i] < height): pixels[(y + dy[i]) * width + (x + dx[i])] += error * distribution[i] -const desaturatedPalette* = @[ - (0, 0, 0), - (255, 255, 255), - (0, 255, 0), - (0, 0, 255), - (255, 0, 0), - (255, 255, 0), - (255, 140, 0), - (255, 255, 255) -] - -const saturatedPalette* = @[ - (57, 48, 57), - (255, 255, 255), - (58, 91, 70), - (61, 59, 94), - (156, 72, 75), - (208, 190, 71), - (177, 106, 73), - (255, 255, 255), -] - proc closestPalette*(palette: seq[(int, int, int)], r, g, b: int): (int, int, int, int) = # TODO: optimize with a lookup table var index: int = 0 @@ -60,39 +87,49 @@ proc closestPalette*(palette: seq[(int, int, int)], r, g, b: int): (int, int, in index = i return (index, palette[index][0], palette[index][1], palette[index][2]) -proc clip8(value: int): uint8 {.inline.} = - if value < 0: return 0 - elif value > 255: return 255 - else: return value.uint8 -proc ditherPalette*(image: var Image, palette: seq[(int, int, int)]) = +proc ditherPaletteIndexed*(image: Image, palette: seq[(int, int, int)]): seq[uint8] = let - width = image.width - height = image.height + img = image.copy + width = img.width + height = img.height distribution = [7, 3, 5, 1] dy = [0, 1, 1, 1] dx = [1, -1, 0, 1] + bits = nextPowerOfTwo(palette.len) + + let rowWidth = ceil(width.float / bits.float).int + var output = newSeq[uint8](height * rowWidth) for y in 0..= 0) and (x + dx[i] < width) and (y + dy[i] < height): let errorIndex = (y + dy[i]) * width + (x + dx[i]) - image.data[errorIndex].r = clip8(image.data[errorIndex].r.int + (errorR * distribution[i] div 16)) - image.data[errorIndex].g = clip8(image.data[errorIndex].g.int + (errorG * distribution[i] div 16)) - image.data[errorIndex].b = clip8(image.data[errorIndex].b.int + (errorB * distribution[i] div 16)) + img.data[errorIndex].r = clip8(img.data[errorIndex].r.int + (errorR * distribution[i] div 16)) + img.data[errorIndex].g = clip8(img.data[errorIndex].g.int + (errorG * distribution[i] div 16)) + img.data[errorIndex].b = clip8(img.data[errorIndex].b.int + (errorB * distribution[i] div 16)) + + return output