265 lines
9.6 KiB
C
265 lines
9.6 KiB
C
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
// See LICENSE.md for license information.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// iigsIcon.c - whether the IIgs Finder shows a program by its baked icon:
|
|
// scripts/verify-iigs-icon.sh's check.
|
|
//
|
|
// iigsIcon SHR.bin ICON LABEL [KEEP.png]
|
|
//
|
|
// SHR.bin is the Super Hi-Res block MAME's memory held ($E1/2000 to
|
|
// $E1/9FFF: pixels, scan line control bytes, palettes); ICON is iconBake's
|
|
// Finder icon file. The screen is read at 640 pixels a line: a 640-mode
|
|
// line's pixel takes its colour from the palette quarter its place in the
|
|
// byte gives it (bits 7-6 from entries 8-11, 5-4 from 12-15, 3-2 from 0-3,
|
|
// 1-0 from 4-7); a 320-mode line's pixel is shown twice. Each 640-mode
|
|
// screen pixel's own 2-bit value is kept (none on a 320-mode line), and
|
|
// the icon is looked for by those: in 640 mode QuickDraw draws a 4-bit icon
|
|
// pixel as two screen pixels, its high two bits then its low two, so every
|
|
// pixel the icon's mask leaves opaque must be its two values, wherever the
|
|
// icon is. Every place it is counts.
|
|
//
|
|
// KEEP, when given and not empty, gets the screen as a PNG at 640x400 (each
|
|
// line twice), as it shows. Prints LABEL: PASS or LABEL: FAIL; exits 0 or
|
|
// 1, or 2 on a bad command line.
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "gateArg.h"
|
|
#include "gateImage.h"
|
|
#include "toolFile.h"
|
|
#include "toolPng.h"
|
|
|
|
#define TOOL_NAME "iigsIcon"
|
|
#define FAIL_EXIT 1
|
|
#define WIDTH_640 640u
|
|
#define HEIGHT 200u
|
|
#define ROW_BYTES 160u
|
|
#define SCB_AT 0x7D00u
|
|
#define PALETTES_AT 0x7E00u
|
|
#define SHR_BYTES 0x8000u
|
|
#define SCB_640 0x80u
|
|
#define SCB_PALETTE_MASK 0x0Fu
|
|
#define PALETTE_BYTES 32u
|
|
#define PALETTE_ENTRIES 16u
|
|
#define ENTRY_BYTES 2u
|
|
#define BYTE_BITS 8u
|
|
#define PIXELS_A_BYTE_640 4u
|
|
// A 320-mode pixel is two screen pixels at 640 a line.
|
|
#define PIXEL_320_WIDTH 2u
|
|
#define NIBBLE_BITS 4u
|
|
#define NIBBLE_MASK 0xFu
|
|
#define KEEP_SCALE_Y 2u
|
|
#define NO_VALUE (-1)
|
|
// A 640-mode screen pixel is 2 bits; so is each half of an icon pixel.
|
|
#define HALF_BITS 2u
|
|
#define HALF_MASK 3u
|
|
#define HALVES 2u
|
|
// The icon file: its header (4 bytes, the file ID, 4 bytes, a 16-byte
|
|
// name), then the record's length word, 64-byte pathname, 16-byte name,
|
|
// file type, and aux type before its big icon: iconType, iconSize, height,
|
|
// width, the image (4 bits a pixel, the even pixel in the high nibble),
|
|
// and the mask, of the same shape.
|
|
#define FILE_HEADER_BYTES 26u
|
|
#define RECORD_HEAD_BYTES 86u
|
|
#define ICON_AT (FILE_HEADER_BYTES + RECORD_HEAD_BYTES)
|
|
#define ICON_SIZE_AT 2u
|
|
#define ICON_HEIGHT_AT 4u
|
|
#define ICON_WIDTH_AT 6u
|
|
#define ICON_HEADER_BYTES 8u
|
|
#define ICON_PIXELS_A_BYTE 2u
|
|
|
|
// One opaque half of an icon pixel: where it is (screen pixels) and its
|
|
// 2-bit value.
|
|
typedef struct HalfT {
|
|
uint32_t x;
|
|
uint32_t y;
|
|
int8_t value;
|
|
} HalfT;
|
|
|
|
static void keepScreen(const char *path, const uint8_t *rgb);
|
|
static void readScreen(const uint8_t *shr, uint8_t *rgb, int8_t *values);
|
|
|
|
// The palette quarter of each 2-bit pixel of a 640-mode byte, high bits
|
|
// first.
|
|
static const uint32_t kQuarterBase[PIXELS_A_BYTE_640] = { 8u, 12u, 0u, 4u };
|
|
|
|
static const char *const kUsage[] = {
|
|
"usage: iigsIcon SHR.bin ICON LABEL [KEEP.png]"
|
|
};
|
|
|
|
|
|
// The screen as it shows, each line twice, as a PNG.
|
|
static void keepScreen(const char *path, const uint8_t *rgb) {
|
|
ToolImageT keep;
|
|
uint32_t x;
|
|
uint32_t y;
|
|
uint8_t *out;
|
|
|
|
if (!toolImageCreate(&keep, WIDTH_640, HEIGHT * KEEP_SCALE_Y, false)) {
|
|
toolFail("out of memory");
|
|
}
|
|
for (y = 0; y < HEIGHT * KEEP_SCALE_Y; y++) {
|
|
for (x = 0; x < WIDTH_640; x++) {
|
|
out = keep.pixels + ((size_t)y * WIDTH_640 + x) * GATE_RGBA_BYTES;
|
|
memcpy(out, rgb + ((size_t)(y / KEEP_SCALE_Y) * WIDTH_640 + x) * GATE_RGB_BYTES, GATE_RGB_BYTES);
|
|
out[GATE_ALPHA] = UINT8_MAX;
|
|
}
|
|
}
|
|
if (!toolPngSave(path, &keep)) {
|
|
toolFail("cannot write %s", path);
|
|
}
|
|
toolImageFree(&keep);
|
|
}
|
|
|
|
|
|
// The screen at 640 pixels a line, as RGB and as each 640-mode pixel's
|
|
// 2-bit value (NO_VALUE on a 320-mode line).
|
|
static void readScreen(const uint8_t *shr, uint8_t *rgb, int8_t *values) {
|
|
uint8_t palette[PALETTE_ENTRIES][GATE_RGB_BYTES];
|
|
uint32_t scb;
|
|
uint32_t x;
|
|
uint32_t y;
|
|
uint32_t i;
|
|
uint32_t p;
|
|
uint32_t value;
|
|
uint8_t byte;
|
|
size_t at;
|
|
|
|
for (y = 0; y < HEIGHT; y++) {
|
|
scb = shr[SCB_AT + y];
|
|
for (i = 0; i < PALETTE_ENTRIES; i++) {
|
|
gateRgb12(gateLe16(shr + PALETTES_AT + (scb & SCB_PALETTE_MASK) * PALETTE_BYTES + i * ENTRY_BYTES), palette[i]);
|
|
}
|
|
for (x = 0; x < ROW_BYTES; x++) {
|
|
byte = shr[y * ROW_BYTES + x];
|
|
for (p = 0; p < PIXELS_A_BYTE_640; p++) {
|
|
at = (size_t)y * WIDTH_640 + x * PIXELS_A_BYTE_640 + p;
|
|
if (scb & SCB_640) {
|
|
value = (byte >> (BYTE_BITS - HALF_BITS - HALF_BITS * p)) & HALF_MASK;
|
|
values[at] = (int8_t)value;
|
|
memcpy(rgb + at * GATE_RGB_BYTES, palette[kQuarterBase[p] + value], GATE_RGB_BYTES);
|
|
} else {
|
|
value = (byte >> ((p / PIXEL_320_WIDTH == 0u) ? NIBBLE_BITS : 0u)) & NIBBLE_MASK;
|
|
values[at] = NO_VALUE;
|
|
memcpy(rgb + at * GATE_RGB_BYTES, palette[value], GATE_RGB_BYTES);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
uint8_t *shr;
|
|
uint8_t *icon;
|
|
uint8_t *rgb;
|
|
int8_t *values;
|
|
HalfT *halves;
|
|
const uint8_t *image;
|
|
const char *label;
|
|
size_t shrLen;
|
|
size_t iconLen;
|
|
uint32_t size;
|
|
uint32_t w;
|
|
uint32_t h;
|
|
uint32_t rowBytes;
|
|
uint32_t halfCount;
|
|
uint32_t found;
|
|
uint32_t firstX;
|
|
uint32_t firstY;
|
|
uint32_t x;
|
|
uint32_t y;
|
|
uint32_t i;
|
|
uint32_t shift;
|
|
uint32_t nibble;
|
|
uint32_t half;
|
|
bool match;
|
|
|
|
toolFileName(TOOL_NAME);
|
|
if (argc != 4 && argc != 5) {
|
|
return gateUsage(kUsage, GATE_COUNT_OF(kUsage));
|
|
}
|
|
label = argv[3];
|
|
if (!toolReadFile(argv[1], &shr, &shrLen) || shrLen < SHR_BYTES) {
|
|
toolFail("cannot read %u bytes of %s", (unsigned)SHR_BYTES, argv[1]);
|
|
}
|
|
rgb = (uint8_t *)malloc((size_t)WIDTH_640 * HEIGHT * GATE_RGB_BYTES);
|
|
values = (int8_t *)malloc((size_t)WIDTH_640 * HEIGHT);
|
|
if (rgb == NULL || values == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
readScreen(shr, rgb, values);
|
|
if (argc == 5 && argv[4][0] != '\0') {
|
|
keepScreen(argv[4], rgb);
|
|
}
|
|
|
|
// The big icon of the file's one record: its 4-bit pixels and mask,
|
|
// the even pixel in the high nibble.
|
|
if (!toolReadFile(argv[2], &icon, &iconLen) || iconLen < ICON_AT + ICON_HEADER_BYTES) {
|
|
toolFail("cannot read the icon record of %s", argv[2]);
|
|
}
|
|
size = gateLe16(icon + ICON_AT + ICON_SIZE_AT);
|
|
h = gateLe16(icon + ICON_AT + ICON_HEIGHT_AT);
|
|
w = gateLe16(icon + ICON_AT + ICON_WIDTH_AT);
|
|
image = icon + ICON_AT + ICON_HEADER_BYTES;
|
|
rowBytes = (w + ICON_PIXELS_A_BYTE - 1u) / ICON_PIXELS_A_BYTE;
|
|
if ((size_t)size + (size_t)rowBytes * h > iconLen - ICON_AT - ICON_HEADER_BYTES) {
|
|
toolFail("%s's icon is truncated", argv[2]);
|
|
}
|
|
halves = (HalfT *)malloc(((size_t)w * h + 1u) * HALVES * sizeof(HalfT));
|
|
halfCount = 0u;
|
|
if (halves == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
shift = (x % ICON_PIXELS_A_BYTE == 0u) ? NIBBLE_BITS : 0u;
|
|
if (((image[size + y * rowBytes + x / ICON_PIXELS_A_BYTE] >> shift) & NIBBLE_MASK) == 0u) {
|
|
continue;
|
|
}
|
|
// In 640 mode an icon pixel is two screen pixels: its high
|
|
// half, then its low half.
|
|
nibble = (image[y * rowBytes + x / ICON_PIXELS_A_BYTE] >> shift) & NIBBLE_MASK;
|
|
for (half = 0; half < HALVES; half++) {
|
|
halves[halfCount].x = x * HALVES + half;
|
|
halves[halfCount].y = y;
|
|
halves[halfCount].value = (int8_t)((half == 0u) ? nibble >> HALF_BITS : nibble & HALF_MASK);
|
|
halfCount++;
|
|
}
|
|
}
|
|
}
|
|
if (halfCount == 0u) {
|
|
toolFail("%s's icon has no opaque pixel", argv[2]);
|
|
}
|
|
|
|
found = 0u;
|
|
firstX = 0u;
|
|
firstY = 0u;
|
|
for (y = 0; y + h <= HEIGHT; y++) {
|
|
for (x = 0; x + w * HALVES <= WIDTH_640; x++) {
|
|
match = true;
|
|
for (i = 0; i < halfCount && match; i++) {
|
|
match = values[(size_t)(y + halves[i].y) * WIDTH_640 + x + halves[i].x] == halves[i].value;
|
|
}
|
|
if (match) {
|
|
firstX = (found == 0u) ? x : firstX;
|
|
firstY = (found == 0u) ? y : firstY;
|
|
found++;
|
|
}
|
|
}
|
|
}
|
|
if (found == 0u) {
|
|
printf("%s: FAIL - no program's icon is in the JOEYLIB window (%u opaque pixels looked for)\n", label, (unsigned)(halfCount / HALVES));
|
|
return FAIL_EXIT;
|
|
}
|
|
printf("%s: PASS (the Finder shows %u programs by the baked %ux%u colour icon, the first at %u,%u: all %u opaque pixels, each its two screen pixels' values)\n", label, (unsigned)found, (unsigned)w, (unsigned)h, (unsigned)firstX, (unsigned)firstY, (unsigned)(halfCount / HALVES));
|
|
free(halves);
|
|
free(icon);
|
|
free(values);
|
|
free(rgb);
|
|
free(shr);
|
|
return 0;
|
|
}
|