JSFiddle - React, Tailwind, and code Playground
HTML
<main>
<div class="toolbar">
<p>Select the handwriting to copy it as clean text.</p>
<button id="toggle" type="button" aria-pressed="false">Show OCR layer</button>
</div>
<figure id="page">
<svg viewBox="0 0 1536 1024" role="img" aria-labelledby="title description">
<title id="title">A Hacker News comment handwritten on lined paper</title>
<desc id="description">From pbronez, 27 minutes ago: The medium is the message! Well written.</desc>
<image width="1536" height="1024"...
CSS
:root { color-scheme: light; }
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body {
display: grid;
place-items: center;
padding: clamp(16px, 4vw, 48px);
background: #e9e5dc;
color: #292721;
font: 15px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
main { width: min(100%, 1100px); }
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 10px;
color: #625f57;
}
.toolbar p { margin: 0; }
button {
flex: none;
border: 1px solid #aaa397;
border-radius: 999px;
padding: 7px 12px;
background: #f8f6f0;
color: #36332d;
font: inherit;
cursor: pointer;
}
button:hover { background: #fff; }
button:focus-visible { outline: 3px solid #d66b42; outline-offset: 2px; }
figure {
position: relative;
margin: 0;
overflow: hidden;
border-radius: 4px;
background: #ddd6c7;
box-shadow: 0 16px 42px rgba(58, 51, 39, .18);
}
svg { display: block; width: 100%; height: auto; }
.ocr {
fill: rgba(0, 0, 0, .003);
stroke: none;
font-family: "Arial Narrow", "Liberation Sans Narrow", sans-serif;
font-style: italic;
user-select: text;
-webkit-user-select: text;
}
.ocr.meta { font-size: 58px; letter-spacing: 2px; }
.ocr.comment { font-size: 74px; letter-spacing: 1px; }
.ocr.reply { font-size: 52px; text-decoration: underline; }
.ocr.arrow { font-size: 62px; font-style: normal; }
figure.show-ocr .ocr {
fill: rgba(235, 55, 34, .88);
stroke: rgba(255, 255, 255, .75);
stroke-width: 1px;
paint-order: stroke fill;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
...
JavaScript
const page = document.querySelector('#page');
const toggle = document.querySelector('#toggle');
toggle.addEventListener('click', () => {
const visible = page.classList.toggle('show-ocr');
toggle.setAttribute('aria-pressed', String(visible));
toggle.textContent = visible ? 'Hide OCR layer' : 'Show OCR layer';
});
document.addEventListener('copy', (event) => {
const selection = document.getSelection();
if (!selection || selection.isCollapsed) return;
const range = selection.getRangeAt(0);
const overlay = document.querySelector('g[aria-label="Selectable text transcription"]');
if (!overlay.contains(range.commonAncestorContainer) &&
!overlay.contains(range.startContainer) &&
!overlay.contains(range.endContainer)) return;
const selectedText = selection.toString()
.replace(/\u2006/g, '')
.replace(/[ \t]+/g, ' ')
.replace(/ *\n */g, '\n')
.trim();
if (!selectedText) return;
event.preventDefault();
event.clipboardData.setData('text/plain', selectedText);
});