0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ASCIIアートの空白を保持して検査する: Python標準ライブラリの厳密なグリッド検査

0
Posted at

ASCIIアートの空白を壊さず検査するために、用途を限定した Python の検査器と実行済みテストを紹介します。Unicode の表示幅や端末エミュレーションは対象外です。

Why inspect the grid rather than the picture?

Character art moves through surprisingly many representations. A browser editor holds a string, a clipboard carries that string into another application, Markdown adds a formatting layer, and a screenshot finally turns the result into pixels. A picture can look plausible while the underlying text has already lost its indentation. Conversely, a perfectly rectangular string can look distorted when displayed in a proportional font. These are different problems, and trying to diagnose both with a screenshot alone makes regressions hard to reproduce.

This article develops a deliberately narrow inspection utility for printable ASCII grids. It reports row widths without trimming spaces, rejects unsupported characters, and fingerprints the original bytes. It is not an image converter or a font measurement library. The useful engineering question is smaller: did the text representation change before a renderer had a chance to draw it? That question can be answered with deterministic fixtures and a small standard-library script instead of a visual judgment.

Disclosure: I help promote ASCII Art Generator, and AI assisted with drafting this article. The Python implementation and tests below were executed locally. They validate only the documented utility contract, not the product's internal implementation, security, or every export format. A browser ascii art generator provides an optional source of character grids; none of the tests require opening it, creating an account, sending an image, or purchasing anything.

Define the accepted representation first

The contract accepts printable characters from the ordinary space through the tilde, together with newline separators. That choice excludes tabs, terminal escapes, carriage returns that are not part of CRLF, and all non-ASCII text. Exclusion is intentional rather than a claim that those characters are bad. Unicode terminal width involves combining marks, emoji sequences, ambiguous-width characters, font selection, and environment-specific behavior. A function that simply counts Python characters should not pretend to solve those problems.

Spaces remain data. Leading spaces position a subject, internal spaces separate features, and trailing spaces can be necessary to preserve the width of a rectangle. Calling strip on the whole document or rstrip on each line would silently rewrite the very property being inspected. The utility therefore performs just one normalization: CRLF becomes LF for row analysis. Its digest still covers the untouched input bytes, so a line-ending conversion remains detectable even when the reported dimensions stay identical.

Empty input deserves its own definition. Here it contains zero rows and has no column count. A single newline represents one empty row with zero columns. One final newline terminates the last row instead of manufacturing another row after it. Two final newlines preserve an actual empty row. These conventions are not universal truth; they are explicit choices that make a later diff meaningful. If your storage format uses another convention, change the tests and implementation together.

A small implementation with a bounded command-line read

The function accepts bytes because decoding and line endings are part of the boundary under inspection. It checks the byte budget before decoding, avoiding unnecessary work on an oversized payload. The command-line wrapper reads only one byte beyond that budget, which is sufficient to detect an oversized file without loading an arbitrarily large file into memory. Callers who invoke the function directly already own a bytes object; the function cannot undo memory allocation that happened before the call.

The result distinguishes a rectangular grid from a ragged one rather than trying to repair it. A ragged input returns its complete width vector and a null column count. That gives a caller enough information to highlight the first mismatch or decide whether explicit padding is appropriate. Silently padding would make inspection destructive and could hide a clipped final row. Similarly, control characters raise an exception instead of being replaced with spaces that might suggest the original content was well formed.

ascii-qiita-gridcheck.py
"""Strict printable-ASCII rectangle inspection, not terminal emulation."""
import hashlib
import json
import sys
from pathlib import Path


def inspect_grid(raw: bytes, *, max_bytes=100_000):
    if max_bytes < 0:
        raise ValueError('max_bytes must be nonnegative')
    if len(raw) > max_bytes:
        raise ValueError('byte budget exceeded')
    # Only normalize CRLF. Bare CR, tabs, escapes and Unicode are rejected.
    text = raw.decode('ascii').replace('\r\n', '\n')
    if any(c != '\n' and not (' ' <= c <= '~') for c in text):
        raise ValueError('only printable ASCII plus newlines is accepted')
    # One final newline terminates the last row, not an additional blank row.
    lines = text.split('\n')
    if text.endswith('\n'):
        lines.pop()
    if not text:
        lines = []
    widths = [len(line) for line in lines]
    return {
        'rows': len(lines),
        'widths': widths,
        'rectangular': len(set(widths)) <= 1,
        'columns': widths[0] if widths and len(set(widths)) == 1 else None,
        'sha256_raw': hashlib.sha256(raw).hexdigest(),
    }


if __name__ == '__main__':
    # Bound the read itself, including the oversize detection byte.
    with Path(sys.argv[1]).open('rb') as stream:
        raw = stream.read(100_001)
    print(json.dumps(inspect_grid(raw), indent=2))

Test the boundary cases, not just the happy path

