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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed: HTML display pages now fit to width, top-aligned

The HTML display pages (`/rotation` for Visionect and the zero-config root `/`)
default to fitting the image to the viewport **width**, keeping the aspect ratio,
pinning the top of the page to the top of the viewport, and clipping whatever
runs past the bottom. This keeps the masthead and lead story large and legible on
panels whose aspect ratio differs from the paper's, instead of shrinking the whole
page to a letterboxed thumbnail.

- New `?fit=` values on the HTML pages: default (width, top-aligned),
`?fit=contain` (the previous behavior — whole page, letterboxed), and
`?fit=cover` (fill, center-crop). Pair with `?margin=0` to pin the image flush
to the top-left corner. `fit=width` is client-side CSS only; the raw-image
endpoint (`/rotation.png`) still frames with `contain`/`cover` server-side.
- The URL builder's Fit control offers all three for the Visionect target
(defaulting to width) and `contain`/`cover` for the image/TRMNL targets.
- **Behavior change:** existing HTML displays render fit-to-width on upgrade.
Add `?fit=contain` to the page URL to restore the previous letterboxed layout.

### Added: content-aware cropping

Served pages are now trimmed to their content bounds before framing
Expand Down
20 changes: 14 additions & 6 deletions cmd/broadsheet-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,14 @@ const displayHTML = `<!doctype html>
<style>
:root { --pad: 3vmin; }
html, body { margin: 0; height: 100%; background: #fff; }
body { box-sizing: border-box; padding: var(--pad); }
.sheet { display: block; width: 100%; height: 100%; object-fit: contain; background: #fff; }
body.cover .sheet { object-fit: cover; }
body { box-sizing: border-box; padding: var(--pad); overflow: hidden; }
/* Default (no fit=): fill the viewport width, keep the aspect ratio, pin to
the top, and clip whatever runs past the bottom edge. */
.sheet { display: block; width: 100%; height: auto; background: #fff; }
/* fit=contain: scale the whole page to fit inside the viewport, letterboxed. */
body.contain .sheet { height: 100%; object-fit: contain; }
/* fit=cover: fill the viewport, center-cropping the overflow. */
body.cover .sheet { height: 100%; object-fit: cover; }
</style>
</head>
<body>
Expand All @@ -213,12 +218,15 @@ const displayHTML = `<!doctype html>
var q = new URLSearchParams(window.location.search);

// Framing is done here in CSS, driven by the page URL:
// ?margin=<n> -> padding, in vmin (default 3)
// ?fit=cover -> object-fit: cover (crop to fill) instead of contain
// ?margin=<n> -> padding, in vmin (default 3)
// default -> fit to width, top-aligned, clipping the bottom overflow
// ?fit=contain -> scale the whole page to fit inside, letterboxed
// ?fit=cover -> fill the viewport, center-cropping the overflow
if (q.has('margin')) {
document.documentElement.style.setProperty('--pad', (parseFloat(q.get('margin')) || 0) + 'vmin');
}
if (q.get('fit') === 'cover') document.body.classList.add('cover');
var fit = q.get('fit');
if (fit === 'contain' || fit === 'cover') document.body.classList.add(fit);

