1 | /*****************************************************************************/ |
2 | /* */ |
3 | /* lynxsprite.c */ |
4 | /* */ |
5 | /* Lynx sprite format backend for the sp65 sprite and bitmap utility */ |
6 | /* */ |
7 | /* */ |
8 | /* */ |
9 | /* (C) 2012, Ullrich von Bassewitz */ |
10 | /* Roemerstrasse 52 */ |
11 | /* D-70794 Filderstadt */ |
12 | /* EMail: uz@cc65.org */ |
13 | /* */ |
14 | /* */ |
15 | /* This software is provided 'as-is', without any expressed or implied */ |
16 | /* warranty. In no event will the authors be held liable for any damages */ |
17 | /* arising from the use of this software. */ |
18 | /* */ |
19 | /* Permission is granted to anyone to use this software for any purpose, */ |
20 | /* including commercial applications, and to alter it and redistribute it */ |
21 | /* freely, subject to the following restrictions: */ |
22 | /* */ |
23 | /* 1. The origin of this software must not be misrepresented; you must not */ |
24 | /* claim that you wrote the original software. If you use this software */ |
25 | /* in a product, an acknowledgment in the product documentation would be */ |
26 | /* appreciated but is not required. */ |
27 | /* 2. Altered source versions must be plainly marked as such, and must not */ |
28 | /* be misrepresented as being the original software. */ |
29 | /* 3. This notice may not be removed or altered from any source */ |
30 | /* distribution. */ |
31 | /* */ |
32 | /*****************************************************************************/ |
33 | |
34 | |
35 | |
36 | #include <stdlib.h> |
37 | |
38 | /* common */ |
39 | #include "attrib.h" |
40 | #include "print.h" |
41 | |
42 | /* sp65 */ |
43 | #include "attr.h" |
44 | #include "error.h" |
45 | #include "lynxsprite.h" |
46 | |
47 | |
48 | |
49 | /*****************************************************************************/ |
50 | /* Data */ |
51 | /*****************************************************************************/ |
52 | |
53 | |
54 | |
55 | /* Sprite mode */ |
56 | enum Mode { |
57 | smAuto, |
58 | smLiteral, |
59 | smPacked, |
60 | smShaped |
61 | }; |
62 | |
63 | |
64 | /*****************************************************************************/ |
65 | /* Code */ |
66 | /*****************************************************************************/ |
67 | |
68 | |
69 | |
70 | static enum Mode GetMode (const Collection* A) |
71 | /* Return the sprite mode from the attribute collection A */ |
72 | { |
73 | /* Check for a mode attribute */ |
74 | const char* Mode = GetAttrVal (A, "mode" ); |
75 | if (Mode) { |
76 | if (strcmp (Mode, "literal" ) == 0) { |
77 | return smLiteral; |
78 | } else if (strcmp (Mode, "packed" ) == 0) { |
79 | return smPacked; |
80 | } else if (strcmp (Mode, "shaped" ) == 0) { |
81 | return smShaped; |
82 | } else { |
83 | Error ("Invalid value for attribute 'mode'" ); |
84 | } |
85 | } |
86 | |
87 | return smAuto; |
88 | } |
89 | |
90 | |
91 | static unsigned GetActionPointX (const Collection* A) |
92 | /* Return the sprite mode from the attribute collection A */ |
93 | { |
94 | /* Check for a action point x attribute */ |
95 | const char* ActionPointX = GetAttrVal (A, "ax" ); |
96 | if (ActionPointX) { |
97 | return atoi(ActionPointX); |
98 | } else { |
99 | return 0; |
100 | } |
101 | } |
102 | |
103 | |
104 | static unsigned GetActionPointY (const Collection* A) |
105 | /* Return the sprite mode from the attribute collection A */ |
106 | { |
107 | /* Check for a action point y attribute */ |
108 | const char* ActionPointY = GetAttrVal (A, "ay" ); |
109 | if (ActionPointY) { |
110 | return atoi(ActionPointY); |
111 | } else { |
112 | return 0; |
113 | } |
114 | } |
115 | |
116 | static unsigned GetEdgeIndex (const Collection* A) |
117 | /* Return the sprite mode from the attribute collection A */ |
118 | { |
119 | /* Get index for edge color in shaped mode */ |
120 | const char* EdgeIndex = GetAttrVal (A, "edge" ); |
121 | if (EdgeIndex) { |
122 | return atoi(EdgeIndex); |
123 | } else { |
124 | return 0; |
125 | } |
126 | } |
127 | |
128 | static char OutBuffer[512]; /* The maximum size is 508 pixels */ |
129 | static unsigned char OutIndex; |
130 | |
131 | static void AssembleByte(unsigned bits, char val) |
132 | { |
133 | static char bit_counter = 8, byte = 0; |
134 | |
135 | /* initialize */ |
136 | if (!bits) { |
137 | OutIndex = 0; |
138 | bit_counter = 8; |
139 | byte = 0; |
140 | return; |
141 | } |
142 | /* handle end of line */ |
143 | if (bits == 8) { |
144 | if (bit_counter != 8) { |
145 | byte <<= bit_counter; |
146 | OutBuffer[OutIndex++] = byte; |
147 | if (!OutIndex) { |
148 | Error ("Sprite is too large for the Lynx" ); |
149 | } |
150 | if (byte & 0x1) { |
151 | OutBuffer[OutIndex++] = byte; |
152 | if (!OutIndex) { |
153 | Error ("Sprite is too large for the Lynx" ); |
154 | } |
155 | } |
156 | } |
157 | return; |
158 | } |
159 | /* handle end of line for literal */ |
160 | if (bits == 7) { |
161 | if (bit_counter != 8) { |
162 | byte <<= bit_counter; |
163 | OutBuffer[OutIndex++] = byte; |
164 | if (!OutIndex) { |
165 | Error ("Sprite is too large for the Lynx" ); |
166 | } |
167 | } |
168 | return; |
169 | } |
170 | val <<= 8 - bits; |
171 | |
172 | do { |
173 | byte <<= 1; |
174 | |
175 | if (val & 0x80) |
176 | ++byte; |
177 | |
178 | if (!(--bit_counter)) { |
179 | OutBuffer[OutIndex++] = byte; |
180 | if (!OutIndex) { |
181 | Error ("Sprite is too large for the Lynx" ); |
182 | } |
183 | byte = 0; |
184 | bit_counter = 8; |
185 | } |
186 | |
187 | val <<= 1; |
188 | |
189 | } while (--bits); |
190 | } |
191 | |
192 | static unsigned char ChoosePackagingMode(signed len, signed index, char ColorBits, char LineBuffer[512]) |
193 | { |
194 | --len; |
195 | if (!len) { |
196 | return 0; |
197 | } |
198 | if (LineBuffer[index] != LineBuffer[index + 1]) { |
199 | return 0; |
200 | } |
201 | if (ColorBits > 2) { |
202 | return 1; |
203 | } |
204 | if (LineBuffer[index] != LineBuffer[index + 2]) { |
205 | return 0; |
206 | } |
207 | if (ColorBits > 1) { |
208 | return 1; |
209 | } |
210 | if (LineBuffer[index] != LineBuffer[index + 3]) { |
211 | return 0; |
212 | } |
213 | return 1; |
214 | } |
215 | |
216 | static void WriteOutBuffer(StrBuf *D) |
217 | { |
218 | signed i; |
219 | |
220 | /* Fix bug in Lynx where the count cannot be 1 */ |
221 | if (OutIndex == 1) { |
222 | OutBuffer[OutIndex++] = 0; |
223 | } |
224 | /* Write the byte count to the end of the scanline */ |
225 | if (OutIndex == 255) { |
226 | Error ("Sprite is too large for the Lynx" ); |
227 | } |
228 | SB_AppendChar (D, OutIndex+1); |
229 | /* Write scanline data */ |
230 | for (i = 0; i < OutIndex; i++) { |
231 | SB_AppendChar (D, OutBuffer[i]); |
232 | } |
233 | } |
234 | |
235 | static void encodeSprite(StrBuf *D, enum Mode M, char ColorBits, char ColorMask, char LineBuffer[512], |
236 | int len, int LastOpaquePixel) { |
237 | /* |
238 | ** The data starts with a byte count. It tells the number of bytes on this |
239 | ** line + 1. |
240 | ** Special case is a count of 1. It will change to next quadrant. |
241 | ** Other special case is 0. It will end the sprite. |
242 | ** |
243 | ** Ordinary data packet. These are bits in a stream. |
244 | ** 1=literal 0=packed |
245 | ** 4 bit count (+1) |
246 | ** for literal you put "count" values |
247 | ** for packed you repeat the value "count" times |
248 | ** Never use packed mode for one pixel |
249 | ** If the last bit on a line is 1 you need to add a byte of zeroes |
250 | ** A sequence 00000 ends a scan line |
251 | ** |
252 | ** All data is high nybble first |
253 | */ |
254 | unsigned char V = 0; |
255 | signed i; |
256 | signed count; |
257 | unsigned char differ[16]; |
258 | unsigned char *d_ptr; |
259 | |
260 | AssembleByte(0, 0); |
261 | switch (M) { |
262 | case smAuto: |
263 | case smLiteral: |
264 | for (i = 0; i < len; i++) { |
265 | /* Fetch next pixel index into pixel buffer */ |
266 | AssembleByte(ColorBits, LineBuffer[i] & ColorMask); |
267 | } |
268 | AssembleByte(7, 0); |
269 | /* Write the buffer to file */ |
270 | WriteOutBuffer(D); |
271 | break; |
272 | case smPacked: |
273 | i = 0; |
274 | while (len) { |
275 | if (ChoosePackagingMode(len, i, ColorBits, LineBuffer)) { |
276 | /* Make runlength packet */ |
277 | V = LineBuffer[i]; |
278 | ++i; |
279 | --len; |
280 | count = 0; |
281 | do { |
282 | ++count; |
283 | ++i; |
284 | --len; |
285 | } while (V == LineBuffer[i] && len && count != 15); |
286 | |
287 | AssembleByte(5, count); |
288 | AssembleByte(ColorBits, V); |
289 | |
290 | } else { |
291 | /* Make packed literal packet */ |
292 | d_ptr = differ; |
293 | V = LineBuffer[i++]; |
294 | *d_ptr++ = V; |
295 | --len; |
296 | count = 0; |
297 | while (ChoosePackagingMode(len, i, ColorBits, LineBuffer) == 0 && len && count != 15) { |
298 | V = LineBuffer[i++]; |
299 | *d_ptr++ = V; |
300 | ++count; |
301 | --len; |
302 | } |
303 | |
304 | AssembleByte(5, count | 0x10); |
305 | d_ptr = differ; |
306 | do { |
307 | AssembleByte(ColorBits, *d_ptr++); |
308 | } while (--count >= 0); |
309 | |
310 | } |
311 | } |
312 | AssembleByte(8, 0); |
313 | /* Write the buffer to file */ |
314 | WriteOutBuffer(D); |
315 | break; |
316 | |
317 | case smShaped: |
318 | if (LastOpaquePixel > -1) { |
319 | if (LastOpaquePixel < len - 1) { |
320 | len = LastOpaquePixel + 1; |
321 | } |
322 | i = 0; |
323 | while (len) { |
324 | if (ChoosePackagingMode(len, i, ColorBits, LineBuffer)) { |
325 | /* Make runlength packet */ |
326 | V = LineBuffer[i]; |
327 | ++i; |
328 | --len; |
329 | count = 0; |
330 | do { |
331 | ++count; |
332 | ++i; |
333 | --len; |
334 | } while (V == LineBuffer[i] && len && count != 15); |
335 | |
336 | AssembleByte(5, count); |
337 | AssembleByte(ColorBits, V); |
338 | |
339 | } else { |
340 | /* Make packed literal packet */ |
341 | d_ptr = differ; |
342 | V = LineBuffer[i++]; |
343 | *d_ptr++ = V; |
344 | --len; |
345 | count = 0; |
346 | while (ChoosePackagingMode(len, i, ColorBits, LineBuffer) == 0 && len && count != 15) { |
347 | V = LineBuffer[i++]; |
348 | *d_ptr++ = V; |
349 | ++count; |
350 | --len; |
351 | } |
352 | |
353 | AssembleByte(5, count | 0x10); |
354 | d_ptr = differ; |
355 | do { |
356 | AssembleByte(ColorBits, *d_ptr++); |
357 | } while (--count >= 0); |
358 | |
359 | } |
360 | } |
361 | AssembleByte(5, 0); |
362 | AssembleByte(8, 0); |
363 | /* Write the buffer to file */ |
364 | WriteOutBuffer(D); |
365 | } |
366 | break; |
367 | } |
368 | } |
369 | |
370 | StrBuf* GenLynxSprite (const Bitmap* B, const Collection* A) |
371 | /* Generate binary output in Lynx sprite format for the bitmap B. The output |
372 | ** is stored in a string buffer (which is actually a dynamic char array) and |
373 | ** returned. |
374 | ** |
375 | ** The Lynx will draw 4 quadrants: |
376 | ** - Down right |
377 | ** - Up right |
378 | ** - Up left |
379 | ** - Down left |
380 | ** |
381 | ** The sprite will end with a byte 0. |
382 | */ |
383 | { |
384 | enum Mode M; |
385 | StrBuf* D; |
386 | signed X, Y; |
387 | unsigned OX, OY; |
388 | char ColorBits; |
389 | char ColorMask; |
390 | char EdgeIndex; |
391 | |
392 | /* Get EdgeIndex */ |
393 | EdgeIndex = GetEdgeIndex (A); |
394 | |
395 | /* Action point of the sprite */ |
396 | OX = GetActionPointX (A); |
397 | OY = GetActionPointY (A); |
398 | if (OX >= GetBitmapWidth (B)) { |
399 | Error ("Action point X cannot be larger than bitmap width" ); |
400 | } |
401 | if (OY >= GetBitmapHeight (B)) { |
402 | Error ("Action point Y cannot be larger than bitmap height" ); |
403 | } |
404 | |
405 | /* Output the image properties */ |
406 | Print (stdout, 1, "Image is %ux%u with %u colors%s\n" , |
407 | GetBitmapWidth (B), GetBitmapHeight (B), GetBitmapColors (B), |
408 | BitmapIsIndexed (B)? " (indexed)" : "" ); |
409 | |
410 | /* Get the sprite mode */ |
411 | M = GetMode (A); |
412 | |
413 | /* Now check if bitmap indexes are ok */ |
414 | if (GetBitmapColors (B) > 16) { |
415 | Error ("Too many colors for a Lynx sprite" ); |
416 | } |
417 | ColorBits = 4; |
418 | ColorMask = 0x0f; |
419 | if (GetBitmapColors (B) < 9) { |
420 | ColorBits = 3; |
421 | ColorMask = 0x07; |
422 | } |
423 | if (GetBitmapColors (B) < 5) { |
424 | ColorBits = 2; |
425 | ColorMask = 0x03; |
426 | } |
427 | if (GetBitmapColors (B) < 3) { |
428 | ColorBits = 1; |
429 | ColorMask = 0x01; |
430 | } |
431 | |
432 | /* Create the output buffer and resize it to the required size. */ |
433 | D = NewStrBuf (); |
434 | SB_Realloc (D, 63); |
435 | |
436 | /* Convert the image for quadrant bottom right */ |
437 | for (Y = OY; Y < (signed)GetBitmapHeight (B); ++Y) { |
438 | signed i = 0; |
439 | signed LastOpaquePixel = -1; |
440 | char LineBuffer[512]; /* The maximum size is 508 pixels */ |
441 | |
442 | /* Fill the LineBuffer for easier optimisation */ |
443 | for (X = OX; X < (signed)GetBitmapWidth (B); ++X) { |
444 | |
445 | /* Fetch next bit into byte buffer */ |
446 | LineBuffer[i] = GetPixel (B, X, Y).Index & ColorMask; |
447 | |
448 | if (LineBuffer[i] != EdgeIndex) { |
449 | LastOpaquePixel = i; |
450 | } |
451 | ++i; |
452 | } |
453 | |
454 | encodeSprite(D, M, ColorBits, ColorMask, LineBuffer, i, LastOpaquePixel); |
455 | } |
456 | |
457 | if ((OY == 0) && (OX == 0)) { |
458 | /* Trivial case only one quadrant */ |
459 | |
460 | /* Mark end of sprite */ |
461 | SB_AppendChar (D, 0); |
462 | |
463 | /* Return the converted bitmap */ |
464 | return D; |
465 | } |
466 | |
467 | /* Next quadrant */ |
468 | SB_AppendChar (D, 1); |
469 | |
470 | /* Convert the image for quadrant top right */ |
471 | for (Y = OY - 1; Y >= 0; --Y) { |
472 | signed i = 0; |
473 | signed LastOpaquePixel = -1; |
474 | char LineBuffer[512]; /* The maximum size is 508 pixels */ |
475 | |
476 | /* Fill the LineBuffer for easier optimisation */ |
477 | for (X = OX; X < (signed)GetBitmapWidth (B); ++X) { |
478 | |
479 | /* Fetch next bit into byte buffer */ |
480 | LineBuffer[i] = GetPixel (B, X, Y).Index & ColorMask; |
481 | |
482 | if (LineBuffer[i] != EdgeIndex) { |
483 | LastOpaquePixel = i; |
484 | } |
485 | ++i; |
486 | } |
487 | |
488 | encodeSprite(D, M, ColorBits, ColorMask, LineBuffer, i, LastOpaquePixel); |
489 | } |
490 | |
491 | if (OX == 0) { |
492 | /* Special case only two quadrants */ |
493 | |
494 | /* Mark end of sprite */ |
495 | SB_AppendChar (D, 0); |
496 | |
497 | /* Return the converted bitmap */ |
498 | return D; |
499 | } |
500 | |
501 | /* Next quadrant */ |
502 | SB_AppendChar (D, 1); |
503 | |
504 | /* Convert the image for quadrant top left */ |
505 | for (Y = OY - 1; Y >= 0; --Y) { |
506 | signed i = 0; |
507 | signed LastOpaquePixel = -1; |
508 | char LineBuffer[512]; /* The maximum size is 508 pixels */ |
509 | |
510 | /* Fill the LineBuffer for easier optimisation */ |
511 | for (X = OX - 1; X >= 0; --X) { |
512 | |
513 | /* Fetch next bit into byte buffer */ |
514 | LineBuffer[i] = GetPixel (B, X, Y).Index & ColorMask; |
515 | |
516 | if (LineBuffer[i] != EdgeIndex) { |
517 | LastOpaquePixel = i; |
518 | } |
519 | ++i; |
520 | } |
521 | |
522 | encodeSprite(D, M, ColorBits, ColorMask, LineBuffer, i, LastOpaquePixel); |
523 | } |
524 | |
525 | /* Next quadrant */ |
526 | SB_AppendChar (D, 1); |
527 | |
528 | /* Convert the image for quadrant bottom left */ |
529 | for (Y = OY; Y < (signed)GetBitmapHeight (B); ++Y) { |
530 | signed i = 0; |
531 | signed LastOpaquePixel = -1; |
532 | char LineBuffer[512]; /* The maximum size is 508 pixels */ |
533 | |
534 | /* Fill the LineBuffer for easier optimisation */ |
535 | for (X = OX - 1; X >= 0; --X) { |
536 | |
537 | /* Fetch next bit into byte buffer */ |
538 | LineBuffer[i] = GetPixel (B, X, Y).Index & ColorMask; |
539 | |
540 | if (LineBuffer[i] != EdgeIndex) { |
541 | LastOpaquePixel = i; |
542 | } |
543 | ++i; |
544 | } |
545 | |
546 | encodeSprite(D, M, ColorBits, ColorMask, LineBuffer, i, LastOpaquePixel); |
547 | } |
548 | |
549 | /* End sprite */ |
550 | SB_AppendChar (D, 0); |
551 | |
552 | /* Return the converted bitmap */ |
553 | return D; |
554 | } |
555 | |