fix: lower Chinese full stop to bitmap cell baseline
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user