From 5dabcd175e8799722eff072e5bd83177ba4393bd Mon Sep 17 00:00:00 2001 From: rain <982766701@qq.com> Date: Wed, 12 Aug 2026 18:24:40 +0800 Subject: [PATCH] chore: reduce design pipeline to runtime asset checks --- .../manifests/application-runtime-assets.json | 66 ++++-- .../manifests/auth-runtime-assets.json | 21 +- .../manifests/g01-state-frame-v3.json | 29 --- .../manifests/page-backgrounds-v3.json | 61 ------ .../retained-generated-runtime-assets.json | 19 ++ design-pipeline/manifests/runtime-assets.json | 4 +- .../manifests/shared-scroll-skins-v3.json | 51 ----- design-pipeline/package.json | 12 +- design-pipeline/requirements.txt | 1 - .../scripts/asset-build-manifest.mjs | 196 ------------------ design-pipeline/scripts/asset_quality.py | 127 ------------ .../scripts/build-raster-assets.mjs | 43 ---- .../scripts/build-shared-scroll-skins.mjs | 28 --- .../scripts/build_raster_assets.py | 132 ------------ design-pipeline/scripts/build_scroll_skins.py | 185 ----------------- design-pipeline/scripts/python-runtime.mjs | 74 ------- design-pipeline/scripts/run-python-tests.mjs | 14 -- .../scripts/runtime-asset-inventory.mjs | 42 ++-- .../scripts/validate-asset-build-manifest.mjs | 15 -- .../validate-runtime-asset-inventory.mjs | 2 +- design-pipeline/scripts/verify-assets.py | 47 ----- .../scripts/verify-shared-scroll-skins.mjs | 21 -- .../tests/asset-build-manifest.test.mjs | 131 ------------ design-pipeline/tests/python-runtime.test.mjs | 68 ------ .../tests/raster-build-manifests.test.mjs | 65 ------ .../tests/runtime-asset-registry.test.mjs | 10 +- .../tests/shared-scroll-skins-v3.test.mjs | 31 --- design-pipeline/tests/test_asset_quality.py | 109 ---------- .../tests/test_build_raster_assets.py | 85 -------- .../tests/test_build_scroll_skins.py | 115 ---------- 30 files changed, 106 insertions(+), 1698 deletions(-) delete mode 100644 design-pipeline/manifests/g01-state-frame-v3.json delete mode 100644 design-pipeline/manifests/page-backgrounds-v3.json create mode 100644 design-pipeline/manifests/retained-generated-runtime-assets.json delete mode 100644 design-pipeline/manifests/shared-scroll-skins-v3.json delete mode 100644 design-pipeline/requirements.txt delete mode 100644 design-pipeline/scripts/asset-build-manifest.mjs delete mode 100644 design-pipeline/scripts/asset_quality.py delete mode 100644 design-pipeline/scripts/build-raster-assets.mjs delete mode 100644 design-pipeline/scripts/build-shared-scroll-skins.mjs delete mode 100644 design-pipeline/scripts/build_raster_assets.py delete mode 100644 design-pipeline/scripts/build_scroll_skins.py delete mode 100644 design-pipeline/scripts/python-runtime.mjs delete mode 100644 design-pipeline/scripts/run-python-tests.mjs delete mode 100644 design-pipeline/scripts/validate-asset-build-manifest.mjs delete mode 100644 design-pipeline/scripts/verify-assets.py delete mode 100644 design-pipeline/scripts/verify-shared-scroll-skins.mjs delete mode 100644 design-pipeline/tests/asset-build-manifest.test.mjs delete mode 100644 design-pipeline/tests/python-runtime.test.mjs delete mode 100644 design-pipeline/tests/raster-build-manifests.test.mjs delete mode 100644 design-pipeline/tests/shared-scroll-skins-v3.test.mjs delete mode 100644 design-pipeline/tests/test_asset_quality.py delete mode 100644 design-pipeline/tests/test_build_raster_assets.py delete mode 100644 design-pipeline/tests/test_build_scroll_skins.py diff --git a/design-pipeline/manifests/application-runtime-assets.json b/design-pipeline/manifests/application-runtime-assets.json index e547e6b..73f981d 100644 --- a/design-pipeline/manifests/application-runtime-assets.json +++ b/design-pipeline/manifests/application-runtime-assets.json @@ -17,24 +17,24 @@ { "id": "app-foundation-transparent-tab-genealogy", "output": "static/assets/foundation/transparent/tab-genealogy.png", "width": 96, "height": 96, "alpha": true, "bytes": 3141, "sha256": "88d5653db7521b846ef81736355e9ccbbb59daf6dfe92c9c2748bef6456af646", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-foundation-transparent-tab-profile-active", "output": "static/assets/foundation/transparent/tab-profile-active.png", "width": 96, "height": 96, "alpha": true, "bytes": 1973, "sha256": "5ff15db3b4cc64934e4ae9602e604345501c1f86160da10d30d9c0a80ef95d36", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-foundation-transparent-tab-profile", "output": "static/assets/foundation/transparent/tab-profile.png", "width": 96, "height": 96, "alpha": true, "bytes": 1973, "sha256": "45fc22b367807bdc465e6e17230a10276a4d7e453e27b7cbc064af7fdc255470", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-family-f08-f08-ancestral-home", "output": "static/assets/modules/family/f08/f08-ancestral-home.png", "width": 1448, "height": 1086, "alpha": true, "bytes": 3370777, "sha256": "b9339cac4fc6e2fe466ae64944140d8b332e019e10985fe900e06c06668391f1", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-family-f08-f08-ancestral-portrait", "output": "static/assets/modules/family/f08/f08-ancestral-portrait.png", "width": 1448, "height": 1086, "alpha": true, "bytes": 3227971, "sha256": "4b541960557d0caab15084348f1ecde5a9f93f7f791e1a7e881deb7a74e8496e", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-family-f08-f08-family-portrait", "output": "static/assets/modules/family/f08/f08-family-portrait.png", "width": 1448, "height": 1086, "alpha": true, "bytes": 4129091, "sha256": "dd221049eb552c098dfe8177e9ac8d4185b9865d7390f745ca91742b700bc68b", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-family-f08-f08-reunion-hero", "output": "static/assets/modules/family/f08/f08-reunion-hero.png", "width": 1672, "height": 940, "alpha": true, "bytes": 4110597, "sha256": "5b296d9254580b3565539813daacf74edaa5471495a3b30b06b512a5b2153dc9", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-family-f08-f08-reunion-table", "output": "static/assets/modules/family/f08/f08-reunion-table.png", "width": 1448, "height": 1086, "alpha": true, "bytes": 3765758, "sha256": "b241176cd3080e7b038b6b84e7cd3cb30f423c175df8cf2a78dc0cff1709e6eb", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-family-transparent-f01-family-letter-card", "output": "static/assets/modules/family/transparent/f01-family-letter-card.png", "width": 2003, "height": 581, "alpha": true, "bytes": 1513491, "sha256": "a3686f99e32cdc255c0fc130294aab1974392d66bc8a7991daeb8f67849015ee", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-family-albums-ancestral-home", "output": "static/assets/modules/family/albums/ancestral-home.png", "width": 1448, "height": 1086, "alpha": true, "bytes": 3370777, "sha256": "b9339cac4fc6e2fe466ae64944140d8b332e019e10985fe900e06c06668391f1", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-family-albums-ancestral-portrait", "output": "static/assets/modules/family/albums/ancestral-portrait.png", "width": 1448, "height": 1086, "alpha": true, "bytes": 3227971, "sha256": "4b541960557d0caab15084348f1ecde5a9f93f7f791e1a7e881deb7a74e8496e", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-family-albums-family-portrait", "output": "static/assets/modules/family/albums/family-portrait.png", "width": 1448, "height": 1086, "alpha": true, "bytes": 4129091, "sha256": "dd221049eb552c098dfe8177e9ac8d4185b9865d7390f745ca91742b700bc68b", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-family-albums-reunion-hero", "output": "static/assets/modules/family/albums/reunion-hero.png", "width": 1672, "height": 940, "alpha": true, "bytes": 4110597, "sha256": "5b296d9254580b3565539813daacf74edaa5471495a3b30b06b512a5b2153dc9", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-family-albums-reunion-table", "output": "static/assets/modules/family/albums/reunion-table.png", "width": 1448, "height": 1086, "alpha": true, "bytes": 3765758, "sha256": "b241176cd3080e7b038b6b84e7cd3cb30f423c175df8cf2a78dc0cff1709e6eb", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-family-transparent-family-letter-card", "output": "static/assets/modules/family/transparent/family-letter-card.png", "width": 2003, "height": 581, "alpha": true, "bytes": 1513491, "sha256": "a3686f99e32cdc255c0fc130294aab1974392d66bc8a7991daeb8f67849015ee", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-family-transparent-module-content-frame", "output": "static/assets/modules/family/transparent/module-content-frame.png", "width": 2003, "height": 581, "alpha": true, "bytes": 1513491, "sha256": "a3686f99e32cdc255c0fc130294aab1974392d66bc8a7991daeb8f67849015ee", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-family-transparent-module-field-frame", "output": "static/assets/modules/family/transparent/module-field-frame.png", "width": 2132, "height": 443, "alpha": true, "bytes": 1065770, "sha256": "bdc65f33e1dbe05ae75562519b0080e2e5cdbe2dba720a6ac49b56b99be75ceb", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-genealogy-opaque-g05-overview-surface", "output": "static/assets/modules/genealogy/opaque/g05-overview-surface.png", "width": 1122, "height": 1506, "alpha": false, "bytes": 2537499, "sha256": "2d908f53c7edb637c7ced7500822bd33d354410ad8e671871d1da3a4cbd0f4db", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-genealogy-opaque-g06-search-button", "output": "static/assets/modules/genealogy/opaque/g06-search-button.png", "width": 300, "height": 132, "alpha": false, "bytes": 74664, "sha256": "bf0abc0a036c07322dd834fe5a9797413afd453b69d2ff8a895f3999fb8846cd", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-genealogy-opaque-g06-search-input-wide", "output": "static/assets/modules/genealogy/opaque/g06-search-input-wide.png", "width": 1120, "height": 248, "alpha": false, "bytes": 319364, "sha256": "2a575eeb5582c8458cbdd04cd328afbbecbdaabf4db46e8f2133bfe2614faa80", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-genealogy-opaque-overview-surface", "output": "static/assets/modules/genealogy/opaque/overview-surface.png", "width": 1122, "height": 1506, "alpha": false, "bytes": 2537499, "sha256": "2d908f53c7edb637c7ced7500822bd33d354410ad8e671871d1da3a4cbd0f4db", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-genealogy-opaque-search-button", "output": "static/assets/modules/genealogy/opaque/search-button.png", "width": 300, "height": 132, "alpha": false, "bytes": 74664, "sha256": "bf0abc0a036c07322dd834fe5a9797413afd453b69d2ff8a895f3999fb8846cd", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-genealogy-opaque-search-input-wide", "output": "static/assets/modules/genealogy/opaque/search-input-wide.png", "width": 1120, "height": 248, "alpha": false, "bytes": 319364, "sha256": "2a575eeb5582c8458cbdd04cd328afbbecbdaabf4db46e8f2133bfe2614faa80", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-add", "output": "static/assets/modules/genealogy/transparent/add.png", "width": 96, "height": 96, "alpha": true, "bytes": 5226, "sha256": "671846ee23f8df701e2e1dfc34e3520dfe9f719f106efce56e30d78b84a47a0b", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-create-cloud", "output": "static/assets/modules/genealogy/transparent/create-cloud.png", "width": 192, "height": 96, "alpha": true, "bytes": 10538, "sha256": "be01c1322181d91264641879162f6371d21ffda1feb971f5f31a791f32d02206", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-current-seal-frame", "output": "static/assets/modules/genealogy/transparent/current-seal-frame.png", "width": 144, "height": 208, "alpha": true, "bytes": 1214, "sha256": "6d9aa61d766eec077cd91fbd5b71079990e67bcdc213d631a1185714dfb09328", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-current-slip-frame", "output": "static/assets/modules/genealogy/transparent/current-slip-frame.png", "width": 720, "height": 272, "alpha": true, "bytes": 18568, "sha256": "47e519768830062a2363bd4bd10fb9ffc82c6a94638d3400a1e4c943a0ca6e66", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-genealogy-transparent-g-form-field-frame", "output": "static/assets/modules/genealogy/transparent/g-form-field-frame.png", "width": 2132, "height": 443, "alpha": true, "bytes": 1065770, "sha256": "bdc65f33e1dbe05ae75562519b0080e2e5cdbe2dba720a6ac49b56b99be75ceb", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-genealogy-transparent-g01-add-sheet-background-v3", "output": "static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png", "width": 1536, "height": 1024, "alpha": true, "bytes": 1423794, "sha256": "181b9d9c95f0e8cce284dc6640adb4fa6c4fa9691306052c9e33558abf930e85", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-genealogy-transparent-g01-dialog-close", "output": "static/assets/modules/genealogy/transparent/g01-dialog-close.png", "width": 1254, "height": 1254, "alpha": true, "bytes": 105163, "sha256": "909fe4badccbfad1ddcfe4a21292ba73fecde32efb914f1c7fcb717f32f9db3f", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-genealogy-transparent-form-field-frame", "output": "static/assets/modules/genealogy/transparent/form-field-frame.png", "width": 2132, "height": 443, "alpha": true, "bytes": 1065770, "sha256": "bdc65f33e1dbe05ae75562519b0080e2e5cdbe2dba720a6ac49b56b99be75ceb", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-genealogy-transparent-add-sheet-background", "output": "static/assets/modules/genealogy/transparent/add-sheet-background.png", "width": 1536, "height": 1024, "alpha": true, "bytes": 1423794, "sha256": "181b9d9c95f0e8cce284dc6640adb4fa6c4fa9691306052c9e33558abf930e85", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-genealogy-transparent-dialog-close", "output": "static/assets/modules/genealogy/transparent/dialog-close.png", "width": 1254, "height": 1254, "alpha": true, "bytes": 105163, "sha256": "909fe4badccbfad1ddcfe4a21292ba73fecde32efb914f1c7fcb717f32f9db3f", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-list-slip-frame", "output": "static/assets/modules/genealogy/transparent/list-slip-frame.png", "width": 720, "height": 144, "alpha": true, "bytes": 2992, "sha256": "1e39ed72c000e569340049833b1afca9e6aaeb077618a91e0c0f4f2758113005", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-row-seal-frame", "output": "static/assets/modules/genealogy/transparent/row-seal-frame.png", "width": 112, "height": 160, "alpha": true, "bytes": 875, "sha256": "f2f2b0260fe54d738a70168f28d3960622d4b3e57c4ef95a032716008de6626b", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-section-divider", "output": "static/assets/modules/genealogy/transparent/section-divider.png", "width": 720, "height": 30, "alpha": true, "bytes": 6645, "sha256": "925241a6cd2bd9897edad60e53eb255033603bef1028afbc189e87a29fddc6cd", "provenance": "committed-binary", "rebuildable": false }, @@ -42,16 +42,44 @@ { "id": "app-modules-genealogy-transparent-shortcut-generation-poem", "output": "static/assets/modules/genealogy/transparent/shortcut-generation-poem.png", "width": 96, "height": 96, "alpha": true, "bytes": 8169, "sha256": "688e593fa88925ea00acf37570852a2dca9e1a505ba3b8107e0ddd3d146a6f4e", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-shortcut-members", "output": "static/assets/modules/genealogy/transparent/shortcut-members.png", "width": 96, "height": 96, "alpha": true, "bytes": 8931, "sha256": "c89824e8e19210dd746f56019a48c9a833abe4a210b222758a56c3d25dec9d0c", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-shortcut-tree", "output": "static/assets/modules/genealogy/transparent/shortcut-tree.png", "width": 96, "height": 96, "alpha": true, "bytes": 6636, "sha256": "cafe12a3fa800ca79d6c559baa533d7e26124ec8ac0072482ddb89ac2167422d", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-notification-transparent-n01-notice-card", "output": "static/assets/modules/notification/transparent/n01-notice-card.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-profile-transparent-m01-profile-summary-card", "output": "static/assets/modules/profile/transparent/m01-profile-summary-card.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-notification-transparent-notice-card", "output": "static/assets/modules/notification/transparent/notice-card.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-summary-card", "output": "static/assets/modules/profile/transparent/summary-card.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-profile-transparent-module-content-frame", "output": "static/assets/modules/profile/transparent/module-content-frame.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-profile-transparent-module-field-frame", "output": "static/assets/modules/profile/transparent/module-field-frame.png", "width": 2132, "height": 443, "alpha": true, "bytes": 1065770, "sha256": "bdc65f33e1dbe05ae75562519b0080e2e5cdbe2dba720a6ac49b56b99be75ceb", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-records-transparent-module-content-frame", "output": "static/assets/modules/records/transparent/module-content-frame.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-records-transparent-module-field-frame", "output": "static/assets/modules/records/transparent/module-field-frame.png", "width": 2132, "height": 443, "alpha": true, "bytes": 1065770, "sha256": "bdc65f33e1dbe05ae75562519b0080e2e5cdbe2dba720a6ac49b56b99be75ceb", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-records-transparent-r01-person-name-card", "output": "static/assets/modules/records/transparent/r01-person-name-card.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-tree-transparent-t01-member-node-selected", "output": "static/assets/modules/tree/transparent/t01-member-node-selected.png", "width": 720, "height": 272, "alpha": true, "bytes": 18568, "sha256": "47e519768830062a2363bd4bd10fb9ffc82c6a94638d3400a1e4c943a0ca6e66", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-tree-transparent-t01-member-node-standard", "output": "static/assets/modules/tree/transparent/t01-member-node-standard.png", "width": 720, "height": 272, "alpha": true, "bytes": 18568, "sha256": "47e519768830062a2363bd4bd10fb9ffc82c6a94638d3400a1e4c943a0ca6e66", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-tree-transparent-t01-state-panel", "output": "static/assets/modules/tree/transparent/t01-state-panel.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-tree-transparent-t07-search-input-frame", "output": "static/assets/modules/tree/transparent/t07-search-input-frame.png", "width": 2132, "height": 443, "alpha": true, "bytes": 1065770, "sha256": "bdc65f33e1dbe05ae75562519b0080e2e5cdbe2dba720a6ac49b56b99be75ceb", "provenance": "committed-binary", "rebuildable": false } + { "id": "app-modules-records-transparent-person-name-card", "output": "static/assets/modules/records/transparent/person-name-card.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-member-node-selected", "output": "static/assets/modules/tree/transparent/member-node-selected.png", "width": 720, "height": 272, "alpha": true, "bytes": 18568, "sha256": "47e519768830062a2363bd4bd10fb9ffc82c6a94638d3400a1e4c943a0ca6e66", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-member-node-standard", "output": "static/assets/modules/tree/transparent/member-node-standard.png", "width": 720, "height": 272, "alpha": true, "bytes": 18568, "sha256": "47e519768830062a2363bd4bd10fb9ffc82c6a94638d3400a1e4c943a0ca6e66", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-state-panel", "output": "static/assets/modules/tree/transparent/state-panel.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-search-input-frame", "output": "static/assets/modules/tree/transparent/search-input-frame.png", "width": 2132, "height": 443, "alpha": true, "bytes": 1065770, "sha256": "bdc65f33e1dbe05ae75562519b0080e2e5cdbe2dba720a6ac49b56b99be75ceb", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-foundation-transparent-book-png", "output": "static/assets/foundation/transparent/book.png", "width": 154, "height": 210, "alpha": false, "bytes": 41283, "sha256": "d565a2ad4f90c07046e9c60ebd6658426cacdd1f2af79e0a28ce9fe59d670630", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-foundation-transparent-default-avatar-female", "output": "static/assets/foundation/transparent/default-avatar-female.png", "width": 128, "height": 128, "alpha": true, "bytes": 38629, "sha256": "a85cf941e531e84217f37b42fe8e8aa9ab6a32457480d45aa3d6fa33da360d2c", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-foundation-transparent-default-avatar-male", "output": "static/assets/foundation/transparent/default-avatar-male.png", "width": 128, "height": 128, "alpha": true, "bytes": 43055, "sha256": "b00cb59f440aa87215432afc64f1463a61f0d9806ba1655ed6f8888a9d04c100", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-genealogy-transparent-settings-gear", "output": "static/assets/modules/genealogy/transparent/settings-gear.png", "width": 1254, "height": 1254, "alpha": true, "bytes": 1305800, "sha256": "0c3f064878a865af57ea3e7de450703a07bbb59b81ef0c44c4a041fa35a5dd1c", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-opaque-archive-hero", "output": "static/assets/modules/profile/opaque/archive-hero.png", "width": 2172, "height": 724, "alpha": false, "bytes": 1965431, "sha256": "eda978404614f2ae14e200d8cf7bb05851cc7bccf15b515655b9387dc39cb829", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-opaque-paper-mountains", "output": "static/assets/modules/profile/opaque/paper-mountains.png", "width": 941, "height": 1672, "alpha": false, "bytes": 1862070, "sha256": "60b54ef2550a2709e9ccbe7eab1cb4fa22c08ebace4be0930f746df1a1d607b2", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-opaque-featured-courtyard", "output": "static/assets/modules/profile/opaque/featured-courtyard.png", "width": 1536, "height": 1024, "alpha": false, "bytes": 2577643, "sha256": "3431f3e84f1521bb73a21407d48e127d9f7d20d99805b11c38b8383f660808c7", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-opaque-vip-hero-landscape", "output": "static/assets/modules/profile/opaque/vip-hero-landscape.png", "width": 2172, "height": 724, "alpha": false, "bytes": 2194380, "sha256": "a286f7b685a5b9f91cdeaf99f3454e5b10c0712d7a062fca5f39f0c4a9028491", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-calendar", "output": "static/assets/modules/profile/transparent/icon-calendar.png", "width": 256, "height": 256, "alpha": true, "bytes": 78155, "sha256": "bec3a1bce56c0bb31cac4a474f2bde9c8d904fbbc22c017e09d733364b014ed8", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-envelope", "output": "static/assets/modules/profile/transparent/icon-envelope.png", "width": 256, "height": 256, "alpha": true, "bytes": 73975, "sha256": "2462b34ef6b48d24be4cd2ce92c7931a057a254a666d7351f77a79263d61c486", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-feedback", "output": "static/assets/modules/profile/transparent/icon-feedback.png", "width": 256, "height": 256, "alpha": true, "bytes": 80060, "sha256": "3d194d983a2b6bea1a398d0055d68b643c2d4d91ecb3857ea967944b0900b352", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-help", "output": "static/assets/modules/profile/transparent/icon-help.png", "width": 256, "height": 256, "alpha": true, "bytes": 79859, "sha256": "a63f9d9756b152d3eed5e2bf5ecd1797204260511bb575b5d20082db2d735cd8", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-message", "output": "static/assets/modules/profile/transparent/icon-message.png", "width": 256, "height": 256, "alpha": true, "bytes": 75063, "sha256": "71a290aa4f76b4d786ab97e9808994636eb5f6ef252b3feaf6ac9c7e3d4c90f9", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-person", "output": "static/assets/modules/profile/transparent/icon-person.png", "width": 256, "height": 256, "alpha": true, "bytes": 76220, "sha256": "e2cb0d86614a1ae4c8da7b56426c1399d47c6b6c198d22fb11e9e21e0f4ab32d", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-promotion", "output": "static/assets/modules/profile/transparent/icon-promotion.png", "width": 256, "height": 256, "alpha": true, "bytes": 77714, "sha256": "81fa5248ce378a0f4b33dc5396a8b1b753c74f1c55feb19c874b43d632c2ae41", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-security", "output": "static/assets/modules/profile/transparent/icon-security.png", "width": 256, "height": 256, "alpha": true, "bytes": 83638, "sha256": "fe7bc7cbda827cba6d2e53add1e601f3296dcf0c7a42973878ea4c23a7a8ad74", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-settings", "output": "static/assets/modules/profile/transparent/icon-settings.png", "width": 256, "height": 256, "alpha": true, "bytes": 84885, "sha256": "f10c83e75135b2cc80c97963be5b9a8552fbe767c0a518f0106ede41cf441ccb", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-icon-vip", "output": "static/assets/modules/profile/transparent/icon-vip.png", "width": 256, "height": 256, "alpha": true, "bytes": 80349, "sha256": "66b08710a87d174cb38d24d6db75883a4819a074b848bbb5e7232bf9389e68ab", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-edit-button", "output": "static/assets/modules/profile/transparent/edit-button.png", "width": 1713, "height": 395, "alpha": true, "bytes": 1215863, "sha256": "58d9d8b2d57e2f7922f608122d1cdb5075e6a9e3b171ba2ce4100817b87d37e8", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-profile-transparent-tree-medallion", "output": "static/assets/modules/profile/transparent/tree-medallion.png", "width": 1254, "height": 1254, "alpha": true, "bytes": 2998737, "sha256": "831ffb68e4d2f0a0c44a9ddabd299e0209fc89fd007c5a1c1d4b709c6859b83d", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-action-arrow", "output": "static/assets/modules/tree/transparent/action-arrow.png", "width": 96, "height": 96, "alpha": true, "bytes": 2353, "sha256": "d7d80ce66b959ff29be31fb21e9b53f32289339bcdc98c13b883693754eedaf9", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-action-close", "output": "static/assets/modules/tree/transparent/action-close.png", "width": 128, "height": 128, "alpha": true, "bytes": 5660, "sha256": "c72e937ef06bf89a42b7958f48f3b38eecea66c16013c703ca4bc76c1b7a02e7", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-action-panel-paper", "output": "static/assets/modules/tree/transparent/action-panel-paper.png", "width": 1034, "height": 1520, "alpha": true, "bytes": 2456802, "sha256": "85d219e828ecbe8042f47fc39587f05134d1bed3300a0ccc201426075849c4ae", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-management-marker", "output": "static/assets/modules/tree/transparent/management-marker.png", "width": 128, "height": 128, "alpha": true, "bytes": 14014, "sha256": "389c4bb4964c0a7f8ef37cd650f6eda8ed50952eb0cce5f86d9643a30bf93e31", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-member-medallion", "output": "static/assets/modules/tree/transparent/member-medallion.png", "width": 192, "height": 192, "alpha": true, "bytes": 37801, "sha256": "9f3765f9ce055b95a84fd0c6f7e4844793caf1ac27c7ac2ac628beccbd5883ec", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-relation-button-frame", "output": "static/assets/modules/tree/transparent/relation-button-frame.png", "width": 1591, "height": 406, "alpha": true, "bytes": 1148107, "sha256": "bffdc12e8d0179fc3c3f38eeb7d5529c1274266205d3f78258887e961f1f8fbb", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-relation-map", "output": "static/assets/modules/tree/transparent/relation-map.png", "width": 1536, "height": 1024, "alpha": true, "bytes": 156562, "sha256": "550f20ea224ca90be6e5a4ee5ff4bf37716bf4d7f0c77f699e788e3a2c07f8f3", "provenance": "committed-binary", "rebuildable": false }, + { "id": "app-modules-tree-transparent-relation-marker", "output": "static/assets/modules/tree/transparent/relation-marker.png", "width": 128, "height": 128, "alpha": true, "bytes": 9268, "sha256": "a5afafa89435f5df5205a6e0537d9982e713fe97b7deb6e80754c71e2a86db31", "provenance": "committed-binary", "rebuildable": false } ] } diff --git a/design-pipeline/manifests/auth-runtime-assets.json b/design-pipeline/manifests/auth-runtime-assets.json index b9dcaaa..efb8c8d 100644 --- a/design-pipeline/manifests/auth-runtime-assets.json +++ b/design-pipeline/manifests/auth-runtime-assets.json @@ -6,19 +6,18 @@ "assets": [ { "id": "auth-page-paper", "output": "static/assets/foundation/opaque/auth-page-paper.jpg", "width": 750, "height": 1334, "alpha": false, "bytes": 60355, "sha256": "3eea1eefa815c306f0f4a95ce79878ca53f8f64a7c9b44144c99ca683a38e7b8", "provenance": "committed-binary", "rebuildable": false }, { "id": "auth-divider-knot", "output": "static/assets/foundation/transparent/auth-divider-knot.png", "width": 160, "height": 96, "alpha": true, "bytes": 6751, "sha256": "d45dc052c8eb2ba214c7d4d5846f5b53f01686e00e671bfe0582ed7d9d4a2419", "provenance": "committed-binary", "rebuildable": false }, - { "id": "auth-login-outline", "output": "static/assets/foundation/transparent/auth-login-outline.png", "width": 96, "height": 96, "alpha": true, "bytes": 1973, "sha256": "45fc22b367807bdc465e6e17230a10276a4d7e453e27b7cbc064af7fdc255470", "provenance": "committed-binary", "rebuildable": false }, { "id": "auth-wechat", "output": "static/assets/foundation/transparent/auth-wechat.png", "width": 96, "height": 96, "alpha": true, "bytes": 2215, "sha256": "5c69addac53afcd34064e48fffc19ef56a5d105c92f6130cd6045dd1591221d1", "provenance": "committed-binary", "rebuildable": false }, { "id": "brand-seal", "output": "static/assets/foundation/transparent/brand-seal.png", "width": 240, "height": 288, "alpha": true, "bytes": 101898, "sha256": "865e15edd6a1b50aa298ccdff5babf0f3140906026ce76d3a64631027820a7ae", "provenance": "committed-binary", "rebuildable": false }, { "id": "chevron-right", "output": "static/assets/foundation/transparent/chevron-right.png", "width": 96, "height": 96, "alpha": true, "bytes": 2106, "sha256": "82f4996e118832108dba4aa33d320131fee300970679f6ce8cf4b09bda62b700", "provenance": "committed-binary", "rebuildable": false }, - { "id": "auth-backdrop-v1", "output": "static/assets/modules/auth/opaque/a01-red-hall-ink-backdrop-v1.png", "width": 824, "height": 1830, "alpha": false, "bytes": 669577, "sha256": "478d0f56d669ac1597cfc2a1082740e73550f07d655286762f3306410c19eb11", "provenance": "committed-binary", "rebuildable": false }, - { "id": "auth-header-v1", "output": "static/assets/modules/auth/opaque/a01-vnext-header-v1.png", "width": 824, "height": 340, "alpha": false, "bytes": 504001, "sha256": "6ce03f4962e8bdd0272ed00a82efded21becdf350a1fb0880c595d7d9c15200d", "provenance": "committed-binary", "rebuildable": false }, - { "id": "auth-eye-closed-v2", "output": "static/assets/modules/auth/transparent/a01-icon-eye-closed-pupil-v2.png", "width": 96, "height": 96, "alpha": true, "bytes": 3906, "sha256": "b9304de963cacaa5ecd133a9835174f0c77761d9f264afec900460c9aa983b4f", "provenance": "committed-binary", "rebuildable": false }, - { "id": "auth-eye-open-v1", "output": "static/assets/modules/auth/transparent/a01-icon-eye-open-v1.png", "width": 96, "height": 96, "alpha": true, "bytes": 2868, "sha256": "aa6a78b3f0ecb2d47963b9aa04c98db1b2981497f52f58ff8c602cff80fab21a", "provenance": "committed-binary", "rebuildable": false }, - { "id": "auth-lock-v1", "output": "static/assets/modules/auth/transparent/a01-icon-lock-v1.png", "width": 96, "height": 96, "alpha": true, "bytes": 2649, "sha256": "c1b13de86ffee9a9f2e7534d58ddc3bf155d5341eeddbd5998ef3747d57fdea8", "provenance": "committed-binary", "rebuildable": false }, - { "id": "auth-phone-v1", "output": "static/assets/modules/auth/transparent/a01-icon-phone-v1.png", "width": 96, "height": 96, "alpha": true, "bytes": 925, "sha256": "5110cfb1cc84b00145506b3cda3fc35860400d16404e606428902dfe0fa7ec2e", "provenance": "committed-binary", "rebuildable": false }, - { "id": "auth-sms-code-v2", "output": "static/assets/modules/auth/transparent/a01-icon-sms-code-v2.png", "width": 1254, "height": 1254, "alpha": true, "bytes": 114061, "sha256": "e932900fa8ab0edcfeed0de0d52c637890e28f58f867aabfeacbb18df060a556", "provenance": "committed-binary", "rebuildable": false }, - { "id": "auth-divider-v1", "output": "static/assets/modules/auth/transparent/a01-vnext-divider-v1.png", "width": 540, "height": 90, "alpha": true, "bytes": 6278, "sha256": "10a3281723abfadcc2cec7b8e4437fe50ecdc36d885d54ebce11f46c46ed4af4", "provenance": "committed-binary", "rebuildable": false }, - { "id": "agreement-checked", "output": "static/assets/modules/auth/transparent/a02-agreement-checked.png", "width": 96, "height": 96, "alpha": true, "bytes": 10602, "sha256": "825fcb945c81330429abc1d3ec1d0c20df86a9634416e244896e452cb08644f4", "provenance": "committed-binary", "rebuildable": false }, - { "id": "agreement-unchecked", "output": "static/assets/modules/auth/transparent/a02-agreement-unchecked.png", "width": 96, "height": 96, "alpha": true, "bytes": 6014, "sha256": "d9843d1c3644272f6eeff9fb44e763c857f409f404551a5e1f032ddf1b07b69f", "provenance": "committed-binary", "rebuildable": false } + { "id": "auth-backdrop", "output": "static/assets/modules/auth/opaque/sign-in-backdrop.png", "width": 824, "height": 1830, "alpha": false, "bytes": 669577, "sha256": "478d0f56d669ac1597cfc2a1082740e73550f07d655286762f3306410c19eb11", "provenance": "committed-binary", "rebuildable": false }, + { "id": "auth-header", "output": "static/assets/modules/auth/opaque/auth-header.png", "width": 824, "height": 340, "alpha": false, "bytes": 504001, "sha256": "6ce03f4962e8bdd0272ed00a82efded21becdf350a1fb0880c595d7d9c15200d", "provenance": "committed-binary", "rebuildable": false }, + { "id": "auth-password-hidden", "output": "static/assets/modules/auth/transparent/password-hidden.png", "width": 96, "height": 96, "alpha": true, "bytes": 3906, "sha256": "b9304de963cacaa5ecd133a9835174f0c77761d9f264afec900460c9aa983b4f", "provenance": "committed-binary", "rebuildable": false }, + { "id": "auth-password-visible", "output": "static/assets/modules/auth/transparent/password-visible.png", "width": 96, "height": 96, "alpha": true, "bytes": 2868, "sha256": "aa6a78b3f0ecb2d47963b9aa04c98db1b2981497f52f58ff8c602cff80fab21a", "provenance": "committed-binary", "rebuildable": false }, + { "id": "auth-icon-password", "output": "static/assets/modules/auth/transparent/icon-password.png", "width": 96, "height": 96, "alpha": true, "bytes": 2649, "sha256": "c1b13de86ffee9a9f2e7534d58ddc3bf155d5341eeddbd5998ef3747d57fdea8", "provenance": "committed-binary", "rebuildable": false }, + { "id": "auth-icon-phone", "output": "static/assets/modules/auth/transparent/icon-phone.png", "width": 96, "height": 96, "alpha": true, "bytes": 925, "sha256": "5110cfb1cc84b00145506b3cda3fc35860400d16404e606428902dfe0fa7ec2e", "provenance": "committed-binary", "rebuildable": false }, + { "id": "auth-icon-verification-code", "output": "static/assets/modules/auth/transparent/icon-verification-code.png", "width": 1254, "height": 1254, "alpha": true, "bytes": 114061, "sha256": "e932900fa8ab0edcfeed0de0d52c637890e28f58f867aabfeacbb18df060a556", "provenance": "committed-binary", "rebuildable": false }, + { "id": "auth-title-divider", "output": "static/assets/modules/auth/transparent/title-divider.png", "width": 540, "height": 90, "alpha": true, "bytes": 6278, "sha256": "10a3281723abfadcc2cec7b8e4437fe50ecdc36d885d54ebce11f46c46ed4af4", "provenance": "committed-binary", "rebuildable": false }, + { "id": "agreement-checked", "output": "static/assets/modules/auth/transparent/agreement-checked.png", "width": 96, "height": 96, "alpha": true, "bytes": 10602, "sha256": "825fcb945c81330429abc1d3ec1d0c20df86a9634416e244896e452cb08644f4", "provenance": "committed-binary", "rebuildable": false }, + { "id": "agreement-unchecked", "output": "static/assets/modules/auth/transparent/agreement-unchecked.png", "width": 96, "height": 96, "alpha": true, "bytes": 6014, "sha256": "d9843d1c3644272f6eeff9fb44e763c857f409f404551a5e1f032ddf1b07b69f", "provenance": "committed-binary", "rebuildable": false } ] } diff --git a/design-pipeline/manifests/g01-state-frame-v3.json b/design-pipeline/manifests/g01-state-frame-v3.json deleted file mode 100644 index 717677b..0000000 --- a/design-pipeline/manifests/g01-state-frame-v3.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "schemaVersion": 3, - "kind": "asset-build-manifest", - "family": "g01-state-frame-v3", - "assets": [ - { - "id": "g01-empty-panel-frame", - "source": "docs/design/assets/g01-state/masters/g01-empty-panel-master.png", - "output": "static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png", - "sourcePixels": { "width": 1122, "height": 1402 }, - "outputPixels": { "width": 1122, "height": 1402 }, - "alpha": { "required": true, "transparentOuterPadding": 0, "cornerMaxAlpha": 0 }, - "edge": { "forbidChromaResidue": false, "forbidLightFringe": false, "premultipliedAlphaCheck": true }, - "quality": { "maxBytes": 2200000, "colorSpace": "sRGB" }, - "processing": { - "mode": "warm-gold-frame-extract", - "borderBand": 110, - "redGreenMin": 15, - "greenBlueMin": 12, - "redBlueMin": 35, - "redMaxExclusive": 245, - "blueMaxExclusive": 180, - "alphaOffset": 25, - "alphaScale": 6, - "outputMode": "RGBA" - } - } - ] -} diff --git a/design-pipeline/manifests/page-backgrounds-v3.json b/design-pipeline/manifests/page-backgrounds-v3.json deleted file mode 100644 index 2a394d8..0000000 --- a/design-pipeline/manifests/page-backgrounds-v3.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "schemaVersion": 3, - "kind": "asset-build-manifest", - "family": "page-backgrounds-v3", - "assets": [ - { - "id": "genealogy-page-background-long", - "source": "docs/design/assets/g01-background/masters/genealogy-page-background-long-flagship-master.png", - "output": "static/assets/modules/genealogy/opaque/genealogy-page-background-long.png", - "sourcePixels": { "width": 1536, "height": 3840 }, - "outputPixels": { "width": 1440, "height": 3600 }, - "quality": { "maxBytes": 7000000, "colorSpace": "sRGB" }, - "processing": { "mode": "opaque-resize", "resample": "lanczos", "outputMode": "RGB" } - }, - { - "id": "tree-page-background-long", - "source": "docs/design/assets/module-backgrounds/masters/tree-page-background-imagegen-source.png", - "output": "static/assets/modules/tree/opaque/tree-page-background-long.png", - "sourcePixels": { "width": 793, "height": 1983 }, - "outputPixels": { "width": 1440, "height": 3600 }, - "quality": { "maxBytes": 7000000, "colorSpace": "sRGB" }, - "processing": { "mode": "opaque-cover-crop", "resample": "lanczos", "anchor": "center", "outputMode": "RGB" } - }, - { - "id": "family-page-background-long", - "source": "docs/design/assets/module-backgrounds/masters/family-page-background-imagegen-source.png", - "output": "static/assets/modules/family/opaque/family-page-background-long.png", - "sourcePixels": { "width": 793, "height": 1983 }, - "outputPixels": { "width": 1440, "height": 3600 }, - "quality": { "maxBytes": 7000000, "colorSpace": "sRGB" }, - "processing": { "mode": "opaque-cover-crop", "resample": "lanczos", "anchor": "center", "outputMode": "RGB" } - }, - { - "id": "records-page-background-long", - "source": "docs/design/assets/module-backgrounds/masters/records-page-background-imagegen-source.png", - "output": "static/assets/modules/records/opaque/records-page-background-long.png", - "sourcePixels": { "width": 793, "height": 1983 }, - "outputPixels": { "width": 1440, "height": 3600 }, - "quality": { "maxBytes": 7000000, "colorSpace": "sRGB" }, - "processing": { "mode": "opaque-cover-crop", "resample": "lanczos", "anchor": "center", "outputMode": "RGB" } - }, - { - "id": "notification-page-background-long", - "source": "docs/design/assets/module-backgrounds/masters/notification-page-background-imagegen-source.png", - "output": "static/assets/modules/notification/opaque/notification-page-background-long.png", - "sourcePixels": { "width": 793, "height": 1983 }, - "outputPixels": { "width": 1440, "height": 3600 }, - "quality": { "maxBytes": 7000000, "colorSpace": "sRGB" }, - "processing": { "mode": "opaque-cover-crop", "resample": "lanczos", "anchor": "center", "outputMode": "RGB" } - }, - { - "id": "profile-page-background-long", - "source": "docs/design/assets/module-backgrounds/masters/profile-page-background-imagegen-source.png", - "output": "static/assets/modules/profile/opaque/profile-page-background-long.png", - "sourcePixels": { "width": 793, "height": 1983 }, - "outputPixels": { "width": 1440, "height": 3600 }, - "quality": { "maxBytes": 7000000, "colorSpace": "sRGB" }, - "processing": { "mode": "opaque-cover-crop", "resample": "lanczos", "anchor": "center", "outputMode": "RGB" } - } - ] -} diff --git a/design-pipeline/manifests/retained-generated-runtime-assets.json b/design-pipeline/manifests/retained-generated-runtime-assets.json new file mode 100644 index 0000000..56df525 --- /dev/null +++ b/design-pipeline/manifests/retained-generated-runtime-assets.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "kind": "runtime-asset-inventory", + "scope": "retained-generated-assets", + "imports": [], + "assets": [ + { "id": "shared-scroll-primary-v3", "output": "static/assets/foundation/transparent/scroll-primary.png", "width": 1866, "height": 276, "alpha": true, "bytes": 576876, "sha256": "2181fece4d1743aa55b919c38ed1cb31662d8d2588f16e2dabb236e24d094040", "provenance": "committed-binary", "rebuildable": false }, + { "id": "shared-scroll-secondary-v3", "output": "static/assets/foundation/transparent/scroll-secondary.png", "width": 1866, "height": 300, "alpha": true, "bytes": 661030, "sha256": "52b8c16968e96cf457b227a3026ea9aed2b2a7b3d01ad56d409eb6b7504fc195", "provenance": "committed-binary", "rebuildable": false }, + { "id": "shared-scroll-toast-v3", "output": "static/assets/foundation/transparent/scroll-toast.png", "width": 1770, "height": 246, "alpha": true, "bytes": 468063, "sha256": "e08bdcb1ab3707762306bc23386f91562798d4efd5a5a917c3e9958fcfa7ddfc", "provenance": "committed-binary", "rebuildable": false }, + { "id": "shared-scroll-dialog-v3", "output": "static/assets/modules/auth/transparent/dialog-scroll.png", "width": 1860, "height": 1560, "alpha": true, "bytes": 355747, "sha256": "dfd0b122447e3a33737a4788efd7be35a2fca937ce600b1e8b14d12c7a9b77fa", "provenance": "committed-binary", "rebuildable": false }, + { "id": "genealogy-page-background-long", "output": "static/assets/modules/genealogy/opaque/genealogy-page-background-long.png", "width": 1440, "height": 3600, "alpha": false, "bytes": 5756868, "sha256": "61b0d1324467e44a8eaaf9fc9609058bae153ef51a91f0be9f9a6ea95175760b", "provenance": "committed-binary", "rebuildable": false }, + { "id": "tree-page-background-long", "output": "static/assets/modules/tree/opaque/tree-page-background-long.png", "width": 1440, "height": 3600, "alpha": false, "bytes": 5298030, "sha256": "de71ce6640a9d67c76d187a82a93ba12a2dbe4c6e1c5aa7eee2b617085967e5f", "provenance": "committed-binary", "rebuildable": false }, + { "id": "family-page-background-long", "output": "static/assets/modules/family/opaque/family-page-background-long.png", "width": 1440, "height": 3600, "alpha": false, "bytes": 5387684, "sha256": "a27d12319ec7adad117ab6536c4f0dd38420dfd4c01fd1dee9ea106a2fd1a971", "provenance": "committed-binary", "rebuildable": false }, + { "id": "records-page-background-long", "output": "static/assets/modules/records/opaque/records-page-background-long.png", "width": 1440, "height": 3600, "alpha": false, "bytes": 5295616, "sha256": "ee3455372b3754fcc19b409ee682efcce07ab7628bca382bb8b6ce87f38e7ec4", "provenance": "committed-binary", "rebuildable": false }, + { "id": "notification-page-background-long", "output": "static/assets/modules/notification/opaque/notification-page-background-long.png", "width": 1440, "height": 3600, "alpha": false, "bytes": 4802510, "sha256": "b7bd37b23141b30c4921690d8156576de314f1a53750ae06c0b72fe55dfa6427", "provenance": "committed-binary", "rebuildable": false }, + { "id": "profile-page-background-long", "output": "static/assets/modules/profile/opaque/profile-page-background-long.png", "width": 1440, "height": 3600, "alpha": false, "bytes": 5373757, "sha256": "6b344014aad20f1ecaa14e236fc2aa9febb2ce9f3206fa53eea2523fc5e2094b", "provenance": "committed-binary", "rebuildable": false }, + { "id": "g01-empty-panel-frame", "output": "static/assets/modules/genealogy/transparent/empty-panel-frame.png", "width": 1122, "height": 1402, "alpha": true, "bytes": 74974, "sha256": "cce62602f9dc25cb9ff1ebf555efe20489513db55e2c5d0e3f762a6f11d7aa48", "provenance": "committed-binary", "rebuildable": false } + ] +} diff --git a/design-pipeline/manifests/runtime-assets.json b/design-pipeline/manifests/runtime-assets.json index 1892a50..2ed606c 100644 --- a/design-pipeline/manifests/runtime-assets.json +++ b/design-pipeline/manifests/runtime-assets.json @@ -5,9 +5,7 @@ "imports": [ "design-pipeline/manifests/auth-runtime-assets.json", "design-pipeline/manifests/application-runtime-assets.json", - "design-pipeline/manifests/shared-scroll-skins-v3.json", - "design-pipeline/manifests/page-backgrounds-v3.json", - "design-pipeline/manifests/g01-state-frame-v3.json" + "design-pipeline/manifests/retained-generated-runtime-assets.json" ], "assets": [] } diff --git a/design-pipeline/manifests/shared-scroll-skins-v3.json b/design-pipeline/manifests/shared-scroll-skins-v3.json deleted file mode 100644 index 7ca78ae..0000000 --- a/design-pipeline/manifests/shared-scroll-skins-v3.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "schemaVersion": 3, - "kind": "asset-build-manifest", - "family": "shared-scroll-skins-v3", - "assets": [ - { - "id": "shared-scroll-primary-v3", - "source": "docs/design/assets/a01-vnext/masters/a01-scroll-primary-master-v3.png", - "output": "static/assets/foundation/transparent/a01-scroll-primary-v3.png", - "sourcePixels": { "width": 2172, "height": 724 }, - "outputPixels": { "width": 1866, "height": 276 }, - "alpha": { "required": true, "transparentOuterPadding": 6, "cornerMaxAlpha": 0 }, - "edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true }, - "quality": { "maxBytes": 1800000, "colorSpace": "sRGB" }, - "processing": { "mode": "chroma-stretch", "capWidth": 380, "keyColor": "#00FF00", "keyTolerance": 96 } - }, - { - "id": "shared-scroll-secondary-v3", - "source": "docs/design/assets/a01-vnext/masters/a01-scroll-secondary-master-v3.png", - "output": "static/assets/foundation/transparent/a01-scroll-secondary-v3.png", - "sourcePixels": { "width": 2172, "height": 724 }, - "outputPixels": { "width": 1866, "height": 300 }, - "alpha": { "required": true, "transparentOuterPadding": 6, "cornerMaxAlpha": 0 }, - "edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true }, - "quality": { "maxBytes": 1800000, "colorSpace": "sRGB" }, - "processing": { "mode": "chroma-stretch", "capWidth": 380, "keyColor": "#00FF00", "keyTolerance": 96 } - }, - { - "id": "shared-scroll-toast-v3", - "source": "docs/design/assets/a01-vnext/masters/a01-scroll-toast-master-v3.png", - "output": "static/assets/foundation/transparent/a01-scroll-toast-v3.png", - "sourcePixels": { "width": 2172, "height": 724 }, - "outputPixels": { "width": 1770, "height": 246 }, - "alpha": { "required": true, "transparentOuterPadding": 6, "cornerMaxAlpha": 0 }, - "edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true }, - "quality": { "maxBytes": 1600000, "colorSpace": "sRGB" }, - "processing": { "mode": "chroma-stretch", "capWidth": 340, "keyColor": "#00FF00", "keyTolerance": 96 } - }, - { - "id": "shared-scroll-dialog-v3", - "source": "docs/design/assets/a01-vnext/masters/a01-scroll-dialog-master-v3-r2.png", - "output": "static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png", - "sourcePixels": { "width": 1370, "height": 1148 }, - "outputPixels": { "width": 1860, "height": 1560 }, - "alpha": { "required": true, "transparentOuterPadding": 6, "cornerMaxAlpha": 0 }, - "edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true }, - "quality": { "maxBytes": 3000000, "colorSpace": "sRGB" }, - "processing": { "mode": "chroma-stretch", "capWidth": 260, "keyColor": "#00FF00", "keyTolerance": 96, "paletteColors": 128, "indexedPng": true } - } - ] -} diff --git a/design-pipeline/package.json b/design-pipeline/package.json index 705ca1b..357596d 100644 --- a/design-pipeline/package.json +++ b/design-pipeline/package.json @@ -3,16 +3,8 @@ "private": true, "type": "module", "scripts": { - "test": "npm run test:node && npm run test:python", - "test:node": "node --test tests/*.test.mjs", - "test:python": "node scripts/run-python-tests.mjs", - "validate:shared-scroll-skins": "node scripts/validate-asset-build-manifest.mjs design-pipeline/manifests/shared-scroll-skins-v3.json", - "validate:page-backgrounds": "node scripts/validate-asset-build-manifest.mjs design-pipeline/manifests/page-backgrounds-v3.json", - "validate:g01-state-frame": "node scripts/validate-asset-build-manifest.mjs design-pipeline/manifests/g01-state-frame-v3.json", + "test": "node --test tests/*.test.mjs", "validate:runtime-assets": "node scripts/validate-runtime-asset-inventory.mjs design-pipeline/manifests/runtime-assets.json", - "build:shared-scroll-skins": "node scripts/build-shared-scroll-skins.mjs", - "verify:shared-scroll-skins": "node scripts/verify-shared-scroll-skins.mjs", - "build:page-backgrounds": "node scripts/build-raster-assets.mjs design-pipeline/manifests/page-backgrounds-v3.json", - "build:g01-state-frame": "node scripts/build-raster-assets.mjs design-pipeline/manifests/g01-state-frame-v3.json" + "check": "npm test && npm run validate:runtime-assets" } } diff --git a/design-pipeline/requirements.txt b/design-pipeline/requirements.txt deleted file mode 100644 index e7cc6c1..0000000 --- a/design-pipeline/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -Pillow==12.3.0 diff --git a/design-pipeline/scripts/asset-build-manifest.mjs b/design-pipeline/scripts/asset-build-manifest.mjs deleted file mode 100644 index 13a3f67..0000000 --- a/design-pipeline/scripts/asset-build-manifest.mjs +++ /dev/null @@ -1,196 +0,0 @@ -import path from 'node:path' - -const assertOnlyFields = (value, fields, label) => { - for (const field of Object.keys(value)) { - if (!fields.has(field)) throw new Error(`${label} contains unknown field: ${field}`) - } -} - -const requireObject = (value, label) => { - if (!value || typeof value !== 'object' || Array.isArray(value)) { - throw new Error(`${label} must be an object`) - } - return value -} - -const requireString = (value, label) => { - if (typeof value !== 'string' || value.trim() === '') { - throw new Error(`${label} must be a non-empty string`) - } - return value -} - -const requirePositiveInteger = (value, label) => { - if (!Number.isInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`) - return value -} - -const requireIntegerInRange = (value, minimum, maximum, label) => { - if (!Number.isInteger(value) || value < minimum || value > maximum) { - throw new Error(`${label} must be an integer from ${minimum} to ${maximum}`) - } - return value -} - -const requireBoolean = (value, label) => { - if (typeof value !== 'boolean') throw new Error(`${label} must be boolean`) - return value -} - -const requireExact = (value, expected, label) => { - if (value !== expected) throw new Error(`${label} must be ${expected}`) - return value -} - -const resolveInsideWorkspace = (workspace, relativePath, label) => { - requireString(relativePath, label) - const absolutePath = path.resolve(workspace, relativePath) - const relative = path.relative(workspace, absolutePath) - if (relative.startsWith('..') || path.isAbsolute(relative)) { - throw new Error(`${label} escapes workspace: ${relativePath}`) - } - return absolutePath -} - -const validatePixels = (value, label) => { - const pixels = requireObject(value, label) - assertOnlyFields(pixels, new Set(['width', 'height']), label) - requirePositiveInteger(pixels.width, `${label}.width`) - requirePositiveInteger(pixels.height, `${label}.height`) -} - -const validateAlphaAndEdge = (asset, id) => { - const alpha = requireObject(asset.alpha, `${id}.alpha`) - assertOnlyFields(alpha, new Set(['required', 'transparentOuterPadding', 'cornerMaxAlpha']), `${id}.alpha`) - requireExact(alpha.required, true, `${id}.alpha.required`) - requireIntegerInRange(alpha.transparentOuterPadding, 0, 4096, `${id}.alpha.transparentOuterPadding`) - requireIntegerInRange(alpha.cornerMaxAlpha, 0, 255, `${id}.alpha.cornerMaxAlpha`) - - const edge = requireObject(asset.edge, `${id}.edge`) - assertOnlyFields( - edge, - new Set(['forbidChromaResidue', 'forbidLightFringe', 'premultipliedAlphaCheck']), - `${id}.edge`, - ) - requireBoolean(edge.forbidChromaResidue, `${id}.edge.forbidChromaResidue`) - requireBoolean(edge.forbidLightFringe, `${id}.edge.forbidLightFringe`) - requireBoolean(edge.premultipliedAlphaCheck, `${id}.edge.premultipliedAlphaCheck`) -} - -const validateProcessing = (processing, id) => { - requireObject(processing, `${id}.processing`) - const mode = requireString(processing.mode, `${id}.processing.mode`) - - if (mode === 'chroma-stretch') { - assertOnlyFields( - processing, - new Set(['mode', 'capWidth', 'keyColor', 'keyTolerance', 'paletteColors', 'indexedPng']), - `${id}.processing`, - ) - requirePositiveInteger(processing.capWidth, `${id}.processing.capWidth`) - if (!/^#[A-Fa-f0-9]{6}$/.test(processing.keyColor)) { - throw new Error(`${id}.processing.keyColor must be a six-digit RGB color`) - } - requireIntegerInRange(processing.keyTolerance, 0, 441, `${id}.processing.keyTolerance`) - if (processing.paletteColors !== undefined) { - requireIntegerInRange(processing.paletteColors, 2, 256, `${id}.processing.paletteColors`) - } - if (processing.indexedPng !== undefined) { - requireBoolean(processing.indexedPng, `${id}.processing.indexedPng`) - } - return true - } - - if (mode === 'opaque-resize') { - assertOnlyFields(processing, new Set(['mode', 'resample', 'outputMode']), `${id}.processing`) - requireExact(processing.resample, 'lanczos', `${id}.processing.resample`) - requireExact(processing.outputMode, 'RGB', `${id}.processing.outputMode`) - return false - } - - if (mode === 'opaque-cover-crop') { - assertOnlyFields(processing, new Set(['mode', 'resample', 'anchor', 'outputMode']), `${id}.processing`) - requireExact(processing.resample, 'lanczos', `${id}.processing.resample`) - requireExact(processing.anchor, 'center', `${id}.processing.anchor`) - requireExact(processing.outputMode, 'RGB', `${id}.processing.outputMode`) - return false - } - - if (mode === 'warm-gold-frame-extract') { - assertOnlyFields( - processing, - new Set([ - 'mode', - 'borderBand', - 'redGreenMin', - 'greenBlueMin', - 'redBlueMin', - 'redMaxExclusive', - 'blueMaxExclusive', - 'alphaOffset', - 'alphaScale', - 'outputMode', - ]), - `${id}.processing`, - ) - requirePositiveInteger(processing.borderBand, `${id}.processing.borderBand`) - for (const field of ['redGreenMin', 'greenBlueMin', 'redBlueMin', 'alphaOffset']) { - requireIntegerInRange(processing[field], 0, 255, `${id}.processing.${field}`) - } - for (const field of ['redMaxExclusive', 'blueMaxExclusive']) { - requireIntegerInRange(processing[field], 1, 256, `${id}.processing.${field}`) - } - requirePositiveInteger(processing.alphaScale, `${id}.processing.alphaScale`) - requireExact(processing.outputMode, 'RGBA', `${id}.processing.outputMode`) - return true - } - - throw new Error(`${id} has unsupported processing.mode: ${mode}`) -} - -export const validateAssetBuildManifest = (manifest, workspace) => { - requireObject(manifest, 'manifest') - assertOnlyFields(manifest, new Set(['schemaVersion', 'kind', 'family', 'assets']), 'manifest') - if (manifest.schemaVersion !== 3) throw new Error('schemaVersion must be 3') - if (manifest.kind !== 'asset-build-manifest') throw new Error('kind must be asset-build-manifest') - requireString(manifest.family, 'family') - if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) { - throw new Error('assets must be a non-empty array') - } - - const ids = new Set() - const outputs = new Set() - for (const asset of manifest.assets) { - requireObject(asset, 'asset') - const processing = requireObject(asset.processing, 'asset.processing') - const mode = requireString(processing.mode, 'asset.processing.mode') - const transparent = ['chroma-stretch', 'warm-gold-frame-extract'].includes(mode) - const fields = new Set(['id', 'source', 'output', 'sourcePixels', 'outputPixels', 'quality', 'processing']) - if (transparent) { - fields.add('alpha') - fields.add('edge') - } - assertOnlyFields(asset, fields, 'asset') - - const id = requireString(asset.id, 'asset.id') - if (ids.has(id)) throw new Error(`duplicate asset id: ${id}`) - ids.add(id) - - resolveInsideWorkspace(workspace, asset.source, `${id}.source`) - resolveInsideWorkspace(workspace, asset.output, `${id}.output`) - if (outputs.has(asset.output)) throw new Error(`duplicate output: ${asset.output}`) - outputs.add(asset.output) - - validatePixels(asset.sourcePixels, `${id}.sourcePixels`) - validatePixels(asset.outputPixels, `${id}.outputPixels`) - - const processingNeedsAlpha = validateProcessing(processing, id) - if (processingNeedsAlpha) validateAlphaAndEdge(asset, id) - - const quality = requireObject(asset.quality, `${id}.quality`) - assertOnlyFields(quality, new Set(['maxBytes', 'colorSpace']), `${id}.quality`) - requirePositiveInteger(quality.maxBytes, `${id}.quality.maxBytes`) - requireExact(quality.colorSpace, 'sRGB', `${id}.quality.colorSpace`) - } - return manifest -} diff --git a/design-pipeline/scripts/asset_quality.py b/design-pipeline/scripts/asset_quality.py deleted file mode 100644 index 5f407eb..0000000 --- a/design-pipeline/scripts/asset_quality.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Deterministic PNG quality checks for generated design assets.""" - -from pathlib import Path -from typing import Any - -from PIL import Image - - -def _outer_ring_pixels(image: Image.Image, thickness: int): - width, height = image.size - thickness = max(0, min(thickness, width // 2, height // 2)) - for y in range(height): - for x in range(width): - if x < thickness or x >= width - thickness or y < thickness or y >= height - thickness: - yield image.getpixel((x, y)) - - -def analyze_asset(path: Path, spec: dict[str, Any], workspace: Path) -> dict[str, Any]: - """按 schema v3 物理规格分析单张 PNG,并只输出工作区相对路径。""" - workspace = Path(workspace).resolve() - path = Path(path).resolve() - try: - report_path = path.relative_to(workspace).as_posix() - except ValueError as error: - raise ValueError(f"asset path escapes workspace: {path}") from error - errors: list[str] = [] - warnings: list[str] = [] - metrics: dict[str, int] = {} - - with Image.open(path) as opened: - width, height = opened.size - mode = opened.mode - info = dict(opened.info) - has_alpha = "A" in opened.getbands() or "transparency" in opened.info - image = opened.convert("RGBA") - - expected = spec["outputPixels"] - if (width, height) != (expected["width"], expected["height"]): - errors.append( - f"dimensions expected {expected['width']}x{expected['height']}, got {width}x{height}" - ) - - alpha = spec.get("alpha", {}) - if alpha.get("required") and not has_alpha: - errors.append(f"alpha-required asset has no alpha channel or transparency table, got {mode}") - - padding = int(alpha.get("transparentOuterPadding", 0)) - max_alpha = int(alpha.get("cornerMaxAlpha", 255)) - corners = ( - image.getpixel((0, 0))[3], - image.getpixel((width - 1, 0))[3], - image.getpixel((0, height - 1))[3], - image.getpixel((width - 1, height - 1))[3], - ) - corner_violations = sum(1 for value in corners if value > max_alpha) - metrics["cornerAlphaViolations"] = corner_violations - if corner_violations: - errors.append(f"corners contain {corner_violations} pixels above alpha {max_alpha}") - - # 不透明长背景没有边缘/透明度扫描需求;避免无意义地把每张 1440×3600 图 - # 展开成数百万个 Python 元组。透明资产仍完整执行原有像素级质量合同。 - opaque_padding = 0 - if padding > 0: - opaque_padding = sum(1 for pixel in _outer_ring_pixels(image, padding) if pixel[3] > max_alpha) - metrics["outerPaddingViolations"] = opaque_padding - if opaque_padding: - errors.append(f"outer padding contains {opaque_padding} pixels above alpha {max_alpha}") - - edge = spec.get("edge", {}) - needs_edge_pixels = any(edge.get(field) for field in ( - "forbidChromaResidue", - "forbidLightFringe", - "premultipliedAlphaCheck", - )) - pixels = list(image.get_flattened_data()) if needs_edge_pixels else [] - - chroma_residue = sum( - 1 - for red, green, blue, pixel_alpha in pixels - if pixel_alpha > 8 and green > 120 and green - max(red, blue) > 80 - ) - metrics["chromaResiduePixels"] = chroma_residue - if edge.get("forbidChromaResidue") and chroma_residue: - errors.append(f"chroma residue detected in {chroma_residue} visible pixels") - - light_fringe = sum( - 1 - for red, green, blue, pixel_alpha in pixels - if 0 < pixel_alpha < 255 and red > 235 and green > 235 and blue > 235 - ) - metrics["lightFringePixels"] = light_fringe - if edge.get("forbidLightFringe") and light_fringe: - errors.append(f"light fringe detected in {light_fringe} partially transparent pixels") - - transparent_rgb = sum( - 1 - for red, green, blue, pixel_alpha in pixels - if pixel_alpha == 0 and (red != 0 or green != 0 or blue != 0) - ) - metrics["transparentRgbPixels"] = transparent_rgb - if edge.get("premultipliedAlphaCheck") and transparent_rgb: - errors.append(f"transparent RGB detected in {transparent_rgb} fully transparent pixels") - - byte_size = path.stat().st_size - max_bytes = int(spec.get("quality", {}).get("maxBytes", 0)) - if max_bytes and byte_size > max_bytes: - errors.append(f"maxBytes {max_bytes} exceeded by file size {byte_size}") - - expected_color_space = spec.get("quality", {}).get("colorSpace") - if expected_color_space == "sRGB" and not ("srgb" in info or "icc_profile" in info): - errors.append("PNG does not declare an sRGB chunk or ICC profile") - - expected_mode = spec.get("processing", {}).get("outputMode") - if expected_mode and mode != expected_mode: - errors.append(f"output mode expected {expected_mode}, got {mode}") - - return { - "id": spec.get("id", path.stem), - "path": report_path, - "width": width, - "height": height, - "mode": mode, - "bytes": byte_size, - "errors": errors, - "warnings": warnings, - "metrics": metrics, - } diff --git a/design-pipeline/scripts/build-raster-assets.mjs b/design-pipeline/scripts/build-raster-assets.mjs deleted file mode 100644 index dc5a2ca..0000000 --- a/design-pipeline/scripts/build-raster-assets.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { validateAssetBuildManifest } from './asset-build-manifest.mjs' -import { resolvePythonExecutable, runPythonCommand } from './python-runtime.mjs' - -const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)) -const pipelineDirectory = path.resolve(scriptDirectory, '..') -const workspace = path.resolve(pipelineDirectory, '..') -const argument = process.argv[2] -if (!argument) throw new Error('用法:node build-raster-assets.mjs <仓库相对清单路径>') - -// 清单参数本身也必须留在工作区;source/output 的边界由 schema validator 逐项负责。 -const manifestPath = path.resolve(workspace, argument) -const relativeManifest = path.relative(workspace, manifestPath) -if (relativeManifest.startsWith('..') || path.isAbsolute(relativeManifest)) { - throw new Error(`构建清单越出工作区:${argument}`) -} - -const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) -validateAssetBuildManifest(manifest, workspace) -const supportedModes = new Set(['opaque-resize', 'opaque-cover-crop', 'warm-gold-frame-extract']) -for (const asset of manifest.assets) { - if (!supportedModes.has(asset.processing.mode)) { - throw new Error(`通用 raster 构建器不支持模式:${asset.processing.mode}`) - } -} - -const python = resolvePythonExecutable({ pipelineDirectory }) -const reportPath = path.join(pipelineDirectory, 'generated', manifest.family, 'quality-report.json') - -// Node 只编排严格 schema、锁定的 Python 入口和质量报告;像素算法只存在于 Python。 -runPythonCommand({ - executable: python, - args: [path.join(scriptDirectory, 'build_raster_assets.py'), manifestPath, '--workspace', workspace], - cwd: workspace, -}) -runPythonCommand({ - executable: python, - args: [path.join(scriptDirectory, 'verify-assets.py'), manifestPath, '--workspace', workspace, '--report', reportPath], - cwd: workspace, -}) diff --git a/design-pipeline/scripts/build-shared-scroll-skins.mjs b/design-pipeline/scripts/build-shared-scroll-skins.mjs deleted file mode 100644 index 4ed357c..0000000 --- a/design-pipeline/scripts/build-shared-scroll-skins.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { validateAssetBuildManifest } from './asset-build-manifest.mjs' -import { resolvePythonExecutable, runPythonCommand } from './python-runtime.mjs' - -const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)) -const pipelineDirectory = path.resolve(scriptDirectory, '..') -const workspace = path.resolve(pipelineDirectory, '..') -const manifestPath = path.join(pipelineDirectory, 'manifests', 'shared-scroll-skins-v3.json') -const reportPath = path.join(pipelineDirectory, 'generated', 'shared-scroll-skins-v3', 'quality-report.json') -const python = resolvePythonExecutable({ pipelineDirectory }) -const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) - -validateAssetBuildManifest(manifest, workspace) - -// Node 只负责编排锁定的清单和 Python 工具;像素处理与质量分析各自只有一个实现。 -function run(script, args) { - runPythonCommand({ - executable: python, - args: [path.join(scriptDirectory, script), ...args], - cwd: workspace, - }) -} - -run('build_scroll_skins.py', [manifestPath, '--workspace', workspace]) -run('verify-assets.py', [manifestPath, '--workspace', workspace, '--report', reportPath]) diff --git a/design-pipeline/scripts/build_raster_assets.py b/design-pipeline/scripts/build_raster_assets.py deleted file mode 100644 index b8c9053..0000000 --- a/design-pipeline/scripts/build_raster_assets.py +++ /dev/null @@ -1,132 +0,0 @@ -"""按 schema v3 清单确定性生成不透明长背景与 G01 暖金边框。""" - -import argparse -import json -from pathlib import Path -from typing import Any - -from PIL import Image, PngImagePlugin - - -def _resolve_inside(workspace: Path, relative_path: str) -> Path: - """解析仓库相对路径,并在任何读写发生前拒绝目录逃逸。""" - absolute = (workspace / relative_path).resolve() - try: - absolute.relative_to(workspace.resolve()) - except ValueError as error: - raise ValueError(f"path escapes workspace: {relative_path}") from error - return absolute - - -def build_opaque_resize(source: Image.Image, output_size: tuple[int, int]) -> Image.Image: - """把同宽高比母版按 LANCZOS 直接缩放为无透明通道的 RGB 正式图。""" - image = source.convert("RGB") - if image.size != output_size: - image = image.resize(output_size, Image.Resampling.LANCZOS) - return image - - -def build_opaque_cover_crop(source: Image.Image, output_size: tuple[int, int]) -> Image.Image: - """等比 cover 后从中心裁切;算法与旧五模块构建器的可见像素完全一致。""" - width, height = output_size - image = source.convert("RGB") - scale = max(width / image.width, height / image.height) - resized = image.resize( - (round(image.width * scale), round(image.height * scale)), - Image.Resampling.LANCZOS, - ) - left = max(0, (resized.width - width) // 2) - top = max(0, (resized.height - height) // 2) - return resized.crop((left, top, left + width, top + height)) - - -def extract_warm_gold_frame(source: Image.Image, config: dict[str, Any]) -> Image.Image: - """复刻旧 Sharp 暖金边框阈值,同时清零全透明像素的隐藏 RGB。""" - image = source.convert("RGBA") - width, height = image.size - border_band = int(config["borderBand"]) - output = Image.new("RGBA", image.size, (0, 0, 0, 0)) - result: list[tuple[int, int, int, int]] = [] - - for index, (red, green, blue, source_alpha) in enumerate(image.get_flattened_data()): - x = index % width - y = index // width - distance_to_edge = min(x, y, width - 1 - x, height - 1 - y) - warm_gold = ( - red > green > blue - and red - green >= int(config["redGreenMin"]) - and green - blue >= int(config["greenBlueMin"]) - and red - blue >= int(config["redBlueMin"]) - and red < int(config["redMaxExclusive"]) - and blue < int(config["blueMaxExclusive"]) - ) - if distance_to_edge >= border_band or not warm_gold: - result.append((0, 0, 0, 0)) - continue - - edge_alpha = max( - 0, - min( - 255, - (red - blue - int(config["alphaOffset"])) * int(config["alphaScale"]), - ), - ) - alpha = min(source_alpha, edge_alpha) - result.append((red, green, blue, alpha) if alpha else (0, 0, 0, 0)) - - output.putdata(result) - return output - - -def save_png(image: Image.Image, path: Path) -> None: - """以固定压缩参数和标准 sRGB chunk 保存,保证同输入得到同字节。""" - path.parent.mkdir(parents=True, exist_ok=True) - png_info = PngImagePlugin.PngInfo() - png_info.add(b"sRGB", b"\x00") - image.save(path, format="PNG", optimize=True, compress_level=9, pnginfo=png_info) - - -def build_manifest(manifest_path: Path, workspace: Path) -> None: - """执行已经由 Node 严格校验的清单,并再次核对真实母版像素尺寸。""" - manifest = json.loads(manifest_path.read_text(encoding="utf-8")) - for asset in manifest["assets"]: - source_path = _resolve_inside(workspace, asset["source"]) - output_path = _resolve_inside(workspace, asset["output"]) - expected_source = asset["sourcePixels"] - output_pixels = asset["outputPixels"] - output_size = (int(output_pixels["width"]), int(output_pixels["height"])) - - with Image.open(source_path) as source: - expected_size = (int(expected_source["width"]), int(expected_source["height"])) - if source.size != expected_size: - raise ValueError( - f"{asset['id']} source expected {expected_size[0]}x{expected_size[1]}, " - f"got {source.width}x{source.height}" - ) - - mode = asset["processing"]["mode"] - if mode == "opaque-resize": - output = build_opaque_resize(source, output_size) - elif mode == "opaque-cover-crop": - output = build_opaque_cover_crop(source, output_size) - elif mode == "warm-gold-frame-extract": - output = extract_warm_gold_frame(source, asset["processing"]) - else: - raise ValueError(f"unsupported raster processing mode: {mode}") - - if output.size != output_size: - raise ValueError(f"{asset['id']} produced unexpected output size: {output.size}") - save_png(output, output_path) - print(f"BUILT {asset['id']} -> {asset['output']}") - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("manifest", type=Path) - parser.add_argument("--workspace", type=Path, required=True) - args = parser.parse_args() - build_manifest(args.manifest.resolve(), args.workspace.resolve()) - - -if __name__ == "__main__": - main() diff --git a/design-pipeline/scripts/build_scroll_skins.py b/design-pipeline/scripts/build_scroll_skins.py deleted file mode 100644 index c700dd9..0000000 --- a/design-pipeline/scripts/build_scroll_skins.py +++ /dev/null @@ -1,185 +0,0 @@ -"""从锁定的色键母版确定性生成项目共享卷轴资产。""" - -import argparse -import json -import math -from pathlib import Path - -from PIL import Image, PngImagePlugin - - -def remove_chroma_background( - image: Image.Image, - key: tuple[int, int, int], - tolerance: int, - feather: int = 72, -) -> Image.Image: - source = image.convert("RGBA") - cleaned = Image.new("RGBA", source.size, (0, 0, 0, 0)) - result = [] - key_red, key_green, key_blue = key - - for red, green, blue, source_alpha in source.get_flattened_data(): - distance = math.sqrt( - (red - key_red) ** 2 + (green - key_green) ** 2 + (blue - key_blue) ** 2 - ) - alpha_fraction = max(0.0, min(1.0, (distance - tolerance) / feather)) - alpha_fraction *= source_alpha / 255 - if alpha_fraction <= 0: - result.append((0, 0, 0, 0)) - continue - - recovered_red = round((red - (1 - alpha_fraction) * key_red) / alpha_fraction) - recovered_green = round((green - (1 - alpha_fraction) * key_green) / alpha_fraction) - recovered_blue = round((blue - (1 - alpha_fraction) * key_blue) / alpha_fraction) - result.append( - ( - max(0, min(255, recovered_red)), - max(0, min(255, recovered_green)), - max(0, min(255, recovered_blue)), - round(alpha_fraction * 255), - ) - ) - - cleaned.putdata(result) - return cleaned - - -def trim_transparent(image: Image.Image) -> Image.Image: - alpha = image.getchannel("A") - bounds = alpha.getbbox() - if bounds is None: - raise ValueError("chroma removal left no visible artwork") - return image.crop(bounds) - - -def stretch_safe_center( - image: Image.Image, - *, - output_size: tuple[int, int], - cap_width: int, - padding: int, -) -> Image.Image: - output_width, output_height = output_size - inner_width = output_width - padding * 2 - inner_height = output_height - padding * 2 - if inner_width <= 0 or inner_height <= 0: - raise ValueError("transparent padding leaves no drawable area") - if cap_width <= 0 or cap_width * 2 >= image.width: - raise ValueError(f"invalid capWidth {cap_width} for artwork width {image.width}") - - target_cap_width = max(1, round(cap_width * inner_height / image.height)) - center_width = inner_width - target_cap_width * 2 - if center_width <= 0: - raise ValueError("scaled caps leave no room for the stretch-safe center") - - left = image.crop((0, 0, cap_width, image.height)) - center = image.crop((cap_width, 0, image.width - cap_width, image.height)) - right = image.crop((image.width - cap_width, 0, image.width, image.height)) - resampling = Image.Resampling.LANCZOS - left = left.resize((target_cap_width, inner_height), resampling) - center = center.resize((center_width, inner_height), resampling) - right = right.resize((target_cap_width, inner_height), resampling) - - output = Image.new("RGBA", output_size, (0, 0, 0, 0)) - output.alpha_composite(left, (padding, padding)) - output.alpha_composite(center, (padding + target_cap_width, padding)) - output.alpha_composite(right, (output_width - padding - target_cap_width, padding)) - return output - - -def sanitize_output_edges(image: Image.Image) -> Image.Image: - cleaned = [] - for red, green, blue, alpha in image.convert("RGBA").get_flattened_data(): - chroma_residue = alpha > 0 and green > 120 and green - max(red, blue) > 80 - light_fringe = 0 < alpha < 255 and red > 235 and green > 235 and blue > 235 - if alpha == 0 or chroma_residue or light_fringe: - cleaned.append((0, 0, 0, 0)) - else: - cleaned.append((red, green, blue, alpha)) - output = Image.new("RGBA", image.size, (0, 0, 0, 0)) - output.putdata(cleaned) - return output - - -def reduce_png_palette(image: Image.Image, colors: int = 192) -> Image.Image: - alpha = image.getchannel("A") - rgb = image.convert("RGB").quantize( - colors=colors, - method=Image.Quantize.MEDIANCUT, - dither=Image.Dither.NONE, - ).convert("RGB") - output = rgb.convert("RGBA") - output.putalpha(alpha) - return sanitize_output_edges(output) - - -def parse_hex_color(value: str) -> tuple[int, int, int]: - if len(value) != 7 or not value.startswith("#"): - raise ValueError(f"invalid RGB hex color: {value}") - return tuple(int(value[index:index + 2], 16) for index in (1, 3, 5)) - - -def build_asset(asset: dict, workspace: Path) -> None: - processing = asset["processing"] - alpha = asset["alpha"] - pixels = asset["outputPixels"] - source_path = workspace / asset["source"] - output_path = workspace / asset["output"] - - if processing["mode"] != "chroma-stretch": - raise ValueError(f"unsupported scroll processing mode: {processing['mode']}") - - with Image.open(source_path) as source: - expected = asset["sourcePixels"] - if source.size != (expected["width"], expected["height"]): - raise ValueError( - f"{asset['id']} source expected {expected['width']}x{expected['height']}, " - f"got {source.width}x{source.height}" - ) - cleaned = remove_chroma_background( - source, - parse_hex_color(processing["keyColor"]), - int(processing["keyTolerance"]), - ) - artwork = trim_transparent(cleaned) - output = stretch_safe_center( - artwork, - output_size=(pixels["width"], pixels["height"]), - cap_width=int(processing["capWidth"]), - padding=int(alpha["transparentOuterPadding"]), - ) - palette_colors = int(processing.get("paletteColors", 192)) - output = sanitize_output_edges(output) - if processing.get("indexedPng"): - output = output.quantize( - colors=palette_colors, - method=Image.Quantize.FASTOCTREE, - dither=Image.Dither.NONE, - ) - else: - output = reduce_png_palette(output, colors=palette_colors) - - output_path.parent.mkdir(parents=True, exist_ok=True) - png_info = PngImagePlugin.PngInfo() - png_info.add(b"sRGB", b"\x00") - output.save(output_path, format="PNG", optimize=True, pnginfo=png_info) - print(f"BUILT {asset['id']} -> {asset['output']}") - - -def build_manifest(manifest_path: Path, workspace: Path) -> None: - manifest = json.loads(manifest_path.read_text(encoding="utf-8")) - for asset in manifest["assets"]: - build_asset(asset, workspace) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("manifest", type=Path) - parser.add_argument("--workspace", type=Path, required=True) - args = parser.parse_args() - build_manifest(args.manifest.resolve(), args.workspace.resolve()) - - -if __name__ == "__main__": - main() diff --git a/design-pipeline/scripts/python-runtime.mjs b/design-pipeline/scripts/python-runtime.mjs deleted file mode 100644 index daadfe1..0000000 --- a/design-pipeline/scripts/python-runtime.mjs +++ /dev/null @@ -1,74 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { spawnSync } from 'node:child_process' - -function canRunPython(executable) { - const result = spawnSync(executable, ['--version'], { - encoding: 'utf8', - stdio: 'ignore', - }) - return !result.error && result.status === 0 -} - -/** - * 解析设计构建管线唯一可用的 Python 入口。 - * - * 顺序必须保持稳定:显式配置 > 项目虚拟环境 > Windows Python Manager > - * 系统命令。这样既尊重调用方选择,也不会把 WindowsApps 的零字节执行别名 - * 误判为真实解释器。每个候选都必须实际执行 `--version`,文件存在本身不算可用。 - */ -export function resolvePythonExecutable({ - pipelineDirectory, - environment = process.env, - platform = process.platform, - pathExists = fs.existsSync, - canRun = canRunPython, -} = {}) { - if (!pipelineDirectory) throw new Error('解析 Python 入口时缺少 design-pipeline 目录') - - const configured = environment.PYTHON?.trim() - if (configured) { - if (canRun(configured)) return configured - throw new Error(`PYTHON 指定的解释器不可用:${configured}`) - } - - const pathApi = platform === 'win32' ? path.win32 : path.posix - const candidates = [] - const virtualEnvironment = platform === 'win32' - ? pathApi.join(pipelineDirectory, '.venv', 'Scripts', 'python.exe') - : pathApi.join(pipelineDirectory, '.venv', 'bin', 'python') - if (pathExists(virtualEnvironment)) candidates.push(virtualEnvironment) - - if (platform === 'win32' && environment.LOCALAPPDATA) { - const managerPython = pathApi.join(environment.LOCALAPPDATA, 'Python', 'bin', 'python.exe') - if (pathExists(managerPython)) candidates.push(managerPython) - } - - candidates.push(platform === 'win32' ? 'python' : 'python3', 'python') - for (const candidate of new Set(candidates)) { - if (canRun(candidate)) return candidate - } - - throw new Error( - '未找到可用的 Python。请设置 PYTHON 为真实解释器路径,或在 design-pipeline/.venv 中安装项目虚拟环境。', - ) -} - -/** - * 通过统一入口执行 Python,保证任何构建和测试都不会向源码目录写入 `.pyc`。 - * 调用方只提供业务参数;`-B`、进程错误和退出码转换由这里集中负责。 - */ -export function runPythonCommand({ executable, args, cwd, spawn = spawnSync } = {}) { - if (typeof executable !== 'string' || executable.trim() === '') throw new Error('执行 Python 时缺少解释器') - if (!Array.isArray(args)) throw new Error('执行 Python 时 args 必须为数组') - if (typeof cwd !== 'string' || cwd.trim() === '') throw new Error('执行 Python 时缺少工作目录') - - const result = spawn(executable, ['-B', ...args], { - cwd, - encoding: 'utf8', - stdio: 'inherit', - }) - if (result.error) throw new Error(`无法启动 Python(${executable}):${result.error.message}`) - if (result.status !== 0) throw new Error(`Python 命令执行失败,退出码:${result.status ?? 'unknown'}`) - return result -} diff --git a/design-pipeline/scripts/run-python-tests.mjs b/design-pipeline/scripts/run-python-tests.mjs deleted file mode 100644 index 47cebb6..0000000 --- a/design-pipeline/scripts/run-python-tests.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { resolvePythonExecutable, runPythonCommand } from './python-runtime.mjs' - -const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)) -const pipelineDirectory = path.resolve(scriptDirectory, '..') -const python = resolvePythonExecutable({ pipelineDirectory }) - -runPythonCommand({ - executable: python, - args: ['-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_*.py'], - cwd: pipelineDirectory, -}) diff --git a/design-pipeline/scripts/runtime-asset-inventory.mjs b/design-pipeline/scripts/runtime-asset-inventory.mjs index 0b1d1ce..3fe1cf4 100644 --- a/design-pipeline/scripts/runtime-asset-inventory.mjs +++ b/design-pipeline/scripts/runtime-asset-inventory.mjs @@ -1,9 +1,8 @@ +import { createHash } from 'node:crypto' import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' -import { validateAssetBuildManifest } from './asset-build-manifest.mjs' - -const formalManifestKinds = new Set(['asset-build-manifest', 'runtime-asset-inventory']) +const formalManifestKinds = new Set(['runtime-asset-inventory']) const assertOnlyFields = (value, fields, label) => { for (const field of Object.keys(value)) { if (!fields.has(field)) throw new Error(`${label} contains unknown field: ${field}`) @@ -88,25 +87,7 @@ export const expandRuntimeAssetInventory = async (rootManifestPath, workspace) = manifests.push(absolutePath) const manifest = await readManifest(absolutePath) - if (manifest.kind === 'asset-build-manifest') { - validateAssetBuildManifest(manifest, workspacePath) - for (const asset of manifest.assets) { - addAsset( - { - id: asset.id, - output: asset.output, - width: asset.outputPixels.width, - height: asset.outputPixels.height, - // 不透明构建模式依法不声明 alpha;只有透明模式存在该对象,避免把“字段缺失”误判为清单损坏。 - alpha: asset.alpha?.required ?? false, - maxBytes: asset.quality.maxBytes, - provenance: 'generated-from-manifest', - rebuildable: true, - }, - absolutePath, - ) - } - } else if (manifest.kind === 'runtime-asset-inventory') { + if (manifest.kind === 'runtime-asset-inventory') { assertOnlyFields(manifest, new Set(['schemaVersion', 'kind', 'scope', 'imports', 'assets']), 'runtime manifest') if (manifest.schemaVersion !== 1) throw new Error('runtime inventory schemaVersion must be 1') if (typeof manifest.scope !== 'string' || manifest.scope.trim() === '') throw new Error('runtime inventory scope is required') @@ -136,6 +117,23 @@ export const validateRuntimeAssetRegistry = async (rootManifestPath, workspace, const inventory = await expandRuntimeAssetInventory(rootManifestPath, workspacePath) const registered = new Set(inventory.manifests.map((manifest) => path.resolve(manifest))) + for (const asset of inventory.assets) { + let binary + try { + binary = await readFile(resolveInsideWorkspace(workspacePath, asset.output, `${asset.id}.output`)) + } catch (error) { + if (error.code === 'ENOENT') throw new Error(`missing runtime asset: ${asset.output}`) + throw error + } + if (binary.length !== asset.bytes) { + throw new Error(`${asset.id}.bytes does not match ${asset.output}`) + } + const digest = createHash('sha256').update(binary).digest('hex') + if (digest !== asset.sha256) { + throw new Error(`${asset.id}.sha256 does not match ${asset.output}`) + } + } + // 顶层注册表必须覆盖目录内每一份正式 owner。这样新增清单若没有接入全局图会立即失败, // output 与 id 的唯一性也就不再局限于某个业务域的 imports 闭包。 for (const entry of await readdir(directory, { withFileTypes: true })) { diff --git a/design-pipeline/scripts/validate-asset-build-manifest.mjs b/design-pipeline/scripts/validate-asset-build-manifest.mjs deleted file mode 100644 index cb97540..0000000 --- a/design-pipeline/scripts/validate-asset-build-manifest.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import { readFile } from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { validateAssetBuildManifest } from './asset-build-manifest.mjs' - -const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)) -const workspace = path.resolve(scriptDirectory, '..', '..') -const manifestArgument = process.argv[2] -if (!manifestArgument) throw new Error('Usage: node validate-asset-build-manifest.mjs ') - -const manifestPath = path.resolve(workspace, manifestArgument) -const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) -validateAssetBuildManifest(manifest, workspace) -process.stdout.write(`ASSET-BUILD-MANIFEST PASS ${path.relative(workspace, manifestPath).replaceAll('\\', '/')}\n`) diff --git a/design-pipeline/scripts/validate-runtime-asset-inventory.mjs b/design-pipeline/scripts/validate-runtime-asset-inventory.mjs index fe14dc2..47ab423 100644 --- a/design-pipeline/scripts/validate-runtime-asset-inventory.mjs +++ b/design-pipeline/scripts/validate-runtime-asset-inventory.mjs @@ -13,4 +13,4 @@ const inventory = await validateRuntimeAssetRegistry( workspace, path.join(workspace, 'design-pipeline', 'manifests'), ) -process.stdout.write(`${JSON.stringify(inventory)}\n`) +process.stdout.write(`RUNTIME ASSET CHECK PASS manifests=${inventory.manifests.length} assets=${inventory.assets.length}\n`) diff --git a/design-pipeline/scripts/verify-assets.py b/design-pipeline/scripts/verify-assets.py deleted file mode 100644 index b69348d..0000000 --- a/design-pipeline/scripts/verify-assets.py +++ /dev/null @@ -1,47 +0,0 @@ -"""校验 schema v3 生成型资产,并写出不含机器绝对路径的确定性报告。""" - -import argparse -import json -import sys -from pathlib import Path - -from asset_quality import analyze_asset - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("manifest", type=Path) - parser.add_argument("--workspace", type=Path, required=True) - parser.add_argument("--report", type=Path, required=True) - args = parser.parse_args() - - manifest = json.loads(args.manifest.read_text(encoding="utf-8")) - reports = [] - failed = False - for asset in manifest["assets"]: - output = args.workspace / asset["output"] - if not output.is_file(): - report = { - "id": asset["id"], - "path": Path(asset["output"]).as_posix(), - "errors": ["output file is missing"], - "warnings": [], - "metrics": {}, - } - else: - report = analyze_asset(output, asset, args.workspace) - reports.append(report) - if report["errors"]: - failed = True - print(f"ASSET-QUALITY FAIL {asset['id']}: {'; '.join(report['errors'])}") - else: - print(f"ASSET-QUALITY PASS {asset['id']}") - - args.report.parent.mkdir(parents=True, exist_ok=True) - args.report.write_text(json.dumps(reports, ensure_ascii=False, indent=2), encoding="utf-8") - if failed: - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/design-pipeline/scripts/verify-shared-scroll-skins.mjs b/design-pipeline/scripts/verify-shared-scroll-skins.mjs deleted file mode 100644 index 72c5fea..0000000 --- a/design-pipeline/scripts/verify-shared-scroll-skins.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { validateAssetBuildManifest } from './asset-build-manifest.mjs' -import { resolvePythonExecutable, runPythonCommand } from './python-runtime.mjs' - -const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)) -const pipelineDirectory = path.resolve(scriptDirectory, '..') -const workspace = path.resolve(pipelineDirectory, '..') -const manifestPath = path.join(pipelineDirectory, 'manifests', 'shared-scroll-skins-v3.json') -const reportPath = path.join(pipelineDirectory, 'generated', 'shared-scroll-skins-v3', 'quality-report.json') -const python = resolvePythonExecutable({ pipelineDirectory }) -const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) - -validateAssetBuildManifest(manifest, workspace) -runPythonCommand({ - executable: python, - args: [path.join(scriptDirectory, 'verify-assets.py'), manifestPath, '--workspace', workspace, '--report', reportPath], - cwd: workspace, -}) diff --git a/design-pipeline/tests/asset-build-manifest.test.mjs b/design-pipeline/tests/asset-build-manifest.test.mjs deleted file mode 100644 index b3646b8..0000000 --- a/design-pipeline/tests/asset-build-manifest.test.mjs +++ /dev/null @@ -1,131 +0,0 @@ -import test from 'node:test' -import assert from 'node:assert/strict' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { validateAssetBuildManifest } from '../scripts/asset-build-manifest.mjs' - -const testDirectory = path.dirname(fileURLToPath(import.meta.url)) -const workspace = path.resolve(testDirectory, '..', '..') - -const baseAsset = () => ({ - id: 'test-asset', - source: 'docs/design/assets/source.png', - output: 'static/assets/output.png', - sourcePixels: { width: 1536, height: 3840 }, - outputPixels: { width: 1440, height: 3600 }, - quality: { maxBytes: 7000000, colorSpace: 'sRGB' }, -}) - -// schema v3 以 processing.mode 为严格判别字段。每个分支只允许自身真正消费的 -// 参数,避免给不透明背景伪造透明边、色键或切片字段来“凑齐”旧结构。 -const chromaAsset = () => ({ - ...baseAsset(), - alpha: { required: true, transparentOuterPadding: 6, cornerMaxAlpha: 0 }, - edge: { forbidChromaResidue: true, forbidLightFringe: true, premultipliedAlphaCheck: true }, - processing: { - mode: 'chroma-stretch', - capWidth: 380, - keyColor: '#00FF00', - keyTolerance: 96, - }, -}) - -const opaqueResizeAsset = () => ({ - ...baseAsset(), - processing: { mode: 'opaque-resize', resample: 'lanczos', outputMode: 'RGB' }, -}) - -const opaqueCoverAsset = () => ({ - ...baseAsset(), - processing: { - mode: 'opaque-cover-crop', - resample: 'lanczos', - anchor: 'center', - outputMode: 'RGB', - }, -}) - -const warmFrameAsset = () => ({ - ...baseAsset(), - alpha: { required: true, transparentOuterPadding: 0, cornerMaxAlpha: 0 }, - edge: { forbidChromaResidue: false, forbidLightFringe: false, premultipliedAlphaCheck: true }, - processing: { - mode: 'warm-gold-frame-extract', - borderBand: 110, - redGreenMin: 15, - greenBlueMin: 12, - redBlueMin: 35, - redMaxExclusive: 245, - blueMaxExclusive: 180, - alphaOffset: 25, - alphaScale: 6, - outputMode: 'RGBA', - }, -}) - -const manifestWith = (asset) => ({ - schemaVersion: 3, - kind: 'asset-build-manifest', - family: 'test-family-v3', - assets: [asset], -}) - -test('接受四种职责严格分离的 schema v3 处理模式', () => { - for (const factory of [chromaAsset, opaqueResizeAsset, opaqueCoverAsset, warmFrameAsset]) { - const manifest = manifestWith(factory()) - assert.equal(validateAssetBuildManifest(manifest, workspace), manifest) - } -}) - -test('拒绝没有 processing.mode 的旧 schema v3 形状', () => { - const asset = chromaAsset() - delete asset.processing.mode - assert.throws(() => validateAssetBuildManifest(manifestWith(asset), workspace), /processing\.mode/i) -}) - -test('不透明分支拒绝透明度、边缘和其他模式的参数', () => { - const withAlpha = opaqueResizeAsset() - withAlpha.alpha = chromaAsset().alpha - assert.throws(() => validateAssetBuildManifest(manifestWith(withAlpha), workspace), /unknown field/i) - - const withAnchor = opaqueResizeAsset() - withAnchor.processing.anchor = 'center' - assert.throws(() => validateAssetBuildManifest(manifestWith(withAnchor), workspace), /unknown field/i) - - const withColorKey = opaqueCoverAsset() - withColorKey.processing.keyColor = '#00FF00' - assert.throws(() => validateAssetBuildManifest(manifestWith(withColorKey), workspace), /unknown field/i) -}) - -test('透明分支拒绝缺失 alpha/edge 及不属于自身的处理字段', () => { - const missingAlpha = chromaAsset() - delete missingAlpha.alpha - assert.throws(() => validateAssetBuildManifest(manifestWith(missingAlpha), workspace), /alpha/i) - - const missingEdge = warmFrameAsset() - delete missingEdge.edge - assert.throws(() => validateAssetBuildManifest(manifestWith(missingEdge), workspace), /edge/i) - - const wrongProcessing = warmFrameAsset() - wrongProcessing.processing.capWidth = 380 - assert.throws(() => validateAssetBuildManifest(manifestWith(wrongProcessing), workspace), /unknown field/i) -}) - -test('拒绝重复输出、越界路径、未知字段和未知模式', () => { - const duplicate = manifestWith(chromaAsset()) - duplicate.assets.push({ ...chromaAsset(), id: 'duplicate-output' }) - assert.throws(() => validateAssetBuildManifest(duplicate, workspace), /duplicate output/i) - - const escaped = manifestWith(opaqueResizeAsset()) - escaped.assets[0].output = '../outside.png' - assert.throws(() => validateAssetBuildManifest(escaped, workspace), /escapes workspace/i) - - const unknownField = manifestWith(opaqueResizeAsset()) - unknownField.assets[0].logicalSlot = 'page' - assert.throws(() => validateAssetBuildManifest(unknownField, workspace), /unknown field/i) - - const unknownMode = manifestWith(opaqueResizeAsset()) - unknownMode.assets[0].processing.mode = 'future-magic' - assert.throws(() => validateAssetBuildManifest(unknownMode, workspace), /unsupported processing\.mode/i) -}) diff --git a/design-pipeline/tests/python-runtime.test.mjs b/design-pipeline/tests/python-runtime.test.mjs deleted file mode 100644 index ed2ef33..0000000 --- a/design-pipeline/tests/python-runtime.test.mjs +++ /dev/null @@ -1,68 +0,0 @@ -import assert from 'node:assert/strict' -import path from 'node:path' -import test from 'node:test' - -import { resolvePythonExecutable, runPythonCommand } from '../scripts/python-runtime.mjs' - -test('显式 PYTHON 配置优先于所有自动发现路径', () => { - const selected = resolvePythonExecutable({ - pipelineDirectory: 'C:\\repo\\design-pipeline', - environment: { - PYTHON: 'D:\\tools\\python.exe', - LOCALAPPDATA: 'C:\\Users\\tester\\AppData\\Local', - }, - platform: 'win32', - pathExists: () => true, - canRun: candidate => candidate === 'D:\\tools\\python.exe', - }) - - assert.equal(selected, 'D:\\tools\\python.exe') -}) - -test('Windows 环境在虚拟环境缺失时选择 Python Manager 的真实入口', () => { - const managerPython = path.win32.join( - 'C:\\Users\\tester\\AppData\\Local', - 'Python', - 'bin', - 'python.exe', - ) - const selected = resolvePythonExecutable({ - pipelineDirectory: 'C:\\repo\\design-pipeline', - environment: { LOCALAPPDATA: 'C:\\Users\\tester\\AppData\\Local' }, - platform: 'win32', - pathExists: candidate => candidate === managerPython, - canRun: candidate => candidate === managerPython, - }) - - assert.equal(selected, managerPython) -}) - -test('所有候选解释器均不可用时给出可执行的中文修复指引', () => { - assert.throws( - () => resolvePythonExecutable({ - pipelineDirectory: 'C:\\repo\\design-pipeline', - environment: {}, - platform: 'win32', - pathExists: () => false, - canRun: () => false, - }), - /未找到可用的 Python.*PYTHON.*\.venv/s, - ) -}) - -test('统一执行器始终把禁止字节码缓存参数放在首位', () => { - let invocation - runPythonCommand({ - executable: 'python-test', - args: ['script.py', '--flag'], - cwd: 'C:\\repo', - spawn: (command, args, options) => { - invocation = { command, args, options } - return { status: 0 } - }, - }) - - assert.equal(invocation.command, 'python-test') - assert.deepEqual(invocation.args, ['-B', 'script.py', '--flag']) - assert.equal(invocation.options.cwd, 'C:\\repo') -}) diff --git a/design-pipeline/tests/raster-build-manifests.test.mjs b/design-pipeline/tests/raster-build-manifests.test.mjs deleted file mode 100644 index 659f31d..0000000 --- a/design-pipeline/tests/raster-build-manifests.test.mjs +++ /dev/null @@ -1,65 +0,0 @@ -import test from 'node:test' -import assert from 'node:assert/strict' -import { readFile } from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { validateAssetBuildManifest } from '../scripts/asset-build-manifest.mjs' - -const testDirectory = path.dirname(fileURLToPath(import.meta.url)) -const pipelineDirectory = path.resolve(testDirectory, '..') -const workspace = path.resolve(pipelineDirectory, '..') - -const readManifest = async (name) => JSON.parse(await readFile( - path.join(pipelineDirectory, 'manifests', name), - 'utf8', -)) - -test('长页面背景只由一份清单拥有六张正式输出', async () => { - const manifest = await readManifest('page-backgrounds-v3.json') - validateAssetBuildManifest(manifest, workspace) - - assert.equal(manifest.family, 'page-backgrounds-v3') - assert.deepEqual( - manifest.assets.map(({ id }) => id), - [ - 'genealogy-page-background-long', - 'tree-page-background-long', - 'family-page-background-long', - 'records-page-background-long', - 'notification-page-background-long', - 'profile-page-background-long', - ], - ) - assert.equal(manifest.assets[0].processing.mode, 'opaque-resize') - for (const asset of manifest.assets.slice(1)) { - assert.equal(asset.processing.mode, 'opaque-cover-crop') - } -}) - -test('G01 空态边框拥有独立的暖金边框提取清单', async () => { - const manifest = await readManifest('g01-state-frame-v3.json') - validateAssetBuildManifest(manifest, workspace) - - assert.equal(manifest.family, 'g01-state-frame-v3') - assert.equal(manifest.assets.length, 1) - assert.equal(manifest.assets[0].id, 'g01-empty-panel-frame') - assert.equal(manifest.assets[0].processing.mode, 'warm-gold-frame-extract') - assert.equal(manifest.assets[0].processing.borderBand, 110) -}) - -test('新构建入口不再保留候选构建和 Sharp 专用命令', async () => { - const packageJson = JSON.parse(await readFile(path.join(pipelineDirectory, 'package.json'), 'utf8')) - assert.equal( - packageJson.scripts['build:page-backgrounds'], - 'node scripts/build-raster-assets.mjs design-pipeline/manifests/page-backgrounds-v3.json', - ) - assert.equal( - packageJson.scripts['build:g01-state-frame'], - 'node scripts/build-raster-assets.mjs design-pipeline/manifests/g01-state-frame-v3.json', - ) - assert.equal(packageJson.scripts['build:g01-background-candidates'], undefined) - assert.equal(packageJson.scripts['build:module-page-backgrounds'], undefined) - assert.equal(packageJson.scripts['build:g01-empty-frame'], undefined) - assert.equal(packageJson.dependencies?.sharp, undefined) -}) diff --git a/design-pipeline/tests/runtime-asset-registry.test.mjs b/design-pipeline/tests/runtime-asset-registry.test.mjs index 6c46586..3f16996 100644 --- a/design-pipeline/tests/runtime-asset-registry.test.mjs +++ b/design-pipeline/tests/runtime-asset-registry.test.mjs @@ -40,19 +40,21 @@ test('顶层注册表拒绝未进入导入闭包的正式 owner', async (t) => { ) }) -test('真实 schema v3 注册表覆盖直接资产与三类正式生成 owner', async () => { +test('真实 schema v3 注册表覆盖应用资产与保留生成资产', async () => { const registry = JSON.parse(await readFile(path.join(realManifestsDirectory, 'runtime-assets.json'), 'utf8')) const auth = JSON.parse(await readFile(path.join(realManifestsDirectory, 'auth-runtime-assets.json'), 'utf8')) + const retained = JSON.parse(await readFile(path.join(realManifestsDirectory, 'retained-generated-runtime-assets.json'), 'utf8')) assert.equal(registry.scope, 'schema-v3') assert.deepEqual(registry.imports, [ 'design-pipeline/manifests/auth-runtime-assets.json', 'design-pipeline/manifests/application-runtime-assets.json', - 'design-pipeline/manifests/shared-scroll-skins-v3.json', - 'design-pipeline/manifests/page-backgrounds-v3.json', - 'design-pipeline/manifests/g01-state-frame-v3.json', + 'design-pipeline/manifests/retained-generated-runtime-assets.json', ]) assert.deepEqual(auth.imports, []) + assert.equal(retained.assets.length, 11) + assert(retained.assets.every((asset) => asset.provenance === 'committed-binary')) + assert(retained.assets.every((asset) => asset.rebuildable === false)) await validateRuntimeAssetRegistry( path.join(realManifestsDirectory, 'runtime-assets.json'), workspaceDirectory, diff --git a/design-pipeline/tests/shared-scroll-skins-v3.test.mjs b/design-pipeline/tests/shared-scroll-skins-v3.test.mjs deleted file mode 100644 index d7caf98..0000000 --- a/design-pipeline/tests/shared-scroll-skins-v3.test.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import test from 'node:test' -import assert from 'node:assert/strict' -import { readFile } from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { validateAssetBuildManifest } from '../scripts/asset-build-manifest.mjs' - -const testDirectory = path.dirname(fileURLToPath(import.meta.url)) -const pipelineDirectory = path.resolve(testDirectory, '..') -const workspace = path.resolve(pipelineDirectory, '..') -const manifestPath = path.join(pipelineDirectory, 'manifests', 'shared-scroll-skins-v3.json') - -test('共享卷轴清单只维护四张正式生成资产', async () => { - const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) - validateAssetBuildManifest(manifest, workspace) - - assert.equal(manifest.kind, 'asset-build-manifest') - assert.equal(manifest.family, 'shared-scroll-skins-v3') - assert.deepEqual( - manifest.assets.map(({ id }) => id), - ['shared-scroll-primary-v3', 'shared-scroll-secondary-v3', 'shared-scroll-toast-v3', 'shared-scroll-dialog-v3'], - ) - for (const asset of manifest.assets) { - assert.deepEqual( - Object.keys(asset).sort(), - ['alpha', 'edge', 'id', 'output', 'outputPixels', 'processing', 'quality', 'source', 'sourcePixels'], - ) - assert.equal(asset.processing.mode, 'chroma-stretch') - } -}) diff --git a/design-pipeline/tests/test_asset_quality.py b/design-pipeline/tests/test_asset_quality.py deleted file mode 100644 index 9b2b598..0000000 --- a/design-pipeline/tests/test_asset_quality.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys -import tempfile -import unittest -from pathlib import Path - -from PIL import Image, PngImagePlugin - -SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" -sys.path.insert(0, str(SCRIPTS)) - -from asset_quality import analyze_asset - - -class AssetQualityTests(unittest.TestCase): - def setUp(self): - self.directory = tempfile.TemporaryDirectory() - self.addCleanup(self.directory.cleanup) - self.root = Path(self.directory.name) - - def spec(self): - return { - "id": "test-button", - "outputPixels": {"width": 32, "height": 16}, - "alpha": {"required": True, "transparentOuterPadding": 2, "cornerMaxAlpha": 0}, - "edge": { - "forbidChromaResidue": True, - "forbidLightFringe": True, - "premultipliedAlphaCheck": True, - }, - "quality": {"maxBytes": 10000, "colorSpace": "sRGB"}, - } - - def clean_image(self): - image = Image.new("RGBA", (32, 16), (0, 0, 0, 0)) - for y in range(2, 14): - for x in range(2, 30): - image.putpixel((x, y), (150, 20, 12, 255)) - return image - - def save(self, image, name="asset.png", include_srgb=True): - path = self.root / name - png_info = PngImagePlugin.PngInfo() - if include_srgb: - png_info.add(b"sRGB", b"\x00") - image.save(path, format="PNG", pnginfo=png_info) - return path - - def error_text(self, report): - return " ".join(report["errors"]) - - def test_accepts_clean_rgba_asset(self): - report = analyze_asset(self.save(self.clean_image()), self.spec(), self.root) - self.assertEqual([], report["errors"]) - self.assertEqual(32, report["width"]) - self.assertEqual(16, report["height"]) - self.assertEqual("asset.png", report["path"]) - - def test_accepts_indexed_png_with_real_transparency(self): - indexed = self.clean_image().quantize( - colors=16, - method=Image.Quantize.FASTOCTREE, - dither=Image.Dither.NONE, - ) - report = analyze_asset(self.save(indexed), self.spec(), self.root) - self.assertEqual([], report["errors"]) - self.assertEqual("P", report["mode"]) - - def test_rejects_wrong_dimensions(self): - report = analyze_asset(self.save(Image.new("RGBA", (31, 16), (0, 0, 0, 0))), self.spec(), self.root) - self.assertIn("dimensions", self.error_text(report)) - - def test_rejects_opaque_outer_padding(self): - image = self.clean_image() - image.putpixel((0, 0), (120, 30, 20, 255)) - report = analyze_asset(self.save(image), self.spec(), self.root) - self.assertIn("outer padding", self.error_text(report)) - - def test_rejects_visible_green_residue(self): - image = self.clean_image() - image.putpixel((16, 8), (0, 255, 0, 255)) - report = analyze_asset(self.save(image), self.spec(), self.root) - self.assertIn("chroma residue", self.error_text(report)) - - def test_rejects_light_partially_transparent_fringe(self): - image = self.clean_image() - image.putpixel((2, 8), (250, 250, 250, 128)) - report = analyze_asset(self.save(image), self.spec(), self.root) - self.assertIn("light fringe", self.error_text(report)) - - def test_rejects_hidden_rgb_in_fully_transparent_pixels(self): - image = self.clean_image() - image.putpixel((0, 0), (255, 255, 255, 0)) - report = analyze_asset(self.save(image), self.spec(), self.root) - self.assertIn("transparent RGB", self.error_text(report)) - - def test_rejects_file_over_max_bytes(self): - spec = self.spec() - spec["quality"]["maxBytes"] = 8 - report = analyze_asset(self.save(self.clean_image()), spec, self.root) - self.assertIn("maxBytes", self.error_text(report)) - - def test_rejects_missing_declared_srgb_metadata(self): - path = self.save(self.clean_image(), include_srgb=False) - report = analyze_asset(path, self.spec(), self.root) - self.assertIn("sRGB", self.error_text(report)) - - -if __name__ == "__main__": - unittest.main() diff --git a/design-pipeline/tests/test_build_raster_assets.py b/design-pipeline/tests/test_build_raster_assets.py deleted file mode 100644 index 84fe53d..0000000 --- a/design-pipeline/tests/test_build_raster_assets.py +++ /dev/null @@ -1,85 +0,0 @@ -import hashlib -import sys -import tempfile -import unittest -from pathlib import Path - -from PIL import Image - -SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" -sys.path.insert(0, str(SCRIPTS)) - -from build_raster_assets import ( - build_opaque_cover_crop, - build_opaque_resize, - extract_warm_gold_frame, - save_png, -) - - -class RasterAssetBuilderTests(unittest.TestCase): - """锁定迁移后的两类背景处理与 G01 暖金边框提取语义。""" - - def test_opaque_resize_uses_locked_output_size_and_rgb(self): - source = Image.new("RGBA", (2, 3), (120, 80, 40, 128)) - output = build_opaque_resize(source, (4, 6)) - self.assertEqual((4, 6), output.size) - self.assertEqual("RGB", output.mode) - - def test_cover_crop_centers_the_scaled_source(self): - source = Image.new("RGB", (2, 1), (255, 0, 0)) - source.putpixel((1, 0), (0, 0, 255)) - output = build_opaque_cover_crop(source, (2, 2)) - self.assertEqual((2, 2), output.size) - self.assertEqual("RGB", output.mode) - self.assertNotEqual(output.getpixel((0, 0)), output.getpixel((1, 0))) - - def test_warm_gold_frame_keeps_only_the_edge_band(self): - source = Image.new("RGBA", (5, 5), (180, 140, 80, 255)) - config = { - "borderBand": 1, - "redGreenMin": 15, - "greenBlueMin": 12, - "redBlueMin": 35, - "redMaxExclusive": 245, - "blueMaxExclusive": 180, - "alphaOffset": 25, - "alphaScale": 6, - } - output = extract_warm_gold_frame(source, config) - self.assertGreater(output.getpixel((0, 2))[3], 0) - self.assertEqual((0, 0, 0, 0), output.getpixel((2, 2))) - - def test_warm_gold_frame_rejects_non_gold_and_clears_hidden_rgb(self): - source = Image.new("RGBA", (1, 1), (100, 150, 100, 255)) - config = { - "borderBand": 1, - "redGreenMin": 15, - "greenBlueMin": 12, - "redBlueMin": 35, - "redMaxExclusive": 245, - "blueMaxExclusive": 180, - "alphaOffset": 25, - "alphaScale": 6, - } - output = extract_warm_gold_frame(source, config) - self.assertEqual((0, 0, 0, 0), output.getpixel((0, 0))) - - def test_png_save_is_srgb_and_byte_deterministic(self): - with tempfile.TemporaryDirectory() as directory: - first = Path(directory) / "first.png" - second = Path(directory) / "second.png" - image = Image.new("RGB", (4, 4), (230, 220, 200)) - save_png(image, first) - save_png(image, second) - self.assertEqual(first.read_bytes(), second.read_bytes()) - self.assertEqual( - hashlib.sha256(first.read_bytes()).hexdigest(), - hashlib.sha256(second.read_bytes()).hexdigest(), - ) - with Image.open(first) as opened: - self.assertIn("srgb", opened.info) - - -if __name__ == "__main__": - unittest.main() diff --git a/design-pipeline/tests/test_build_scroll_skins.py b/design-pipeline/tests/test_build_scroll_skins.py deleted file mode 100644 index 88a7892..0000000 --- a/design-pipeline/tests/test_build_scroll_skins.py +++ /dev/null @@ -1,115 +0,0 @@ -import hashlib -import json -import sys -import tempfile -import unittest -from pathlib import Path - -from PIL import Image - -SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" -sys.path.insert(0, str(SCRIPTS)) - -from asset_quality import analyze_asset -from build_scroll_skins import ( - build_asset, - remove_chroma_background, - sanitize_output_edges, - stretch_safe_center, -) - - -class BuildScrollSkinsTests(unittest.TestCase): - def setUp(self): - self.directory = tempfile.TemporaryDirectory() - self.addCleanup(self.directory.cleanup) - self.root = Path(self.directory.name) - - def test_chroma_background_becomes_transparent_black(self): - image = Image.new("RGB", (4, 2), (0, 255, 0)) - image.putpixel((1, 0), (180, 30, 20)) - cleaned = remove_chroma_background(image, (0, 255, 0), tolerance=80) - self.assertEqual((0, 0, 0, 0), cleaned.getpixel((0, 0))) - self.assertEqual((180, 30, 20, 255), cleaned.getpixel((1, 0))) - - def test_safe_center_stretch_preserves_caps_and_exact_size(self): - image = Image.new("RGBA", (30, 10), (150, 20, 10, 255)) - for y in range(10): - for x in range(5): - image.putpixel((x, y), (220, 170, 40, 255)) - image.putpixel((29 - x, y), (220, 170, 40, 255)) - - output = stretch_safe_center(image, output_size=(60, 20), cap_width=5, padding=2) - self.assertEqual((60, 20), output.size) - self.assertEqual((0, 0, 0, 0), output.getpixel((0, 0))) - self.assertEqual((220, 170, 40, 255), output.getpixel((3, 10))) - self.assertEqual((150, 20, 10, 255), output.getpixel((30, 10))) - self.assertEqual((220, 170, 40, 255), output.getpixel((56, 10))) - - def test_sanitizes_only_forbidden_edge_artifacts(self): - image = Image.new("RGBA", (3, 1), (248, 240, 220, 255)) - image.putpixel((0, 0), (0, 255, 0, 128)) - image.putpixel((1, 0), (250, 250, 250, 96)) - cleaned = sanitize_output_edges(image) - self.assertEqual((0, 0, 0, 0), cleaned.getpixel((0, 0))) - self.assertEqual((0, 0, 0, 0), cleaned.getpixel((1, 0))) - self.assertEqual((248, 240, 220, 255), cleaned.getpixel((2, 0))) - - def asset(self, asset_id): - """返回与现行判别式 schema 一致的最小透明卷轴测试资产。""" - return { - "id": asset_id, - "source": "source.png", - "output": "output.png", - "sourcePixels": {"width": 32, "height": 16}, - "outputPixels": {"width": 60, "height": 30}, - "alpha": {"required": True, "transparentOuterPadding": 1, "cornerMaxAlpha": 0}, - "edge": { - "forbidChromaResidue": True, - "forbidLightFringe": True, - "premultipliedAlphaCheck": True, - }, - "quality": {"maxBytes": 10000, "colorSpace": "sRGB"}, - "processing": { - "mode": "chroma-stretch", - "capWidth": 4, - "keyColor": "#00FF00", - "keyTolerance": 80, - "paletteColors": 8, - }, - } - - def save_test_source(self): - source = Image.new("RGB", (32, 16), (0, 255, 0)) - for y in range(1, 15): - for x in range(1, 31): - source.putpixel((x, y), (120 + (x % 30) * 4, 20 + y, 10)) - source.save(self.root / "source.png") - - def test_build_asset_honors_declared_palette_size(self): - self.save_test_source() - build_asset(self.asset("palette-test"), self.root) - with Image.open(self.root / "output.png") as output: - visible_colors = { - pixel[:3] for pixel in output.convert("RGBA").get_flattened_data() if pixel[3] > 0 - } - self.assertLessEqual(len(visible_colors), 8) - - def test_same_input_produces_identical_bytes_and_report_twice(self): - self.save_test_source() - asset = self.asset("deterministic-test") - snapshots = [] - for _ in range(2): - build_asset(asset, self.root) - output = self.root / "output.png" - snapshots.append( - ( - hashlib.sha256(output.read_bytes()).hexdigest(), - json.dumps(analyze_asset(output, asset, self.root), ensure_ascii=False, sort_keys=True), - ) - ) - self.assertEqual(snapshots[0], snapshots[1]) - - -if __name__ == "__main__": - unittest.main()