Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
- name: Install package
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install -e ".[test]"
python -m pip install -e ".[test,cv]"

- name: Run tests
run: python -m pytest
Expand All @@ -100,6 +100,16 @@ jobs:
- name: CLI smoke — validate example
run: hletterscriptgen validate examples/letter_set/writer_example.json --format json

- name: CLI smoke — scan-blobs
run: |
python -c "
import cv2, numpy as np
img = np.full((100, 100, 3), 255, dtype=np.uint8)
img[10:30, 10:30] = 0
cv2.imwrite('/tmp/smoke_scan.png', img)
"
hletterscriptgen scan-blobs /tmp/smoke_scan.png --format json

package:
name: Build distributions
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ hletterscriptgen validate examples/letter_set/writer_example.json --format json

## Stable public surfaces

- CLI: `hletterscriptgen {version, schema, validate, generate, check-eligible}`.
- CLI: `hletterscriptgen {version, schema, validate, generate, check-eligible, scan-blobs}`.
- Output contract: `letter_set.v1` (see
`src/hletterscriptgen/schemas/letter_set.schema.json` and
`docs/letter_set_v1.md`).
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,7 @@ enable_error_code = ["redundant-expr", "truthy-bool", "ignore-without-code"]
[[tool.mypy.overrides]]
module = "jsonschema.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "cv2"
ignore_missing_imports = true
128 changes: 118 additions & 10 deletions src/hletterscriptgen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,34 @@ def _build_parser() -> argparse.ArgumentParser:
help="Output format (default: text).",
)

sub.add_parser(
generate_p = sub.add_parser(
"generate",
help="(Not yet implemented) Generate letter sets from upstream scans.",
help="Generate letter sets from upstream scans using a generation profile.",
)
generate_p.add_argument(
"--profile",
type=Path,
required=True,
metavar="PROFILE",
help="Path to a generation profile JSON file.",
)
generate_p.add_argument(
"--output",
type=Path,
required=True,
metavar="DIR",
help="Output directory (created if absent).",
)
generate_p.add_argument(
"--generated-at",
type=str,
default=None,
metavar="ISO8601",
help=(
"Override the generated_at timestamp in output documents "
"(ISO 8601 format, e.g. '2025-01-01T00:00:00+00:00'). "
"Useful for deterministic / reproducible builds."
),
)

eligible_p = sub.add_parser(
Expand All @@ -74,6 +99,39 @@ def _build_parser() -> argparse.ArgumentParser:
help="Output format (default: text).",
)

scan_blobs_p = sub.add_parser(
"scan-blobs",
help=(
"Detect glyph blobs in a scan image via CCA. "
"Use the output to populate a generation profile."
),
)
scan_blobs_p.add_argument(
"image",
type=Path,
help="Path to the scan image (JPEG, PNG, TIFF, …).",
)
scan_blobs_p.add_argument(
"--min-dim",
type=int,
default=16,
metavar="PX",
help="Minimum blob dimension in pixels (default: 16).",
)
scan_blobs_p.add_argument(
"--max-area",
type=int,
default=None,
metavar="PX2",
help="Maximum blob area in pixels (default: 10%% of image area).",
)
scan_blobs_p.add_argument(
"--format",
choices=("text", "json"),
default="json",
help="Output format (default: json).",
)

return parser


Expand Down Expand Up @@ -160,13 +218,61 @@ def _cmd_check_eligible(args: argparse.Namespace) -> int:
return EXIT_OK if ok else EXIT_VALIDATION_FAILED


def _cmd_generate() -> int:
print(
"generate: not yet implemented in this scaffolding release. "
"See docs/roadmap.md for planned milestones.",
file=sys.stderr,
)
return EXIT_NOT_IMPLEMENTED
def _cmd_generate(args: argparse.Namespace) -> int:
from hletterscriptgen.generate_profile import GenerateProfileError, load_generate_profile
from hletterscriptgen.generator import GeneratorError, generate

try:
profile = load_generate_profile(args.profile)
except GenerateProfileError as exc:
print(str(exc), file=sys.stderr)
return EXIT_INPUT_ERROR

try:
output_paths = generate(
profile,
args.output,
generated_at=args.generated_at,
)
except GeneratorError as exc:
print(str(exc), file=sys.stderr)
return EXIT_INPUT_ERROR

for p in output_paths:
print(f"OK {p}")
return EXIT_OK


def _cmd_scan_blobs(args: argparse.Namespace) -> int:
from hletterscriptgen.extractor import ExtractionError, extract_glyphs

try:
glyphs = extract_glyphs(
args.image,
min_dimension=args.min_dim,
max_area=args.max_area,
)
except ExtractionError as exc:
print(str(exc), file=sys.stderr)
return EXIT_INPUT_ERROR

if args.format == "json":
payload = {
"image": str(args.image),
"count": len(glyphs),
"blobs": [
{"x": g.x, "y": g.y, "width": g.width, "height": g.height}
for g in glyphs
],
}
json.dump(payload, sys.stdout, indent=2)
sys.stdout.write("\n")
else:
for i, g in enumerate(glyphs):
print(f"blob {i:4d}: x={g.x:5d} y={g.y:5d} w={g.width:5d} h={g.height:5d}")
print(f"{len(glyphs)} blob(s) detected in {args.image}")

return EXIT_OK


def main(argv: list[str] | None = None) -> int:
Expand All @@ -180,8 +286,10 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "validate":
return _cmd_validate(args)
if args.command == "generate":
return _cmd_generate()
return _cmd_generate(args)
if args.command == "check-eligible":
return _cmd_check_eligible(args)
if args.command == "scan-blobs":
return _cmd_scan_blobs(args)

parser.error(f"unknown command: {args.command}")
Loading