diff --git a/js/script.js b/js/script.js index 1c92b7d..bf1249e 100644 --- a/js/script.js +++ b/js/script.js @@ -155,6 +155,18 @@ const ConversionFunctions = { }; settings.conversionFunction = ConversionFunctions.horizontal1bit; +// Bytes per output value for each conversion function, matching the +// hexWidth passed to writer.push() in that function (hexWidth / 2). +// Needed by downloadBinFile() to reconstruct the true byte stream instead +// of truncating multi-byte values (565/888) down to a single byte. +const CONVERSION_BYTES_PER_VALUE = new Map([ + [ConversionFunctions.horizontal1bit, 1], + [ConversionFunctions.vertical1bit, 1], + [ConversionFunctions.horizontal565, 2], + [ConversionFunctions.horizontal888, 4], + [ConversionFunctions.horizontalAlpha, 1], +]); + // An images collection with helper methods function Images() { const collection = []; @@ -983,20 +995,37 @@ function copyOutput() { navigator.clipboard.writeText(output.value); } +// Rebuilds the true byte stream for the current images/conversion mode by +// re-splitting each formatted output value (which may be 1, 2 or 4 bytes +// wide, depending on drawMode) back into its individual bytes, most- +// significant first. Exposed standalone so it can be exercised directly in +// tests without going through a real browser file download. +function computeBinData() { + const bytesPerValue = CONVERSION_BYTES_PER_VALUE.get(settings.conversionFunction) || 1; + const raw = []; + images.each((image) => { + const values = imageToString(image) + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + .map((token) => parseInt(token, 16)); + values.forEach((value) => { + // Split back into individual bytes, most-significant first, matching + // the left-to-right digit order produced by toString(16).padStart(). + for (let shift = (bytesPerValue - 1) * 8; shift >= 0; shift -= 8) { + // eslint-disable-next-line no-bitwise + raw.push((value >> shift) & 0xff); + } + }); + }); + return new Uint8Array(raw); +} + // eslint-disable-next-line no-unused-vars function downloadBinFile() { if (!checkImagesAvailable()) return; - let raw = []; - images.each((image) => { - const data = imageToString(image) - .split(',') - .map((s) => s.trim()) - .filter(Boolean) - .map((byte) => parseInt(byte, 16)); - raw = raw.concat(data); - }); - const data = new Uint8Array(raw); + const data = computeBinData(); const a = document.createElement('a'); a.style = 'display: none'; document.body.appendChild(a); diff --git a/tests/test_conversions.py b/tests/test_conversions.py index 6bfb06e..59d856d 100644 --- a/tests/test_conversions.py +++ b/tests/test_conversions.py @@ -129,6 +129,45 @@ SCENARIOS = [ ), ] +# Bytes per output value per drawMode, mirroring CONVERSION_BYTES_PER_VALUE in +# js/script.js. Used to verify downloadBinFile()'s byte stream (via the +# computeBinData() helper) matches the hex values shown in the text output, +# instead of the multi-byte values (565/888) getting truncated to one byte. +BYTES_PER_VALUE = { + "horizontal1bit": 1, + "vertical1bit": 1, + "horizontal565": 2, + "horizontal888": 4, + "horizontalAlpha": 1, +} + +# Scenario names to also verify against computeBinData(); restricted to ones +# using the default plain/comma-separated output (outputFormat and separator +# untouched), since that's the format downloadBinFile's parser expects. +BIN_CHECK_NAMES = { + "default", + "draw_mode_vertical1bit", + "draw_mode_horizontal565", + "draw_mode_horizontal888", + "draw_mode_horizontal_alpha", + "rotate_90_horizontal565", + "rotate_90_horizontal888", + "rotate_90_horizontal_alpha", +} + + +def expected_bin_bytes(output, draw_mode): + """Reconstruct the byte stream a correct downloadBinFile() should produce + from the plain-format hex text output, expanding each value back to its + full byte width (MSB first) instead of assuming one byte per value.""" + bytes_per_value = BYTES_PER_VALUE[draw_mode] + values = (int(tok, 16) for tok in re.findall(r"0x[0-9a-fA-F]+", output)) + result = [] + for value in values: + for shift in range((bytes_per_value - 1) * 8, -1, -8): + result.append((value >> shift) & 0xFF) + return result + def reset_canvas_size(page, width, height): """Set canvas size through the real width/height text inputs.""" @@ -272,6 +311,15 @@ def main(): print(f"ok {name}") + if name in BIN_CHECK_NAMES: + expected_bytes = expected_bin_bytes(output, settings["drawMode"]) + actual_bytes = page.evaluate("Array.from(computeBinData())") + if actual_bytes != expected_bytes: + failures.append(f"{name}: bin data mismatch") + print(f"FAIL {name} (bin)") + else: + print(f"ok {name} (bin)") + if roundtrip_eligible(settings): width, height = resize or (NATIVE_WIDTH, NATIVE_HEIGHT) roundtripped = run_roundtrip(page, output, settings["drawMode"], width, height)