メインコンテンツへスキップ
見出し画像
Photo bymaki121416

【片手操作】写真をタップ操作で範囲指定して切り取るiPhoneショートカットレシピ

    相変わらず仕事が忙しく更新頻度がピンチとなっております。
    仕事で割と画期的なツールを作ったのですが、ここに載せられないのが残念(泣)

    さて、今回は写真を切り取るiPhoneショートカットレシピの三つ目となります。どんだけ切り取りたいのでしょうか?(笑)

    1つ目:画像にマス目(グリッド線)を入れて、範囲指定で写真をざっくり切り取るやつ

    2つ目:写真を矢印ボタン操作で少しずつ削って調整するやつ


    今回のものを含めての共通点は、
    絶対手で操作したほうが速いです(笑)
    それでも作りたいものを衝動のままに作るのがこのnoteのスタイルです!

    切り取り精度の点で言えば前の二つよりも低いかもしれませんが、操作性の手軽さで言えば一番良さそうな印象でした。

    まずは操作画面イメージから

    画像
    慣れるまで範囲切り取る感覚つかむのがムズイ・・・

    画像を取り込んで、始点と終点タップしてボタンを押すと範囲切り取ってくれる仕組みです。
    今回は範囲の画像の文字起こし機能もつけてみました。

    今回のレシピ本体
    (レシピとは別にScriptableというアプリとコードの設定が必要となります。後述します)



    1.作った経緯

    実は今回、別のショートカットレシピを紹介する予定でした。

    そのレシピの初期設定のためにiPhone画面上のxy座標を取得する仕組みを付けようと思っていて。

    そしてchatGPTさんに聞いて作ってたら、何故か別のレシピになっていたという。

    僕はこういうケースよくあります(笑)

    尚、今回は(今回も?)このアプリが必要となります。


    2.仕組み

    ①iPhoneショートカットを起動して写真を選択
    ②選択した写真をScriptable渡します。
    ③Scriptableのスクリプトが実行され、HTMLの画面が開きます。
    ④画像の始点(切り取る長方形の左上)をタップ。
    ⑤画像の終点(切り取る長方形の右下)をタップ。
    ⑥保存を押すとその範囲の画像が写真アプリに保存され、文字起こしを押すと画像内の文字がクリップボードにコピーされます。


    3.設定方法

    【Scriptableの設定】
    ✅今回もショートカットレシピ自体の設定は不要ですが、Scriptableの設定および注意事項があります。

    ①Scriptableをダウンロードして開きます。右上の「+」ボタンを押すとコードを入力する画面が開きます。

    画像
    右側の「+」ボタンを押すと左の画面が開くので
    「1」の右側にコードをコピペします。

    そこに以下のコードをコピペして、「Done」を押してください

    // ========= 設定 =========
    const shortcutName = "ImageTap"
    
    // ========= 画像 =========
    let img = args.images && args.images.length ? args.images[0] : null
    
    if (!img) {
      Script.complete()
      return
    }
    
    const base64 = Data.fromPNG(img).toBase64String()
    
    // ========= HTML =========
    const html = `
    <!DOCTYPE html>
    <html>
    
    <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
    body {
      margin:0;
      font-family:-apple-system;
      touch-action:none;
      overflow:auto;
    }
    
    .notice {
      background:#ffecec;
      color:#c00;
      padding:6px;
      font-size:13px;
      text-align:center;
    }
    
    img {
      width:100%;
      max-height:70vh;
      object-fit:contain;
    }
    
    .button-row {
      display:flex;
      gap:6px;
      padding:6px;
    }
    
    button {
      flex:1;
      height:40px;
      font-size:14px;
    }
    </style>
    </head>
    
    <body>
    
    <div class="notice">
    ⚠ closeは使わないでください(下のボタンで終了)
    </div>
    
    <img id="img" src="data:image/png;base64,${base64}">
    
    <div style="padding:6px;text-align:center;font-size:14px">
    タップ①:開始位置 → タップ②:終了位置
    </div>
    
    <div class="button-row">
    <button onclick="save()">保存</button>
    <button onclick="ocr()">文字起こし</button>
    <button onclick="cancel()">終了</button>
    </div>
    
    <script>
    let points = []
    
    const imgEl = document.getElementById("img")
    
    imgEl.addEventListener("click", function(e){
    
      const rect = imgEl.getBoundingClientRect()
    
      const x = (e.clientX - rect.left) * (imgEl.naturalWidth / rect.width)
      const y = (e.clientY - rect.top) * (imgEl.naturalHeight / rect.height)
    
      points.push({x,y})
    
      if(points.length > 2) points = [points[1]]
    
    })
    
    function crop(addMargin=false){
      if(points.length < 2) return null
    
      const p1 = points[0]
      const p2 = points[1]
    
      let x = Math.min(p1.x, p2.x)
      let y = Math.min(p1.y, p2.y)
      let w = Math.abs(p1.x - p2.x)
      let h = Math.abs(p1.y - p2.y)
    
      if(addMargin){
        const margin = 20
        x -= margin
        y -= margin
        w += margin * 2
        h += margin * 2
    
        if(x < 0) x = 0
        if(y < 0) y = 0
      }
    
      const canvas = document.createElement("canvas")
      const ctx = canvas.getContext("2d")
    
      canvas.width = w
      canvas.height = h
    
      ctx.drawImage(imgEl, x, y, w, h, 0, 0, w, h)
    
      return canvas.toDataURL()
    }
    
    function save(){
      const data = crop(false)
      if(data){
        window.result = JSON.stringify({type:"save",data:data})
      }
    }
    
    function ocr(){
      const data = crop(true)
      if(data){
        window.result = JSON.stringify({type:"ocr",data:data})
      }
    }
    
    function cancel(){
      window.result = "NO_SAVE"
    }
    </script>
    
    </body>
    </html>
    `
    
    // ========= WebView =========
    let wv = new WebView()
    await wv.loadHTML(html)
    wv.present(true)
    
    // ========= 結果待ち =========
    let result = null
    
    while (true) {
      await new Promise(resolve => Timer.schedule(0.2, false, resolve))
    
      try {
        result = await wv.evaluateJavaScript("window.result", false)
      } catch(e) {
        break
      }
    
      if (result) break
    }
    
    // ========= 分岐 =========
    if (result === "NO_SAVE") {
    
      Pasteboard.copyString("NO_SAVE")
    
      try { wv.dismiss() } catch(e) {}
    
      Safari.open(
        "shortcuts://run-shortcut?name=" + encodeURIComponent(shortcutName)
      )
    
      Script.complete()
      return
    }
    
    if (!result) {
      try { wv.dismiss() } catch(e) {}
      Script.complete()
      return
    }
    
    // ========= JSONそのまま渡す =========
    Pasteboard.copyString(result)
    
    try { wv.dismiss() } catch(e) {}
    
    Safari.open(
      "shortcuts://run-shortcut?name=" + encodeURIComponent(shortcutName)
    )
    
    Script.complete()

    ②初期状態のスクリプト名は「Untitled Script」となっているので、その名称を「ImageTap」に変更します。スクリプトを長押しするとメニューが出てくるのでその中から「Rename」を選んでスクリプト名を変更してください。

    【注意点】
    ✅ダウンロードしたショートカット名「ImageTap」で動作させることを前提にコードを作っております。もしショートカット名を変更した場合は上記コード内の以下の名称部分を変更してください。

    // ========= 設定 =========
    const shortcutName = "ImageTap"

    ✅そのあと、ショートカット名の以下の部分も変更してください。

    画像
    ショートカット名と上記の赤枠の名称は
    同じである必要があります。

    これで設定は完了です。


    4.文字起こしについて

    画像については、切り取られた画像が写真アプリに保存されるだけなので、特に動作説明として書くことありません。

    文字起こしについて、動作と注意点を簡単に記載します。

    ✅文字起こしボタンを押したあとの流れ

    →以下のように入力欄に文字起こしされた内容が表示されます。
     文字起こしの精度によっては余計な文字が含まれることが多いのでこの画面で加工して完了を押すと加工された内容がクリップボードに格納される仕組みとしております。

    画像
    Yahooニュースの内容

    ✅文字起こしの精度について

     →余白が充分にあるレイアウトの画面の場合は問題ないのですが、文字と文字の距離や文字と枠線・画像の距離が近い画面の場合は途端に精度が低くなります。

    文字起こしの際は切り取った画像を若干広げて余白をとった状態にしてOCRをかける仕組みにして精度を上げる試みを実装してますが、効果あったのかは微妙・・・

    【問題点】
    このショートカットは以下の動作を行うと異常終了して、その直後に再開すると正常に動作しなくなります。

    ✅HTML画面の「Close」ボタンで終了した場合
    ✅ショートカット終了後にScriptableを開いた場合

    この動作を行った場合はAppスイッチャーを起動して「Scriptable」「ショートカット」のアイコンを上にスワイプして完全終了させると再びお使いになれます。


    5.まとめ

    Scriptableさんにすっかりハマってしまって、最近のショートカットはほぼScriptable絡みのものになっています。

    chatGPTさんからの苦情として「Scriptableはクセあるし、普通にhtmlのみのWebツール作ったほうが速いで」と言われました(笑)

    今度はショートカットから離れてそういうツール作るのも楽しいかもしれませんね。


    iPhoneショートカットアプリ関連




    この記事が参加している募集

     
     
    本当のお仕事は底辺の非正規労働者。東京都在住。現実逃避のためchatGPTと一緒に無駄に真面目な開発ごっこをしています。効率化とタスク管理を好む一方で非効率でも自動で動くカッコよさげな仕組みも好き。実はITスキル低め。趣味はAbemaの報道リアリティショー『アベマプライム』視聴。

    あなたへのおすすめ