// Fetch the image exactly once per load — each fetch advances this device's
// rotation cursor server-side, so one fetch == one advance. We ask only for a
Expand Down
15 changes: 11 additions & 4 deletions cmd/broadsheet-server/rotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,14 @@ var rotationTmpl = template.Must(template.New("rotation").Parse(`<!doctype html>
<style>
:root { --pad: 3vmin; }
html, body { margin: 0; height: 100%; background: #fff; }
body { box-sizing: border-box; padding: var(--pad); }
.sheet { display: block; width: 100%; height: 100%; object-fit: contain; background: #fff; }
body.cover .sheet { object-fit: cover; }
body { box-sizing: border-box; padding: var(--pad); overflow: hidden; }
/* Default (no fit=): fill the viewport width, keep the aspect ratio, pin to
the top, and clip whatever runs past the bottom edge. */
.sheet { display: block; width: 100%; height: auto; background: #fff; }
/* fit=contain: scale the whole page to fit inside the viewport, letterboxed. */
body.contain .sheet { height: 100%; object-fit: contain; }
/* fit=cover: fill the viewport, center-cropping the overflow. */
body.cover .sheet { height: 100%; object-fit: cover; }
</style>
</head>
<body>
Expand All @@ -180,7 +185,9 @@ var rotationTmpl = template.Must(template.New("rotation").Parse(`<!doctype html>
if (margin !== null) {
document.documentElement.style.setProperty('--pad', (parseFloat(margin) || 0) + 'vmin');
}
if (qparam('fit') === 'cover') document.body.classList.add('cover');
// Default is fit-to-width, top-aligned (CSS above); contain/cover opt out.
var fit = qparam('fit');
if (fit === 'contain' || fit === 'cover') document.body.classList.add(fit);

var I = {{.IntervalSec}}; // dwell seconds
var phase = {{.Phase}};
Expand Down
27 changes: 25 additions & 2 deletions cmd/broadsheet-server/web/templates/builder.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ <h1>URL builder</h1>
<span id="pngopts">
<label>Width <input id="w" type="number" placeholder="800" size="6"></label>
<label>Height <input id="h" type="number" placeholder="480" size="6"></label>
<label>Fit <select id="fit"><option value="">contain</option><option value="cover">cover</option></select></label>
</span>
<label id="fitopt">Fit <select id="fit"><option value="contain">contain</option><option value="cover">cover</option></select></label>
<label id="sleepopt">Panel sleep <select id="sleep"><option value="">auto (Visionect)</option><option value="off">off</option></select></label>
</form>
<p><input id="out" readonly onclick="this.select()"> <button id="copy">copy</button></p>
Expand All @@ -41,8 +41,12 @@ <h2>Preview</h2>
if (!forPage) {
if ($('w').value) ps.push('w=' + $('w').value);
if ($('h').value) ps.push('h=' + $('h').value);
if ($('fit').value) ps.push('fit=' + $('fit').value);
} else if ($('sleep').value) ps.push('sleep=' + $('sleep').value);
// Fit applies to the HTML page (client-side CSS) and the raw image
// (server-side framing); emit it only when it differs from the target's
// default (width for the HTML page, contain for the image).
var fitDefault = $('target').value === 'visionect' ? 'width' : 'contain';
if ($('fit').value && $('fit').value !== fitDefault) ps.push('fit=' + $('fit').value);
return ps.length ? '?' + ps.join('&') : '';
}
var HINTS = {
Expand All @@ -62,8 +66,27 @@ <h2>Preview</h2>
ps.push('w=420');
return '?' + ps.join('&');
}
// The HTML page defaults to fit=width (fill width, top-align) and also offers
// contain/cover; the raw image and TRMNL envelope go through the server
// renderer, which frames with contain/cover alone. Reset to the target's
// default only when the target itself changes, so edits elsewhere on the form
// don't clobber a deliberate fit choice.
var lastTarget = null;
function setFitOptions(t) {
var sel = $('fit');
var opts = [['contain', 'contain'], ['cover', 'cover']];
if (t === 'visionect') opts.push(['width', 'width (top-align)']);
var def = t === 'visionect' ? 'width' : 'contain';
var want = t === lastTarget ? sel.value : def;
lastTarget = t;
sel.innerHTML = opts.map(function (o) {
return '<option value="' + o[0] + '">' + o[1] + '</option>';
}).join('');
sel.value = opts.some(function (o) { return o[0] === want; }) ? want : def;
}
function update() {
var t = $('target').value, url;
setFitOptions(t);
if (t === 'visionect') url = base + '/rotation' + params(true);
else if (t === 'png') url = base + '/rotation.png' + params(false);
else url = base + '/api/display' + params(false);
Expand Down
Loading