{"id":19472948,"url":"https://github.com/eskils/ditheringengine","last_synced_at":"2025-06-22T22:34:11.111Z","repository":{"id":167941924,"uuid":"643512745","full_name":"Eskils/DitheringEngine","owner":"Eskils","description":"iOS and MacCatalyst framework for dithering images and videos. Used in Ditherable.","archived":false,"fork":false,"pushed_at":"2025-04-17T18:20:18.000Z","size":18671,"stargazers_count":16,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-18T08:39:58.945Z","etag":null,"topics":["bayer-dither","ditherable","dithering","dithering-algorithms","floyd-steinberg","ios","maccatalyst","swift","swiftpackage"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Eskils.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-05-21T12:03:46.000Z","updated_at":"2025-04-09T10:25:17.000Z","dependencies_parsed_at":null,"dependency_job_id":"da3e5188-932a-4dfb-8e8e-8b9df289b2e3","html_url":"https://github.com/Eskils/DitheringEngine","commit_stats":null,"previous_names":["eskils/ditheringengine"],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eskils%2FDitheringEngine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eskils%2FDitheringEngine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eskils%2FDitheringEngine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eskils%2FDitheringEngine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Eskils","download_url":"https://codeload.github.com/Eskils/DitheringEngine/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250817659,"owners_count":21492192,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bayer-dither","ditherable","dithering","dithering-algorithms","floyd-steinberg","ios","maccatalyst","swift","swiftpackage"],"created_at":"2024-11-10T19:16:18.280Z","updated_at":"2025-04-25T12:31:27.241Z","avatar_url":"https://github.com/Eskils.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"![DitheringEngine](Documentation/Resources/DitheringEngineLogo.png)\n# Dithering Engine\n\nFramework for iOS and Mac Catalyst to dither images and videos.\n\nDithering is the process of adding noise to an image in order for us to perceive the image more colorful.\n\n![A dithered image with four colors. A rose bush on a field in foreground with a pergola in the background.](Documentation/Resources/DitheredImage2.png)\n\n\u003e This image has only four colors: black, white, cyan, and magenta.\n\nCheck out the [demo application](./Documentation/Demo/) for iOS and macOS.\n\n## Table of contents\n   * [Installation](#installation)\n   * [Usage](#usage)\n      * [Dithering images](#dithering-images)\n      * [Dithering videos](#dithering-videos)\n   * [Dithering methods](#dithering-methods)\n      * [Threshold](#threshold)\n      * [Floyd-Steinberg](#floyd-steinberg)\n      * [Atkinson](#atkinson)\n      * [Jarvis-Judice-Ninke](#jarvis-judice-ninke)\n      * [Bayer](#bayer)\n      * [White noise](#white-noise)\n      * [Noise](#noise)\n   * [Built-in palettes](#built-in-palettes)\n      * [Black \u0026 White](#black--white)\n      * [Grayscale](#grayscale)\n      * [Quantized Color](#quantized-color)\n      * [CGA](#cga)\n      * [Apple II](#apple-ii)\n      * [Game Boy](#game-boy)\n      * [Intellivision](#intellivision)\n   * [Creating your own palette](#creating-your-own-palette)\n   * [Video Dithering Engine](#video-dithering-engine)\n     * [Ordered dithering is more suitable](#ordered-dithering-is-more-suitable)\n     * [Video framerate](#video-framerate)\n     * [Concurrent frame processing](#concurrent-frame-processing)\n     * [Video Dither Options](#video-dither-options)\n     * [Video Description](#video-description)\n\n## Installation\nTo use this package in a SwiftPM project, you need to set it up as a package dependency:\n\n```swift\n// swift-tools-version:5.9\nimport PackageDescription\n\nlet package = Package(\n  name: \"MyPackage\",\n  dependencies: [\n    .package(\n      url: \"https://github.com/Eskils/DitheringEngine\", \n      .upToNextMinor(from: \"1.8.2\") // or `.upToNextMajor\n    )\n  ],\n  targets: [\n    .target(\n      name: \"MyTarget\",\n      dependencies: [\n        .product(name: \"DitheringEngine\", package: \"DitheringEngine\")\n      ]\n    )\n  ]\n)\n```\n\n## Usage\n\nThe engine works on CGImages and video URLs/AVAsset.\n\nSupported dithering methods are: \n  * [Threshold](#threshold)\n  * [Floyd-Steinberg](#floyd-steinberg)\n  * [Atkinson](#atkinson)\n  * [Jarvis-Judice-Ninke](#jarvis-judice-ninke)\n  * [Bayer (Ordered dithering)](#bayer)\n  * [White noise (Ordered dithering)](#white-noise)\n  * [Noise (Ordered dithering)](#noise)\n  \n  \u003e **NOTE:** The ordered dither methods are computed on the GPU using Metal by default. You can specify to run them on the CPU if desired.\n\nSupported out of the box palettes are:\n  * [Black \u0026 White](#black--white)\n  * [Grayscale](#grayscale)\n  * [Quantized Color](#quantized-color)\n  * [CGA](#cga)\n  * [Apple II](#apple-ii)\n  * [Game Boy](#game-boy)\n  * [Intellivision](#intellivision)\n\n### Dithering images\nExample usage: \n```swift\n// Create an instance of DitheringEngine\nlet ditheringEngine = DitheringEngine()\n// Set input image\ntry ditheringEngine.set(image: inputCGImage)\n// Dither to quantized color with 5 bits using Floyd-Steinberg.\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .floydSteinberg,\n    andPalette: .quantizedColor,\n    withDitherMethodSettings: FloydSteinbergSettingsConfiguration(direction: .leftToRight),\n    withPaletteSettings: QuantizedColorSettingsConfiguration(bits: 5)\n)\n```\n\n### Dithering videos\nExample usage: \n```swift\n// Create an instance of VideoDitheringEngine\nlet videoDitheringEngine = VideoDitheringEngine()\n// Create a video description\nlet videoDescription = VideoDescription(url: inputVideoURL)\n// Set preferred output size.\nvideoDescription.renderSize = CGSize(width: 320, height: 568)\n// Dither to quantized color with 5 bits using Floyd-Steinberg.\nvideoDitheringEngine.dither(\n    videoDescription: videoDescription,  \n    usingMethod: .floydSteinberg, \n    andPalette: .quantizedColor,\n    withDitherMethodSettings: FloydSteinbergSettingsConfiguration(direction: .leftToRight), \n    andPaletteSettings: QuantizedColorSettingsConfiguration(bits: 5), \n    outputURL: outputURL, \n    progressHandler: progressHandler,   // Optional block to receive progress.\n    completionHandler: completionHandler\n)\n```\n\n## Dithering methods\n\nHere is an overview over the available dithering methods.\n\n### Threshold\n\nThreshold gives the nearest match of the color in the image to the color in the palette without adding any noise or improvements.\n\n![Threshold with default settings. CGA Mode 4 | Palette 0 | High](Documentation/Resources/ThresholdAlg.png)\n\n**Token:** `.threshold`  \n**Settings:** `EmptyPaletteSettingsConfiguration`\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .threshold,\n    andPalette: .cga,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: CGASettingsConfiguration(mode: .palette0High)\n)\n```\n\n### Floyd-Steinberg\n\nFloyd-Steinberg dithering spreads the error from reducing the color of a pixel to the neighbouring pixels—yielding an image looking close to the original in areas of fine detail (e.g. grass and trees) and with interesting artifacts in areas of little detail (e.g. the sky).\n\n![Floyd-Steinberg dithering with default settings. CGA Text Mode palette](Documentation/Resources/FSAlg2.png)\n\n**Token:** `.floydSteinberg`  \n**Settings:** `FloydSteinbergSettingsConfiguration`\n\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| direction | FloydSteinbergDitheringDirection | `.leftToRight` | Specifies in what order to go through the pixels of the image. This has an effect on where the error is distributed. |\n| matrix | [Int] | [7, 3, 5, 1] | A matrix (array of four numbers) which specifies what weighting of the error to give the neighbouring pixels. The weighing is a fraction of the number and the sum of all numbers in the matrix. For instance: in the default matrix, the first is given the weight 7/16. The image explains how the weights in the matrix are distributed. ![](Documentation/Resources/FSMatrixExplanation.png).\n\n**`FloydSteinbergDitheringDirection`**:\n  - `.leftToRight`\n  - `.rightToLeft`\n  - `.topToBottom`\n  - `.bottomToTop`\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .floydSteinberg,\n    andPalette: .cga,\n    withDitherMethodSettings: FloydSteinbergSettingsConfiguration(direction: .leftToRight),\n    withPaletteSettings: CGASettingsConfiguration(mode: .textMode)\n)\n```\n\n### Atkinson\n\nAtkinson dithering is a variant of Floyd-Steinberg dithering, and works by spreading error from reducing the color of a pixel to the neighbouring pixels. Atkinson spreads over a larger area, but does not distribute the full error—making colors matching the palette have less noise.\n\n![Atkinson dithering with default settings. CGA Text Mode](Documentation/Resources/Atkinson.png)\n\n**Token:** `.atkinson`  \n**Settings:** `EmptyPaletteSettingsConfiguration`\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .atkinson,\n    andPalette: .cga,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: CGASettingsConfiguration(mode: .textMode)\n)\n```\n\n### Jarvis-Judice-Ninke\n\nJarvis-Judice-Ninke dithering is a variant of Floyd-Steinberg dithering, and works by spreading error from reducing the color of a pixel to the neighbouring pixels. This method spreads distributes the error over a larger area and therefore leaves a smoother look to your image.\n\n![Jarvis-Judice-Ninke dithering with default settings. CGA Text Mode](Documentation/Resources/JarvisJudiceNinke.png)\n\n**Token:** `.jarvisJudiceNinke`  \n**Settings:** `EmptyPaletteSettingsConfiguration`\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .jarvisJudiceNinke,\n    andPalette: .cga,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: CGASettingsConfiguration(mode: .textMode)\n)\n```\n\n### Bayer\n\nBayer dithering is a type of ordered dithering which adds a precalculated threshold to every pixel, baking in a special pattern.\n\n![Bayer dithering with default settings. CGA Mode 5 | High palette](Documentation/Resources/BayerAlg.png)\n\n**Token:** `.bayer`  \n**Settings:** `BayerSettingsConfiguration`\n\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| thresholdMapSize | Int | `4` | Specifies the size of the square threshold matrix. Default is 4x4. |\n| intensity | Float | `1` | Specifies the intensity of the noise pattern. Intensity is calculated from the thresholdMapSize, and this property specifies the fraction of the calculated intensity to apply. |\n| performOnCPU | Bool | `false` | Determines wether to perform the computation on the CPU. If false, the GPU is used for quicker performance. |\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .bayer,\n    andPalette: .cga,\n    withDitherMethodSettings: BayerSettingsConfiguration(),\n    withPaletteSettings: CGASettingsConfiguration(mode: .mode5High)\n)\n```\n\n### White noise\n\nWhite noise dithering adds random noise to the image when converting to the selected palette, leaving a grained and messy look to your image.\n\n![White noise dithering with default settings. CGA Mode 5 | High palette](Documentation/Resources/WhiteNoiseApple2.png)\n\n**Token:** `.whiteNoise`  \n**Settings:** `WhiteNoiseSettingsConfiguration`\n\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| thresholdMapSize | Int | `7` | Specifies the size of the square threshold matrix. Default is 128x128. |\n| intensity | Float | `0.5` | Specifies the intensity of the noise pattern. |\n| performOnCPU | Bool | `false` | Determines wether to perform the computation on the CPU. If false, the GPU is used for quicker performance. |\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .whiteNoise,\n    andPalette: .apple2,\n    withDitherMethodSettings: WhiteNoiseSettingsConfiguration(),\n    withPaletteSettings: Apple2SettingsConfiguration(mode: .hiRes)\n)\n```\n\n### Noise\n\nYou can provide your own noise texture to sample when performing ordered dithering.\n\n![Bayer dithering with default settings. CGA Mode 5 | High palette](Documentation/Resources/BlueNoiseGameBoy.png)\n\n\u003e This image is dithered using a blue noise pattern — leaving a grained, organic look.\n\n**Token:** `.noise`  \n**Settings:** `NoiseDitheringSettingsConfiguration`\n\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| noisePattern | CGImage? | `nil` | Specifies the noise pattern to use for ordered dithering. |\n| intensity | Float | `0.5` | Specifies the intensity of the noise pattern. |\n| performOnCPU | Bool | `false` | Determines wether to perform the computation on the CPU. If false, the GPU is used for quicker performance. |\n\nExample: \n```swift\nlet noisePatternImage: CGImage = ...\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .noise,\n    andPalette: .gameBoy,\n    withDitherMethodSettings: NoiseDitheringSettingsConfiguration(noisePattern: noisePatternImage),\n    withPaletteSettings: EmptySettingsConfiguration()\n)\n```\n\n## Built-in palettes\n\nHere is an overview of the built-in palettes:\n\n### Black \u0026 White\n\nA palette with the two colors: black, and white.\n\n![Floyd-Steinberg dithering with the black and white palette](Documentation/Resources/FSBW.png)\n\n**Token:** `.bw`  \n**Settings:** `EmptyPaletteSettingsConfiguration`\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .floydSteinberg,\n    andPalette: .bw,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: EmptyPaletteSettingsConfiguration()\n)\n```\n\n### Grayscale\n\nA palette with all shades of gray.\n\n![Floyd-Steinberg dithering with the grayscale palette](Documentation/Resources/FSGray.png)\n\n**Token:** `.grayscale`  \n**Settings:** `QuantizedColorSettingsConfiguration`\n\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| bits | Int | 0 | Specifies the number of bits to quantize to. The number of bits can be between 0 and 8. The number of shades of gray is given by 2^n where n is the number of bits. |\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .floydSteinberg,\n    andPalette: .grayscale,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: EmptyPaletteSettingsConfiguration()\n)\n```\n\n### Quantized Color\n\nA palette with quantized bits for the color channel. Specify the number of bits to use for color—from 0 to 8. The number of colors is given by 2^n where n is the number of bits.\n\n![Floyd-Steinberg dithering with the quantized palette. Here with 2 bits](Documentation/Resources/FSQuant.png)\n\n**Token:** `.quantizedColor`  \n**Settings:** `QuantizedColorSettingsConfiguration`\n\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| bits | Int | 0 | Specifies the number of bits to quantize to. The number of bits can be between 0 and 8. The number of colors is given by 2^n where n is the number of bits. |\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .floydSteinberg,\n    andPalette: .quantizedColor,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: QuantizedColorSettingsConfiguration(bits: 2)\n)\n```\n\n### CGA\n\nA palette with the oldschool CGA palettes. CGA was a graphics card introduced in 1981 with the ability to display colour on the IBM PC. It used a 4 bit interface (Red, Green, Blue, Intensity) giving a total of 16 possible colors. Due to limited video memory however, the most common resolution of 320x200 would only allow you four colors on screen simultaneously. In this mode, d developer could choose from four palettes, with beautiful colour combinations such as black, cyan, magenta and white or black, green, red and yellow.\n\n![Floyd-Steinberg dithering with the CGA palette. Here in mode 4 with pallet 1 high](Documentation/Resources/FSCGA.png)\n\n**Token:** `.cga`  \n**Settings:** `CGASettingsConfiguration`\n\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| mode | CGAMode | `.palette1High` | Specifies the graphics mode to use. Each graphics mode has a unique set of colors. The one with the most colors is `.textMode`. |\n\n**`CGAMode`**:\n| Name | Colors | Image |\n|------|------|---------|\n| `.palette0Low` | Black, green, red, brown | \u003cimg alt=\"Palette 0 Low\" src=\"Documentation/Resources/CGAp0l.png\" height=\"200\" width=\"auto\"\u003e |\n| `.palette0High` | Black, light green, light red, yellow | \u003cimg alt=\"Palette 0 High\" src=\"Documentation/Resources/CGAp0h.png\" height=\"200\" width=\"auto\"\u003e |\n| `.palette1Low` | Black, cyan, magenta, light gray | \u003cimg alt=\"palette 1 Low\" src=\"Documentation/Resources/CGAp1l.png\" height=\"200\" width=\"auto\"\u003e |\n| `.palette1High` | Black, light cyan, light magenta, white | \u003cimg alt=\"Palette 1 High\" src=\"Documentation/Resources/CGAp1h.png\" height=\"200\" width=\"auto\"\u003e |\n| `.mode5Low` | Black, cyan, red, light gray | \u003cimg alt=\"Mode 5 Low\" src=\"Documentation/Resources/CGAm5l.png\" height=\"200\" width=\"auto\"\u003e |\n| `.mode5High` | Black, light cyan, light red, white | \u003cimg alt=\"Mode 5 High\" src=\"Documentation/Resources/CGAm5h.png\" height=\"200\" width=\"auto\"\u003e |\n| `.textMode` | All 16 colors | \u003cimg alt=\"Text Mode\" src=\"Documentation/Resources/CGAtm.png\" height=\"200\" width=\"auto\"\u003e |\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .floydSteinberg,\n    andPalette: .quantizedColor,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: CGASettingsConfiguration(mode: .palette1High)\n)\n```\n\n### Apple II\n\nThe Apple II was one of the first personal computers with color. Technical challenges related to reducing cost enabled two modes for graphics—a high resolution mode with six colors, and a low resolution mode with 16 colors.\n\n![Atkinson dithering with the Apple II palette. Here in HiRes graphics mode.](Documentation/Resources/AtkApple2HiRes.png)\n\n**Token:** `.apple2`  \n**Settings:** `Apple2SettingsConfiguration`\n\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| mode | Apple2Mode | `.hiRes` | Specifies the graphics mode to use. Each graphics mode has a unique set of colors. |\n\n**`Apple2Mode`**:\n| Name | Num. Colors | Image |\n|------|------|---------|\n| `.hiRes` | 6 colors | \u003cimg alt=\"Hi-Res\" src=\"Documentation/Resources/AtkApple2HiRes.png\" height=\"200\" width=\"auto\"\u003e |\n| `.loRes` | 16 colors | \u003cimg alt=\"Lo-Res\" src=\"Documentation/Resources/AtkApple2LoRes.png\" height=\"200\" width=\"auto\"\u003e |\n\n\u003e **Note:** The 16 colors of the Apple2 Lo-Res palette are different from CGA’s text mode palette.\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .atkinson,\n    andPalette: .apple2,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: Apple2SettingsConfiguration(mode: .hiRes)\n)\n```\n\n### Game Boy\n\nOldschool four color green-shaded monochrome display.\n\n![Atkinson dithering with the Game Boy palette.](Documentation/Resources/AtkGameBoy.png)\n\n**Token:** `.gameBoy`  \n**Settings:** `EmptyPaletteSettingsConfiguration`\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .atkinson,\n    andPalette: .gameBoy,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: EmptyPaletteSettingsConfiguration()\n)\n```\n\n### Intellivision\n\nThe Intellivision was a game console from the late 70’s. Its graphics was powered by the Standard Television Interface Chip, which came with a 16-color palette.\n\n![Atkinson dithering with the Game Boy palette.](Documentation/Resources/JJNIntellivision.png)\n\n**Token:** `.intellivision`  \n**Settings:** `EmptyPaletteSettingsConfiguration`\n\nExample: \n```swift\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\nlet cgImage = try ditheringEngine.dither(\n    usingMethod: .atkinson,\n    andPalette: .jarvisJudiceNinke,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: EmptyPaletteSettingsConfiguration()\n)\n```\n\n## Creating your own palette\n\nYou can create your own palettes using the appropriate APIs.\n\n![Floyd-Steinberg dithering with a custom palette](Documentation/Resources/DitheredImageCustomPalette.png)\n\nA palette is represented with the `BytePalette` structure, which can be constructed from a lookup-table (LUT), and a collection of colors (LUTCollection). The most useful is perhaps the LUTCollection.\n\nIf you have an array of UIColors contained in the palette, you first need to extract the color values into a list of `SIMD3\u003cUInt8\u003e`s. This can be done as follows:\n\n```swift\nlet entries = colors.map { color in\n    var redNormalized: CGFloat = 0\n    var greenNormalized: CGFloat = 0\n    var blueNormalized: CGFloat = 0\n\n    color.getRed(\u0026redNormalized, green: \u0026greenNormalized, blue: \u0026blueNormalized, alpha: nil)\n\n    let red = UInt8(clamp(redDouble * 255, min: 0, max: 255))\n    let green = UInt8(clamp(greenDouble * 255, min: 0, max: 255))\n    let blue = UInt8(clamp(blueDouble * 255, min: 0, max: 255))\n\n    return SIMD3(x: red, y: green, z: blue)\n}\n```\n\nAfter this, you can make a `LUTCollection` and from it a palette:\n\n```swift\nlet collection = LUTCollection\u003cUInt8\u003e(entries: entries)\nlet palette = BytePalette.from(lutCollection: collection)\n```\n\nWhen dithering an image, choose the `.custom` palette and provide your palette in the `CustomPaletteSettingsConfiguration`:\n\n```swift\ntry ditheringEngine.dither(\n    usingMethod: .floydSteinberg,\n    andPalette: .custom,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: CustomPaletteSettingsConfiguration(palette: palette)\n)\n```\n\nFull example: \n```swift\nlet entries = colors.map { color in\n    var redNormalized: CGFloat = 0\n    var greenNormalized: CGFloat = 0\n    var blueNormalized: CGFloat = 0\n\n    color.getRed(\u0026redNormalized, green: \u0026greenNormalized, blue: \u0026blueNormalized, alpha: nil)\n\n    let red = UInt8(clamp(redDouble * 255, min: 0, max: 255))\n    let green = UInt8(clamp(greenDouble * 255, min: 0, max: 255))\n    let blue = UInt8(clamp(blueDouble * 255, min: 0, max: 255))\n\n    return SIMD3(x: red, y: green, z: blue)\n}\nlet collection = LUTCollection\u003cUInt8\u003e(entries: entries)\nlet palette = BytePalette.from(lutCollection: collection)\n\nlet ditheringEngine = DitheringEngine()\ntry ditheringEngine.set(image: inputCGImage)\ntry ditheringEngine.dither(\n    usingMethod: .floydSteinberg,\n    andPalette: .custom,\n    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),\n    withPaletteSettings: CustomPaletteSettingsConfiguration(palette: palette)\n)\n```\n\n## Video Dithering Engine\n\nIn addition to `DitheringEngine` dithering images, `VideoDitheringEngine` exists to dither videos. The VideoDitheringEngine works by applying a palette and dither method to every frame in the video. You may also choose to resize the video as part of this process.\n\n![VideoDitheringEngine works by applying a palette and dither method to every frame in the video](Documentation/Resources/VideoDitheringEngineFigure.png#gh-light-mode-only)\n\n![VideoDitheringEngine works by applying a palette and dither method to every frame in the video](Documentation/Resources/VideoDitheringEngineFigureDark.png#gh-dark-mode-only)\n\nExample usage: \n```swift\n// Create an instance of VideoDitheringEngine\nlet videoDitheringEngine = VideoDitheringEngine()\n// Create a video description\nlet videoDescription = VideoDescription(url: inputVideoURL)\n// Set preferred output size.\nvideoDescription.renderSize = CGSize(width: 320, height: 568)\n// Dither to quantized color with 5 bits using Floyd-Steinberg.\nvideoDitheringEngine.dither(\n    videoDescription: videoDescription,  \n    usingMethod: .floydSteinberg, \n    andPalette: .quantizedColor,\n    withDitherMethodSettings: FloydSteinbergSettingsConfiguration(direction: .leftToRight), \n    andPaletteSettings: QuantizedColorSettingsConfiguration(bits: 5), \n    outputURL: outputURL, \n    progressHandler: progressHandler,\n    completionHandler: completionHandler\n)\n```\n\n### Ordered dithering is more suitable\n\nUsing an ordered dither method is faster, and will give the best result as the pattern will not “move” (like static noise).\n\n### Video framerate\n\nBy default, the final video has a framerate of 30. You may adjust the final framerate by providing a frame rate when initializing VideoDitheringEngine. The final frame rate is less than or equal to the specified value.:\n```swift\nVideoDitheringEngine(frameRate: Int)\n```\n\n### Concurrent frame processing\n\nBy default, video frames are rendered concurrently. You can disable this behaviour, or change the number of frames processed simultaneously using the `numberOfConcurrentFrames` property. \n\nSetting this to 1 will effectively disable concurrent frame processing. A higher number will be faster if the CPU has enough cores to handle the load, but will also use more memory.\n\n### Video Dither Options\n\nWhen dithering a video, you may provide options for how the video should be processed. The following options are available:\n\n- `precalculateDitheredColorForAllColors`: Makes an indexed map of all colors to dithered color. This adds an increased wait time in the begining. Might be faster with large LUTCollections (e.g. CGA) and longer videos. Is ignored with LUT (e.g. Quantized Color) which is already index based.\n\n- `removeAudio`: Does not transfer audio from the original video.\n\n### Video Description\n\nYou set the video you want to use as input through the `VideoDescription` type. This is a convenient wrapper around `AVAsset` and lets you set the preferred output size.\n\n![A VideoDescription can be made from an AVAsset, and is what you pass to VideoDitheringEngine in order to dither a video.](Documentation/Resources/VideoDitheringEngineProcedureMap.png#gh-light-mode-only)\n![A VideoDescription can be made from an AVAsset, and is what you pass to VideoDitheringEngine in order to dither a video.](Documentation/Resources/VideoDitheringEngineProcedureMapDark.png#gh-dark-mode-only)\n\n**Properites**\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| renderSize | CGSize? { get set } | nil | Specifies the size for which to render the final dithered video. |\n| framerate | Float? { get } | nominalFrameRate | Returns the number of frames per second. Nil if the asset does not contain video. |\n| transform| CGAffineTransform? { get } | preferredTransform | The transfor (orientation, scale) of the video.\n| duration | TimeInterval { get } | duration.seconds | Returns the duration of the video. |\n| sampleRate | Int? { get } | naturalTimeScale | Returns the number of audio samples per second. Nil if the asset does not contain audio. |\n| size | CGSize? { get } | naturalSize | Returns the size of the video. Nil if the asset does not contain video. |\n\n**Methods**\n\n```swift\n/// Reads the first frame in the video as an image.\nfunc getPreviewImage() async throws -\u003e CGImage\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feskils%2Fditheringengine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feskils%2Fditheringengine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feskils%2Fditheringengine/lists"}