* Update server-side database and dynamic resource code for Python 3 * Preserve existing Flipnote, UGO, NTFT, CSS, localization, and Hatena assets * Port Flipnote upload and movie endpoints * Update front page UGO menu generators * Preserve existing database and sample Flipnote data * Add regression tests for database operations and binary formats * Add server resource loading tests * Maintain compatibility with the original DSi Flipnote Studio protocol
99 lines
3.6 KiB
Python
99 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from Hatenatools import NTFT, PPM, TMB, UGO
|
|
from Hatenatools.PPM import WriteImage, get_metadata
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SAMPLE_PPM = ROOT / "database" / "Creators" / "913304C0CC74CC1F" / "74CC1F_0D3AA40F2B3A0_000.ppm"
|
|
NEWS_NTFT = ROOT / "hatenadir" / "ds" / "v2-xx" / "__ntfts" / "News.ntft"
|
|
KAERU_NTFT = ROOT / "hatenadir" / "images" / "ds" / "kaeru.ntft"
|
|
INDEX_UGOXML = ROOT / "hatenadir" / "ds" / "v2-xx" / "index.ugoxml"
|
|
INBOX_UGOXML = ROOT / "hatenadir" / "ds" / "v2-xx" / "inbox.ugoxml"
|
|
|
|
|
|
class PPMTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.data = SAMPLE_PPM.read_bytes()
|
|
|
|
def test_tmb_metadata_and_exact_pack(self):
|
|
tmb = TMB().Read(self.data[:0x6A0], DecodeThumbnail=True)
|
|
self.assertTrue(tmb)
|
|
self.assertEqual(tmb.EditorAuthorID, "913304C0CC74CC1F")
|
|
self.assertEqual(tmb.CurrentFilename, "74CC1F_0D3AA40F2B3A0_000.tmb")
|
|
self.assertEqual(tmb.FrameCount, 3)
|
|
self.assertEqual(tmb.GetThumbnail().shape, (64, 48))
|
|
self.assertEqual(tmb.Pack(), self.data[:0x6A0])
|
|
|
|
def test_ppm_full_parse_and_frame(self):
|
|
ppm = PPM().Read(self.data, DecodeThumbnail=True, ReadFrames=True, ReadSound=True)
|
|
self.assertTrue(ppm)
|
|
self.assertEqual(ppm.EditorAuthorID, "913304C0CC74CC1F")
|
|
self.assertEqual(ppm.CurrentFilename, "74CC1F_0D3AA40F2B3A0_000.ppm")
|
|
self.assertEqual(ppm.FrameCount, 3)
|
|
self.assertEqual(ppm.GetThumbnail().shape, (64, 48))
|
|
frame = ppm.GetFrame(0)
|
|
self.assertIsInstance(frame, np.ndarray)
|
|
self.assertEqual(frame.shape, (256, 192))
|
|
self.assertEqual(len(ppm.SoundData), 4)
|
|
|
|
def test_metadata_helper_infers_filetype(self):
|
|
ppm = PPM().Read(self.data, ReadFrames=False)
|
|
metadata = get_metadata(ppm)
|
|
self.assertEqual(metadata["Current filename"], "74CC1F_0D3AA40F2B3A0_000.ppm")
|
|
self.assertEqual(metadata["Number of frames"], 3)
|
|
self.assertIn("Frame speed", metadata)
|
|
|
|
def test_image_export(self):
|
|
ppm = PPM().Read(self.data, DecodeThumbnail=True, ReadFrames=False)
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
output = Path(tmp) / "thumb.png"
|
|
self.assertTrue(WriteImage(ppm.GetThumbnail(), str(output)))
|
|
self.assertGreater(output.stat().st_size, 0)
|
|
|
|
|
|
class NTFTTests(unittest.TestCase):
|
|
def test_exact_roundtrip_news(self):
|
|
raw = NEWS_NTFT.read_bytes()
|
|
image = NTFT().Read(raw, (32, 32))
|
|
self.assertTrue(image)
|
|
self.assertEqual(image.Pack(), raw)
|
|
|
|
def test_exact_roundtrip_kaeru(self):
|
|
raw = KAERU_NTFT.read_bytes()
|
|
image = NTFT().Read(raw, (36, 30))
|
|
self.assertTrue(image)
|
|
self.assertEqual(image.Pack(), raw)
|
|
|
|
|
|
class UGOTests(unittest.TestCase):
|
|
def _roundtrip_xml(self, path: Path):
|
|
ugo = UGO().ReadXML(str(path), False)
|
|
self.assertTrue(ugo)
|
|
packed = ugo.Pack()
|
|
self.assertIsInstance(packed, bytes)
|
|
self.assertEqual(packed[:4], b"UGAR")
|
|
decoded = UGO().Read(packed)
|
|
self.assertTrue(decoded)
|
|
# Embedded-file names are XML-side metadata and are not stored in the
|
|
# binary UGO format. The meaningful binary round-trip is idempotence.
|
|
self.assertEqual(decoded.Pack(), packed)
|
|
|
|
def test_index_xml(self):
|
|
self._roundtrip_xml(INDEX_UGOXML)
|
|
|
|
def test_inbox_xml(self):
|
|
self._roundtrip_xml(INBOX_UGOXML)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
