<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zane ⚡️ Zipp開発者</title>
    <description>The latest articles on DEV Community by Zane ⚡️ Zipp開発者 (@dayu510).</description>
    <link>https://dev.to/dayu510</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4244744%2Fd01ea8b6-90dc-4fc6-9941-422147da9ca1.jpg</url>
      <title>DEV Community: Zane ⚡️ Zipp開発者</title>
      <link>https://dev.to/dayu510</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dayu510"/>
    <language>en</language>
    <item>
      <title>Local Apple Vision OCR for uncopyable macOS errors and two-column PDFs</title>
      <dc:creator>Zane ⚡️ Zipp開発者</dc:creator>
      <pubDate>Sat, 14 Nov 2026 16:52:40 +0000</pubDate>
      <link>https://dev.to/dayu510/local-apple-vision-ocr-for-uncopyable-macos-errors-and-two-column-pdfs-2008</link>
      <guid>https://dev.to/dayu510/local-apple-vision-ocr-for-uncopyable-macos-errors-and-two-column-pdfs-2008</guid>
      <description>&lt;h2&gt;
  
  
  The problem: the text exists, but you cannot select it
&lt;/h2&gt;

&lt;p&gt;On macOS, a lot of the text that blocks you is &lt;strong&gt;drawn, not selectable&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity's console output (&lt;code&gt;NullReferenceException: Object reference not set to an instance of an object&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Blender's status/error dialogs (&lt;code&gt;Error: Cannot read file "...": No such file or directory&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Xcode's red inline diagnostics&lt;/li&gt;
&lt;li&gt;Gatekeeper and permission dialogs (&lt;code&gt;"AppName" cannot be opened because the developer cannot be verified.&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Any English text baked into a screenshot or a scanned PDF&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy-paste translation pipelines fail on all of these, because there is no text layer to copy. And text-extraction order from a two-column PDF frequently &lt;strong&gt;interleaves the columns&lt;/strong&gt;, so pasting into a translator produces scrambled sentences even when extraction succeeds.&lt;/p&gt;

&lt;p&gt;So the job is: &lt;strong&gt;read what is on screen, in place, without shipping the screenshot anywhere.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach: on-device OCR, then translate the text only
&lt;/h2&gt;

&lt;p&gt;The split I settled on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;th&gt;What leaves the machine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Screen capture → text recognition&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;On-device&lt;/strong&gt; (Apple Vision, &lt;code&gt;VNRecognizeTextRequest&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;nothing — pixels stay local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recognized text → translation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Online&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the recognized string only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is deliberate. Sending screenshots of internal code, customer names, or unreleased assets to a cloud OCR endpoint is a decision most of us are not authorized to make. On-device recognition removes that decision from the critical path.&lt;/p&gt;

&lt;p&gt;It is &lt;em&gt;not&lt;/em&gt; a "100% offline" tool. Translation needs the network. That boundary is worth stating plainly, because &lt;code&gt;NSURLErrorDomain error -1009&lt;/code&gt; (offline) is a real failure mode, not a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation notes that actually matter
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Vision&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;recognize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;cgImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CGImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orientation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CGImagePropertyOrientation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VNRecognizeTextRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recognitionLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accurate&lt;/span&gt;       &lt;span class="c1"&gt;// small UI text needs .accurate&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recognitionLanguages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ja-JP"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usesLanguageCorrection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;     &lt;span class="c1"&gt;// never "fix" identifiers&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minimumTextHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VNImageRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cgImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cgImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orientation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;orientation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compactMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;topCandidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things that cost me the most time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;usesLanguageCorrection&lt;/code&gt; must be &lt;code&gt;false&lt;/code&gt; for code.&lt;/strong&gt; With correction on, &lt;code&gt;E0382&lt;/code&gt; and &lt;code&gt;dmodel&lt;/code&gt; get "helpfully" rewritten, and you can no longer search for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass the orientation.&lt;/strong&gt; Rotated scans silently return garbage unless you hand &lt;code&gt;VNImageRequestHandler&lt;/code&gt; a &lt;code&gt;CGImagePropertyOrientation&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild reading order from &lt;code&gt;boundingBox&lt;/code&gt;.&lt;/strong&gt; For two-column layouts, cluster observations into rows by &lt;code&gt;y&lt;/code&gt;, then sort each cluster by &lt;code&gt;x&lt;/code&gt;. Reading order is a layout problem, not an OCR problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two-column PDFs: why region capture helps
&lt;/h2&gt;

&lt;p&gt;A region capture preserves the visual reading path, so the left column's continuation does not get glued to the right column's opening. Nothing magic here — it is a different extraction route, and that difference is what fixes the symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;This is a small macOS app I built for my own workflow and now distribute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;macOS 13.0+ / Apple Silicon (arm64) only — does not run on Intel Macs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;No account, no sign-up&lt;/li&gt;
&lt;li&gt;Free to use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Download page (with campaign parameters): &lt;a href="https://www.zipp-mac.com/?utm_source=devto&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;https://www.zipp-mac.com/?utm_source=devto&amp;amp;utm_medium=social&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source and builds: &lt;a href="https://github.com/dayu-999/zipp-mac" rel="noopener noreferrer"&gt;https://github.com/dayu-999/zipp-mac&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First launch on an unsigned/not-notarized build: &lt;strong&gt;right-click → Open&lt;/strong&gt; (the standard Gatekeeper path — the app is not "damaged").&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth repeating
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Translation is &lt;strong&gt;online&lt;/strong&gt;; offline means no translation.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;screenshot is not uploaded&lt;/strong&gt;, but the &lt;strong&gt;recognized text is&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Apple Silicon only.&lt;/li&gt;
&lt;li&gt;I am &lt;strong&gt;not&lt;/strong&gt; claiming a latency figure here. Numbers on a dev machine do not transfer to yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;If your blocker is "the error is on screen but I can't copy it", local OCR is the shortest path that does not require a policy decision about sending your screen to someone else's server. The translation step can stay online; the pixels do not have to leave.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>ocr</category>
      <category>vision</category>
    </item>
  </channel>
</rss>
