200 lines
6.8 KiB
Python
Executable file
200 lines
6.8 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# compareFrames.py - compare a frame from Sierra's interpreter with one
|
|
# from JoeyAGI. Both are mapped to the 16 EGA colours, then compared one
|
|
# 8x8 text cell at a time. JoeyAGI draws text in its own font, so a cell
|
|
# where both frames hold a character -- read with each interpreter's font,
|
|
# captured from the "font" probe -- counts as equal when the characters
|
|
# and colours match. Every other differing pixel is a real difference.
|
|
#
|
|
# Sierra shows its text screen (text.screen, the inventory) in the PC's
|
|
# 40-column text mode, which DOSBox captures at 360x400 (9x16 cells).
|
|
# JoeyAGI draws the same screen on the bitmap, so those frames are
|
|
# compared cell by cell: blank or not, and the colours.
|
|
#
|
|
# compareFrames.py ORACLE JOEY [DIFF.png] [--fonts ORACLEFONT JOEYFONT]
|
|
# prints "real=N text=M" and the cells that differ; exit 1 if any do.
|
|
import sys
|
|
from collections import Counter
|
|
|
|
from PIL import Image
|
|
|
|
EGA = [(0, 0, 0), (0, 0, 170), (0, 170, 0), (0, 170, 170), (170, 0, 0), (170, 0, 170), (170, 85, 0),
|
|
(170, 170, 170), (85, 85, 85), (85, 85, 255), (85, 255, 85), (85, 255, 255), (255, 85, 85),
|
|
(255, 85, 255), (255, 255, 85), (255, 255, 255)]
|
|
WIDTH = 320
|
|
HEIGHT = 200
|
|
TEXT_MODE_SIZE = (360, 400)
|
|
TEXT_CELL_W = 9
|
|
TEXT_CELL_H = 16
|
|
CELL = 8
|
|
COLS = WIDTH // CELL
|
|
ROWS = HEIGHT // CELL
|
|
WINDOW_FRAME = 4
|
|
WINDOW_BG = 15
|
|
WINDOW_MIN_RUN = 16
|
|
UNKNOWN = "?"
|
|
# The font probe displays these rows from row 0, column 0.
|
|
FONT_ROWS = ['!"#$&\'()*+,-./0123456789:;<=>?', '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^', '_`abcdefghijklmnopqrstuvwxyz{|', '}~']
|
|
|
|
_nearest = {}
|
|
|
|
|
|
def isTextMode(path):
|
|
return Image.open(path).size == TEXT_MODE_SIZE
|
|
|
|
|
|
def egaGrid(im):
|
|
px = im.load()
|
|
grid = []
|
|
for y in range(im.size[1]):
|
|
row = []
|
|
for x in range(im.size[0]):
|
|
c = px[x, y]
|
|
if c not in _nearest:
|
|
_nearest[c] = min(range(16), key=lambda i: sum((a - b) ** 2 for a, b in zip(EGA[i], c)))
|
|
row.append(_nearest[c])
|
|
grid.append(row)
|
|
return grid
|
|
|
|
|
|
def load(path):
|
|
# A frame as rows of EGA colour numbers, scaled to 320x200 if need be.
|
|
im = Image.open(path).convert("RGB")
|
|
if im.size != (WIDTH, HEIGHT):
|
|
im = im.resize((WIDTH, HEIGHT), Image.NEAREST)
|
|
return egaGrid(im)
|
|
|
|
|
|
def textCells(grid, cellW, cellH):
|
|
# Each cell as (blank?, foreground, background): the background is
|
|
# the cell's commonest colour.
|
|
cells = []
|
|
for cy in range(ROWS):
|
|
row = []
|
|
for cx in range(COLS):
|
|
colours = Counter(grid[cy * cellH + y][cx * cellW + x] for y in range(cellH) for x in range(cellW))
|
|
common = colours.most_common()
|
|
if len(common) == 1:
|
|
row.append((True, None, common[0][0]))
|
|
else:
|
|
row.append((False, common[1][0], common[0][0]))
|
|
cells.append(row)
|
|
return cells
|
|
|
|
|
|
def compareTextMode(oraclePath, joeyPath):
|
|
a = textCells(egaGrid(Image.open(oraclePath).convert("RGB")), TEXT_CELL_W, TEXT_CELL_H)
|
|
b = textCells(load(joeyPath), CELL, CELL)
|
|
cells = [(cx, cy) for cy in range(ROWS) for cx in range(COLS) if a[cy][cx] != b[cy][cx]]
|
|
return 0, len(cells), cells
|
|
|
|
|
|
def cellBits(g, cx, cy, bg):
|
|
return tuple(tuple(1 if g[cy * CELL + y][cx * CELL + x] != bg else 0 for x in range(CELL)) for y in range(CELL))
|
|
|
|
|
|
def cellColours(g, cx, cy):
|
|
return Counter(g[cy * CELL + y][cx * CELL + x] for y in range(CELL) for x in range(CELL))
|
|
|
|
|
|
def fontTemplates(path):
|
|
g = load(path)
|
|
t = {}
|
|
for r, s in enumerate(FONT_ROWS):
|
|
for i, ch in enumerate(s):
|
|
t[cellBits(g, i, r, 0)] = ch
|
|
return t
|
|
|
|
|
|
def readCell(g, cx, cy, templates):
|
|
# (character, foreground, background) or None when it is not text.
|
|
colours = cellColours(g, cx, cy)
|
|
if len(colours) > 2:
|
|
return None
|
|
if len(colours) == 1:
|
|
return (" ", None, next(iter(colours)))
|
|
(a, na), (b, nb) = colours.most_common()
|
|
for bg, fg in ((a, b), (b, a)):
|
|
ch = templates.get(cellBits(g, cx, cy, bg))
|
|
if ch is not None:
|
|
return (ch, fg, bg)
|
|
return None
|
|
|
|
|
|
def compare(a, b, fontA=None, fontB=None):
|
|
real = 0
|
|
text = 0
|
|
cells = []
|
|
for cy in range(ROWS):
|
|
for cx in range(COLS):
|
|
diff = sum(1 for y in range(cy * CELL, cy * CELL + CELL) for x in range(cx * CELL, cx * CELL + CELL)
|
|
if a[y][x] != b[y][x])
|
|
if diff == 0:
|
|
continue
|
|
if fontA and fontB:
|
|
ta = readCell(a, cx, cy, fontA)
|
|
tb = readCell(b, cx, cy, fontB)
|
|
if ta and tb:
|
|
if ta[0] == tb[0] and (ta[0] == " " or ta[1:] == tb[1:]) and ta[2] == tb[2]:
|
|
continue
|
|
text += 1
|
|
cells.append((cx, cy, ta[0], tb[0]))
|
|
continue
|
|
real += diff
|
|
cells.append((cx, cy))
|
|
return real, text, cells
|
|
|
|
|
|
def messageWindows(g):
|
|
# Message windows: a red frame run of at least 16 pixels below white.
|
|
found = []
|
|
for y in range(1, HEIGHT - 1):
|
|
x = 0
|
|
while x < WIDTH:
|
|
if g[y][x] == WINDOW_FRAME and g[y - 1][x] == WINDOW_BG:
|
|
x2 = x
|
|
while x2 < WIDTH and g[y][x2] == WINDOW_FRAME:
|
|
x2 += 1
|
|
if x2 - x >= WINDOW_MIN_RUN:
|
|
found.append((x, y, x2 - 1))
|
|
x = x2
|
|
else:
|
|
x += 1
|
|
return found
|
|
|
|
|
|
def diffImage(a, b, cells, path):
|
|
marked = set((c[0], c[1]) for c in cells)
|
|
out = Image.new("RGB", (WIDTH * 3, HEIGHT))
|
|
for y in range(HEIGHT):
|
|
for x in range(WIDTH):
|
|
out.putpixel((x, y), EGA[a[y][x]])
|
|
out.putpixel((WIDTH + x, y), EGA[b[y][x]])
|
|
if a[y][x] != b[y][x] and (x // CELL, y // CELL) in marked:
|
|
d = (255, 0, 0)
|
|
else:
|
|
d = tuple(v // 4 for v in EGA[a[y][x]])
|
|
out.putpixel((2 * WIDTH + x, y), d)
|
|
out.resize((WIDTH * 6, HEIGHT * 2), Image.NEAREST).save(path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = sys.argv[1:]
|
|
fonts = (None, None)
|
|
if "--fonts" in args:
|
|
i = args.index("--fonts")
|
|
fonts = (fontTemplates(args[i + 1]), fontTemplates(args[i + 2]))
|
|
del args[i:i + 3]
|
|
if len(args) < 2:
|
|
sys.exit("usage: compareFrames.py ORACLE JOEY [DIFF.png] [--fonts ORACLEFONT JOEYFONT]")
|
|
if isTextMode(args[0]):
|
|
real, text, cells = compareTextMode(args[0], args[1])
|
|
print("text mode: cells=%d %s" % (text, cells[:16]))
|
|
else:
|
|
a = load(args[0])
|
|
b = load(args[1])
|
|
real, text, cells = compare(a, b, fonts[0], fonts[1])
|
|
print("real=%d text=%d %s" % (real, text, cells[:16]))
|
|
if len(args) > 2:
|
|
diffImage(a, b, cells, args[2])
|
|
sys.exit(1 if real or text else 0)
|