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

【機能追加】写真を切り取るレシピ2種を改善してみた【iPhoneショートカットレシピ】

    これまでに記事にした写真を切り取る系のレシピについて、機能を改善してみました。

    改善理由としては以下となります。

    ①特に文字起こしについてはスクリーンショットしたものを切り取る用途の方が多そう。
    ②以前、別のレシピで覚えたボタン長押しによる連続実行機能があればより操作性があがると思ったため。

    対象レシピ①:写真を矢印ボタン操作で少しずつ削って調整

    対象レシピ②:タップ操作で範囲指定して切り取る



    1.対象レシピ①(矢印ボタン操作)の改善点

    【変更点】

    ✅ボタンを削る矢印ボタン4種について、ボタン長押しで高速で削る機能を付与しました。
    ✅画像が削られると画像およびボタンの位置が変わる挙動が誤操作に繋がると思ったので、画像・ボタンは位置固定に変更。
    ✅対象画像について、写真アプリ内から選ぶ方法とスクリーンショットをその場で撮ってそれを活用する方法とで選べるようにしました
    ✅切り取った画像の処理として、写真保存と写真内の文字起こしとで選べるようにしました。
    ✅各アプリの共有ボタンから実行できるようにしました。

    【動作説明】

    ①レシピを実行すると写真フォルダの選択画面が開かれますが、この時「スクショ」というダミー画像がフォルダに含まれています。これを選ぶとスクリーンショット操作に進み、他の写真を選ぶとその写真を加工する操作に進みます。※ダミー画像はレシピ終了時に自動で削除されます。

    画像
    「スクショ」ダミー画像は自動的に削除されます。

    ②スクショを選ぶと以下のアラートが表示されます。これを表示させたままスクリーンショット撮影したい場所・アプリに移動します。準備ができたらOKを押すとスクリーンショット画像を読み込んでツールが起動します。これ以降の操作は以前のバージョンとほぼ同じです。

    画像
    アラートを表示させたまま
    スクショを撮るアプリに移動します。

    ③ツールで削る操作を行い、保存を押したあとで以下の選択メニューが表示されます。

    画像
    scriptable側のUI変えるのが面倒くさかったので、
    ショートカット側で対応しました

    【今回のレシピ本体】

    (レシピとは別にScriptableというアプリとコードの設定が必要となります。設定は前回の記事をご参照ください)

    【Scriptableのコード】

    // ========= 画像 =========
    Pasteboard.copyString("")
    const img = args.images[0]
    const base64 = Data.fromPNG(img).toBase64String()
    
    // ★ 自分のショートカット名
    const shortcutName = "ImageCropFlow"
    // ========= HTML =========
    const html = `
    <!DOCTYPE html>
    <html>
    <head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <style>
    body { margin:0; text-align:center; font-family:-apple-system; }
    
    .notice {
      background:#ffecec;
      color:#c00;
      padding:6px;
      font-size:13px;
    }
    
    .imgWrap {
      width:100%;
      height:52vh;
      display:flex;
      align-items:center;
      justify-content:center;
      overflow:hidden;
      background:#000;
    }
    
    .imgWrap img {
      max-width:100%;
      max-height:100%;
      object-fit:contain;
    }
    
    button {
      width:100%;
      margin:3px 0;
      padding:10px;
      font-size:14px;
    }
    
    .row {
      display:flex;
      justify-content:center;
      gap:6px;
      padding:0 6px;
    }
    
    .col {
      width:48%;
    }
    
    input {
      width:60px;
      text-align:center;
    }
    
    body {
      touch-action: manipulation;
      -webkit-user-select: none;
      -webkit-touch-callout: none;
    }
    </style>
    </head>
    
    <body>
    
    <div class="notice">
    ⚠ closeは使わないでください(下のボタンで終了)
    </div>
    
    <div class="imgWrap">
      <img id="img" src="data:image/png;base64,${base64}">
    </div>
    
    <!-- 削り値 -->
    <div class="row">
      <div class="col">
        <button ontouchstart="startDown()" ontouchend="stopStep()" ontouchcancel="stopStep()">
          削り値(px) −
        </button>
      </div>
    
      <input id="stepInput" value="20">
    
      <div class="col">
        <button ontouchstart="startUp()" ontouchend="stopStep()" ontouchcancel="stopStep()">
          削り値(px) +
        </button>
      </div>
    </div>
    
    <!-- 左右 -->
    <div class="row">
      <div class="col">
        <button ontouchstart="startCut('left')" ontouchend="stopCut()" ontouchcancel="stopCut()">
          ⏪ 左
        </button>
      </div>
      <div class="col">
        <button ontouchstart="startCut('right')" ontouchend="stopCut()" ontouchcancel="stopCut()">
          ➡ 右
        </button>
      </div>
    </div>
    
    <!-- 上下 -->
    <div class="row">
      <div class="col">
        <button ontouchstart="startCut('top')" ontouchend="stopCut()" ontouchcancel="stopCut()">
          🔼 上
        </button>
      </div>
      <div class="col">
        <button ontouchstart="startCut('bottom')" ontouchend="stopCut()" ontouchcancel="stopCut()">
          🔽 下
        </button>
      </div>
    </div>
    
    <!-- リセット / 戻る -->
    <div class="row">
      <div class="col"><button onclick="reset()">🔙 リセット</button></div>
      <div class="col"><button onclick="undo()">↩ 戻る</button></div>
    </div>
    
    <!-- 保存 / 終了 -->
    <div class="row">
      <div class="col"><button onclick="save()">💾 保存</button></div>
      <div class="col"><button onclick="finish()">🛑 終了</button></div>
    </div>
    
    <script>
    let original = new Image()
    original.src = document.getElementById("img").src
    
    let cut = {l:0,t:0,r:0,b:0}
    let history = []
    
    original.onload = () => draw()
    
    function getStep() {
      let val = parseInt(document.getElementById("stepInput").value)
      if (isNaN(val) || val <= 0) return 20
      return val
    }
    
    function draw() {
      const canvas = document.createElement("canvas")
      const ctx = canvas.getContext("2d")
    
      const w = original.width
      const h = original.height
    
      const newW = w - cut.l - cut.r
      const newH = h - cut.t - cut.b
    
      if (newW <= 0 || newH <= 0) return
    
      canvas.width = newW
      canvas.height = newH
    
      ctx.drawImage(original, -cut.l, -cut.t)
    
      document.getElementById("img").src = canvas.toDataURL()
    }
    
    function saveHistory() {
      history.push({...cut})
    }
    
    function undo() {
      if (history.length === 0) return
      cut = history.pop()
      draw()
    }
    
    function reset() {
      cut = {l:0,t:0,r:0,b:0}
      draw()
    }
    
    function cutDir(dir) {
      saveHistory()
      const step = getStep()
    
      if (dir==="left") cut.l += step
      if (dir==="right") cut.r += step
      if (dir==="top") cut.t += step
      if (dir==="bottom") cut.b += step
    
      draw()
    }
    
    function save() {
      const data = document.getElementById("img").src
      if (!data || !data.startsWith("data:image")) return
      window.result = data
    }
    
    function finish() {
      window.result = "NO_SAVE"
    }
    
    function stepUp() {
      let input = document.getElementById("stepInput")
      let val = parseInt(input.value) || 0
      input.value = val + 1
    }
    
    function stepDown() {
      let input = document.getElementById("stepInput")
      let val = parseInt(input.value) || 0
      if (val > 1) input.value = val - 1
    }
    
    let interval = null
    let cutInterval = null
    
    function startUp() {
      stepUp()
      interval = setInterval(stepUp, 100)
    }
    
    function startDown() {
      stepDown()
      interval = setInterval(stepDown, 100)
    }
    
    function stopStep() {
      clearInterval(interval)
    }
    
    function startCut(dir) {
      saveHistory()
      cutDir(dir)
      cutInterval = setInterval(() => cutDir(dir), 100)
    }
    
    function stopCut() {
      clearInterval(cutInterval)
    }
    </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.3, 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
    }
    
    // ========= 保存 =========
    Pasteboard.copyString(result)
    
    try { wv.dismiss() } catch(e) {}
    
    Safari.open(
      "shortcuts://run-shortcut?name=" + encodeURIComponent(shortcutName)
    )
    
    Script.complete()

    2.対象レシピ②:タップ操作で範囲指定の改善点

    基本的には対象レシピ①の変更点とほぼ同じです。
    (②は元々文字起こし機能がありました)

    実行時の写真選択の挙動も①とほぼ同じなので動作説明は割愛させていただきます。

    【変更点】

    ✅対象画像について、写真アプリ内から選ぶ方法とスクリーンショットをその場で撮ってそれを活用する方法とで選べるようにしました
    ✅各アプリの共有ボタンから実行できるようにしました。

    【今回のレシピ本体】

    (レシピとは別にScriptableというアプリとコードの設定が必要となります。設定は前回の記事をご参照ください)

    【Scriptableのコード】

    // ========= 設定 =========
    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;
    }
    
    .imgWrap {
      width:100%;
      height:52vh;
      display:flex;
      align-items:center;
      justify-content:center;
      overflow:hidden;
      background:#000;
    }
    
    .imgWrap img {
      max-width:100%;
      max-height:100%;
      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>
    
    <div class="imgWrap">
      <img id="img" src="data:image/png;base64,${base64}">
    </div>
    
    <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()

    3.共有メニューからの実行に対応

    共有メニューからの実行に対応しました。

    SafariやXなどを開いた状態で共有メニューからショートカットを呼び出すイメージですね。スクリーンショットの際はこちらの方が便利かと。

    画像
    共有ボタンを押します
    画像
    初期状態では上のレシピは一覧内に埋もれているので、
    上の方に移動させると便利です

    4.まとめ

    1回ツールを作ったあとに、他のツール開発で覚えたワザを活用して改善するという行為が結構好きでして(笑)

    作ってすぐ記事にしている場合は用途やニーズも不明瞭だったりするので、しばらく使ってみると違うアイデアも出てきて「直したい!」ってなってしまうんですよね。

    少し寝かせてから記事にしたほうが良いのかも(せっかちですので・・・)


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




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

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

    あなたへのおすすめ