fix: lower Chinese full stop to bitmap cell baseline

This commit is contained in:
DragonHD
2026-09-20 11:55:26 +08:00
parent 8a061bc3a0
commit 997ae29355
4 changed files with 100 additions and 0 deletions
+8
View File
@@ -179,3 +179,11 @@ so centred labels (the "Start New Game" button) were flushed right. Fixed to
- Exactly eight escape bytes changed (`\b` to `\a`) across the intro message and repeated mission description.
- Center-coordinate test first failed on prior left-aligned mapping (0 != 66). New mapping passes at widths 360/400/480 using engine font metrics including 5px spaces. All 32 tests pass; rollback restores the previous copy exactly.
- User exited game; deployed centered file and reread/verified it. SHA256: `941fb4899d37304a304a985743c18cfd4b7dfed7613d831f8359c4d6b6972d6d`. Visual acceptance pending.
## 2026-09-20 — Chinese full-stop baseline correction
- Inspected live 12px SimSun atlas: U+3002 ink bounding box `(2,5,6,9)` in a 12x12 cell leaves three empty rows below, reproducing the user's raised punctuation.
- Anchored only U+3002 to the cell bottom: bbox `(2,8,6,12)`, same eight pixels and shape. All other atlas pixels and bytes remain identical. Updated builder to retain this placement on future generation; `fix_full_stop.py` patches existing TGA without rebuilding other glyphs.
- Prepared matching loose atlas and in-place same-size `Textures.res` entry; bytes before and after that entry remain identical. No executable, metrics, UV table, script, or other punctuation changes.
- 34 tests pass. Checked original/fixed glyph bounds and exact shape preservation; rollback of both files tested against backups in an isolated directory. Comparison PNG uses actual atlas pixels enlarged 8x.
- Backup: `E:/DragonHD/backup_game/full-stop-20260920`. User explicitly authorized terminating the running game for installation. Real in-game appearance pending.
+12
View File
@@ -157,6 +157,16 @@ def pick_font(size):
raise SystemExit("no CJK ttf found")
def lower_full_stop(tile):
"""Anchor the Chinese full stop at the bottom of its full-width cell."""
bbox = tile.getchannel('A').getbbox()
if bbox is None:
return tile.copy()
result = Image.new('RGBA', tile.size, (0, 0, 0, 0))
result.paste(tile.crop(bbox), (bbox[0], tile.height - (bbox[3] - bbox[1])))
return result
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--game", default=DEFAULT_GAME)
@@ -211,6 +221,8 @@ def main():
x = col * cell
y = args.cjk_y0 + row * cell
d.text((x, y + args.draw_dy), uni, font=font, fill=(255, 255, 255, 255))
if uni == '\u3002':
atlas.paste(lower_full_stop(atlas.crop((x, y, x + cell, y + cell))), (x, y))
cells[code] = {"x": x, "y": y, "uni": uni}
# sanity: ink must fit the cell
bb = atlas.crop((x, y, x + cell, y + cell)).getbbox()
+45
View File
@@ -0,0 +1,45 @@
"""Patch only U+3002 pixels in an existing CJK atlas; preserve its TGA bytes."""
import argparse
import json
from pathlib import Path
from build_cjk import lower_full_stop, tga_to_image, tga_geometry
def patch_atlas(raw, metadata):
image, desc = tga_to_image(raw)
cell = metadata['cell']
if list(image.size) != metadata['atlas']:
raise ValueError('atlas geometry mismatch')
entry = metadata['cells'][str(int.from_bytes(''.encode('gb2312'), 'big'))]
if entry['uni'] != '':
raise ValueError('full-stop cell mismatch')
x, y = entry['x'], entry['y']
tile = image.crop((x, y, x + cell, y + cell))
fixed = lower_full_stop(tile)
idlen, _, w, h, _, _ = tga_geometry(raw)
result = bytearray(raw)
for j in range(cell):
row = y + j if desc & 0x20 else h - 1 - y - j
for i in range(cell):
offset = 18 + idlen + (row * w + x + i) * 4
r, g, b, a = fixed.getpixel((i, j))
result[offset:offset + 4] = bytes((b, g, r, a))
return bytes(result)
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--source', type=Path, required=True)
p.add_argument('--cells', type=Path, required=True)
p.add_argument('--out', type=Path, required=True)
args = p.parse_args()
if args.source.resolve() == args.out.resolve():
p.error('output must differ from source')
result = patch_atlas(args.source.read_bytes(), json.loads(args.cells.read_text(encoding='utf-8')))
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_bytes(result)
print('Full stop anchored at cell bottom; other atlas bytes preserved.')
if __name__ == '__main__':
main()
+35
View File
@@ -0,0 +1,35 @@
import unittest
from PIL import Image
from build_cjk import lower_full_stop, image_to_tga, tga_to_image
from fix_full_stop import patch_atlas
class FullStopTests(unittest.TestCase):
def test_ink_moves_to_bottom_without_shape_loss(self):
tile = Image.new('RGBA', (12, 12))
for x, y in [(3, 5), (4, 5), (2, 6), (5, 6), (2, 7), (5, 7), (3, 8), (4, 8)]:
tile.putpixel((x, y), (255, 255, 255, 255))
fixed = lower_full_stop(tile)
self.assertEqual(fixed.getbbox(), (2, 8, 6, 12))
self.assertEqual(tile.crop((2, 5, 6, 9)).tobytes(), fixed.crop((2, 8, 6, 12)).tobytes())
self.assertEqual(lower_full_stop(fixed).tobytes(), fixed.tobytes())
def test_patch_preserves_neighbors_header_footer_and_orientation(self):
for desc in (8, 40):
image = Image.new('RGBA', (24, 24), (7, 8, 9, 255))
image.paste((0, 0, 0, 0), (12, 12, 24, 24))
image.putpixel((14, 17), (255, 255, 255, 255))
raw = image_to_tga(image, desc, b'unchanged-footer')
meta = {'cell': 12, 'atlas': [24, 24], 'cells': {'41379': {'uni': '', 'x': 12, 'y': 12}}}
fixed = patch_atlas(raw, meta)
after, _ = tga_to_image(fixed)
self.assertEqual(fixed[:18], raw[:18])
self.assertTrue(fixed.endswith(b'unchanged-footer'))
self.assertEqual(after.crop((0, 0, 24, 12)).tobytes(), image.crop((0, 0, 24, 12)).tobytes())
self.assertEqual(after.crop((0, 12, 12, 24)).tobytes(), image.crop((0, 12, 12, 24)).tobytes())
self.assertEqual(after.getpixel((14, 23)), (255, 255, 255, 255))
self.assertEqual(patch_atlas(fixed, meta), fixed)
if __name__ == '__main__':
unittest.main()