権限キーを追加する
macOS のスクリーン録画許可 (Screen Recording)
これはアプリが画面をキャプチャする場合に必須。
- キー名(生):
NSMicrophoneUsageDescriptionとかの iOS系じゃない - macOS では実はこれ ↓
▶ NSCameraUsageDescription や NSMicrophoneUsageDescription のような専用キーは無い
スクリーン録画には Info.plist キーは不要
ただし 利用する API に応じて説明文が必要。
ScreenCaptureKit は Info.plist 許可不要
→ 代わりに macOS システム側の「画面収録」許可ダイアログが自動で出る
🛠 Sandbox をチェックする
- Xcode 左ペイン →
Projectを選択 - TARGETS →
YourApp Signing & CapabilitiesApp Sandboxの項目を確認
以下を必ずチェック:
■ App Sandbox
- ☑ User Selected File (Read/Write) → スクショ保存する場合
- Screen Recording → この項目は Xcode 14 以降で自動表示される場合もある
もし “Screen Recording” がない → OK(macOS のシステム許可が自動で出る)
📌 ScreenCaptureKit 使用時に必要な Info.plist のキー
実際には 特別な説明文は不要
でも OCR などで画像を処理するならユーザー安心のため説明文を入れることはある。
例として:
任意
| キー名 | 説明 |
|---|---|
NSPhotoLibraryAddUsageDescription | スクショをフォトへ保存する場合 |
NSPhotoLibraryUsageDescription | 写真へアクセスする場合 |
💬 Info.plist に手動で追加する例
|
1 2 |
<key>NSPhotoLibraryAddUsageDescription</key> <string>アプリがスクリーンショットを保存するためフォトライブラリへアクセスします。</string> |
デバッグコード
スクショ出来ているか判定
負の値が入っていると切り取りが出来ないので入力 rect によりローカル計算が必要かどうか変わる
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
func captureScreenRegion99(rect: CGRect, display: SCDisplay) async throws -> Void { print("=== デバッグ情報 ===") print("入力rect: \(rect)") print("Display frame: \(display.frame)") print("Display size: \(display.width) x \(display.height)") let filter = SCContentFilter(display: display, excludingWindows: []) let config = SCStreamConfiguration() config.width = display.width config.height = display.height config.scalesToFit = false config.showsCursor = false let image = try await SCScreenshotManager.captureImage( contentFilter: filter, configuration: config ) print("取得した画像サイズ: \(image.width) x \(image.height)") let scale = CGFloat(display.width) / CGFloat(display.frame.width) print("Scale: \(scale)") // rectをディスプレイのローカル座標に変換 let displayOrigin = display.frame.origin let localRect = CGRect( x: rect.origin.x - displayOrigin.x, y: rect.origin.y - displayOrigin.y, width: rect.width, height: rect.height ) print("Local rect: \(localRect)") let scaledRect = CGRect( x: localRect.origin.x * scale, y: localRect.origin.y * scale, width: localRect.width * scale, height: localRect.height * scale ) print("Scaled rect: \(scaledRect)") // Y座標を反転 let flippedY = CGFloat(image.height) - scaledRect.origin.y - scaledRect.height let cropRect = CGRect( x: scaledRect.origin.x, y: flippedY, width: scaledRect.width, height: scaledRect.height ) print("Crop rect: \(cropRect)") // cropRectが画像範囲内かチェック let imageBounds = CGRect(x: 0, y: 0, width: image.width, height: image.height) print("Image bounds: \(imageBounds)") print("cropRect is valid: \(imageBounds.contains(cropRect.origin) && cropRect.maxX <= CGFloat(image.width) && cropRect.maxY <= CGFloat(image.height))") guard let croppedImage = image.cropping(to: cropRect) else { print("❌ 切り取り失敗") throw NSError(domain: "ScreenCapture", code: -2, userInfo: [NSLocalizedDescriptionKey: "画像の切り取りに失敗しました"]) } print("✅ 切り取り成功: \(croppedImage.width) x \(croppedImage.height)") } |
OCR本体
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
import Vision import CoreGraphics func recognizeText1(from cgImage: CGImage, completion: @escaping (String) -> Void) { let request = VNRecognizeTextRequest { request, error in // エラーハンドリングを追加 if let error = error { print("OCRエラー: \(error.localizedDescription)") completion("") return } guard let results = request.results as? [VNRecognizedTextObservation] else { completion("") return } let text = results .compactMap { $0.topCandidates(1).first?.string } .joined(separator: "\n") // メインスレッドでコールバック DispatchQueue.main.async { completion(text) } } // 認識レベルを設定(accurate推奨) request.recognitionLevel = .accurate // 認識対象言語(macOS 13以降) if #available(macOS 13.0, *) { request.recognitionLanguages = ["ja", "en"] } // 自動言語補正を有効化 request.usesLanguageCorrection = true // 実行 let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) DispatchQueue.global(qos: .userInitiated).async { do { try handler.perform([request]) } catch { print("Vision実行エラー: \(error.localizedDescription)") DispatchQueue.main.async { completion("") } } } } |
🟨 画像形式判別
|
1 2 3 4 |
func isImageFile(_ url: URL) -> Bool { let ex = url.pathExtension.lowercased() return ["png", "jpg", "jpeg", "heic", "bmp", "tiff"].contains(ex) } |
🟧 NSImage → CGImage 変換
|
1 2 3 4 5 6 |
extension NSImage { func toCGImage() -> CGImage? { var rect = CGRect(origin: .zero, size: self.size) return self.cgImage(forProposedRect: &rect, context: nil, hints: nil) } } |
🟧 ゴミ文字判定ロジック
|
1 2 3 4 5 6 |
func isMostlyGarbage(_ text: String) -> Bool { let allowed = CharacterSet.alphanumerics.union(.whitespaces) let filtered = text.unicodeScalars.filter { allowed.contains($0) } let ratio = Double(filtered.count) / Double(text.unicodeScalars.count) return ratio < 0.3 // 許容率30%未満 → ゴミと判定 } |
指定したディスプレイのスクショ撮影(指定した矩形を切り抜く)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
import CoreGraphics import ScreenCaptureKit func captureScreenRegion3(rect: CGRect, display: SCDisplay) async throws -> CGImage? { // スクリーンショット設定 let filter = SCContentFilter(display: display, excludingWindows: []) let config = SCStreamConfiguration() config.width = display.width config.height = display.height config.scalesToFit = false config.showsCursor = false // スクリーンショットを撮影 let image = try await SCScreenshotManager.captureImage( contentFilter: filter, configuration: config ) // 指定範囲を切り取り let scale = CGFloat(display.width) / CGFloat(display.frame.width) // rectをディスプレイのローカル座標に変換 //rect の状態に注意---------------------- let displayOrigin = display.frame.origin let localRect = CGRect( x: rect.origin.x - displayOrigin.x, y: rect.origin.y - displayOrigin.y, width: rect.width, height: rect.height ) let scaledRect = CGRect( x: localRect.origin.x * scale, y: localRect.origin.y * scale, width: localRect.width * scale, height: localRect.height * scale ) // Y座標を反転 let flippedY = CGFloat(image.height) - scaledRect.origin.y - scaledRect.height let cropRect = CGRect( x: scaledRect.origin.x, y: flippedY, width: scaledRect.width, height: scaledRect.height ) guard let croppedImage = image.cropping(to: cropRect) else { throw NSError(domain: "ScreenCapture", code: -2, userInfo: [NSLocalizedDescriptionKey: "画像の切り取りに失敗しました"]) } return croppedImage } |
PDFファイルの判別法
🟦 PDF の中身を判別する仕組み
✔ パターン1:テキストベースのPDF
・実際の”テキストオブジェクト”が内部に存在する
・ 選択やコピーができる
・ 軽い(数+KB~数百KB)
・ 文字情報を PDFKit から直接取得できる
→ Vision OCR は不要
✔ パターン2:画像ベースのPDF(スキャンPDF)
・ スキャナーで取り込んだ画像をそのまま貼っている
・ 文字は画像のピクセルに含まれているだけ
・PDFとしては「画像を貼っただけ」
・ 選択できない
・重い(数MB~数+MB)
→ Vision OCR が必要
🟩 Swift(PDFKit)で判別可能
PDFKit の PDFPage にはこういうメソッドがあります:
✔ ① string(PDFPage.string)
PDFページから抽出可能なテキストを返す。
|
1 2 3 4 5 |
if let text = page.string, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { print("テキストPDF") } else { print("画像PDF") } |
ただし注意点:
- レイアウトが複雑だったり埋め込みフォントによってはテキストが取得できないこともある(例:変なPDF生成ツール)
- それでもかなり実用的
✔ ② 画像を抽出してチェックする方法(確実)
|
1 |
let image = page.thumbnail(of: CGSize(width: 100, height: 100), for: .mediaBox) |
ここで Vision の
VNDetectTextRectanglesRequestを使えば
「画像に文字があるか?」も判別できる。
🟩 Swift で PDF を判別するコード例
|
1 2 3 4 5 6 7 8 9 10 11 |
func isTextBasedPDF(url: URL) -> Bool { guard let pdf = PDFDocument(url: url) else { return false } for i in 0..<pdf.pageCount { guard let page = pdf.page(at: i) else { continue } if let text = page.string, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true // 1ページでもテキストがあれば「テキストPDF」 } } return false } |
画像判定(OCR判定)の方が確実なパターンもある
複雑なフォント・アウトライン化(文字が図形になってる)などは
PDFPage.string では取得できないけど、
OCR を通せば文字があることが分かることもある。


コメント