
【機能追加】写真を切り取るレシピ2種を改善してみた【iPhoneショートカットレシピ】
これまでに記事にした写真を切り取る系のレシピについて、機能を改善してみました。
改善理由としては以下となります。
①特に文字起こしについてはスクリーンショットしたものを切り取る用途の方が多そう。
②以前、別のレシピで覚えたボタン長押しによる連続実行機能があればより操作性があがると思ったため。
対象レシピ①:写真を矢印ボタン操作で少しずつ削って調整
対象レシピ②:タップ操作で範囲指定して切り取る
1.対象レシピ①(矢印ボタン操作)の改善点
【変更点】
✅ボタンを削る矢印ボタン4種について、ボタン長押しで高速で削る機能を付与しました。
✅画像が削られると画像およびボタンの位置が変わる挙動が誤操作に繋がると思ったので、画像・ボタンは位置固定に変更。
✅対象画像について、写真アプリ内から選ぶ方法とスクリーンショットをその場で撮ってそれを活用する方法とで選べるようにしました
✅切り取った画像の処理として、写真保存と写真内の文字起こしとで選べるようにしました。
✅各アプリの共有ボタンから実行できるようにしました。
【動作説明】
①レシピを実行すると写真フォルダの選択画面が開かれますが、この時「スクショ」というダミー画像がフォルダに含まれています。これを選ぶとスクリーンショット操作に進み、他の写真を選ぶとその写真を加工する操作に進みます。※ダミー画像はレシピ終了時に自動で削除されます。

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

スクショを撮るアプリに移動します。
③ツールで削る操作を行い、保存を押したあとで以下の選択メニューが表示されます。

ショートカット側で対応しました
【今回のレシピ本体】
(レシピとは別に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ショートカットアプリ関連