251 lines
21 KiB
Markdown
251 lines
21 KiB
Markdown
# ADA §703 Font Analyzer — Decision Trail & Session History
|
||
|
||
**Companion to:** `ada_tool_context.md` + `ada-font-analyzer.html`
|
||
**Session date:** May 2026
|
||
**Session type:** claude.ai (not CoWork — this was a design/build session in the standard Claude interface)
|
||
**Participants:** Bryan Gilliom
|
||
|
||
---
|
||
|
||
## How This Tool Came to Exist
|
||
|
||
Bryan asked how to go through the Google Fonts foundry at `fonts.google.com` and identify a subset of 20–30 fonts that are fully compatible with ADA Signage Guidelines for digital signage, with primary reference to the 2010 ADA Standards for Accessible Design, Chapter 7 (Signs), Section §703.5.
|
||
|
||
The reference document in the MPM Odoo Knowledge base is at `https://portal.mpmedia.tv/knowledge/article/4638`. That article was fetched at the start of the session but returned no content (likely authentication-gated). The ADA §703.5 criteria were sourced instead from the Access Board's published standard and web search results.
|
||
|
||
The tool evolved across multiple iterations in a single session, each adding a new capability layer.
|
||
|
||
---
|
||
|
||
## Critical Clarification — What the First Version Actually Did
|
||
|
||
**This is the most important thing to preserve.**
|
||
|
||
The first version of the tool (the interactive widget rendered inline in the chat) presented 25 Google Fonts with compliance scores. Bryan asked directly: *"Did you parse the entire font foundry at Google Fonts to get these selections, and these are the only appropriate selections, or did you pick 25 at random?"*
|
||
|
||
**The honest answer, given directly:** No. The 25 fonts were selected from Claude's training knowledge — well-known sans-serif fonts that Claude already knew about. The Google Fonts foundry was never actually crawled or parsed. There are 1,500+ fonts in the catalog and the curated 25 represent a small, training-knowledge-biased subset.
|
||
|
||
**Implication:** The curated list is a reasonable starting point but is not comprehensive. There are almost certainly dozens of additional fully-compliant fonts in the Google Fonts catalog that were not included simply because they weren't prominent enough to appear in Claude's training data.
|
||
|
||
**What a comprehensive approach would require:** A programmatic scan using the Google Fonts Developer API to pull all font families, filter to sans-serif category, then either parse font metric files or use the canvas measurement approach on each one. This is flagged as a planned improvement in `ada_tool_context.md` but has not been built.
|
||
|
||
---
|
||
|
||
## The "Transit-Tested" Badge
|
||
|
||
Bryan also asked specifically how the Transit-tested badge was assigned.
|
||
|
||
**The honest answer:** Also heuristic, from training knowledge. The badge was applied to fonts known from training data to have been deployed in real transit systems — Roboto (Android/Google Maps), Open Sans (various municipal systems), Inter (modern transit apps), Work Sans (wayfinding projects), Noto Sans (multilingual transit), Atkinson Hyperlegible (Braille Institute, adopted by some transit agencies), Montserrat (widely used in transit signage).
|
||
|
||
**What it is not:** A formal registry, a survey of documented deployments, or a certification. It's a reasonable signal but should not be treated as authoritative.
|
||
|
||
**Decision made:** Keep the badge in the UI because it provides useful signal even if imprecise, but document its heuristic nature clearly. If MPM wants to formalize it, the right approach would be to build a curated list of documented transit system font deployments.
|
||
|
||
---
|
||
|
||
## ADA §703.5 Criteria — How We Mapped Them
|
||
|
||
The five criteria implemented correspond directly to §703.5 subsections:
|
||
|
||
**§703.5.3 — Style (two criteria rolled into one subsection)**
|
||
- Requires sans-serif typeface
|
||
- Prohibits italic, oblique, script, highly decorative, or "unusual" forms
|
||
- Implemented as: name heuristics (regex patterns for known serif/script/decorative families) + OS/2 `fsSelection` italic/oblique bits + `panose bFamilyType` from font binary
|
||
|
||
**§703.5.4 — Character proportion**
|
||
- Uppercase O width must be 55–110% of uppercase I height
|
||
- This is the canonical ADA proportion test — it filters condensed and ultra-wide fonts
|
||
- Implemented as: canvas rendered pixel width ratio (Tab 2) and `hmtx` advance width ÷ `sCapHeight` (Tab 3)
|
||
- Note: the standard says "height of uppercase I" not "width of uppercase I" — the denominator is height, not width. The implementation uses cap height as the denominator, which is correct per the standard.
|
||
|
||
**§703.5.7 — Stroke width**
|
||
- Stroke thickness of uppercase I must be 10–30% of character height
|
||
- This is the most common failure point for light-weight fonts (100–300)
|
||
- Implemented as: canvas thinnest-row pixel scan (Tab 2/3) and `usWeightClass` → stroke% mapping (Tab 3)
|
||
- The weight class mapping (wc100→4%, wc200→7%, wc300→9%, wc400→12%, etc.) was derived from common font metrics — it's a model, not a measurement. Canvas cross-check exists specifically because of this.
|
||
|
||
**§703.5.8 — Character spacing**
|
||
- Spacing between characters must be 10–35% of character height
|
||
- Hardest to measure without rendering a full text string and analyzing inter-glyph spacing
|
||
- Implemented as: condensed-variant name detection only (a proxy, not a direct measurement)
|
||
- Known limitation: this is the weakest of the five measurements. A future improvement would render a test string and measure actual advance-width-based spacing.
|
||
|
||
**§703.5.9 — Line spacing** *(not implemented)*
|
||
- Line spacing 135–170% of character height, baseline to baseline
|
||
- Not currently measured — would require rendering multi-line text to canvas and measuring baseline distances
|
||
- Flagged as planned improvement. This is a rendering/usage setting as much as a font property, so it's partially outside the scope of font selection.
|
||
|
||
---
|
||
|
||
## Architecture Decisions & Why
|
||
|
||
### Decision: Single HTML file, no build step
|
||
|
||
**Why:** The primary deployment target is an Odoo Knowledge article iframe. Odoo Knowledge iframes need a stable URL to a single file. A multi-file build (separate CSS, JS, assets) would require either a proper web server or bundling. Single-file keeps the hosting requirement to "serve one static file from anywhere."
|
||
|
||
**Considered and rejected:** React component (would require bundler + hosting), separate JS file (complicates hosting), CDN-hosted script dependencies (would add external failure points).
|
||
|
||
**Consequence:** All code — HTML, CSS, JS, data, the full TTF binary parser — lives in one ~970-line file. This is a deliberate tradeoff: more complex to read, simpler to deploy and host.
|
||
|
||
### Decision: No external JS libraries
|
||
|
||
**Why:** External libraries (opentype.js for font parsing, pako for WOFF2 decompression) would either need CDN links (adds failure/CORS risk) or need to be bundled (defeats single-file goal). The TTF tables we need (`head`, `OS/2`, `hhea`, `hmtx`, `cmap`, `name`) are well-documented and straightforward to parse with a DataView.
|
||
|
||
**Considered and rejected:** opentype.js via CDN (would handle more font formats, but adds ~200KB dependency and CDN risk), fontkit (Node.js only), font-metadata npm package (build step required).
|
||
|
||
**Consequence:** WOFF/WOFF2 parsing is not supported in Tab 3 because those formats compress the table data (DEFLATE for WOFF, Brotli for WOFF2) and implementing decompressors in vanilla JS without a library is disproportionate to the value. Falls back to canvas gracefully.
|
||
|
||
**Future option:** If WOFF2 support becomes important, pako.js (for WOFF) or a Brotli WASM module (for WOFF2) could be loaded via CDN link. This would need to be evaluated for CORS and reliability in the Odoo iframe context.
|
||
|
||
### Decision: Canvas measurement as primary method for Tab 2 (Google Fonts)
|
||
|
||
**Why:** Google Fonts serves font files as WOFF2 in modern browsers. We can't parse the binary tables from a CSS-loaded font — the browser loads it internally but doesn't expose the raw bytes. So canvas pixel measurement is the only practical approach for Tab 2.
|
||
|
||
**How accurate:** ~±5% for O:I ratio (sub-pixel rendering and hinting affect exact pixel counts). Stroke measurement is less reliable — the "thinnest ink row" approach works well for geometric fonts but can be thrown off by fonts with very thin decorative horizontal elements at the top/bottom of the I glyph. In practice it's directionally correct for the weight range that matters (passing vs. failing).
|
||
|
||
**Canvas measurement approach in detail:**
|
||
1. Render uppercase I and O to a 800×300 canvas at 120px, weight as selected
|
||
2. For O:I ratio: use `ctx.measureText()` advance widths — this is more reliable than pixel bounding box for proportions
|
||
3. For stroke: render just the I, then `getImageData()` and scan every horizontal row counting non-transparent pixels. The minimum row width across the full cap height is taken as the stroke thickness estimate.
|
||
4. The minimum-row approach assumes the narrowest point of the I is the stem width. This holds for standard sans-serif I glyphs. It breaks for fonts where the I has very thin horizontal serifs (but those fonts would fail the sans-serif criterion anyway) or fonts where the I has a very unusual form.
|
||
|
||
### Decision: Tab 3 uses binary TTF parser + canvas cross-check
|
||
|
||
**Why:** When the user uploads a TTF/OTF, we have the raw bytes. Parsing the actual font tables gives substantially better accuracy than canvas pixel scanning, particularly for O:I proportion (exact advance widths in font units) and stroke weight (usWeightClass is a defined value, not estimated from rendering).
|
||
|
||
**The cross-check:** Canvas still runs on the uploaded font (loaded via FontFace API + blob URL) and the stroke measurement is shown alongside the table-derived estimate in the raw metrics table. This lets users see both values and identify if they diverge significantly (which can happen with unusual fonts).
|
||
|
||
**Why show both:** If usWeightClass says 400 (→ ~12% stroke) but canvas says 8%, that's meaningful — it could indicate the font designer set the weight class incorrectly, or the font has unusually thin strokes for its stated weight. Both values are surfaced to the user rather than hiding the discrepancy.
|
||
|
||
### Decision: Google Fonts download ZIP endpoint for all download links
|
||
|
||
**Early version:** The first iteration generated a Google Fonts CSS2 API import URL and offered a "Copy" button. Bryan pointed out that their CMS requires uploading actual TTF/OTF files — a CSS import URL is useless for that workflow.
|
||
|
||
**What changed:** All download links were replaced with `https://fonts.google.com/download?family=[Family Name]` — Google's own download endpoint that returns a ZIP of all weights and styles for a family. This is the same URL used by the Google Fonts website's own download button.
|
||
|
||
**What the ZIP contains:** All static per-weight TTF files (e.g. `Roboto-Regular.ttf`, `Roboto-Bold.ttf`) plus variable font TTF files where available (single file with axis ranges). For CMS upload, the static files in the `static/` subfolder are what you want — most CMS font uploaders don't handle variable fonts.
|
||
|
||
**CORS note:** The download ZIP link opens in a new tab (target="_blank") rather than a programmatic fetch + blob download. This is intentional — `fonts.google.com` doesn't include CORS headers that would allow a cross-origin fetch, so attempting `fetch()` + `URL.createObjectURL()` for the ZIP would fail. Opening the URL in a new tab works fine because the browser handles it as a direct navigation.
|
||
|
||
### Decision: Scoring threshold for "Fully compliant" vs "Conditionally compliant"
|
||
|
||
- **5/5 criteria pass, weight ≥ 400:** Fully compliant
|
||
- **3–4/5 criteria pass, OR 5/5 but weight < 400:** Conditionally compliant
|
||
- **0–2/5:** Does not meet ADA §703.5
|
||
|
||
The weight < 400 flag was added because many fonts that pass all five criteria at Regular weight fail §703.5.7 (stroke weight) at Thin/Light weights. The tool warns when the selected test weight is below 400 regardless of measured stroke — this is a conservative guard because the canvas stroke measurement at thin weights can be inconsistent due to anti-aliasing.
|
||
|
||
---
|
||
|
||
## What Was Considered and Not Built
|
||
|
||
### Full Google Fonts API catalog scan
|
||
|
||
A programmatic scan of all ~1,500 Google Fonts was discussed. The approach would be:
|
||
1. Call the Google Fonts Developer API (`https://www.googleapis.com/webfonts/v1/webfonts?key=...`) to get all families with metadata
|
||
2. Filter to `category: "sans-serif"`
|
||
3. For each font, either: (a) load via CSS API and canvas-measure at weight 400, or (b) fetch the TTF file URL from the CSS API response and run the binary parser
|
||
4. Score against §703.5 criteria
|
||
5. Output a complete ranked list
|
||
|
||
**Why not built in this session:** Would require a Google Fonts API key and either a Node.js script or a substantially more complex browser-based tool that can iterate ~800 sans-serif fonts with rate limiting. This is a CoWork tool project in its own right — probably a Node.js CLI that outputs a CSV, or an Odoo-integrated report.
|
||
|
||
**Current state of the curated list:** The 25 fonts are a good representative set covering the main compliance tiers (fully compliant, conditionally compliant, use with caution). They are not exhaustive. Fonts likely missing that would be fully compliant include: Fira Sans, Space Grotesk, Jost, Instrument Sans, Hanken Grotesk, Albert Sans, Sora, Onest, Be Vietnam Pro, Geologica, Schibsted Grotesk, and others — these appear in the "quick test suggestions" on Tab 2 specifically because they are strong candidates but haven't been formally evaluated.
|
||
|
||
### Per-weight compliance table
|
||
|
||
The `compliance` object in the FONTS array is currently a single flat pass/fail per criterion, representing worst-case (which is usually the lightest available weight). A more nuanced approach would store compliance per weight range, e.g.:
|
||
|
||
```js
|
||
compliance: {
|
||
stroke: { pass_at: [400,500,600,700,800,900], fail_at: [100,200,300] }
|
||
}
|
||
```
|
||
|
||
This was considered but added complexity to the card rendering and filter logic. The current approach of showing a note ("Use 400+ only") on the card was judged sufficient for the immediate use case. Could be revisited if the MPM team wants weight-specific guidance.
|
||
|
||
### Line spacing measurement (§703.5.9)
|
||
|
||
Requires rendering a two-line text block to canvas and measuring the pixel distance between baselines. Not technically difficult — just not implemented in this session. The `lineGap` value from the `hhea` table is available from the TTF parser and could be used as an approximation. Deferred because line spacing is also a CMS/template setting, not purely a font property.
|
||
|
||
### WOFF2 decompression
|
||
|
||
WOFF2 uses Brotli compression on the font table data. Implementing a Brotli decompressor in browser JS is possible (pako doesn't support Brotli; would need a separate Brotli WASM module). The effort-to-value ratio was judged too high for this session — TTF and OTF files are always available from Google Fonts ZIPs and most type foundries, so WOFF2 upload is an edge case. Noted in Known Limitations.
|
||
|
||
---
|
||
|
||
## UI/UX Decisions
|
||
|
||
### Card click vs. button click for selection
|
||
|
||
Early version had the entire font card as a clickable selection target (`onclick` on the card div). The action buttons (Google Fonts link, Download) were added in a later iteration and conflicted with card-level click — clicking the GF link would also toggle selection.
|
||
|
||
**Resolution:** Card-level click was removed. Selection is only via the explicit "Select" / "Selected" button in the action bar. The Google Fonts and Download links have `onclick="event.stopPropagation()"` to prevent any remaining bubbling.
|
||
|
||
### Download "all selected" — tab stagger
|
||
|
||
The "Download all selected" button opens each family's ZIP in a new tab. Without a stagger, browsers block most after the first as pop-ups. A 700ms stagger between opens was chosen empirically — long enough to avoid pop-up blocking in Chrome and Firefox, short enough that downloading 5–6 fonts doesn't feel slow.
|
||
|
||
**User note:** Pop-up blocking must be allowed for the tool's origin for this to work. If the tool is hosted at the Odoo portal domain, users may need to allow pop-ups from that domain once.
|
||
|
||
### Light-on-dark preview panel
|
||
|
||
Added because MPM's transit signage displays predominantly use light text on dark backgrounds (standard for public transit legibility). The second preview zone with inverted colors lets the team see the font in its actual deployment context, not just the standard black-on-white specimen.
|
||
|
||
### Raw metrics table in Tab 3
|
||
|
||
The decision to show the raw font table values (not just the scored results) was intentional. The audience is the MPM team — technical enough to understand what `sCapHeight: 720` or `usWeightClass: 400` means, and better served by transparency than a black box. The source attribution column (`TTF table` vs `canvas`) makes the measurement methodology visible.
|
||
|
||
---
|
||
|
||
## Odoo Knowledge Embedding Context
|
||
|
||
The tool is intended to live inside the existing ADA Signage Guidelines article at `https://portal.mpmedia.tv/knowledge/article/4638`. The article content couldn't be fetched during this session (likely requires authentication), but Bryan confirmed the article exists and is the right home for this tool.
|
||
|
||
**Embedding method:** Odoo Knowledge supports iframe embedding via the HTML block element (insert via `/` command → HTML block). The recommended iframe tag is in `ada_tool_context.md`.
|
||
|
||
**Hosting requirement:** The HTML file needs to be served from a URL accessible to the Odoo portal's browser context. Options in priority order:
|
||
1. Gitea raw file URL — `https://git.alwisp.com/jason/ada-font-analyzer/raw/branch/main/ada-font-analyzer.html`
|
||
2. MPM web server / CDN
|
||
3. Odoo attachment with a raw file URL (check whether `portal.mpmedia.tv` serves raw attachment content)
|
||
|
||
**Not tested yet:** Whether the Google Fonts API calls work from within an Odoo iframe context. Odoo may have a Content Security Policy that restricts external requests. If `fonts.googleapis.com` is blocked by CSP, Tab 2 preview loading will fail silently (fonts won't load but the tool won't break). Tab 3 doesn't depend on external requests at all.
|
||
|
||
---
|
||
|
||
## Session Flow Summary
|
||
|
||
For reference, the sequence of what was built in this session:
|
||
|
||
1. **Initial request:** How to evaluate Google Fonts against ADA §703.5 for digital signage
|
||
2. **First artifact:** Inline interactive widget (claude.ai visualizer) with 25 curated fonts, filter UI, compliance scoring
|
||
3. **Clarification:** Bryan asked if the fonts were actually parsed from GF. Claude confirmed they were from training knowledge, not a catalog scan. Transit-tested badge also confirmed as heuristic.
|
||
4. **Second artifact:** Standalone `ada-font-analyzer.html` — same curated list but as a downloadable/embeddable file
|
||
5. **Feature request:** Add "View on Google Fonts" button and replace CSS import URL with actual TTF download links
|
||
6. **Third artifact:** Updated HTML with GF specimen links, per-card download buttons, sidebar download actions
|
||
7. **Feature request:** Add ability to upload a non-Google TTF file for evaluation
|
||
8. **Fourth artifact:** Final HTML with Tab 3 — drag-and-drop upload, binary TTF parser (pure JS DataView), FontFace API preview, raw metrics table, canvas cross-check
|
||
9. **Transition request:** How to move this to CoWork
|
||
10. **Decision:** Write context files (this file + `ada_tool_context.md`) co-located with the HTML, then use them to "adopt" the project in a new CoWork session rather than trying to run the CoWork registry update process from within this claude.ai session
|
||
11. **This file:** Full decision trail written for session continuity
|
||
|
||
---
|
||
|
||
## Recommended Next Steps for CoWork Session
|
||
|
||
In priority order:
|
||
|
||
1. **Register in CoWork Project Registry** — ✅ Done as CW-005. Drive folder, Gitea repo, and Wiki created 2026-05-05.
|
||
|
||
2. **Establish hosting** — Gitea raw URL is available: `https://git.alwisp.com/jason/ada-font-analyzer/raw/branch/main/ada-font-analyzer.html`. Test whether this URL works from within the Odoo Knowledge iframe context.
|
||
|
||
3. **Verify CSP compatibility** — Check if Odoo's Content Security Policy blocks requests to `fonts.googleapis.com` from within the iframe. If it does, Tab 2 needs a fallback (either CSP header adjustment or a note in the UI).
|
||
|
||
4. **Embed in Odoo article** — Once hosting is confirmed, add the iframe to the ADA Signage Guidelines Knowledge article at `https://portal.mpmedia.tv/knowledge/article/4638`.
|
||
|
||
5. **Build the full GF catalog scan** — Separate CoWork project. Node.js script using Google Fonts Developer API + canvas or TTF metric extraction, outputs a complete compliance-scored list of all sans-serif Google Fonts. Would replace/supplement the current hardcoded 25-font list.
|
||
|
||
6. **Add line spacing measurement** — Small enhancement to Tab 3. Read `hhea.lineGap` from the already-parsed TTF data (it's already extracted) and add a §703.5.9 row to the metrics table and score grid.
|
||
|
||
7. **Expand curated list** — Run the 16 quick-test suggestion fonts through the Tab 2 or Tab 3 evaluator, score them, and add passing ones to the FONTS array with appropriate tags and notes.
|