{"slug": "ocr-it-pull-text-out-of-un-copyable-documents-for-your-llm", "title": "OCR It – pull text out of un-copyable documents for your LLM", "summary": "Thiago Tigaz released OCR It, a free Chrome extension that captures a user-defined screen region on each page of a paginated document, runs OCR locally with a bundled Tesseract build, and appends the text to a running transcript, enabling users to convert un-copyable scanned books, PDFs, or slide decks into text files for use with LLMs like Claude or ChatGPT. The extension makes no outbound requests, requires no API key, and offers hotkeys for single captures (⌥⇧S), automatic runs (⌥⇧A), and region drawing (⌥⇧R), with a popup Allow button for durable site access when needed.", "body_md": "**Pin a region once. Hit a hotkey on every page. Get the whole book as text.**\n\nA Chrome extension for reading a **paginated document trapped in a viewer** — a\nscanned book, a slide deck, a PDF, a reader that won't let you select text.\n\nYou drag out the capture region **once**. After that every press of the hotkey\nscreenshots that exact rectangle, OCRs it, and appends the text to a running\ntranscript. Or hand the whole job over: `⌥⇧A` starts a run that\ncaptures, turns the page, and repeats until the document ends.\n\nThen paste the result wherever it's useful — an LLM being the obvious one, since a few hundred pages you couldn't select are now a text file you can hand to Claude or ChatGPT to summarise, search or ask questions about.\n\nOCR runs locally with a bundled Tesseract build. **No API key, no network, no\nimages leaving your machine** — the extension makes no outbound requests at all.\n\n[Download this repo](/thiagotigaz/ocr-it/archive/refs/heads/main.zip)or`git clone`\n\nit- Open\n`chrome://extensions`\n\nand turn on**Developer mode** **Load unpacked**→ select the folder- Pin the extension — the toolbar icon doubles as the page counter\n\nEverything needed is committed. There's no build step: `npm install`\n\nis only for\nrunning the tests or re-vendoring Tesseract.\n\nThen check `chrome://extensions/shortcuts`\n\nand confirm the hotkeys landed —\nChrome silently leaves them blank when something else already claims them.\n\n**It asks for no site access at install.** Single captures ride on `activeTab`\n\n,\nwhich Chrome hands over when you press the hotkey or open the popup. Two things\nneed a durable grant — an auto-run that outlives a page load, and turning pages\ninside a **cross-origin iframe** — and the popup offers an **Allow** button for\nthe site you're on when it matters.\n\n⌥⇧S |\nCapture the region once |\n⌥⇧A |\nStart / stop an automatic run |\n⌥⇧R |\nDraw or redraw the region |\n\n`⌥⇧R`, then drag a box over the text. Before saving you can drag it\naround, pull the handles, or nudge it a pixel at a time with the arrow keys\n(hold `⇧` to resize). `Enter` keeps it.\n\nDraw a little *inside* the text margins — everything in the rectangle gets read,\npage numbers and running headers included.\n\nPress `⌥⇧S` once per page. The screenshot is taken immediately and OCR\nruns in the background, so you never wait between pages — captures queue up and\nthe badge counts what's still being read.\n\nSet up a next-page control (below) and `⌥⇧A` takes over completely:\ncapture, turn, capture, turn, until the document ends. `Esc` on the\npage stops it.\n\nEvery page is listed with a thumbnail of **exactly what was cropped**, so a\ndrifted region is obvious at a glance instead of eighty pages later. Text is\neditable in place; a bad read can be re-run on its own.\n\n**Copy all** and **Download .txt** emit the pages in order with `--- page N ---`\n\nseparators.\n\nA page marked `DUPLICATE`\n\nhad text identical to the one before it — nearly\nalways because the document didn't actually turn.\n\nEnable **Turn the page automatically after capture**, then:\n\n**Click a control**— hit** Pick control**and click the viewer's next-page button. What gets stored is a** point**, not a CSS selector.** Press a key**— dispatches a keyboard event (default`ArrowRight`\n\n) into whichever frame owns the middle of your capture region, so the reader gets it rather than the host page.\n\n**Test now** fires an advance immediately, without capturing, and reports what\nhappened — worth using before starting a long run.\n\nA stored point survives the DOM re-renders that routinely invalidate a CSS selector, and it reaches two places a selector cannot:\n\n**Cross-origin iframes.** Most embedded readers are iframes, and nothing the top frame can express addresses an element inside one.**Shadow DOM.**`document.querySelector`\n\ncan't see into a shadow root.\n\nAt advance time the point is offered to every frame and the one that actually\nowns it acts. A frame works out where it sits inside the top-level viewport by\nwalking up its same-origin ancestors; across an origin boundary the parent hands\nthe offset down by `postMessage`\n\n. (`window.screenX`\n\nis no help — inside an\niframe it reports the browser window, not the frame.) The owning frame resolves\nthe point through any shadow roots, walks up to the nearest real control, and\nemits the full `pointerdown → mousedown → pointerup → mouseup → click`\n\nsequence,\nso viewers that page on `pointerdown`\n\nbehave like those listening for `click`\n\n.\n\nEvery attempt records a verdict, shown in the popup and as an on-page toast:\n\n| Verdict | Meaning |\n|---|---|\n`no next-page control picked yet` |\nAuto-advance is on but nothing was picked |\n`an embedded viewer owns that point` |\nChrome's PDF viewer or a plugin — unreachable by any extension |\n`only the page background is at that point` |\nThe control moved; pick it again |\n`a nested frame owns that point` |\nA frame that couldn't be injected into |\n\nBecause the target is a fixed point on screen, **resizing the window or changing\nzoom mid-run breaks it**, exactly as it breaks the capture region.\n\n`⌥⇧A` — or **Start auto-run** — captures, turns, and repeats on its own.\n\nEach cycle waits for that page's OCR to come back before turning. That costs\nnothing in practice (OCR is faster than a page turn) and buys the one thing an\nunattended loop needs: **reliable end-detection**. A run that only fired\nscreenshots on a timer would sail past the last page and fill the transcript\nwith copies of it.\n\nStop it with `Esc` on the page, the hotkey, or the popup. It also stops\nitself when:\n\n| Condition | Default |\n|---|---|\n| The text stops changing | after 2 identical pages — you've hit the end |\n| The page can't be turned | immediately, quoting the reason |\n| OCR fails or stalls | immediately |\n| Page cap reached | 300 pages |\n| The tab closes, or Chrome restarts | immediately |\n\nWhatever ended it is reported in the popup, so a run you walked away from never just stops being mysterious. A run refuses to start without a working next-page control rather than spinning on one page.\n\nChrome's built-in PDF viewer works — text comes straight out of it. Draw the\nregion over the page area (not the thumbnail sidebar) and page with your own\n`↓` / `PageDown`.\n\nAuto-advance does **not** work inside the PDF viewer, in either mode: the viewer\nis a plugin no extension can inject into, so a click lands on the `<embed>`\n\n, and\nits paging is native scrolling that synthetic key events can't drive. Since\nyou're already pressing a hotkey per page, pressing your own page-down key costs\nnothing.\n\nFor a PDF on disk (`file:///…`\n\n), open `chrome://extensions`\n\n→ **Details** on OCR\nIt → enable **Allow access to file URLs**. Chrome withholds `file://`\n\nfrom every\nextension until you do.\n\n| Setting | What it does |\n|---|---|\nLanguage |\nEnglish, Portuguese and Spanish ship with it — see below to add more |\nLayout |\nTesseract's page segmentation. Single block suits one column of body text; Auto handles mixed layouts |\nSharpen crop before OCR |\nUpscales the crop to ~2× and flattens it to a stretched greyscale ramp. Helps a lot on non-retina displays; leave it on |\nFlag pages identical to the previous one |\nMarks repeats as `DUPLICATE` and, in a run, ends it |\nAuto-run |\nPause between pages, how many repeats end a run, and the hard page cap |\n\nThree ship with the extension — English, Portuguese and Spanish. Any of Tesseract's other ~100 languages can be added, but nothing is fetched at runtime, so the model has to be vendored into the extension first.\n\n```\nnpm install                    # once, for the tooling\nnpm run vendor -- fra deu jpn  # any tesseract language codes\n```\n\nThat pulls each `<code>.traineddata.gz`\n\ninto `vendor/lang/`\n\n. Then add the codes\nto `LANGUAGES`\n\nin `src/shared.js`\n\nso they appear in the popup's dropdown:\n\n``` js\nexport const LANGUAGES = [\n  { code: 'eng', label: 'English' },\n  { code: 'por', label: 'Portuguese' },\n  { code: 'spa', label: 'Spanish' },\n  { code: 'fra', label: 'French' },       // added\n];\n```\n\nReload the extension at `chrome://extensions`\n\nand the new entry is there.\n\n**Codes** are the three-letter ones Tesseract uses: `fra`\n\nFrench, `deu`\n\nGerman,\n`ita`\n\nItalian, `nld`\n\nDutch, `rus`\n\nRussian, `jpn`\n\nJapanese, `chi_sim`\n\nsimplified\nChinese, `ara`\n\nArabic. The full list lives in the\n[tessdata repository](https://github.com/tesseract-ocr/tessdata).\n\n**Two languages at once** work as well — give a `code`\n\nof `eng+por`\n\nand\nTesseract loads both models into one worker, reading a page that mixes them:\n\n```\n{ code: 'eng+por', label: 'English + Portuguese' },\n```\n\nIt costs a little speed and a little accuracy, so prefer a single language when the document only has one.\n\n**Size.** Each language adds roughly 0.7–3 MB to the extension — English is the\nbiggest at 2.9 MB, French one of the smallest at 0.7 MB. The models come from\n`@tesseract.js-data/<code>/4.0.0_best_int`\n\n: the \"best\" models quantised to\nintegers, meaningfully more accurate than the `fast`\n\nvariants.\n\nTo drop a language, delete its `.gz`\n\nfrom `vendor/lang/`\n\nand its entry from\n`LANGUAGES`\n\n.\n\nMV3 service workers have no DOM and no `Worker`\n\n, so the heavy lifting lives in\nan offscreen document.\n\n```\nrun loop ─┐                        (⌥⇧A: capture → turn → repeat)\nhotkey ───┴▶ background.js ─▶ hide our own HUD, wait for a paint\n                            ─▶ chrome.tabs.captureVisibleTab   (whole viewport)\n                            ─▶ offscreen: crop to the region, upscale, greyscale\n                            ─▶ store the page + thumbnail, turn the page\n                            ─▶ queue ─▶ offscreen: Tesseract ─▶ text into storage\n```\n\n| Path | Role |\n|---|---|\n`src/background.js` |\nHotkeys, capture pipeline, serial OCR queue, auto-advance, the run loop |\n`src/offscreen/` |\nCanvas cropping and the Tesseract worker |\n`src/content/overlay.js` |\nRegion picker, point picker, on-page HUD, cross-frame offset cascade |\n`src/popup/` |\nPage list, editing, settings, export |\n`src/shared.js` |\nStorage schema and helpers shared by the worker and the popup |\n`vendor/` |\nTesseract runtime + `.traineddata` , committed so there's no build |\n`tools/` |\nIcon generator, vendoring, screenshots, end-to-end test |\n\nDetails that matter:\n\n- The region is stored in\n**CSS pixels relative to the viewport**. At capture time the screenshot's own width is divided by the live`innerWidth`\n\n, so zoom changes and retina/non-retina differences come out right without trusting a stored DPR. - The HUD is hidden and given two animation frames to disappear\n**before** the screenshot, so the extension's own toast can never end up inside the crop. - Captures are serialised and OCR runs one job at a time, so mashing the hotkey queues work instead of corrupting the page list.\n- Full-size crops are kept only until a page is read successfully, then discarded; the thumbnail stays for verification.\n- A run is cancelled by bumping a token the loop re-checks at every\n`await`\n\n, so stopping lands at a checkpoint rather than mid-write. Storage reads inside the loop double as keep-alive for the service worker, and a one-minute alarm restarts the loop if the worker is recycled anyway.\n\n```\nnpm install\nnpm test            # add -- --headed to watch it\nnpm run shots       # regenerate the screenshots in docs/\n```\n\nThe suite installs the unpacked extension into a real headless Chrome over the\nDevTools protocol, serves fixture documents, and drives the actual product: it\ndrags out a region with synthetic mouse events, fires captures, checks the OCR\ntext against what was rendered, verifies nothing outside the region leaked in,\nchecks the shipped manifest requests no host access and that one toolbar click\nis enough for a plain capture, exercises duplicate detection, drives\nauto-advance against three DOM shapes — a\nplain page, a **cross-origin iframe** and an **open shadow root** — confirms a\nmisconfigured auto-advance reports itself instead of failing silently, runs an\nunattended loop to the end of a finite document and asserts it stopped on its\nown with every page in order, and checks a run stops dead on request.\n\nChrome 137+ ignores\n\n`--load-extension`\n\n, so the harness installs over CDP with`Extensions.loadUnpacked`\n\nand`--enable-unsafe-extension-debugging`\n\n. Headless Chrome can't show the permission prompt either, so the behaviour tests install a copy of the extension with the grant baked in — the state of a user who clicked Allow — while the permissions section checks the real manifest and proves the ungranted path still works via`Extensions.triggerAction`\n\n, which is a genuine toolbar click.\n\n`chrome://`\n\npages, the Web Store and other extensions' pages are off limits to every extension, including this one.- Only the visible viewport can be captured — the region has to be on screen.\n- Chrome rate-limits screenshots to a couple per second; captures retry with backoff, so fast mashing just queues.\n- Accuracy tracks the source. Crisp rendered text reads at 90 %+ confidence; low-resolution scans and handwriting will need cleanup.\n- Local\n`file:///`\n\ndocuments need**Allow access to file URLs** switched on. `activeTab`\n\ndoes not reach cross-origin iframes. If your reader lives in one, grant the site from the popup before setting up auto-advance.\n\nMIT — see [LICENSE](/thiagotigaz/ocr-it/blob/main/LICENSE). Bundled Tesseract components keep their own\nlicences: [ vendor/LICENSE.tesseract-core](/thiagotigaz/ocr-it/blob/main/vendor/LICENSE.tesseract-core) and\n\n[.](/thiagotigaz/ocr-it/blob/main/vendor/tesseract.min.js.LICENSE.txt)\n\n`vendor/tesseract.min.js.LICENSE.txt`\n\nBuilt on [tesseract.js](https://github.com/naptha/tesseract.js).", "url": "https://wpnews.pro/news/ocr-it-pull-text-out-of-un-copyable-documents-for-your-llm", "canonical_source": "https://github.com/thiagotigaz/ocr-it", "published_at": "2026-08-24 06:25:31+00:00", "updated_at": "2026-08-24 07:12:55.646585+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Thiago Tigaz", "OCR It", "Chrome", "Tesseract", "Claude", "ChatGPT"], "alternates": {"html": "https://wpnews.pro/news/ocr-it-pull-text-out-of-un-copyable-documents-for-your-llm", "markdown": "https://wpnews.pro/news/ocr-it-pull-text-out-of-un-copyable-documents-for-your-llm.md", "text": "https://wpnews.pro/news/ocr-it-pull-text-out-of-un-copyable-documents-for-your-llm.txt", "jsonld": "https://wpnews.pro/news/ocr-it-pull-text-out-of-un-copyable-documents-for-your-llm.jsonld"}}