The symptom everyone recognizes
Copy a paragraph from a two-column paper, paste it into a translator, and you get something like this:
…the model consistently underestimates variance under high noise conditions. We therefore propose a two-stage framework for robust detection. Figure 3 shows the ablation results for the proposed module. In contrast, prior work assumes homoscedastic error…
The last sentence belongs to the right column. The extractor glued it onto the left column's ending. Nothing is corrupted — the text is all there — but the reading order is wrong, so the meaning inverts.
This is worth stating precisely, because it changes what you should fix:
Two-column scrambling is a text-extraction ordering problem, not an OCR-accuracy problem and not a translation problem.
If you improve OCR accuracy, the order stays wrong. If you switch translators, the order stays wrong.
Why the ordering breaks
PDF is a display format, not a document structure. The bytes generally describe where glyphs are painted, not what a paragraph is. A two-column page therefore offers at least two plausible orderings of the same glyph sequence:
- Row-major — sweep left to right across the full page width, then down. This interleaves the columns.
- Column-major — read the left column top to bottom, then the right column top to bottom. This matches how humans read it.
Many extractors default to something close to (1), because it is the simplest geometric rule and it works fine for single-column text. Academic templates — LaTeX twocolumn, IEEE, ACM, Springer — are exactly the case where (1) fails badly.
Additional traps that compound the problem:
- Figure captions live in a narrow band that often spans one column but is vertically centered near the page middle, so they survive in odd places.
- Footnotes sit at the page bottom and get appended at the end of a row sweep.
- Inline math splits a sentence into fragments that get re-ordered independently.
- Hyphenation at line ends leaves broken words (
detec-/tion) that no translator can recover after reordering.
The two clean fixes
Fix A: copy per column
If the PDF has a text layer, select only the left column, paste, then the only the right column. This is the cheapest fix for a single paragraph and it needs no tooling.
Its weakness is operational: for a 14-page paper you will do this dozens of times, and it is easy to lose your place.
Fix B: region capture with on-device OCR
Capture the region you are actually reading — a paragraph, a results table, a figure caption — and recognize the text in place. Because the capture is spatially constrained, the character stream follows the visual reading path, so column interleaving cannot occur.
The tradeoff versus Fix A: you need a capture tool, and you are re-recognizing text that already exists as a text layer (slightly redundant). The benefit: one gesture per paragraph, and it also works on scanned PDFs where no text layer exists at all.
Implementation notes that decide success or failure
If you build this yourself on macOS with VNRecognizeTextRequest, these four settings matter more than anything else:
import Vision func recognize(_ cgImage: CGImage, orientation: CGImagePropertyOrientation = .up) throws -> [String] { let request = VNRecognizeTextRequest() request.recognitionLevel = .accurate // small caption text needs .accurate request.recognitionLanguages = ["en-US", "ja-JP"] request.usesLanguageCorrection = false // keep identifiers and units intact request.minimumTextHeight = 0.0 request.customWords = ["homoscedastic", "p-value", "HR", "CI"] let handler = VNImageRequestHandler(cgImage: cgImage, orientation: orientation) try handler.perform([request]) return (request.results ?? []).compactMap { $0.topCandidates(1).first?.string } } -
usesLanguageCorrection = false. With correction on, statistical notation and identifiers get "helpfully" normalized. For academic text this is worse than useless —pbecomesP, subscripts drift, and symbols change class. -
orientation.VNImageRequestHandler(cgImage:orientation:)with aCGImagePropertyOrientationis mandatory for rotated scans. Without it you get empty results, not an error — the worst possible failure mode. - Reading order from
boundingBox. Observations come per visual line, in normalized coordinates with the origin at the bottom-left. To reconstruct reading order: cluster bymidY(descending), then sort each cluster byminX(ascending). Use a tolerance derived from the median line height (roughly 0.6×), never a hard-coded constant — zoom level and resolution will break a constant immediately. - Hyphenation on join. Drop a trailing hyphen only when the next fragment starts lowercase; otherwise you will destroy real identifiers such as
--legacy-peer-deps.
Where the data goes (the part that decides adoption)
Splitting the pipeline matters more than the OCR settings for anyone reading internal documents:
| Stage | Runs | Leaves the machine |
|---|---|---|
| Screen capture → text recognition | On-device (Apple Vision) | nothing — pixels stay local |
| Recognized text → translation | Online | the recognized string only |
So: the screenshot of an unpublished manuscript is never uploaded, but the recognized text is sent for translation. Offline, recognition still works and translation does not (NSURLErrorDomain error -1009). That is a design boundary, not a defect, and it should be stated plainly rather than buried.
What this does not solve
- Scanned papers with no text layer are recognized, but OCR of dense math remains unreliable regardless of settings.
- Vector figures are not text; nothing here reads a plot.
- Column detection on three-column layouts (some Nature/Nature-style formats) needs a different clustering strategy.
- Latency is machine- and resolution-dependent; I am deliberately not quoting a number, because a figure measured on one Mac does not transfer to another.
The tool I use
I built this into a small macOS app for my own reading workflow and distribute it:
- macOS 13.0+ / Apple Silicon (arm64) only — does not run on Intel Macs
- No account required, free to use
- First launch of a non-notarized build: right-click → Open
Product page: https://www.zipp-mac.com/?utm_source=devto&utm_medium=social
Source and builds: https://github.com/dayu-999/zipp-mac
Takeaway
When a two-column paper "breaks" after copy-paste, do not tune your translator. The text is intact; the order is wrong. Either copy per column, or capture the region so the reading path is preserved — and if you go the OCR route, set usesLanguageCorrection = false and derive your line-clustering tolerance from the observed text height.
