outloud 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- outloud/__init__.py +47 -0
- outloud/cli.py +125 -0
- outloud/content.py +527 -0
- outloud/criteria.py +250 -0
- outloud/findings.py +109 -0
- outloud/fonts.py +344 -0
- outloud/model.py +490 -0
- outloud/registry.py +257 -0
- outloud/report.py +322 -0
- outloud/rules/__init__.py +4 -0
- outloud/rules/_common.py +129 -0
- outloud/rules/catalogue.yaml +927 -0
- outloud/rules/document.py +206 -0
- outloud/rules/figures.py +75 -0
- outloud/rules/headings.py +99 -0
- outloud/rules/links.py +170 -0
- outloud/rules/lists.py +43 -0
- outloud/rules/navigation.py +35 -0
- outloud/rules/notes_math.py +106 -0
- outloud/rules/order.py +53 -0
- outloud/rules/pagination.py +56 -0
- outloud/rules/quality.py +105 -0
- outloud/rules/tables.py +308 -0
- outloud/rules/tagging.py +358 -0
- outloud/rules/text.py +154 -0
- outloud/viewer.py +521 -0
- outloud-0.2.0.dist-info/METADATA +244 -0
- outloud-0.2.0.dist-info/RECORD +31 -0
- outloud-0.2.0.dist-info/WHEEL +4 -0
- outloud-0.2.0.dist-info/entry_points.txt +2 -0
- outloud-0.2.0.dist-info/licenses/LICENSE +202 -0
outloud/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""outloud: a fast PDF accessibility checker for the terminal.
|
|
2
|
+
|
|
3
|
+
from outloud import check
|
|
4
|
+
result = check("report.pdf")
|
|
5
|
+
print(result.verdict, result.errors, result.warnings)
|
|
6
|
+
for row in result.criteria["wcag22"]:
|
|
7
|
+
print(row["id"], row["name"], row["status"])
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from .findings import Result
|
|
16
|
+
from .model import Document
|
|
17
|
+
from .registry import features, load_catalogue, run_rules
|
|
18
|
+
|
|
19
|
+
__version__ = "0.2.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check(path: str, source: Optional[str] = None, only=None, skip=None, layers=None) -> Result:
|
|
23
|
+
"""Check one PDF and return a Result; never raises for a bad file."""
|
|
24
|
+
from .criteria import evaluate # noqa: PLC0415
|
|
25
|
+
|
|
26
|
+
t0 = time.perf_counter()
|
|
27
|
+
result = Result(path=path)
|
|
28
|
+
doc = None
|
|
29
|
+
try:
|
|
30
|
+
doc = Document(path, source=source)
|
|
31
|
+
pages = doc.pages
|
|
32
|
+
result.stats["pages"] = len(pages)
|
|
33
|
+
result.stats["tagged"] = bool(doc.struct_root is not None)
|
|
34
|
+
feats = features(doc)
|
|
35
|
+
result.stats["features"] = {k: v for k, v in feats.items() if v}
|
|
36
|
+
run_rules(doc, result, only=set(only) if only else None, skip=set(skip) if skip else None,
|
|
37
|
+
layers=set(layers) if layers else None, feats=feats)
|
|
38
|
+
result.stats["elements"] = len(doc.elements)
|
|
39
|
+
result.stats["fonts"] = len(doc.fonts)
|
|
40
|
+
result.criteria = evaluate(result, load_catalogue())
|
|
41
|
+
except Exception as exc: # noqa: BLE001
|
|
42
|
+
result.error = f"{type(exc).__name__}: {exc}"[:300]
|
|
43
|
+
finally:
|
|
44
|
+
if doc is not None:
|
|
45
|
+
doc.close()
|
|
46
|
+
result.seconds = time.perf_counter() - t0
|
|
47
|
+
return result
|
outloud/cli.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""The outloud command.
|
|
2
|
+
|
|
3
|
+
outloud report.pdf one file, findings on the terminal
|
|
4
|
+
outloud a.pdf b.pdf dir/ several; directories are searched for *.pdf
|
|
5
|
+
outloud out.pdf --source in.pdf also compare the output with its source
|
|
6
|
+
outloud *.pdf --json report.json --sarif report.sarif --html report.html
|
|
7
|
+
outloud --list-rules
|
|
8
|
+
|
|
9
|
+
Exit status: 0 when every file passes or needs review, 1 when any file
|
|
10
|
+
fails, 2 when any file could not be read. --fail-on changes the threshold.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from . import __version__, check
|
|
21
|
+
from .findings import Result
|
|
22
|
+
from .report import batch_table, criteria_table, one_line, rules_table, terminal, to_html, to_json, to_sarif
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _expand(paths: tuple[str, ...]) -> list[str]:
|
|
26
|
+
out: list[str] = []
|
|
27
|
+
for p in paths:
|
|
28
|
+
if os.path.isdir(p):
|
|
29
|
+
for root, _dirs, files in os.walk(p):
|
|
30
|
+
for f in sorted(files):
|
|
31
|
+
if f.lower().endswith(".pdf"):
|
|
32
|
+
out.append(os.path.join(root, f))
|
|
33
|
+
else:
|
|
34
|
+
out.append(p)
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
|
|
39
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
40
|
+
@click.option("--source", type=click.Path(exists=True), help="The original PDF the checked file was made from; enables the SEM-* comparisons.")
|
|
41
|
+
@click.option("--json", "json_path", type=click.Path(), help="Write a JSON report to this path ('-' for stdout).")
|
|
42
|
+
@click.option("--sarif", "sarif_path", type=click.Path(), help="Write a SARIF 2.1.0 report to this path.")
|
|
43
|
+
@click.option("--html", "html_path", type=click.Path(), help="Write an HTML report to this path.")
|
|
44
|
+
@click.option("--only", multiple=True, help="Run only these rule ids (repeatable, or comma-separated).")
|
|
45
|
+
@click.option("--skip", multiple=True, help="Skip these rule ids (repeatable, or comma-separated).")
|
|
46
|
+
@click.option("--layer", type=click.Choice(["conformance", "semantic"]), multiple=True, help="Run only rules of this layer.")
|
|
47
|
+
@click.option("--fail-on", type=click.Choice(["error", "warning", "never"]), default="error", show_default=True,
|
|
48
|
+
help="Which severity makes the exit status non-zero.")
|
|
49
|
+
@click.option("-q", "--quiet", is_flag=True, help="One line per file, no findings.")
|
|
50
|
+
@click.option("--no-info", is_flag=True, help="Hide info-level findings on the terminal.")
|
|
51
|
+
@click.option("--criteria", is_flag=True,
|
|
52
|
+
help="Print the criteria view: every Matterhorn checkpoint and WCAG 2.2 A/AA success criterion with its status (pass, fail, warning, not applicable, needs a person) and the rules behind it.")
|
|
53
|
+
@click.option("--view", is_flag=True, help="Open the app in the browser: drop or choose PDFs, or pass them here; pages with findings outlined, criteria, structure tree and a screen-reader preview.")
|
|
54
|
+
@click.option("--port", type=int, default=0, help="Port for --view (default: any free port).")
|
|
55
|
+
@click.option("--no-browser", is_flag=True, help="With --view: serve without opening a browser window; print the URL instead.")
|
|
56
|
+
@click.option("--list-rules", is_flag=True, help="Print the rule catalogue and exit.")
|
|
57
|
+
@click.version_option(__version__, prog_name="outloud")
|
|
58
|
+
def main(paths, source, json_path, sarif_path, html_path, only, skip, layer, fail_on, quiet, no_info, criteria, view, port, no_browser, list_rules):
|
|
59
|
+
"""Check PDF files for accessibility: PDF/UA-1 conformance plus semantic checks a validator cannot make."""
|
|
60
|
+
if list_rules:
|
|
61
|
+
click.echo(rules_table())
|
|
62
|
+
return
|
|
63
|
+
files = _expand(paths)
|
|
64
|
+
if not files and not view:
|
|
65
|
+
raise click.UsageError("give at least one PDF file or directory, or --list-rules, or --view to open the app")
|
|
66
|
+
only_ids = {x.strip() for item in only for x in item.split(",") if x.strip()} or None
|
|
67
|
+
skip_ids = {x.strip() for item in skip for x in item.split(",") if x.strip()} or None
|
|
68
|
+
results: list[Result] = []
|
|
69
|
+
for path in files:
|
|
70
|
+
if not os.path.exists(path):
|
|
71
|
+
r = Result(path=path, error="no such file")
|
|
72
|
+
else:
|
|
73
|
+
r = check(path, source=source, only=only_ids, skip=skip_ids, layers=set(layer) or None)
|
|
74
|
+
results.append(r)
|
|
75
|
+
if json_path == "-":
|
|
76
|
+
continue
|
|
77
|
+
if quiet or len(files) > 1:
|
|
78
|
+
click.echo(one_line(r))
|
|
79
|
+
else:
|
|
80
|
+
click.echo(terminal(r, verbose=True, show_info=not no_info))
|
|
81
|
+
if criteria and not r.error:
|
|
82
|
+
if len(files) > 1:
|
|
83
|
+
click.echo(f" {os.path.basename(r.path)}")
|
|
84
|
+
click.echo(criteria_table(r, "both"))
|
|
85
|
+
click.echo("")
|
|
86
|
+
if len(files) > 1 and json_path != "-" and not quiet:
|
|
87
|
+
click.echo("")
|
|
88
|
+
click.echo(batch_table(results))
|
|
89
|
+
if json_path:
|
|
90
|
+
text = to_json(results)
|
|
91
|
+
if json_path == "-":
|
|
92
|
+
click.echo(text)
|
|
93
|
+
else:
|
|
94
|
+
with open(json_path, "w", encoding="utf-8") as fh:
|
|
95
|
+
fh.write(text)
|
|
96
|
+
if sarif_path:
|
|
97
|
+
with open(sarif_path, "w", encoding="utf-8") as fh:
|
|
98
|
+
fh.write(to_sarif(results))
|
|
99
|
+
if html_path:
|
|
100
|
+
with open(html_path, "w", encoding="utf-8") as fh:
|
|
101
|
+
fh.write(to_html(results))
|
|
102
|
+
if view:
|
|
103
|
+
from .model import Document # noqa: PLC0415
|
|
104
|
+
from .viewer import serve # noqa: PLC0415
|
|
105
|
+
|
|
106
|
+
ok = [r for r in results if not r.error]
|
|
107
|
+
docs = [Document(r.path, source=source) for r in ok]
|
|
108
|
+
click.echo("viewer running; press Ctrl+C to stop", err=True)
|
|
109
|
+
try:
|
|
110
|
+
serve(ok, docs, port=port, open_browser=not no_browser)
|
|
111
|
+
finally:
|
|
112
|
+
for d in docs:
|
|
113
|
+
d.close()
|
|
114
|
+
code = 0
|
|
115
|
+
if any(r.error for r in results):
|
|
116
|
+
code = 2
|
|
117
|
+
elif fail_on == "error" and any(r.errors for r in results):
|
|
118
|
+
code = 1
|
|
119
|
+
elif fail_on == "warning" and any(r.errors or r.warnings for r in results):
|
|
120
|
+
code = 1
|
|
121
|
+
sys.exit(code)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|