/* oh4_halftone.h - v1.1 - public domain Authored 2026 by Eric Scrivner no warranty implied; use at your own risk Before including, #define OH4_HALFTONE_IMPLEMENTATION in the file that you want to have the implementation. ABOUT: Converts continuous-tone images to 1-bit (bilevel) output via grayscale conversion, tonal adjustment, sharpening, edge detection, and dithering. Output is packed MSB-first, 8 pixels per byte (Playdate bitmap format). Use HT_BilevelSize(w,h) for the required output buffer size. Based on potch's playdither tool (https://potch.me/demos/playdither/). PIPELINE: All processing uses float, avoiding quantization loss between steps. HT_GrayscaleFromRGBAF32(out, rgba, w, h, method) HT_AdjustF32(buf, w, h, gamma, gain, contrast, lift) HT_SharpenF32(buf, w, h, scratch, amount, radius) HT_EdgeDetectF32(edge_out, grayscale, w, h, method) HT_EdgeCompositeF32(buf, edge, w, h, strength) HT_DitherBiased(out, mask, work, src_rgba, w, h, method, params) HT_DitherBiased combines an ordered dither matrix with error diffusion in a single pass. See HT_Dither_Biased_Params for configuration. Pass kHT_Method_None to disable diffusion, or NULL matrix for to disable ordered dither. ORDERED DITHER PATTERNS: Bayer HT_BayerDefault2/4/8/16/32 Blue noise HT_BlueNoiseGenerate64 Halftone HT_HalftoneDefault8 Diagonal HT_ClusteredDotDiagonal8 HLine HT_HorizontalLine6 VLine HT_VerticalLine6 White noise HT_WhiteNoiseGenerate64 IGN HT_IGNGenerate64 ERROR DIFFUSION METHODS (eHT_Method): Simple 1D forward (1/1) Box equal-weight 2x2 Atkinson partial diffusion (6/8) Burkes two-row, wide spread Floyd-Steinberg classic two-row (16ths) Jarvis-Judice-Ninke three-row, wide spread Pigeon minimal forward-only Sierra Lite simplified Sierra (4ths) Sierra 2 two-row Sierra Stucki three-row, fine detail SCRATCH BUFFERS: HT_SharpenF32, HT_EdgeDetectF32, and HT_DitherBiased each require a w*h float scratch buffer. HT_BlueNoiseGenerate64 requires 12288 floats (~48KB). OVERRIDES: #define HT_Pow(x, e) your_powf(x, e) #define HT_Exp(x) your_expf(x) EXAMPLE: #define W 400 #define H 240 float gray[W*H]; float scratch[W*H]; unsigned char bilevel[HT_BilevelSize(W,H)]; unsigned char mask[HT_BilevelSize(W,H)]; memset(bilevel, 0, sizeof(bilevel)); memset(mask, 0, sizeof(mask)); HT_GrayscaleFromRGBAF32(gray, rgba, W, H, kHT_GrayMethod_BT601); HT_AdjustF32(gray, W, H, 1.1f, 1.0f, 1.2f, 0.0f); HT_SharpenF32(gray, W, H, scratch, 0.5f, 1); unsigned char blue_noise[4096]; float bn_scratch[12288]; HT_BlueNoiseGenerate64(blue_noise, bn_scratch, my_rand); HT_Dither_Biased_Params params = {0}; params.OrderedMatrix = blue_noise; params.OrderedN = 64; params.OrderedMax = 255.0f; params.OrderedScale = 0.3f; params.Threshold = 128.0f; params.Serpentine = 1; params.DiffusionStrength = 1.0f; HT_DitherBiased(bilevel, mask, gray, rgba, W, H, kHT_Method_FloydSteinberg, ¶ms); */ #ifndef OH4_HALFTONE_H #define OH4_HALFTONE_H #ifndef HT_DEF #ifdef OH4_HALFTONE_STATIC #define HT_DEF static #else #ifdef __cplusplus #define HT_DEF extern "C" #else #define HT_DEF extern #endif #endif #endif /* === Constants === */ #define kHT_Grayscale_BT601_R 0.299f #define kHT_Grayscale_BT601_G 0.587f #define kHT_Grayscale_BT601_B 0.114f #define kHT_Grayscale_BT709_R 0.2126f #define kHT_Grayscale_BT709_G 0.7152f #define kHT_Grayscale_BT709_B 0.0722f #define kHT_Grayscale_Avg_R 0.3333f #define kHT_Grayscale_Avg_G 0.3334f #define kHT_Grayscale_Avg_B 0.3333f /* === Enums === */ typedef unsigned char eHT_GrayMethod; enum { kHT_GrayMethod_BT601, kHT_GrayMethod_BT709, kHT_GrayMethod_Average, kHT_GrayMethod_COUNT, }; typedef unsigned char eHT_EdgeMethod; enum { kHT_EdgeMethod_Sobel, kHT_EdgeMethod_Scharr, kHT_EdgeMethod_Laplacian, kHT_EdgeMethod_COUNT, }; typedef unsigned char eHT_OrderedMethod; enum { kHT_OrderedMethod_None, kHT_OrderedMethod_Bayer2, kHT_OrderedMethod_Bayer4, kHT_OrderedMethod_Bayer8, kHT_OrderedMethod_Bayer16, kHT_OrderedMethod_Bayer32, kHT_OrderedMethod_BlueNoise, kHT_OrderedMethod_Halftone, kHT_OrderedMethod_Diagonal, kHT_OrderedMethod_HLine, kHT_OrderedMethod_VLine, kHT_OrderedMethod_WhiteNoise, kHT_OrderedMethod_IGN, kHT_OrderedMethod_COUNT }; typedef unsigned char eHT_Method; enum { kHT_Method_None, kHT_Method_Simple, kHT_Method_Box, kHT_Method_Atkinson, kHT_Method_Burkes, kHT_Method_FloydSteinberg, kHT_Method_JarvisJudiceNinke, kHT_Method_Pigeon, kHT_Method_SierraLite, kHT_Method_Sierra2, kHT_Method_Stucki, kHT_Method_COUNT, }; /* === Structs === */ typedef struct HT_Dither_Biased_Params HT_Dither_Biased_Params; struct HT_Dither_Biased_Params { /* Ordered dither bias */ unsigned char* OrderedMatrix; unsigned short* OrderedMatrixU16; int OrderedN; float OrderedMax; float OrderedScale; /* Threshold and scanning */ float Threshold; int Serpentine; /* Error diffusion strength (0.0-1.0). Scales how much quantization error is distributed to neighbors. Values < 1.0 reduce "worm" artifacts. */ float DiffusionStrength; }; /* === Functions === */ HT_DEF int HT_BilevelSize(int w, int h); /* Grayscale conversion */ HT_DEF void HT_GrayscaleFromRGBAWeightedF32(float* out, unsigned char* rgba, int w, int h, float r, float g, float b); HT_DEF void HT_GrayscaleFromRGBAF32(float* out, unsigned char* rgba, int w, int h, eHT_GrayMethod method); /* Tonal adjustment */ HT_DEF void HT_AdjustF32(float* buf, int w, int h, float gamma, float gain, float contrast, float lift); /* Sharpening */ HT_DEF void HT_SharpenF32(float* buf, int w, int h, float* scratch, float amount, int radius); /* Edge detection */ HT_DEF void HT_EdgeSobelF32(float* edge_out, float* grayscale, int w, int h); HT_DEF void HT_EdgeScharrF32(float* edge_out, float* grayscale, int w, int h); HT_DEF void HT_EdgeLaplacianF32(float* edge_out, float* grayscale, int w, int h); HT_DEF void HT_EdgeCompositeF32(float* buf, float* edge, int w, int h, float strength); HT_DEF void HT_EdgeDetectF32(float* edge_out, float* grayscale, int w, int h, eHT_EdgeMethod method); /* Ordered dither matrices */ HT_DEF unsigned char* HT_BayerDefault2(void); HT_DEF unsigned char* HT_BayerDefault4(void); HT_DEF unsigned char* HT_BayerDefault8(void); HT_DEF unsigned char* HT_BayerDefault16(void); HT_DEF unsigned short* HT_BayerDefault32(void); HT_DEF unsigned char* HT_HalftoneDefault8(void); HT_DEF unsigned char* HT_ClusteredDotDiagonal8(void); HT_DEF unsigned char* HT_HorizontalLine6(void); HT_DEF unsigned char* HT_VerticalLine6(void); HT_DEF void HT_WhiteNoiseGenerate64(unsigned char out[4096], int (*rand_fn)(void)); HT_DEF void HT_IGNGenerate64(unsigned char out[4096]); HT_DEF void HT_BlueNoiseGenerate64(unsigned char out[4096], float scratch[12288], int (*rand_fn)(void)); HT_DEF void HT_WhiteNoiseSetDefault(unsigned char matrix[4096]); HT_DEF void HT_BlueNoiseSetDefault(unsigned char matrix[4096]); HT_DEF void HT_IGNSetDefault(unsigned char matrix[4096]); HT_DEF void* HT_OrderedMatrixFromMethod(eHT_OrderedMethod method); /* Dithering */ HT_DEF void HT_DitherBiased(unsigned char* out, unsigned char* mask, float* work, unsigned char* src_rgba, int w, int h, eHT_Method diffusion_method, HT_Dither_Biased_Params* params); #endif /* OH4_HALFTONE_H */ #ifdef OH4_HALFTONE_IMPLEMENTATION #if !defined(HT_Pow) || !defined(HT_Exp) #include #ifndef HT_Pow #define HT_Pow(x, e) powf(x, e) #endif #ifndef HT_Exp #define HT_Exp(x) expf(x) #endif #endif static float ht__clamp(float v, float lo, float hi) { float result = v; if (result < lo) { result = lo; } else if (result > hi) { result = hi; } return( result ); } HT_DEF int HT_BilevelSize(int w, int h) { return( ((w * h) + 7) / 8 ); } /* ========================================================================== Grayscale Conversion ========================================================================== */ HT_DEF void HT_GrayscaleFromRGBAWeightedF32(float* out, unsigned char* rgba, int w, int h, float r, float g, float b) { int count = w * h; int i; for (i = 0; i < count; i++) { out[i] = (float)rgba[i * 4 + 0] * r + (float)rgba[i * 4 + 1] * g + (float)rgba[i * 4 + 2] * b; } } HT_DEF void HT_GrayscaleFromRGBAF32(float* out, unsigned char* rgba, int w, int h, eHT_GrayMethod method) { switch (method) { case kHT_GrayMethod_BT601: { HT_GrayscaleFromRGBAWeightedF32(out, rgba, w, h, kHT_Grayscale_BT601_R, kHT_Grayscale_BT601_G, kHT_Grayscale_BT601_B); } break; case kHT_GrayMethod_BT709: { HT_GrayscaleFromRGBAWeightedF32(out, rgba, w, h, kHT_Grayscale_BT709_R, kHT_Grayscale_BT709_G, kHT_Grayscale_BT709_B); } break; case kHT_GrayMethod_Average: { HT_GrayscaleFromRGBAWeightedF32(out, rgba, w, h, kHT_Grayscale_Avg_R, kHT_Grayscale_Avg_G, kHT_Grayscale_Avg_B); } break; default: { HT_GrayscaleFromRGBAWeightedF32(out, rgba, w, h, kHT_Grayscale_BT601_R, kHT_Grayscale_BT601_G, kHT_Grayscale_BT601_B); } break; } } /* ========================================================================== HT_Adjust ========================================================================== */ HT_DEF void HT_AdjustF32(float* buf, int w, int h, float gamma, float gain, float contrast, float lift) { int count = w * h; int i; for (i = 0; i < count; i++) { float v = buf[i]; v = HT_Pow(v / 255.0f, gamma) * 255.0f; v = v * gain; v = (v - 128.0f) * contrast + 128.0f + lift * 255.0f; buf[i] = v; } } /* ========================================================================== Edge Detection (F32) ========================================================================== */ static void ht__edge_gradient_f32(float* edge_out, float* grayscale, int w, int h, int w0, int w1, int w2, float divisor) { int x, y; for (x = 0; x < w; x++) { edge_out[x] = 0.0f; edge_out[(h - 1) * w + x] = 0.0f; } for (y = 0; y < h; y++) { edge_out[y * w] = 0.0f; edge_out[y * w + (w - 1)] = 0.0f; } for (y = 1; y < h - 1; y++) { for (x = 1; x < w - 1; x++) { float tl = grayscale[(y - 1) * w + (x - 1)]; float tc = grayscale[(y - 1) * w + x]; float tr = grayscale[(y - 1) * w + (x + 1)]; float ml = grayscale[y * w + (x - 1)]; float mr = grayscale[y * w + (x + 1)]; float bl = grayscale[(y + 1) * w + (x - 1)]; float bc = grayscale[(y + 1) * w + x]; float br = grayscale[(y + 1) * w + (x + 1)]; float gx = (float)(-w0) * tl + (float)w0 * tr + (float)(-w1) * ml + (float)w1 * mr + (float)(-w2) * bl + (float)w2 * br; float gy = (float)(-w0) * tl + (float)(-w1) * tc + (float)(-w2) * tr + (float)w0 * bl + (float)w1 * bc + (float)w2 * br; float mag = (gx < 0.0f ? -gx : gx) + (gy < 0.0f ? -gy : gy); mag = mag / divisor; if (mag > 255.0f) { mag = 255.0f; } edge_out[y * w + x] = mag; } } } HT_DEF void HT_EdgeSobelF32(float* edge_out, float* grayscale, int w, int h) { ht__edge_gradient_f32(edge_out, grayscale, w, h, 1, 2, 1, 2.0f); } HT_DEF void HT_EdgeScharrF32(float* edge_out, float* grayscale, int w, int h) { ht__edge_gradient_f32(edge_out, grayscale, w, h, 3, 10, 3, 16.0f); } HT_DEF void HT_EdgeLaplacianF32(float* edge_out, float* grayscale, int w, int h) { int x, y; for (x = 0; x < w; x++) { edge_out[x] = 0.0f; edge_out[(h - 1) * w + x] = 0.0f; } for (y = 0; y < h; y++) { edge_out[y * w] = 0.0f; edge_out[y * w + (w - 1)] = 0.0f; } for (y = 1; y < h - 1; y++) { for (x = 1; x < w - 1; x++) { float sum = grayscale[(y - 1) * w + (x - 1)] + grayscale[(y - 1) * w + x] + grayscale[(y - 1) * w + (x + 1)] + grayscale[y * w + (x - 1)] - 8.0f * grayscale[y * w + x] + grayscale[y * w + (x + 1)] + grayscale[(y + 1) * w + (x - 1)] + grayscale[(y + 1) * w + x] + grayscale[(y + 1) * w + (x + 1)]; if (sum < 0.0f) { sum = -sum; } if (sum > 255.0f) { sum = 255.0f; } edge_out[y * w + x] = sum; } } } HT_DEF void HT_EdgeCompositeF32(float* buf, float* edge, int w, int h, float strength) { int count = w * h; int i; for (i = 0; i < count; i++) { float v = buf[i] - edge[i] * strength; buf[i] = ht__clamp(v, 0.0f, 255.0f); } } HT_DEF void HT_EdgeDetectF32(float* edge_out, float* grayscale, int w, int h, eHT_EdgeMethod method) { switch (method) { case kHT_EdgeMethod_Sobel: { HT_EdgeSobelF32(edge_out, grayscale, w, h); } break; case kHT_EdgeMethod_Scharr: { HT_EdgeScharrF32(edge_out, grayscale, w, h); } break; case kHT_EdgeMethod_Laplacian: { HT_EdgeLaplacianF32(edge_out, grayscale, w, h); } break; default: break; } } /* ========================================================================== Bayer Matrices ========================================================================== */ static unsigned char ht__bayer2[4] = { 0, 2, 3, 1, }; static unsigned char ht__bayer4[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5, }; static unsigned char ht__bayer8[64] = { 0, 32, 8, 40, 2, 34, 10, 42, 48, 16, 56, 24, 50, 18, 58, 26, 12, 44, 4, 36, 14, 46, 6, 38, 60, 28, 52, 20, 62, 30, 54, 22, 3, 35, 11, 43, 1, 33, 9, 41, 51, 19, 59, 27, 49, 17, 57, 25, 15, 47, 7, 39, 13, 45, 5, 37, 63, 31, 55, 23, 61, 29, 53, 21, }; static unsigned char ht__bayer16[256] = { 0, 128, 32, 160, 8, 136, 40, 168, 2, 130, 34, 162, 10, 138, 42, 170, 192, 64, 224, 96, 200, 72, 232, 104, 194, 66, 226, 98, 202, 74, 234, 106, 48, 176, 16, 144, 56, 184, 24, 152, 50, 178, 18, 146, 58, 186, 26, 154, 240, 112, 208, 80, 248, 120, 216, 88, 242, 114, 210, 82, 250, 122, 218, 90, 12, 140, 44, 172, 4, 132, 36, 164, 14, 142, 46, 174, 6, 134, 38, 166, 204, 76, 236, 108, 196, 68, 228, 100, 206, 78, 238, 110, 198, 70, 230, 102, 60, 188, 28, 156, 52, 180, 20, 148, 62, 190, 30, 158, 54, 182, 22, 150, 252, 124, 220, 92, 244, 116, 212, 84, 254, 126, 222, 94, 246, 118, 214, 86, 3, 131, 35, 163, 11, 139, 43, 171, 1, 129, 33, 161, 9, 137, 41, 169, 195, 67, 227, 99, 203, 75, 235, 107, 193, 65, 225, 97, 201, 73, 233, 105, 51, 179, 19, 147, 59, 187, 27, 155, 49, 177, 17, 145, 57, 185, 25, 153, 243, 115, 211, 83, 251, 123, 219, 91, 241, 113, 209, 81, 249, 121, 217, 89, 15, 143, 47, 175, 7, 135, 39, 167, 13, 141, 45, 173, 5, 133, 37, 165, 207, 79, 239, 111, 199, 71, 231, 103, 205, 77, 237, 109, 197, 69, 229, 101, 63, 191, 31, 159, 55, 183, 23, 151, 61, 189, 29, 157, 53, 181, 21, 149, 255, 127, 223, 95, 247, 119, 215, 87, 253, 125, 221, 93, 245, 117, 213, 85, }; static unsigned short ht__bayer32[1024] = { 0, 512, 128, 640, 32, 544, 160, 672, 8, 520, 136, 648, 40, 552, 168, 680, 2, 514, 130, 642, 34, 546, 162, 674, 10, 522, 138, 650, 42, 554, 170, 682, 768, 256, 896, 384, 800, 288, 928, 416, 776, 264, 904, 392, 808, 296, 936, 424, 770, 258, 898, 386, 802, 290, 930, 418, 778, 266, 906, 394, 810, 298, 938, 426, 192, 704, 64, 576, 224, 736, 96, 608, 200, 712, 72, 584, 232, 744, 104, 616, 194, 706, 66, 578, 226, 738, 98, 610, 202, 714, 74, 586, 234, 746, 106, 618, 960, 448, 832, 320, 992, 480, 864, 352, 968, 456, 840, 328, 1000, 488, 872, 360, 962, 450, 834, 322, 994, 482, 866, 354, 970, 458, 842, 330, 1002, 490, 874, 362, 48, 560, 176, 688, 16, 528, 144, 656, 56, 568, 184, 696, 24, 536, 152, 664, 50, 562, 178, 690, 18, 530, 146, 658, 58, 570, 186, 698, 26, 538, 154, 666, 816, 304, 944, 432, 784, 272, 912, 400, 824, 312, 952, 440, 792, 280, 920, 408, 818, 306, 946, 434, 786, 274, 914, 402, 826, 314, 954, 442, 794, 282, 922, 410, 240, 752, 112, 624, 208, 720, 80, 592, 248, 760, 120, 632, 216, 728, 88, 600, 242, 754, 114, 626, 210, 722, 82, 594, 250, 762, 122, 634, 218, 730, 90, 602, 1008, 496, 880, 368, 976, 464, 848, 336, 1016, 504, 888, 376, 984, 472, 856, 344, 1010, 498, 882, 370, 978, 466, 850, 338, 1018, 506, 890, 378, 986, 474, 858, 346, 12, 524, 140, 652, 44, 556, 172, 684, 4, 516, 132, 644, 36, 548, 164, 676, 14, 526, 142, 654, 46, 558, 174, 686, 6, 518, 134, 646, 38, 550, 166, 678, 780, 268, 908, 396, 812, 300, 940, 428, 772, 260, 900, 388, 804, 292, 932, 420, 782, 270, 910, 398, 814, 302, 942, 430, 774, 262, 902, 390, 806, 294, 934, 422, 204, 716, 76, 588, 236, 748, 108, 620, 196, 708, 68, 580, 228, 740, 100, 612, 206, 718, 78, 590, 238, 750, 110, 622, 198, 710, 70, 582, 230, 742, 102, 614, 972, 460, 844, 332, 1004, 492, 876, 364, 964, 452, 836, 324, 996, 484, 868, 356, 974, 462, 846, 334, 1006, 494, 878, 366, 966, 454, 838, 326, 998, 486, 870, 358, 60, 572, 188, 700, 28, 540, 156, 668, 52, 564, 180, 692, 20, 532, 148, 660, 62, 574, 190, 702, 30, 542, 158, 670, 54, 566, 182, 694, 22, 534, 150, 662, 828, 316, 956, 444, 796, 284, 924, 412, 820, 308, 948, 436, 788, 276, 916, 404, 830, 318, 958, 446, 798, 286, 926, 414, 822, 310, 950, 438, 790, 278, 918, 406, 252, 764, 124, 636, 220, 732, 92, 604, 244, 756, 116, 628, 212, 724, 84, 596, 254, 766, 126, 638, 222, 734, 94, 606, 246, 758, 118, 630, 214, 726, 86, 598, 1020, 508, 892, 380, 988, 476, 860, 348, 1012, 500, 884, 372, 980, 468, 852, 340, 1022, 510, 894, 382, 990, 478, 862, 350, 1014, 502, 886, 374, 982, 470, 854, 342, 3, 515, 131, 643, 35, 547, 163, 675, 11, 523, 139, 651, 43, 555, 171, 683, 1, 513, 129, 641, 33, 545, 161, 673, 9, 521, 137, 649, 41, 553, 169, 681, 771, 259, 899, 387, 803, 291, 931, 419, 779, 267, 907, 395, 811, 299, 939, 427, 769, 257, 897, 385, 801, 289, 929, 417, 777, 265, 905, 393, 809, 297, 937, 425, 195, 707, 67, 579, 227, 739, 99, 611, 203, 715, 75, 587, 235, 747, 107, 619, 193, 705, 65, 577, 225, 737, 97, 609, 201, 713, 73, 585, 233, 745, 105, 617, 963, 451, 835, 323, 995, 483, 867, 355, 971, 459, 843, 331, 1003, 491, 875, 363, 961, 449, 833, 321, 993, 481, 865, 353, 969, 457, 841, 329, 1001, 489, 873, 361, 51, 563, 179, 691, 19, 531, 147, 659, 59, 571, 187, 699, 27, 539, 155, 667, 49, 561, 177, 689, 17, 529, 145, 657, 57, 569, 185, 697, 25, 537, 153, 665, 819, 307, 947, 435, 787, 275, 915, 403, 827, 315, 955, 443, 795, 283, 923, 411, 817, 305, 945, 433, 785, 273, 913, 401, 825, 313, 953, 441, 793, 281, 921, 409, 243, 755, 115, 627, 211, 723, 83, 595, 251, 763, 123, 635, 219, 731, 91, 603, 241, 753, 113, 625, 209, 721, 81, 593, 249, 761, 121, 633, 217, 729, 89, 601, 1011, 499, 883, 371, 979, 467, 851, 339, 1019, 507, 891, 379, 987, 475, 859, 347, 1009, 497, 881, 369, 977, 465, 849, 337, 1017, 505, 889, 377, 985, 473, 857, 345, 15, 527, 143, 655, 47, 559, 175, 687, 7, 519, 135, 647, 39, 551, 167, 679, 13, 525, 141, 653, 45, 557, 173, 685, 5, 517, 133, 645, 37, 549, 165, 677, 783, 271, 911, 399, 815, 303, 943, 431, 775, 263, 903, 391, 807, 295, 935, 423, 781, 269, 909, 397, 813, 301, 941, 429, 773, 261, 901, 389, 805, 293, 933, 421, 207, 719, 79, 591, 239, 751, 111, 623, 199, 711, 71, 583, 231, 743, 103, 615, 205, 717, 77, 589, 237, 749, 109, 621, 197, 709, 69, 581, 229, 741, 101, 613, 975, 463, 847, 335, 1007, 495, 879, 367, 967, 455, 839, 327, 999, 487, 871, 359, 973, 461, 845, 333, 1005, 493, 877, 365, 965, 453, 837, 325, 997, 485, 869, 357, 63, 575, 191, 703, 31, 543, 159, 671, 55, 567, 183, 695, 23, 535, 151, 663, 61, 573, 189, 701, 29, 541, 157, 669, 53, 565, 181, 693, 21, 533, 149, 661, 831, 319, 959, 447, 799, 287, 927, 415, 823, 311, 951, 439, 791, 279, 919, 407, 829, 317, 957, 445, 797, 285, 925, 413, 821, 309, 949, 437, 789, 277, 917, 405, 255, 767, 127, 639, 223, 735, 95, 607, 247, 759, 119, 631, 215, 727, 87, 599, 253, 765, 125, 637, 221, 733, 93, 605, 245, 757, 117, 629, 213, 725, 85, 597, 1023, 511, 895, 383, 991, 479, 863, 351, 1015, 503, 887, 375, 983, 471, 855, 343, 1021, 509, 893, 381, 989, 477, 861, 349, 1013, 501, 885, 373, 981, 469, 853, 341, }; HT_DEF unsigned char* HT_BayerDefault2(void) { return( ht__bayer2 ); } HT_DEF unsigned char* HT_BayerDefault4(void) { return( ht__bayer4 ); } HT_DEF unsigned char* HT_BayerDefault8(void) { return( ht__bayer8 ); } HT_DEF unsigned char* HT_BayerDefault16(void) { return( ht__bayer16 ); } HT_DEF unsigned short* HT_BayerDefault32(void) { return( ht__bayer32 ); } HT_DEF void HT_WhiteNoiseGenerate64(unsigned char out[4096], int (*rand_fn)(void)) { int i; for (i = 0; i < 4096; i++) { out[i] = (unsigned char)(rand_fn() % 256); } } HT_DEF void HT_IGNGenerate64(unsigned char out[4096]) { int x, y; for (y = 0; y < 64; y++) { for (x = 0; x < 64; x++) { double dot = 0.06711056 * (double)x + 0.00583715 * (double)y; double inner = dot - (int)dot; /* fract */ double outer = 52.9829189 * inner; double frac = outer - (int)outer; /* fract */ int val; if (frac < 0.0) { frac += 1.0; } val = (int)(frac * 255.0 + 0.5); if (val > 255) { val = 255; } out[y * 64 + x] = (unsigned char)val; } } } /* ========================================================================== Blue Noise Generation (void-and-cluster algorithm) Incremental energy updates based on approach by Martin Fiedler (kajott). ========================================================================== */ #define HT__BN_GET(bmp, i) (((bmp)[(i) >> 5] >> ((i) & 31)) & 1u) #define HT__BN_SET(bmp, i) do { (bmp)[(i) >> 5] |= (1u << ((i) & 31)); } while (0) #define HT__BN_CLR(bmp, i) do { (bmp)[(i) >> 5] &= ~(1u << ((i) & 31)); } while (0) static void ht__bn_update(float* energy, unsigned int* bitmap, int index, float sign, float lut[17][17], int* out_imin, int* out_imax) { int px = index & 63, py = index >> 6; float emin = 1e30f, emax = -1e30f; int imin = 0, imax = 0; int i; for (i = 0; i < 4096; i++) { int x = i & 63, y = i >> 6; int dx = x - px, dy = y - py; if (dx > 31) { dx -= 64; } else if (dx < -32) { dx += 64; } if (dy > 31) { dy -= 64; } else if (dy < -32) { dy += 64; } if (dx >= -8 && dx <= 8 && dy >= -8 && dy <= 8) { energy[i] += lut[dy + 8][dx + 8] * sign; } if (HT__BN_GET(bitmap, i)) { if (energy[i] > emax) { emax = energy[i]; imax = i; } } else { if (energy[i] < emin) { emin = energy[i]; imin = i; } } } *out_imin = imin; *out_imax = imax; } HT_DEF void HT_BlueNoiseGenerate64(unsigned char out[4096], float scratch[12288], int (*rand_fn)(void)) { float* energy = scratch; float* saved_energy = scratch + 4096; unsigned int bitmap[128]; unsigned int saved_bitmap[128]; float lut[17][17]; int imin = 0, imax = 0, points = 0, i; float sigma = 1.5f; float inv_2sigma2 = 1.0f / (2.0f * sigma * sigma); /* Build Gaussian LUT */ { int dx, dy; for (dy = -8; dy <= 8; dy++) { for (dx = -8; dx <= 8; dx++) { lut[dy + 8][dx + 8] = HT_Exp(-(float)(dx * dx + dy * dy) * inv_2sigma2); } } } /* Zero state */ for (i = 0; i < 4096; i++) { energy[i] = 0.0f; out[i] = 0; } for (i = 0; i < 128; i++) { bitmap[i] = 0; } /* Seed ~10% random points */ { int initial_count = 4096 / 10; while (points < initial_count) { int idx = rand_fn() & 4095; if (!HT__BN_GET(bitmap, idx)) { HT__BN_SET(bitmap, idx); ht__bn_update(energy, bitmap, idx, +1.0f, lut, &imin, &imax); points++; } } } /* Tighten: redistribute until convergence */ { int last = -1; while (imax != last) { HT__BN_CLR(bitmap, imax); ht__bn_update(energy, bitmap, imax, -1.0f, lut, &imin, &imax); last = imin; HT__BN_SET(bitmap, imin); ht__bn_update(energy, bitmap, imin, +1.0f, lut, &imin, &imax); } } /* Save balanced state */ for (i = 0; i < 4096; i++) { saved_energy[i] = energy[i]; } for (i = 0; i < 128; i++) { saved_bitmap[i] = bitmap[i]; } /* Phase 1: remove tightest clusters, assign decreasing ranks */ { int rank = points - 1; while (rank >= 0) { out[imax] = (unsigned char)((rank * 255) / 4095); HT__BN_CLR(bitmap, imax); ht__bn_update(energy, bitmap, imax, -1.0f, lut, &imin, &imax); rank--; } } /* Restore balanced state */ for (i = 0; i < 4096; i++) { energy[i] = saved_energy[i]; } for (i = 0; i < 128; i++) { bitmap[i] = saved_bitmap[i]; } /* Phase 2: fill voids, assign increasing ranks up to half */ { int rank = points; while (rank < 2048) { out[imin] = (unsigned char)((rank * 255) / 4095); HT__BN_SET(bitmap, imin); ht__bn_update(energy, bitmap, imin, +1.0f, lut, &imin, &imax); rank++; } points = rank; } /* Invert bitmap and rebuild energy for the empty side */ for (i = 0; i < 128; i++) { bitmap[i] ^= ~0u; } for (i = 0; i < 4096; i++) { energy[i] = 0.0f; } for (i = 0; i < 4096; i++) { if (HT__BN_GET(bitmap, i)) { ht__bn_update(energy, bitmap, i, +1.0f, lut, &imin, &imax); } } /* Phase 3: remove tightest from inverted side, assign remaining ranks */ while (points < 4096) { out[imax] = (unsigned char)((points * 255) / 4095); HT__BN_CLR(bitmap, imax); ht__bn_update(energy, bitmap, imax, -1.0f, lut, &imin, &imax); points++; } } #undef HT__BN_GET #undef HT__BN_SET #undef HT__BN_CLR HT_DEF unsigned char* HT_HalftoneDefault8(void) { /* 8x8 clustered-dot: values 0-63, closest to center activate first */ static unsigned char ht__halftone8[64] = { 63, 58, 50, 40, 41, 51, 59, 60, 57, 33, 27, 18, 19, 28, 34, 52, 49, 26, 13, 7, 8, 14, 29, 43, 39, 17, 6, 1, 2, 9, 20, 35, 38, 16, 5, 0, 3, 10, 21, 36, 48, 25, 12, 4, 11, 15, 30, 44, 56, 32, 24, 23, 22, 31, 37, 53, 62, 55, 47, 46, 45, 42, 54, 61, }; return ht__halftone8; } HT_DEF unsigned char* HT_ClusteredDotDiagonal8(void) { /* 8x8 diagonal clustered-dot: 45-degree rotated halftone screen (comic book / newspaper look) */ static unsigned char ht__clustered_dot_diagonal_8x8[64] = { 24, 10, 12, 26, 35, 47, 49, 37, 8, 0, 2, 14, 45, 59, 61, 51, 22, 6, 4, 16, 43, 57, 63, 53, 30, 20, 18, 28, 33, 41, 55, 39, 34, 46, 48, 36, 25, 11, 13, 27, 44, 58, 60, 50, 9, 1, 3, 15, 42, 56, 62, 52, 23, 7, 5, 17, 32, 40, 54, 38, 31, 21, 19, 29, }; return ht__clustered_dot_diagonal_8x8; } HT_DEF unsigned char* HT_HorizontalLine6(void) { /* 6x6 horizontal line screen: produces scanline / engraving look */ static unsigned char ht__horizontal_line_6x6[36] = { 35, 33, 31, 30, 32, 34, 23, 21, 19, 18, 20, 22, 11, 9, 7, 6, 8, 10, 5, 3, 1, 0, 2, 4, 17, 15, 13, 12, 14, 16, 29, 27, 25, 24, 26, 28, }; return ht__horizontal_line_6x6; } HT_DEF unsigned char* HT_VerticalLine6(void) { /* 6x6 vertical line screen: produces vertical stripe pattern */ static unsigned char ht__vertical_line_6x6[36] = { 35, 23, 11, 5, 17, 29, 33, 21, 9, 3, 15, 27, 31, 19, 7, 1, 13, 25, 30, 18, 6, 0, 12, 24, 32, 20, 8, 2, 14, 26, 34, 22, 10, 4, 16, 28, }; return ht__vertical_line_6x6; } static unsigned char* ht__matrix_blue_noise_default = 0; static unsigned char* ht__matrix_white_noise_default = 0; static unsigned char* ht__matrix_ign_default = 0; HT_DEF void HT_WhiteNoiseSetDefault(unsigned char matrix[4096]) { ht__matrix_white_noise_default = matrix; } HT_DEF void HT_BlueNoiseSetDefault(unsigned char matrix[4096]) { ht__matrix_blue_noise_default = matrix; } HT_DEF void HT_IGNSetDefault(unsigned char matrix[4096]) { ht__matrix_ign_default = matrix; } HT_DEF void* HT_OrderedMatrixFromMethod(eHT_OrderedMethod method) { void* result = 0; switch ( method ) { default: { } break; case kHT_OrderedMethod_None: { result = 0; } break; case kHT_OrderedMethod_Bayer2: { result = HT_BayerDefault2(); } break; case kHT_OrderedMethod_Bayer4: { result = HT_BayerDefault4(); } break; case kHT_OrderedMethod_Bayer8: { result = HT_BayerDefault8(); } break; case kHT_OrderedMethod_Bayer16: { result = HT_BayerDefault16(); } break; case kHT_OrderedMethod_Bayer32: { result = HT_BayerDefault32(); } break; case kHT_OrderedMethod_BlueNoise: { result = ht__matrix_blue_noise_default; } break; case kHT_OrderedMethod_Halftone: { result = HT_HalftoneDefault8(); } break; case kHT_OrderedMethod_Diagonal: { result = HT_ClusteredDotDiagonal8(); } break; case kHT_OrderedMethod_HLine: { result = HT_HorizontalLine6(); } break; case kHT_OrderedMethod_VLine: { result = HT_VerticalLine6(); } break; case kHT_OrderedMethod_WhiteNoise: { result = ht__matrix_white_noise_default; } break; case kHT_OrderedMethod_IGN: { result = ht__matrix_ign_default; } break; } return( result ); } typedef struct { signed char dx; signed char dy; unsigned char weight; } ht__kernel_entry; typedef struct { ht__kernel_entry* entries; int count; int scale; } ht__kernel; static ht__kernel_entry ht__simple_entries[] = { { 1, 0, 1 }, { 0, 1, 1 }, }; static ht__kernel_entry ht__box_entries[] = { { 1, 0, 1 }, { -1, 1, 1 }, { 0, 1, 1 }, { 1, 1, 1 }, }; static ht__kernel_entry ht__atkinson_entries[] = { { 1, 0, 1 }, { 2, 0, 1 }, { -1, 1, 1 }, { 0, 1, 1 }, { 1, 1, 1 }, { 0, 2, 1 }, }; static ht__kernel_entry ht__burkes_entries[] = { { 1, 0, 8 }, { 2, 0, 4 }, { -2, 1, 2 }, { -1, 1, 4 }, { 0, 1, 8 }, { 1, 1, 4 }, { 2, 1, 2 }, }; static ht__kernel_entry ht__floyd_steinberg_entries[] = { { 1, 0, 7 }, { -1, 1, 3 }, { 0, 1, 5 }, { 1, 1, 1 }, }; static ht__kernel_entry ht__jarvis_judice_ninke_entries[] = { { 1, 0, 7 }, { 2, 0, 5 }, { -2, 1, 3 }, { -1, 1, 5 }, { 0, 1, 7 }, { 1, 1, 5 }, { 2, 1, 3 }, { -1, 2, 3 }, { 0, 2, 5 }, { 1, 2, 3 }, }; static ht__kernel_entry ht__pigeon_entries[] = { { 1, 0, 2 }, { 2, 0, 1 }, { -1, 1, 2 }, { 0, 1, 2 }, { 1, 1, 2 }, { -2, 2, 1 }, { 0, 2, 1 }, { 2, 2, 1 }, }; static ht__kernel_entry ht__sierra_lite_entries[] = { { 1, 0, 2 }, { -1, 1, 1 }, { 0, 1, 1 }, }; static ht__kernel_entry ht__sierra2_entries[] = { { 1, 0, 4 }, { 2, 0, 3 }, { -2, 1, 1 }, { -1, 1, 2 }, { 0, 1, 3 }, { 1, 1, 2 }, { 2, 1, 1 }, }; static ht__kernel_entry ht__stucki_entries[] = { { 1, 0, 8 }, { 2, 0, 4 }, { -2, 1, 2 }, { -1, 1, 4 }, { 0, 1, 8 }, { 1, 1, 4 }, { 2, 1, 2 }, { -2, 2, 1 }, { -1, 2, 2 }, { 0, 2, 4 }, { 1, 2, 2 }, { 2, 2, 1 }, }; static ht__kernel ht__kernel_simple = { ht__simple_entries, 2, 2 }; static ht__kernel ht__kernel_box = { ht__box_entries, 4, 4 }; static ht__kernel ht__kernel_atkinson = { ht__atkinson_entries, 6, 8 }; static ht__kernel ht__kernel_burkes = { ht__burkes_entries, 7, 32 }; static ht__kernel ht__kernel_floyd_steinberg = { ht__floyd_steinberg_entries, 4, 16 }; static ht__kernel ht__kernel_jarvis_judice_ninke = { ht__jarvis_judice_ninke_entries, 10, 46 }; static ht__kernel ht__kernel_pigeon = { ht__pigeon_entries, 8, 14 }; static ht__kernel ht__kernel_sierra_lite = { ht__sierra_lite_entries, 3, 4 }; static ht__kernel ht__kernel_sierra2 = { ht__sierra2_entries, 7, 16 }; static ht__kernel ht__kernel_stucki = { ht__stucki_entries, 12, 42 }; /* ========================================================================== Sharpening (unsharp mask) ========================================================================== */ HT_DEF void HT_SharpenF32(float* buf, int w, int h, float* scratch, float amount, int radius) { if (radius > 0 && amount != 0.0f) { int count = w * h; int x, y, dx, dy; int diam = 2 * radius + 1; float inv_area = 1.0f / (float)(diam * diam); for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { float sum = 0.0f; for (dy = -radius; dy <= radius; dy++) { int sy = y + dy; if (sy < 0) { sy = 0; } if (sy >= h) { sy = h - 1; } for (dx = -radius; dx <= radius; dx++) { int sx = x + dx; if (sx < 0) { sx = 0; } if (sx >= w) { sx = w - 1; } sum += buf[sy * w + sx]; } } scratch[y * w + x] = sum * inv_area; } } for (y = 0; y < count; y++) { buf[y] = buf[y] + amount * (buf[y] - scratch[y]); } } } /* ========================================================================== HT_DitherBiased ========================================================================== */ static ht__kernel* ht__kernel_from_method(eHT_Method method) { switch (method) { case kHT_Method_None: { return( 0 ); } case kHT_Method_Simple: { return( &ht__kernel_simple ); } case kHT_Method_Box: { return( &ht__kernel_box ); } case kHT_Method_Atkinson: { return( &ht__kernel_atkinson ); } case kHT_Method_Burkes: { return( &ht__kernel_burkes ); } case kHT_Method_FloydSteinberg: { return( &ht__kernel_floyd_steinberg ); } case kHT_Method_JarvisJudiceNinke: { return( &ht__kernel_jarvis_judice_ninke ); } case kHT_Method_Pigeon: { return( &ht__kernel_pigeon ); } case kHT_Method_SierraLite: { return( &ht__kernel_sierra_lite ); } case kHT_Method_Sierra2: { return( &ht__kernel_sierra2 ); } case kHT_Method_Stucki: { return( &ht__kernel_stucki ); } default: { return( 0 ); } } } HT_DEF void HT_DitherBiased(unsigned char* out, unsigned char* mask, float* work, unsigned char* src_rgba, int w, int h, eHT_Method diffusion_method, HT_Dither_Biased_Params* params) { ht__kernel* kernel = ht__kernel_from_method(diffusion_method); int has_ordered = (params->OrderedMatrix || params->OrderedMatrixU16) && params->OrderedMax > 0.0f; float bayer_scale = 0.0f; float strength = params->DiffusionStrength; int x, y, k; if (has_ordered) { bayer_scale = params->OrderedScale * (255.0f / params->OrderedMax); } for (y = 0; y < h; y++) { int reverse = params->Serpentine && (y & 1); int x_start = reverse ? w - 1 : 0; int x_end = reverse ? -1 : w; int x_step = reverse ? -1 : 1; for (x = x_start; x != x_end; x += x_step) { int i = y * w + x; float lum = work[i]; int out_bit; /* Add ordered dither bias */ if (has_ordered) { int mi = (x % params->OrderedN) + (y % params->OrderedN) * params->OrderedN; float mv = params->OrderedMatrix ? (float)params->OrderedMatrix[mi] : (float)params->OrderedMatrixU16[mi]; float bd = (mv - params->OrderedMax * 0.5f) * bayer_scale; lum += bd; } /* Threshold */ out_bit = lum > params->Threshold ? 1 : 0; if (out_bit) { out[i / 8] |= (unsigned char)(1 << (7 - (i % 8))); } /* Alpha mask */ if (mask && src_rgba) { unsigned char alpha = src_rgba[i * 4 + 3]; if (alpha >= 128) { mask[i / 8] |= (unsigned char)(1 << (7 - (i % 8))); } } /* Error diffusion */ if (kernel) { float err = (lum - (out_bit ? 255.0f : 0.0f)) * strength; for (k = 0; k < kernel->count; k++) { int nx = x + (reverse ? -kernel->entries[k].dx : kernel->entries[k].dx); int ny = y + kernel->entries[k].dy; if (nx >= 0 && nx < w && ny >= 0 && ny < h) { work[ny * w + nx] += err * (float)kernel->entries[k].weight / (float)kernel->scale; } } } } } } #endif /* OH4_HALFTONE_IMPLEMENTATION */