From 6c88844ffbc4e51f67aee5788fc25110dfb3219e Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Wed, 7 Oct 2026 02:14:35 +0800 Subject: [PATCH] fix(screenshots): use Pro captures and native 14-inch Mac frames --- Scripts/ASSET_WORKFLOW.md | 49 ++++++++++---------- Scripts/Tests/test_screenshots.rb | 77 ++++++++++++++++--------------- com.akinokaede.termind | 2 +- fastlane/lib/screenshot_frames.rb | 22 ++++----- fastlane/lib/screenshots.rb | 13 ++++-- 5 files changed, 86 insertions(+), 77 deletions(-) diff --git a/Scripts/ASSET_WORKFLOW.md b/Scripts/ASSET_WORKFLOW.md index cbf35230..e2374174 100644 --- a/Scripts/ASSET_WORKFLOW.md +++ b/Scripts/ASSET_WORKFLOW.md @@ -77,6 +77,8 @@ bundle exec fastlane ios screenshots plan:true `output:/absolute/path` stages a review capture outside the source asset tree. The default remains `com.akinokaede.termind/assets/captures///.png`. +Capture and frame directories use `iphone-6.3`, `ipad-15`, and `mac-14`; command +selectors remain `iphone`, `ipad`, and `mac`. Duo selectors keep their existing directory names. Logs and build products live under `.build/fastlane-screenshots//`; each run has its own logs. Existing captures are backed up with `SHA256SUMS` under `.build/screenshot-backups//` before replacement. A failed test, missing @@ -95,7 +97,7 @@ the downstream workflow described below. produces Command Generator, and `agent` produces Assistant. Metrics detail is a development demo only; do not retain its captures as marketing assets. -The mobile lane requires iOS/iPadOS 27. iPhone 18 Pro Max and iPad Pro use +The mobile lane requires iOS/iPadOS 27. iPhone 18 Pro and iPad Pro use **iOS/iPadOS 27.0**; Duo uses **iOS 27.1**. The lane selects 27.1 for Duo-only batches and 27.0 otherwise. The installed 27.1 runtime supports only Duo, so capture it separately from iPhone and iPad. @@ -103,7 +105,7 @@ It checks both the simulator model identifier and the raw pixel dimensions: | Selector | Capture device | Native capture size | | --- | --- | --- | -| `iphone` | iPhone 18 Pro Max (6.9-inch), portrait | 1320×2868 | +| `iphone` | iPhone 18 Pro (6.3-inch), portrait | 1206×2622 | | `iphone-duo-outer` | iPhone Duo, closed, portrait | 1398×2034 | | `iphone-duo-inner` | iPhone Duo, open, landscape | 2853×2007 | | `ipad` | iPad Pro 13-inch (M5), landscape | 2752×2064 | @@ -111,13 +113,13 @@ It checks both the simulator model identifier and the raw pixel dimensions: Create the exact simulators once if they are missing: ```sh -xcrun simctl create "iPhone 18 Pro Max" com.apple.CoreSimulator.SimDeviceType.iPhone-18-Pro-Max com.apple.CoreSimulator.SimRuntime.iOS-27-0 +xcrun simctl create "iPhone 18 Pro" com.apple.CoreSimulator.SimDeviceType.iPhone-18-Pro com.apple.CoreSimulator.SimRuntime.iOS-27-0 xcrun simctl create "iPhone Duo" com.apple.CoreSimulator.SimDeviceType.iPhone-Duo com.apple.CoreSimulator.SimRuntime.iOS-27-1 xcrun simctl create "iPad Pro 13-inch (M5)" com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-12GB com.apple.CoreSimulator.SimRuntime.iOS-27-0 ``` The lane does not resize iPhone captures or substitute another device. Existing -`com.akinokaede.termind/assets/app_store/*/iphone-6.9` artwork uses a separate 1320×2868 composition canvas; +`com.akinokaede.termind/assets/app_store/*/iphone-6.3` artwork uses a separate 1206×2622 composition canvas; that folder describes the final artwork, not the screenshot device. Recapturing does not regenerate those downstream assets automatically. @@ -172,9 +174,9 @@ outside can leave a hovered row highlighted in the captured image. In Figma, keep Mac artwork 02–06 as native windows in all three languages. Mac and iPad artwork 01 retain the three-device composition. iPhone artwork 01 uses two devices with aligned bottom edges: iPhone Assistant fully visible in -the right foreground (width 820), with Mac Workspace enlarged behind it (width -1800). The Mac layer deliberately extends left of the clipping Product imagery -frame; only part of the laptop is shown. No iPad appears in this composition. +the right foreground, with Mac Workspace enlarged behind it. Keep their +relative sizes and compact overlap when adjusting the 1206×2622 canvas. The Mac +layer deliberately extends left of the clipping Product imagery frame; only part of the laptop is shown. No iPad appears in this composition. Keep the existing export filenames. Replace screenshot layers while preserving their geometry, text, shared shadows, and stacking order, then export the affected boards as opaque RGB JPGs with an sRGB ICC profile. Refresh the mobile @@ -198,18 +200,18 @@ selected jobs without changing files. Defaults include only website-used scenes: iPhone Assistant/SSH/Vault, iPad Metrics/SFTP/Vault, and Mac Workspace, in all three locales. Use `scenes:` explicitly when another composition is needed. The lane stages source copies and publishes -`//.png` only after every selected output succeeds. Old Pro captures -must be recaptured on iPhone 18 Pro Max before framing; they are rejected by size. +`//.png` only after every selected output succeeds. Old Pro Max captures +must be recaptured on iPhone 18 Pro before framing; they are rejected by size. -- iPhone uses Apple's official iPhone 18 Pro Max Silver portrait frame at native +- iPhone uses Apple's official iPhone 18 Pro Silver portrait frame at native screenshot size. Import it locally or pass `iphone_frame:/path/to/portrait.png`. - iPad uses Apple's official iPad Pro (M5) 13-inch Space Black PNG as a custom frameit device. Its opening matches the native capture exactly, with no resize. Import the PNG once as described in the local frame templates section below, or pass `ipad_frame:/path/to/portrait.png`. The licensed source artwork stays local. - Mac Workspace fills the MacBook frame with the full-screen app capture. - Capture aspect ratio is preserved while trimming the top of the black camera - strip to match the aperture. No wallpaper or Dock is composited. Other Mac scenes stay as native + The 14-inch aperture matches the capture at native scale. No wallpaper or Dock + is composited. Other Mac scenes stay as native window captures for final artwork. Frame dimensions now come from frameit. Astro imports image metadata directly, @@ -220,12 +222,12 @@ the upstream device artwork; follow its usage terms. ## Local frame templates -### Official iPhone 18 Pro Max frame +### Official iPhone 18 Pro frame Download the [iPhone 18 bezels](https://devimages-cdn.apple.com/design/resources/download/Bezel-iPhone-18.dmg) -from Apple Design Resources. Copy `PNG/iPhone 18 Pro Max/iPhone 18 Pro Max - Silver - Portrait.png` -to `.build/frameit-assets/iphone-18-pro-max.png`, or pass `iphone_frame:/path/to/frame.png`. -The template is 1470×3000 with a native 1320×2868 opening at (75, 66). +from Apple Design Resources. Copy `PNG/iPhone 18 Pro/iPhone 18 Pro - Silver - Portrait.png` +to `.build/frameit-assets/iphone-18-pro.png`, or pass `iphone_frame:/path/to/frame.png`. +The template is 1350×2760 with a native 1206×2622 opening at (72, 69). The lane registers the template as a custom frameit device. Keep the licensed source local. ### Official iPhone Duo frames @@ -272,23 +274,22 @@ M5 frame, so the lane registers this device at runtime. ### MacBook Pro narrow frame -Mac Overview compositions use Apple's MacBook Pro M5 16-inch Space Black PNG, -prepared locally as `.build/frameit-assets/macbook_pro_template.png`. Supply an +Mac Overview compositions use Apple's MacBook Pro M5 14-inch Space Black PNG, +prepared locally as `.build/frameit-assets/macbook-pro-14-template.png`. Supply an alternate location with `mac_frame:/absolute/path/to/frame.png`. Keep the source artwork outside version control and retain its original usage terms. Download the MacBook Pro M5 bezel from Apple's Design Resources and accept its license locally. Remove only the 220-pixel transparent top margin from the -official 4260×2840 PNG: +official 3860×2540 PNG: ```sh -magick "/Volumes/Bezel-MacBook-Pro-M5/PNG/MacBook Pro M5 16-inch Space Black.png" \ - -crop 4260x2620+0+220 +repage .build/frameit-assets/macbook_pro_template.png +magick "/Volumes/Bezel-MacBook-Pro-M5/PNG/MacBook Pro M5 14-inch Space Black.png" \ + -crop 3860x2320+0+220 +repage .build/frameit-assets/macbook-pro-14-template.png ``` -The prepared transparent template is 4260×2620, with a 3456×2234 screen opening -at (402, 83). The native 3024×1964 capture scales proportionally to 3456×2245; -11 pixels are trimmed from its black full-screen camera strip to fill the opening. +The prepared transparent template is 3860×2320, with a 3024×1964 screen opening +at (418, 68). The native Workspace capture fits without resizing or trimming. No desktop or personal system UI is added. Do not reuse a template extracted from a composed screenshot: it can retain wallpaper or mismatched rounded corners. A missing template fails before publishing outputs. diff --git a/Scripts/Tests/test_screenshots.rb b/Scripts/Tests/test_screenshots.rb index b8fa6010..2847c3da 100644 --- a/Scripts/Tests/test_screenshots.rb +++ b/Scripts/Tests/test_screenshots.rb @@ -30,8 +30,8 @@ class ScreenshotsTests < Minitest::Test source = File.join(@root, "capture.png") File.binwrite(source, "review capture") capture = runner(:ios, output: "review") - capture.promote([[source, "en/ipad/vault.png"]]) - assert_equal "review capture", File.binread(File.join(@root, "review/en/ipad/vault.png")) + capture.promote([[source, "en/ipad-15/vault.png"]]) + assert_equal "review capture", File.binread(File.join(@root, "review/en/ipad-15/vault.png")) refute Dir.exist?(File.join(@root, "com.akinokaede.termind")) end @@ -56,18 +56,18 @@ class ScreenshotsTests < Minitest::Test runner(:ios, { devices: "iphone-duo-inner", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-27-0" }).plan[:runtime] end - def test_a_renamed_pro_cannot_be_used_as_the_pro_max_capture_device + def test_a_renamed_pro_max_cannot_be_used_as_the_pro_capture_device capture = runner(:ios, devices: "iphone") capture.define_singleton_method(:command) do |*args| JSON.generate("devices" => { "com.apple.CoreSimulator.SimRuntime.iOS-27-0" => [ - { "name" => "iPhone 18 Pro Max", "udid" => "wrong-model", - "deviceTypeIdentifier" => "com.apple.CoreSimulator.SimDeviceType.iPhone-18-Pro" } + { "name" => "iPhone 18 Pro", "udid" => "wrong-model", + "deviceTypeIdentifier" => "com.apple.CoreSimulator.SimDeviceType.iPhone-18-Pro-Max" } ] }) end assert_raises(RuntimeError) { capture.find_devices } - capture.define_singleton_method(:dimensions) { |_| [1206, 2622] } + capture.define_singleton_method(:dimensions) { |_| [1320, 2868] } assert_raises(RuntimeError) do - capture.validate_dimensions("pro.png", Screenshots::DEVICES.fetch("iphone").fetch(:dimensions)) + capture.validate_dimensions("pro-max.png", Screenshots::DEVICES.fetch("iphone").fetch(:dimensions)) end end @@ -116,17 +116,17 @@ class ScreenshotsTests < Minitest::Test def test_successful_capture_backs_up_original_bytes_and_manifest capture = runner - original = File.join(@root, "com.akinokaede.termind/assets/captures/en/ipad/vault.png") + original = File.join(@root, "com.akinokaede.termind/assets/captures/en/ipad-15/vault.png") FileUtils.mkdir_p(File.dirname(original)) File.binwrite(original, "old image bytes") source = File.join(@root, "new.png") File.binwrite(source, "new image bytes") - capture.promote([[source, "en/ipad/vault.png"]]) + capture.promote([[source, "en/ipad-15/vault.png"]]) assert_equal "new image bytes", File.binread(original) - backup = Dir.glob(File.join(@root, ".build/screenshot-backups/*/en/ipad/vault.png")).fetch(0) + backup = Dir.glob(File.join(@root, ".build/screenshot-backups/*/en/ipad-15/vault.png")).fetch(0) assert_equal "old image bytes", File.binread(backup) manifest = File.read(File.join(File.dirname(File.dirname(File.dirname(backup))), "SHA256SUMS")) - assert_equal "#{Digest::SHA256.hexdigest('old image bytes')} en/ipad/vault.png\n", manifest + assert_equal "#{Digest::SHA256.hexdigest('old image bytes')} en/ipad-15/vault.png\n", manifest end def test_mac_captures_publish_localized_underscore_filenames @@ -146,7 +146,7 @@ class ScreenshotsTests < Minitest::Test end capture.define_singleton_method(:dimensions) { |_| [2360, 1480] } capture.capture_mac(scene, "zh-Hant", "platform=macOS") - destination = File.join(@root, "com.akinokaede.termind/assets/captures/zh-hant/mac/#{filename}.png") + destination = File.join(@root, "com.akinokaede.termind/assets/captures/zh-hant/mac-14/#{filename}.png") assert_equal "captured #{scene}", File.binread(destination) end end @@ -179,7 +179,7 @@ class ScreenshotsTests < Minitest::Test end def test_mobile_capture_rediscovers_container_after_host_reinstallation - %w[iphone iphone-duo-outer iphone-duo-inner].each do |device| + %w[iphone ipad iphone-duo-outer iphone-duo-inner].each do |device| assert_mobile_capture_rediscovers_container(device) end end @@ -223,7 +223,8 @@ class ScreenshotsTests < Minitest::Test Open3.stub(:capture2e, responses) do capture.capture_mobile(device, { "udid" => "fixture", "state" => "Booted" }, "zh-Hans") end - destination = File.join(@root, "com.akinokaede.termind/assets/captures/zh-hans/#{device}/vault.png") + folder = { "iphone" => "iphone-6.3", "ipad" => "ipad-15" }.fetch(device, device) + destination = File.join(@root, "com.akinokaede.termind/assets/captures/zh-hans/#{folder}/vault.png") assert_equal "new container capture", File.binread(destination) assert File.exist?(File.join(control, "ack-#{token}")) refute Dir.exist?(old_container) @@ -288,7 +289,7 @@ class ScreenshotFramesTests < Minitest::Test File.write(File.join(@root, "com.akinokaede.termind/.git"), "gitdir: fixture") FileUtils.mkdir_p(File.join(@root, "fastlane")) FileUtils.cp(File.expand_path("../../fastlane/Framefile.json", __dir__), File.join(@root, "fastlane")) - frame_template(File.join(@root, ".build/frameit-assets/iphone-18-pro-max.png"), 1470, 3000) + frame_template(File.join(@root, ".build/frameit-assets/iphone-18-pro.png"), 1350, 2760) @templates = File.join(@root, "templates") FileUtils.mkdir_p(@templates) end @@ -350,30 +351,34 @@ class ScreenshotFramesTests < Minitest::Test def test_mac_composition_clips_corners_and_preserves_source_and_content template = File.join(@root, "mac-frame.png") runner = ScreenshotFrames.new({ mac_frame: template }, root: @root) - runner.command("magick", "-size", "4260x2620", "xc:none", "-fill", "none", + runner.command("magick", "-size", "3860x2320", "xc:none", "-fill", "none", "-stroke", "black", "-strokewidth", "20", - "-draw", "roundrectangle 392,73,3867,2326,100,100", template) + "-draw", "roundrectangle 408,58,3451,2041,100,100", template) source = File.join(@root, "capture.png") output = File.join(@root, "composed.png") - runner.command("magick", "-size", "3024x1964", "xc:#327abe", source) + runner.command("magick", "-size", "3024x1964", "xc:#327abe", "-fill", "red", + "-draw", "point 1512,982", source) original = Digest::SHA256.file(source).hexdigest runner.compose_mac(source, output) - assert_equal [4260, 2620], FastImage.size(output) + assert_equal [3860, 2320], FastImage.size(output) + # Native screen pixels must land at the aperture offset without resampling. + assert_equal "255,0,0", runner.command("magick", output, "-format", + "%[fx:round(255*p{1930,1050}.r)],%[fx:round(255*p{1930,1050}.g)],%[fx:round(255*p{1930,1050}.b)]", "info:").strip assert_equal original, Digest::SHA256.file(source).hexdigest # The old rectangular placement leaked here, outside the curved bezel. - assert_equal "0", runner.command("magick", output, "-format", "%[fx:p{402,83}.a]", "info:").strip - assert_equal "1", runner.command("magick", output, "-format", "%[fx:p{2130,1200}.a]", "info:").strip + assert_equal "0", runner.command("magick", output, "-format", "%[fx:p{418,68}.a]", "info:").strip + assert_equal "1", runner.command("magick", output, "-format", "%[fx:p{1930,1160}.a]", "info:").strip assert_equal "50,122,190", runner.command("magick", output, "-format", - "%[fx:round(255*p{2130,1200}.r)],%[fx:round(255*p{2130,1200}.g)],%[fx:round(255*p{2130,1200}.b)]", "info:").strip + "%[fx:round(255*p{1930,1160}.r)],%[fx:round(255*p{1930,1160}.g)],%[fx:round(255*p{1930,1160}.b)]", "info:").strip end def test_default_frames_only_include_website_used_scenes jobs = ScreenshotFrames.new({}, root: @root).plan assert_equal 21, jobs.size - refute jobs.any? { |job| job[:input].end_with?("/ipad/ssh.png", "/iphone/metrics.png") } + refute jobs.any? { |job| job[:input].end_with?("/ipad-15/ssh.png", "/iphone-6.3/metrics.png") } explicit = ScreenshotFrames.new({ devices: "ipad", scenes: "ssh", locales: "en" }, root: @root).plan assert_equal 1, explicit.size - assert explicit.first[:input].end_with?("/ipad/ssh.png") + assert explicit.first[:input].end_with?("/ipad-15/ssh.png") end def test_default_frames_fail_before_creating_outputs_when_submodule_is_missing @@ -386,8 +391,8 @@ class ScreenshotFramesTests < Minitest::Test def test_explicit_frame_paths_work_without_submodule_and_keep_source_bytes FileUtils.remove_entry(File.join(@root, "com.akinokaede.termind")) - source = File.join(@root, "review-input/en/iphone/vault.png") - png(source, 1320, 2868) + source = File.join(@root, "review-input/en/iphone-6.3/vault.png") + png(source, 1206, 2622) original_hash = Digest::SHA256.file(source).hexdigest runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en", input: "review-input", output: "review-output" }, root: @root) @@ -396,21 +401,21 @@ class ScreenshotFramesTests < Minitest::Test Frameit::FrameDownloader.stub(:templates_path, @templates) do Frameit::FrameDownloader.stub(:new, downloader) do runner.run do |options| - assert_equal "iPhone 18 Pro Max", options.fetch(:force_device_type) + assert_equal "iPhone 18 Pro", options.fetch(:force_device_type) framefile = JSON.parse(File.read(File.join(options.fetch(:path), "Framefile.json"))) - assert_equal({ "offset" => "+75+66", "width" => 1320 }, framefile.fetch("default").fetch("offset")) - png(File.join(options.fetch(:path), "iphone_framed.png"), 1470, 3000) + assert_equal({ "offset" => "+72+69", "width" => 1206 }, framefile.fetch("default").fetch("offset")) + png(File.join(options.fetch(:path), "iphone_framed.png"), 1350, 2760) end end end - assert File.exist?(File.join(@root, "review-output/en/iphone/vault.png")) + assert File.exist?(File.join(@root, "review-output/en/iphone-6.3/vault.png")) assert_equal original_hash, Digest::SHA256.file(source).hexdigest refute Dir.exist?(File.join(@root, "com.akinokaede.termind")) end - def test_rejects_pro_sources_before_calling_frameit - source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone/vault.png") - png(source, 1206, 2622) + def test_rejects_pro_max_sources_before_calling_frameit + source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone-6.3/vault.png") + png(source, 1320, 2868) runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en" }, root: @root) called = false assert_raises(RuntimeError) { runner.run { called = true } } @@ -461,12 +466,12 @@ class ScreenshotFramesTests < Minitest::Test def test_missing_frameit_output_keeps_all_existing_outputs_and_sources %w[en zh-hans].each do |locale| - png(File.join(@root, "com.akinokaede.termind/assets/captures/#{locale}/iphone/vault.png"), 1320, 2868) + png(File.join(@root, "com.akinokaede.termind/assets/captures/#{locale}/iphone-6.3/vault.png"), 1206, 2622) end - destination = File.join(@root, "com.akinokaede.termind/assets/device_frames/en/iphone/vault.png") + destination = File.join(@root, "com.akinokaede.termind/assets/device_frames/en/iphone-6.3/vault.png") FileUtils.mkdir_p(File.dirname(destination)) File.write(destination, "existing frame") - source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone/vault.png") + source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone-6.3/vault.png") original_hash = Digest::SHA256.file(source).hexdigest runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en,zh-Hans" }, root: @root) downloader = Object.new diff --git a/com.akinokaede.termind b/com.akinokaede.termind index 6051cf1d..d6d10431 160000 --- a/com.akinokaede.termind +++ b/com.akinokaede.termind @@ -1 +1 @@ -Subproject commit 6051cf1d829e5571b9a93a18db16436bc0ee4fb0 +Subproject commit d6d104316827c9d46bbe57919a47c160b89ebf10 diff --git a/fastlane/lib/screenshot_frames.rb b/fastlane/lib/screenshot_frames.rb index 10dc85eb..16d34d1a 100644 --- a/fastlane/lib/screenshot_frames.rb +++ b/fastlane/lib/screenshot_frames.rb @@ -6,8 +6,8 @@ require_relative "screenshots" class ScreenshotFrames DEVICES = %w[iphone iphone-duo-outer iphone-duo-inner ipad mac].freeze CUSTOM_FRAMES = { - "iphone" => { name: "iPhone 18 Pro Max", size: [1470, 3000], offset: "+75+66", width: 1320, - color: "Silver", template: "iphone-18-pro-max" }, + "iphone" => { name: "iPhone 18 Pro", size: [1350, 2760], offset: "+72+69", width: 1206, + color: "Silver", template: "iphone-18-pro" }, "iphone-duo-outer" => { name: "iPhone Duo Outer", size: [1574, 2194], offset: "+88+80", width: 1398, color: "Star White" }, "iphone-duo-inner" => { name: "iPhone Duo Inner", size: [2247, 3093], offset: "+120+120", width: 2007, color: "Star White" } }.freeze @@ -18,7 +18,7 @@ class ScreenshotFrames def initialize(options = {}, root: Screenshots::ROOT) @root = root - @mac_frame = File.expand_path(options[:mac_frame] || ".build/frameit-assets/macbook_pro_template.png", root) + @mac_frame = File.expand_path(options[:mac_frame] || ".build/frameit-assets/macbook-pro-14-template.png", root) @ipad_frame = File.expand_path(options[:ipad_frame] || ".build/frameit-assets/ipad-pro-13-m5.png", root) @custom_frames = CUSTOM_FRAMES.to_h do |device, specification| option = device.tr("-", "_").concat("_frame").to_sym @@ -35,7 +35,7 @@ class ScreenshotFrames scenes = device == "mac" ? ["workspace"] : Screenshots::MOBILE_SCENES selected = selector.selection(@scenes, WEBSITE_SCENES.fetch(device), scenes, "#{device} scenes") selected.product(@locales).map do |scene, language| - relative = "#{language.downcase}/#{device}/#{scene}" + relative = "#{language.downcase}/#{Screenshots.asset_device_directory(device)}/#{scene}" { device: device, input: File.join(@input, "#{relative}.png"), output: File.join(@output, "#{relative}.png") } end @@ -75,7 +75,7 @@ class ScreenshotFrames raise "Import the official iPad Pro (M5) 13-inch Space Black portrait PNG first; " \ "see Scripts/ASSET_WORKFLOW.md or pass ipad_frame:/path/to/frame.png" end - if @devices.include?("mac") && FastImage.size(@mac_frame) != [4260, 2620] + if @devices.include?("mac") && FastImage.size(@mac_frame) != [3860, 2320] raise "Import the narrow MacBook Pro template first; see Scripts/ASSET_WORKFLOW.md " \ "or pass mac_frame:/path/to/frame.png" end @@ -156,15 +156,13 @@ class ScreenshotFrames end def compose_mac(source, output) - # The official M5 16-inch aperture is 3456x2234 after removing the - # template's 220px transparent top margin. Preserve the capture's aspect - # ratio and trim only 11px from its black full-screen camera strip. + # The official M5 14-inch aperture matches the 3024x1964 capture exactly. + # Only the template's 220px transparent top margin is removed in preparation. Dir.mktmpdir("mac-screen-", File.dirname(output)) do |directory| screen = File.join(directory, "screen.png") - command("magick", source, "-resize", "3456x", "-gravity", "south", - "-extent", "3456x2234", screen) - mask_screen(screen, @mac_frame, "+402+83") - command("magick", @mac_frame, "-colorspace", "sRGB", screen, "-gravity", "northwest", "-geometry", "+402+83", + FileUtils.cp(source, screen) + mask_screen(screen, @mac_frame, "+418+68") + command("magick", @mac_frame, "-colorspace", "sRGB", screen, "-gravity", "northwest", "-geometry", "+418+68", "-compose", "DstOver", "-composite", output) end end diff --git a/fastlane/lib/screenshots.rb b/fastlane/lib/screenshots.rb index 1d261bbe..c2807173 100644 --- a/fastlane/lib/screenshots.rb +++ b/fastlane/lib/screenshots.rb @@ -9,6 +9,11 @@ require "securerandom" class Screenshots ROOT = File.expand_path("../..", __dir__) ASSET_DIRECTORY = "com.akinokaede.termind".freeze + ASSET_DEVICE_DIRECTORIES = { "iphone" => "iphone-6.3", "ipad" => "ipad-15", "mac" => "mac-14" }.freeze + + def self.asset_device_directory(device) + ASSET_DEVICE_DIRECTORIES.fetch(device, device) + end def self.require_assets!(root) directory = File.join(root, ASSET_DIRECTORY) @@ -27,8 +32,8 @@ class Screenshots }.freeze MOBILE_SCENES = %w[vault sftp ssh metrics assistant].freeze DEVICES = { - "iphone" => { name: "iPhone 18 Pro Max", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-18-Pro-Max", - dimensions: [1320, 2868] }, + "iphone" => { name: "iPhone 18 Pro", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-18-Pro", + dimensions: [1206, 2622] }, "iphone-duo-outer" => { name: "iPhone Duo", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-Duo", dimensions: [1398, 2034], display: "primary" }, "iphone-duo-inner" => { name: "iPhone Duo", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-Duo", @@ -219,7 +224,7 @@ class Screenshots command("sips", "-c", height.to_s, width.to_s, captured) end validate_dimensions(captured, native_size) - promote([[captured, "#{language.downcase}/mac/#{prefix == 'mac-overview' ? 'metrics_overview' : feature.tr('-', '_')}.png"]]) + promote([[captured, "#{language.downcase}/#{self.class.asset_device_directory("mac")}/#{prefix == 'mac-overview' ? 'metrics_overview' : feature.tr('-', '_')}.png"]]) ensure FileUtils.rm_f(xctestrun) if xctestrun end @@ -294,7 +299,7 @@ class Screenshots command(*arguments, captured) end validate_dimensions(captured, DEVICES.fetch(device).fetch(:dimensions)) - captures[feature] = [captured, "#{locale}/#{device}/#{feature}.png"] + captures[feature] = [captured, "#{locale}/#{self.class.asset_device_directory(device)}/#{feature}.png"] tokens << token FileUtils.touch(File.join(control, "ack-#{token}")) end