1 /**
2     re-implementation of the GermSerk ColorBleedingEffect texture padding algorithm
3     https://github.com/gemserk/imageprocessing/blob/master/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java
4 
5     This allows textures to be padded in such a way that there'll be no artifacts.
6 */
7 module creator.core.colorbleed;
8 import inochi2d.core.texture;
9 
10 /**
11     Does texture bleeding on a texture.
12 */
13 void incColorBleedPixels(ShallowTexture* texture, int maxIterations = 32) {
14     int width = texture.width;
15     int height = texture.height;
16 
17     union TexData {
18         ubyte[] bytes;
19         Pixel[] pixels;
20     }
21 
22     TexData textureData = TexData(texture.data);
23     Mask* mask = new Mask(textureData.bytes);
24 
25     int iterations = 0;
26     int lastPending = -1;
27     while (mask.getPendingSize > 0) {
28         if (iterations >= maxIterations) break;
29 
30         lastPending = mask.getPendingSize();
31         executeIteration(textureData.pixels, mask, width, height);
32         iterations++;
33 
34         // Break on infinite loop.
35         if (mask.getPendingSize == lastPending) break;
36     }
37 
38     texture.data = textureData.bytes;
39 }
40 
41 /**
42     Does texture bleeding on a texture.
43 */
44 void incColorBleedPixels(Texture texture, int maxIterations = 32) {
45     int width = texture.width;
46     int height = texture.height;
47 
48     union TexData {
49         ubyte[] bytes;
50         Pixel[] pixels;
51     }
52 
53     TexData textureData = TexData(texture.getTextureData());
54     Mask* mask = new Mask(textureData.bytes);
55 
56     int iterations = 0;
57     int lastPending = -1;
58     while (mask.getPendingSize > 0) {
59         if (iterations >= maxIterations) break;
60 
61         lastPending = mask.getPendingSize();
62         executeIteration(textureData.pixels, mask, width, height);
63         iterations++;
64 
65         // Break on infinite loop.
66         if (mask.getPendingSize == lastPending) break;
67     }
68 
69     texture.setData(textureData.bytes);
70 }
71 
72 private:
73 enum TOPROCESS = 0;
74 enum INPROCESS = 1;
75 enum PIXELDATA = 2;
76 
77 
78 struct Pixel {
79 align(1):
80     ubyte r;
81     ubyte g;
82     ubyte b;
83     ubyte a;
84 }
85 
86 struct Mask {
87     ubyte[] data;
88     size_t[] pending;
89     size_t[] changing;
90 
91     this(ubyte[] texture) {
92         ubyte[] colorData = texture;
93         data = new ubyte[colorData.length/4];
94 
95         foreach(i; 0..data.length) {
96             size_t aRed = i*4;
97             size_t aGreen = aRed+1;
98             size_t aBlue = aRed+2;
99             size_t aAlpha = aRed+3;
100             Pixel data = Pixel(
101                 colorData[aRed],
102                 colorData[aGreen],
103                 colorData[aBlue],
104                 colorData[aAlpha],
105             );
106 
107             if (data.a == 0) {
108                 this.data[i] = TOPROCESS;
109                 this.pending ~= i;
110             } else {
111                 this.data[i] = PIXELDATA;
112             }
113         }   
114     }
115 
116     int getPendingSize() {
117         return cast(int)pending.length;
118     }
119 
120     ubyte getMask(size_t index) {
121         return data[index];
122     }
123 
124     size_t removeIndex(size_t index) {
125         if (index >= pending.length) {
126             throw new Exception("Out of bounds write to mask");
127         }
128 
129         size_t value = pending[index];
130         pending[index] = pending[$-1];
131         pending.length--;
132 
133         return value;
134     }
135 }
136 
137 struct MaskIterator {
138     Mask* mask;
139     size_t index;
140 
141     this(Mask* mask) {
142         this.mask = mask;
143     }
144 
145     bool hasNext() {
146         return index < mask.getPendingSize;
147     }
148 
149     int next() {
150         return cast(int)mask.pending[index++];
151     }
152 
153     void markAsInProgress() {
154         index--;
155         size_t removed = mask.removeIndex(index);
156         mask.changing ~= removed;
157     }
158 
159     void reset() {
160         index = 0;
161         foreach(i; 0..mask.changing.length) {
162             size_t index = mask.changing[i];
163             mask.data[index] = PIXELDATA;
164         }
165         mask.changing.length = 0;
166     }
167 }
168 
169 void executeIteration(Pixel[] rgba, Mask* mask, int width, int height) {
170     int[2][8] offsets = [
171         [-1, -1],
172         [0, -1],
173         [1, -1],
174         [-1, 0],
175         [1, 0],
176         [-1, 1],
177         [0, 1],
178         [1, 1],
179     ];
180 
181 
182 
183     MaskIterator iterator = MaskIterator(mask);
184     while(iterator.hasNext) {
185         int pixelIndex = iterator.next;
186 
187         int x = pixelIndex % width;
188         int y = pixelIndex / width;
189 
190         int r = 0;
191         int g = 0;
192         int b = 0;
193         int cant = 0;
194 
195         foreach(i, offset; offsets) {
196             int column = x + offset[0];
197             int row = y + offset[1];
198 
199             if (column < 0 || column >= width || row < 0 || row >= height) continue;
200 
201             int currentPixelIndex = row * width + column;
202             Pixel pixelData = rgba[currentPixelIndex];
203             if (mask.getMask(currentPixelIndex) == PIXELDATA) {
204                 r += pixelData.r;
205                 g += pixelData.g;
206                 b += pixelData.b;
207                 cant++;
208             }
209         }
210 
211         if (cant != 0) {
212             rgba[pixelIndex] = Pixel(
213                 cast(ubyte)(r / cant), 
214                 cast(ubyte)(g / cant), 
215                 cast(ubyte)(b / cant), 
216                 0
217             );
218             iterator.markAsInProgress();
219         }
220     }
221 
222     iterator.reset();
223 }