Skip to content

fix(scratch-gui): ブロックのスクリーンショットを高解像度で書き出す (#1176) - #1178

Merged
takaokouji merged 2 commits into
developfrom
topic/autopilot-1176
Sep 9, 2026
Merged

takaokouji merged 2 commits into
developfrom
topic/autopilot-1176

Conversation

@smalruby3-editor-bot

Copy link
Copy Markdown

Summary

ブロックのスクリーンショット(コードタブのカメラボタン / クラス機能の提出 / 不具合報告)が
低解像度で書き出される問題を修正した。blocks-screenshot.js に書き出し倍率を導入し、
レイアウト倍率を workspace.scale(ユーザーの現在ズーム。既定 0.675)から切り離した。

ブロックは SVG なので、倍率を上げるとベクタとして再描画され、ぼやけずに高解像度になる。

Changes Made

packages/scratch-gui/src/lib/blocks-screenshot.js

  • LOGICAL_SCALE = 1 — レイアウトは常にブロックの自然サイズ。出力サイズがユーザーの
    ズーム状態に依存しなくなる
    (同じプログラムなら誰が撮っても同じサイズ)
  • DEFAULT_EXPORT_SCALE = 2 — renderBlocksToCanvas の既定。提出 / 不具合報告経路が使う。
    Ruby タブの ruby-screenshot.js (PIXEL_RATIO = 2) と同等でアップロード / S3 サイズを抑える
  • DOWNLOAD_EXPORT_SCALE = 4 — downloadBlocksAsImage(カメラボタン)の既定。教材・
    ワークシート・印刷での判読性を優先
  • 倍率は呼び出し側が {exportScale} で上書きできる(renderBlocksToCanvas(ws, uri, {exportScale}) /
    downloadBlocksAsImage(ws, title, name, uri, {exportScale}))。既存の呼び出し元 3 箇所は
    変更不要(既定で妥当な倍率になる)
  • 倍率は EXPORT_PADDING / SPRITE_IMAGE_SIZE / SPRITE_IMAGE_GAP / スプライト描画オフセット
    にも一律で掛けている(掛け忘れると余白やスプライトだけ小さくなる)
  • clampExportScale() を追加 — 巨大なプログラムでブラウザのキャンバス上限
    (MAX_EXPORT_DIMENSION = 8192 / MAX_EXPORT_PIXELS = 32Mpx) を超えないよう倍率を自動的に
    下げる(下限は等倍。toBlob() が null を返して画像が出ない事態を避ける)

docs/screenshot/README.md — 「出力形式」に書き出し倍率の節を追加。

Test Coverage

packages/scratch-gui/test/unit/lib/blocks-screenshot.test.js(37 tests, all pass)

  • clampExportScale の 4 ケース(通常 / 辺の上限 / 面積の上限 / 下限 1)
  • calculateCanvasDimensions の padding も倍率が掛かること
  • downloadBlocksAsImage のキャンバスが (論理サイズ) × DOWNLOAD_EXPORT_SCALE であること
  • ズーム状態(0.675 / 1 / 2)を変えても出力サイズが変わらないこと
  • renderBlocksToCanvas の既定倍率 = 2、明示 exportScale の反映、空ワークスペースで null

lint / prettier ともにクリーン。ブラウザでの実描画確認(クリッピング / スプライトヘッダの
見た目)は headless では判断しづらいため、DoD フェーズでの目視確認に回している。

Related Issues

Closes #1176

書き出し倍率を導入し、レイアウトを workspace.scale(既定 0.675 のユーザーズーム)から
切り離した。ブロックは SVG なので倍率を上げるとベクタとして再描画され、そのまま高解像度に
なる。ダウンロードは 4 倍(教材・印刷用途)、提出/不具合報告はサイズを抑えて 2 倍
(ruby-screenshot.js の PIXEL_RATIO=2 と同等)。巨大なプログラムでは clampExportScale が
キャンバス上限に収まるよう倍率を自動的に下げる。

Closes #1176

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@smalruby3-editor-bot smalruby3-editor-bot Bot added the 🤖 autopilot autopilot が処理対象にした Issue/PR(daemon が付与) label Sep 9, 2026
@smalruby3-editor-bot

smalruby3-editor-bot Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Author

🤖 autopilot status

field value
Status Review
AI Status —
HITL Yes
Size middle

Linked issue #1176. Maintained by autopilot (single writer); do not edit.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

🚀 Preview deployed: https://smalruby.jp/smalruby3-editor/topic/autopilot-1176/

- MAX_EXPORT_PIXELS を 32M から iOS/iPadOS Safari のキャンバス面積上限
  16,777,216 px に下げる。授業で使う iPad で書き出しが失敗しないようにする
- クランプの対象をスプライトヘッダーを含めた合成後サイズにする。ブロック部分
  だけで判定すると最終キャンバスがヘッダー分だけ上限を超えていた
- toBlob() が null を返したときに downloadBlob() へ渡さず警告のみにする。
  従来は blob.type 参照で TypeError になっていた
- 上記 3 点の単体テストを追加
@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[Must] MAX_EXPORT_PIXELS = 32 * 1024 * 1024 が iPad の実上限を超えていて、セーフティが効いていなかった(修正済み: 0bef21e)

clampExportScale() は「ブラウザのキャンバス上限に収める」ためのガードだが、上限値が
デスクトップ基準で、iOS / iPadOS Safari のキャンバス面積上限 16,777,216 px の約 2 倍に
設定されていた。iPad は授業で使う主要端末(.claude/rules/scratch-gui/mobile-ui.md /
download-blob.js の Apple mobile 分岐が示すとおりサポート対象)なので、
ガードが効かない環境が残るのは避けたい。

実測(この PR の実装で計算):

bbox クランプ後 scale 合成キャンバス 面積
400×300 4.00 1728×1552 2.7 Mpx
1600×1250 4.00 6528×5352 34.9 Mpx
3000×2000 2.33 7076×4873 34.5 Mpx
6000×4000 1.18 7086×4801 34.0 Mpx

倍率 4 で面積クランプが効き始めるのは論理面積 2 Mpx(≒ 1600×1250)あたりで、これは
スクリプトが数本並んだ程度の普通のプロジェクトで到達する。その規模で iPad から
カメラボタンを押すと toBlob() が null になり書き出しが失敗する。

対応: MAX_EXPORT_PIXELS を 16 * 1024 * 1024 に下げ、由来(iOS Safari の上限であること、
デスクトップ基準に緩めると iPad だけ壊れること)をコメントと docs/screenshot/README.md に
明記した。通常サイズのプログラムは倍率 4 のままで、影響を受けるのは元々クランプされていた
巨大プログラムだけ。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[Must] toBlob() が null を返す経路が未処理で、downloadBlob() が TypeError で落ちる(修正済み: 0bef21e)

clampExportScale の JSDoc 自身が「toBlob() then returns null」と書いているのに、
downloadBlocksAsImage() はその null をそのまま downloadBlob() に渡していた。

canvas.toBlob((blob) => {
    downloadBlob(buildFilename(projectTitle, spriteName), blob);  // blob が null になりうる
}, 'image/png');

download-blob.js は null で必ず落ちる:

  • デスクトップ(anchor 経路): downloadLink.type = blob.type → TypeError: Cannot read properties of null
  • iOS / iPadOS: makeFile() が try/catch で null を返す → canShareFile = false →
    openBlobInNewTab(null) → createObjectURL(null) で TypeError

いずれも unhandled rejection になり、ユーザーには「ボタンを押しても何も起きない」ように見える。
この PR は書き出し面積を約 8.8 倍(レイアウト 0.675→1 かつピクセル 1→2〜4 倍)にするので、
もともと到達しにくかったこの経路が現実的に到達可能になる点が問題。

なお同じ renderBlocksToCanvas を使う use-student-submit.js / use-bug-report-submit.js は
既に if (blob) でガードしており、ダウンロード経路だけが非対称だった。

対応: null のときはダウンロードせず console.warn を出して resolve するようにし、
他 2 経路とガードの形を揃えた。単体テストも追加。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[Question] クランプがスプライトヘッダーを勘定に入れておらず、自分で決めた上限自体を超えていた(修正済み: 0bef21e)

clampExportScale() に渡していたのは calculateCanvasDimensions(bbox, LOGICAL_SCALE)=
ブロック部分だけの論理サイズ。実際の合成キャンバスはこれに
(SPRITE_IMAGE_SIZE + SPRITE_IMAGE_GAP) * exportScale(倍率 4 なら 224 px)が乗る。

結果、クランプ後でも合成キャンバスは MAX_EXPORT_PIXELS を約 1.4 Mpx 超えていた
(上の表の 34.5 Mpx vs 上限 33.5 Mpx)。ガードとしては「ほぼ効いているが厳密には破れている」状態で、
上限ぎりぎりを狙う設計である以上ここは合わせておきたい。

対応: クランプ対象を合成後サイズ(ブロック+スプライトヘッダー)にした。加えて
実キャンバス側の Math.ceil() によるサブピクセル切り上げ分を各軸 +1 論理 px で吸収している
(exportScale >= 1 が保証されているので必ず足りる)。「合成後キャンバスが
MAX_EXPORT_PIXELS 以下」であることを検証するテストを追加した。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[Question] 提出 / 不具合報告のアップロード量が 1 枚あたり約 8.8 倍になる(コメントのみ・人間の判断に委ねます)

Issue #1176 の「影響範囲」でも挙げられていた点の確認です。既定経路
(use-student-submit.js / use-bug-report-submit.js)は倍率だけ見ると 2 倍ですが、
レイアウト倍率も workspace.scale の既定 0.675 から 1 に上がるので、線形で
0.675 → 2.0(約 2.96 倍)、面積で約 8.8 倍になります。

  • クラス提出は 1 提出あたり最大 MAX_SCREENSHOT_COUNT = 20 枚
    (infra/smalruby-classroom/lambda/handler.ts)
  • スクショの presigned PUT には content-length-range 等のサイズ条件が付いていないので、
    サーバー側で弾かれることはなく、失敗はしません。増えるのは学校ネットワークの
    アップロード時間と S3 保存量です

つまり「壊れない」ことは確認済みで、残るのは どこまで許容するか という判断です。
判読性を優先して現状(DEFAULT_EXPORT_SCALE = 2 × 論理等倍)で行くか、提出経路だけ
{exportScale: 1.5} 程度に絞るかは、教室の回線事情とコストを知っている人間の判断が
適切と考え、コードは変更していません。倍率は呼び出し側から {exportScale} で
渡せる設計になっているので、後から絞るのは 1 行です。

@smalruby3-editor-bot

smalruby3-editor-bot Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Author

[FYI] テストのモック周りの小さな気になり点(対応不要と判断)

  1. // eslint-disable-line accessor-pairs が無効

    set src(_url) {
        // eslint-disable-line accessor-pairs
        setTimeout(() => this.onload && this.onload(), 0);
    }

    eslint-disable-line は そのコメントが置かれた行 にしか効かないので、set src の行には
    かかっていません(現状 lint は通っているので実害なし)。効かせるなら set src(_url) { の
    行末に置くか eslint-disable-next-line にします。既存 downloadBlocksAsImage describe から
    のコピーで、新規に持ち込まれた問題ではないため触っていません。

  2. renderBlocksToCanvas describe の beforeEach が既存 describe とほぼ同一
    URL.createObjectURL / getContext / Image のモックが 2 箇所に重複しています。共通
    ヘルパーに切り出せますが、toBlob の有無など差分もあり、今の 2 箇所なら重複のままでも
    読みやすさは損なわれないと判断しました。

  3. HTMLCanvasElement.prototype.getContext / global.Image が代入で上書きされ復元されない
    jest.spyOn ではなく素の代入なので、後ろに別の describe を足すと引きずります。現状は
    ファイル末尾の describe なので問題ありません。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[FYI] 中間キャンバスの分だけピーク時メモリが 2 倍

renderSVGToCanvas() がブロック用キャンバスを作り、それを合成用キャンバスへ drawImage
しているため、同サイズのキャンバスが一時的に 2 枚存在します。上限いっぱい(16 Mpx)だと
4 bytes/px で 64 MB × 2 = 128 MB。SVG の Image を合成キャンバスへ直接
drawImage(img, 0, spriteHeaderHeight) すれば 1 枚で済みますが、既存の構造変更に
なるので今回は指摘のみとします。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

🤖 敵対的レビュー完了。指摘 6 件(Must 2 / Question 2 / FYI 2)。Must は修正済み(0bef21ee43)。Question 1 件(スプライトヘッダーを含めたクランプ)も併せて修正済み、残り 1 件(提出経路のアップロード量が約 8.8 倍になる件)はコメントのみで人間の判断に委ねます。lint / 単体テスト 40 件はローカルで green。

@smalruby3-editor-bot smalruby3-editor-bot Bot added the 🙋 HITL 人間の対応待ち(Project HITL=Yes の投影) label Sep 9, 2026
@smalruby3-editor-bot
smalruby3-editor-bot Bot marked this pull request as ready for review September 9, 2026 08:36
@takaokouji
takaokouji merged commit d0bb7fc into develop Sep 9, 2026
9 checks passed
@takaokouji
takaokouji deleted the topic/autopilot-1176 branch September 9, 2026 10:45
github-actions Bot pushed a commit that referenced this pull request Sep 9, 2026
…lot-1176

fix(scratch-gui): ブロックのスクリーンショットを高解像度で書き出す (#1176)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖 autopilot autopilot が処理対象にした Issue/PR(daemon が付与) 🙋 HITL 人間の対応待ち(Project HITL=Yes の投影)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(scratch-gui): ブロックのスクリーンショットが低解像度(書き出し倍率がなく workspace.scale 0.675 のまま)

1 participant