Save the implementation and tests using the filenames shown in the code blocks. The test loader resolves the implementation relative to the test file, so the example works without a package installation. Run the test module directly with Python. The suite has eight test methods, with subcases for several controls and Unicode inputs. All eight methods passed in the local execution used to prepare this article. That is evidence for these examples, not an exhaustive proof of correctness for every possible byte sequence.

The rectangular fixture includes both leading and trailing whitespace, while another fixture is intentionally ragged. The CRLF fixture checks equal dimensions but different raw digests. Empty input and a newline-only input test the chosen row convention separately. Budget tests cover the accepted boundary, one byte over it, and a negative configuration. These small examples protect semantic choices that a refactoring could easily change accidentally, especially if someone replaces the explicit splitting logic with a more convenient but differently specified helper.

ascii-qiita-test-gridcheck.py
import importlib.util
import unittest
from pathlib import Path

spec = importlib.util.spec_from_file_location('gridcheck', Path(__file__).with_name('ascii-qiita-gridcheck.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
inspect_grid = module.inspect_grid


class GridTests(unittest.TestCase):
    def test_rectangle(self):
        result = inspect_grid(b' .#\n## \n')
        self.assertEqual(result['widths'], [3, 3])
        self.assertEqual((result['rows'], result['columns']), (2, 3))
        self.assertTrue(result['rectangular'])

    def test_ragged(self):
        result = inspect_grid(b'a\nbb')
        self.assertFalse(result['rectangular'])
        self.assertIsNone(result['columns'])

    def test_crlf_and_digest(self):
        a, b = inspect_grid(b'xx\n'), inspect_grid(b'xx\r\n')
        self.assertEqual(a['widths'], b['widths'])
        self.assertNotEqual(a['sha256_raw'], b['sha256_raw'])

    def test_whitespace_preserved(self):
        self.assertEqual(inspect_grid(b'  a  ')['widths'], [5])
        self.assertEqual(inspect_grid(b'a\n\n')['widths'], [1, 0])

    def test_empty_and_blank(self):
        self.assertEqual(inspect_grid(b'')['rows'], 0)
        self.assertIsNone(inspect_grid(b'')['columns'])
        self.assertEqual(inspect_grid(b'\n')['widths'], [0])
        self.assertEqual(inspect_grid(b'\n')['columns'], 0)

    def test_reject_controls(self):
        for raw in (b'a\tb', b'x\r', b'\x1b[31mx', b'\0', b'\x7f'):
            with self.subTest(raw=raw), self.assertRaises(ValueError):
                inspect_grid(raw)

    def test_reject_unicode(self):
        for text in ('é', '界', 'e\u0301'):
            with self.subTest(text=text), self.assertRaises(UnicodeDecodeError):
                inspect_grid(text.encode())

    def test_byte_budget(self):
        self.assertEqual(inspect_grid(b'ab', max_bytes=2)['columns'], 2)
        with self.assertRaises(ValueError):
            inspect_grid(b'abc', max_bytes=2)
        with self.assertRaises(ValueError):
            inspect_grid(b'', max_bytes=-1)


if __name__ == '__main__':
    unittest.main()

Use the report in a real debugging workflow

Start with a small non-sensitive grid and save it as a plain text fixture. Inspect that file before involving a rich editor. Then paste the same text into the destination application, copy it back into a second file, and run the inspector again. Compare the raw digests first. Equal digests establish byte-for-byte identity for those two saved files. Different digests tell you to investigate; they do not tell you which application is at fault or whether the difference is visually important.

Next compare rows and widths. If dimensions match but the digest differs, line endings or a substituted printable character may explain the result. If widths differ, check trimming, automatic wrapping that became actual newlines, and indentation changes. A soft visual wrap does not necessarily change the source string, so distinguish the editor's display from the saved text. A Markdown code fence often preserves whitespace more reliably than an ordinary paragraph, but verify the rendered result rather than assuming all publishing systems behave alike.

Only after the text survives this round trip should you diagnose presentation. Choose a monospace font, inspect the container's whitespace rules, and check whether the viewport is narrow enough to cause clipping or scrolling. Screenshot dimensions and text-grid dimensions measure different things. A screenshot can be useful for comparing font appearance, but it cannot by itself prove that a trailing space survived copying. Keep both kinds of artifact when the failure crosses the text and rendering boundaries.

Limits and possible extensions

This inspector does not validate a particular browser tool's downloads. ASCII Art Generator's public interface offers image and text workflows, manual cell editing, and export controls. Its pricing page distinguishes free core browser tools from optional credit-based AI rendering. Those product facts do not establish that any specific saved file satisfies this utility's strict printable-ASCII policy. An export using block characters will be rejected by design. Choose an appropriate inspection method instead of changing the file merely to obtain a passing report.

For a broader library, consider returning structured rejection details with the offending offset, or adding a separately named Unicode inspection mode backed by a documented width model. Keep the strict function stable rather than silently broadening it. Record environment assumptions with visual fixtures, and retain the original bytes before any normalization. The central lesson is not that every grid must be rectangular: it is that transformations should be explicit, observable, and tested at the boundary where information might disappear.
current-public-home.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?