@lark-apaas/coding-steering 0.1.32-dev.5abff3b → 0.1.32-dev.7b2c2ad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +13 -6
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +8 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +121 -30
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +229 -10
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +19 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +4 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +113 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +308 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +13 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +301 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +600 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +421 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
|
@@ -15,6 +15,7 @@ from draft import (asset_vision_contexts, bound_visual_candidates, # noqa: E402
|
|
|
15
15
|
cover_background_media,
|
|
16
16
|
emit_asset_vision_groups, emit_layouts, emit_manifest,
|
|
17
17
|
fullscreen_overlay_media,
|
|
18
|
+
layouts_from_template,
|
|
18
19
|
needs_asset_judgment, preserve_image_bearing_groups,
|
|
19
20
|
select_asset_vision_groups, slide_image_marks, slot_style,
|
|
20
21
|
visual_slot_candidates)
|
|
@@ -24,6 +25,125 @@ from package import (FONTSIZE_RE, Fail, apply_asset_decisions, # noqa: E402
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class LayoutCssTest(unittest.TestCase):
|
|
28
|
+
def test_template_layouts_use_sampled_theme_as_default_and_keep_theme_scope(self):
|
|
29
|
+
dark_master = 'ppt/slideMasters/slideMaster1.xml'
|
|
30
|
+
light_master = 'ppt/slideMasters/slideMaster2.xml'
|
|
31
|
+
dark_layout = 'ppt/slideLayouts/slideLayout1.xml'
|
|
32
|
+
light_layout = 'ppt/slideLayouts/slideLayout2.xml'
|
|
33
|
+
data = {
|
|
34
|
+
'layouts': [
|
|
35
|
+
{'part': dark_layout, 'name': 'Dark content', 'used_by_slides': 2},
|
|
36
|
+
{'part': light_layout, 'name': 'Light content', 'used_by_slides': 0},
|
|
37
|
+
],
|
|
38
|
+
'slides': [],
|
|
39
|
+
'theme_topology': {
|
|
40
|
+
'themes': ['light', 'dark'],
|
|
41
|
+
'default': 'light',
|
|
42
|
+
'per_master': [
|
|
43
|
+
{'master': dark_master, 'theme_label': 'dark'},
|
|
44
|
+
{'master': light_master, 'theme_label': 'light'},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
'reference_graph': {
|
|
48
|
+
'master_of_layout': {
|
|
49
|
+
dark_layout: dark_master,
|
|
50
|
+
light_layout: light_master,
|
|
51
|
+
},
|
|
52
|
+
'layout_of_slide': {
|
|
53
|
+
'ppt/slides/slide1.xml': dark_layout,
|
|
54
|
+
'ppt/slides/slide2.xml': dark_layout,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
'background_composites': {},
|
|
58
|
+
}
|
|
59
|
+
shapes = [
|
|
60
|
+
{
|
|
61
|
+
'part': part,
|
|
62
|
+
'layer': 'layout',
|
|
63
|
+
'kind': 'sp',
|
|
64
|
+
'box': {'x': 100, 'y': 100, 'w': 800, 'h': 100},
|
|
65
|
+
'ph': {'type': 'title', 'idx': '1'},
|
|
66
|
+
'text': {'paragraphs': [{'runs': [{'text': 'Title', 'sz_px': 40}]}]},
|
|
67
|
+
}
|
|
68
|
+
for part in (dark_layout, light_layout)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
archetypes = layouts_from_template(data, shapes, 1920, 1080)
|
|
72
|
+
|
|
73
|
+
self.assertEqual('dark', data['theme_topology']['default'])
|
|
74
|
+
self.assertEqual(
|
|
75
|
+
{'content': 'dark', 'content-2': 'light'},
|
|
76
|
+
{item['name']: item['theme'] for item in archetypes},
|
|
77
|
+
)
|
|
78
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
79
|
+
emit_layouts(archetypes, output_dir)
|
|
80
|
+
emit_manifest(data, [], [], output_dir, archetypes)
|
|
81
|
+
with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
|
|
82
|
+
layouts = stream.read()
|
|
83
|
+
with open(os.path.join(output_dir, 'manifest.yaml'), encoding='utf-8') as stream:
|
|
84
|
+
manifest = stream.read()
|
|
85
|
+
self.assertIn(' themes: [dark]', layouts)
|
|
86
|
+
self.assertIn(' themes: [light]', layouts)
|
|
87
|
+
self.assertIn('default-theme: dark', manifest)
|
|
88
|
+
|
|
89
|
+
def test_template_twin_layouts_keep_topology_but_scope_retained_layout(self):
|
|
90
|
+
dark_master = 'ppt/slideMasters/slideMaster1.xml'
|
|
91
|
+
light_master = 'ppt/slideMasters/slideMaster2.xml'
|
|
92
|
+
dark_layout = 'ppt/slideLayouts/slideLayout1.xml'
|
|
93
|
+
light_layout = 'ppt/slideLayouts/slideLayout2.xml'
|
|
94
|
+
data = {
|
|
95
|
+
'layouts': [
|
|
96
|
+
{'part': dark_layout, 'name': 'Content', 'used_by_slides': 2},
|
|
97
|
+
{'part': light_layout, 'name': 'Content', 'used_by_slides': 0},
|
|
98
|
+
],
|
|
99
|
+
'slides': [],
|
|
100
|
+
'theme_topology': {
|
|
101
|
+
'themes': ['light', 'dark'],
|
|
102
|
+
'default': 'light',
|
|
103
|
+
'per_master': [
|
|
104
|
+
{'master': dark_master, 'theme_label': 'dark'},
|
|
105
|
+
{'master': light_master, 'theme_label': 'light'},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
'reference_graph': {
|
|
109
|
+
'master_of_layout': {
|
|
110
|
+
dark_layout: dark_master,
|
|
111
|
+
light_layout: light_master,
|
|
112
|
+
},
|
|
113
|
+
'layout_of_slide': {
|
|
114
|
+
'ppt/slides/slide1.xml': dark_layout,
|
|
115
|
+
'ppt/slides/slide2.xml': dark_layout,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
'background_composites': {},
|
|
119
|
+
}
|
|
120
|
+
shapes = [
|
|
121
|
+
{
|
|
122
|
+
'part': part,
|
|
123
|
+
'layer': 'layout',
|
|
124
|
+
'kind': 'sp',
|
|
125
|
+
'box': {'x': 100, 'y': 100, 'w': 800, 'h': 100},
|
|
126
|
+
'ph': {'type': 'title', 'idx': '1'},
|
|
127
|
+
'text': {'paragraphs': [{'runs': [{'text': 'Title', 'sz_px': 40}]}]},
|
|
128
|
+
}
|
|
129
|
+
for part in (dark_layout, light_layout)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
archetypes = layouts_from_template(data, shapes, 1920, 1080)
|
|
133
|
+
|
|
134
|
+
self.assertEqual(1, len(archetypes))
|
|
135
|
+
self.assertEqual('dark', archetypes[0]['theme'])
|
|
136
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
137
|
+
emit_layouts(archetypes, output_dir)
|
|
138
|
+
emit_manifest(data, [], [], output_dir, archetypes)
|
|
139
|
+
with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
|
|
140
|
+
layouts = stream.read()
|
|
141
|
+
with open(os.path.join(output_dir, 'manifest.yaml'), encoding='utf-8') as stream:
|
|
142
|
+
manifest = stream.read()
|
|
143
|
+
self.assertIn(' themes: [dark]', layouts)
|
|
144
|
+
self.assertIn('themes: [light, dark]', manifest)
|
|
145
|
+
self.assertIn('default-theme: dark', manifest)
|
|
146
|
+
|
|
27
147
|
def test_text_slot_emits_rendering_style_as_css_only(self):
|
|
28
148
|
shape = {
|
|
29
149
|
'text': {
|
|
@@ -1087,6 +1207,183 @@ layouts:
|
|
|
1087
1207
|
self.assertIn('avoid: TODO禁放区列表', background_block)
|
|
1088
1208
|
self.assertIn('pairing_rule: "TODO', background_block)
|
|
1089
1209
|
|
|
1210
|
+
def test_background_controls_only_edit_existing_image_backgrounds(self):
|
|
1211
|
+
archetype = {
|
|
1212
|
+
'name': 'content',
|
|
1213
|
+
'zh': '内容页',
|
|
1214
|
+
'role': 'content',
|
|
1215
|
+
'pages': [1],
|
|
1216
|
+
'rep': 1,
|
|
1217
|
+
'bg': 'bg-content',
|
|
1218
|
+
'slots': [],
|
|
1219
|
+
'decor': [],
|
|
1220
|
+
'pic_n': 0,
|
|
1221
|
+
'confidence': 'high',
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
1225
|
+
emit_layouts([archetype], output_dir)
|
|
1226
|
+
with open(os.path.join(output_dir, 'layout-controls.yaml'),
|
|
1227
|
+
encoding='utf-8') as stream:
|
|
1228
|
+
controls = stream.read()
|
|
1229
|
+
|
|
1230
|
+
self.assertIn('只编辑本草案已列出的真实图片背景', controls)
|
|
1231
|
+
self.assertIn('纯色、渐变、外框、几何装饰和透明叠层不新建 bg_rules', controls)
|
|
1232
|
+
self.assertIn('bg-content:', controls)
|
|
1233
|
+
|
|
1234
|
+
def test_template_layout_keeps_inherited_linear_gradient_background_as_decor(self):
|
|
1235
|
+
layout_part = 'ppt/slideLayouts/slideLayout1.xml'
|
|
1236
|
+
data = {
|
|
1237
|
+
'canvas': {'px': [1920, 1080]},
|
|
1238
|
+
'form_hint': {'form': 3},
|
|
1239
|
+
'layouts': [{
|
|
1240
|
+
'part': layout_part,
|
|
1241
|
+
'name': '内容',
|
|
1242
|
+
'used_by_slides': 1,
|
|
1243
|
+
'background': {
|
|
1244
|
+
'type': 'gradient',
|
|
1245
|
+
'stops': [
|
|
1246
|
+
{'pos': 0, 'color': {'resolved': '#4B5CF5'}},
|
|
1247
|
+
{'pos': 100, 'color': {'resolved': '#233AFB'}},
|
|
1248
|
+
],
|
|
1249
|
+
'angle_deg': 0,
|
|
1250
|
+
},
|
|
1251
|
+
}, {
|
|
1252
|
+
'part': 'ppt/slideLayouts/slideLayout2.xml',
|
|
1253
|
+
'name': '目录',
|
|
1254
|
+
'used_by_slides': 0,
|
|
1255
|
+
}, {
|
|
1256
|
+
'part': 'ppt/slideLayouts/slideLayout3.xml',
|
|
1257
|
+
'name': '封底',
|
|
1258
|
+
'used_by_slides': 0,
|
|
1259
|
+
}],
|
|
1260
|
+
'slides': [{
|
|
1261
|
+
'part': 'ppt/slides/slide1.xml',
|
|
1262
|
+
'layout': layout_part,
|
|
1263
|
+
'background': None,
|
|
1264
|
+
}],
|
|
1265
|
+
'images': [],
|
|
1266
|
+
'media': [],
|
|
1267
|
+
'theme_topology': {'themes': ['single'], 'per_master': [], 'default': 'single'},
|
|
1268
|
+
'reference_graph': {
|
|
1269
|
+
'master_of_layout': {},
|
|
1270
|
+
'layout_of_slide': {'ppt/slides/slide1.xml': layout_part},
|
|
1271
|
+
},
|
|
1272
|
+
'background_composites': {},
|
|
1273
|
+
}
|
|
1274
|
+
shapes = []
|
|
1275
|
+
for index, part in enumerate((layout_part,
|
|
1276
|
+
'ppt/slideLayouts/slideLayout2.xml',
|
|
1277
|
+
'ppt/slideLayouts/slideLayout3.xml'), 1):
|
|
1278
|
+
shapes.append({
|
|
1279
|
+
'part': part,
|
|
1280
|
+
'layer': 'layout',
|
|
1281
|
+
'kind': 'sp',
|
|
1282
|
+
'box': {'x': 100, 'y': 100, 'w': 600, 'h': 100},
|
|
1283
|
+
'ph': {'type': 'title', 'idx': str(index)},
|
|
1284
|
+
'text': {'paragraphs': [{'runs': [{'text': 'Title %d' % index,
|
|
1285
|
+
'sz_px': 40}]}]},
|
|
1286
|
+
})
|
|
1287
|
+
|
|
1288
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
1289
|
+
os.makedirs(os.path.join(output_dir, 'ref'))
|
|
1290
|
+
with open(os.path.join(output_dir, 'ref', 'shapes.json'), 'w',
|
|
1291
|
+
encoding='utf-8') as stream:
|
|
1292
|
+
import json
|
|
1293
|
+
json.dump({'shapes': shapes}, stream)
|
|
1294
|
+
archetypes, _, _ = draft_layouts(data, output_dir)
|
|
1295
|
+
emit_layouts(archetypes, output_dir)
|
|
1296
|
+
emit_manifest(data, [], [], output_dir, archetypes)
|
|
1297
|
+
with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
|
|
1298
|
+
layouts_yaml = stream.read()
|
|
1299
|
+
with open(os.path.join(output_dir, 'manifest.yaml'), encoding='utf-8') as stream:
|
|
1300
|
+
manifest = stream.read()
|
|
1301
|
+
|
|
1302
|
+
self.assertIn('box: [0, 0, 1920, 1080]', layouts_yaml)
|
|
1303
|
+
self.assertIn('background-image: linear-gradient(90deg, #4B5CF5 0%, #233AFB 100%)',
|
|
1304
|
+
layouts_yaml)
|
|
1305
|
+
self.assertIn('value: "[0, 0, 1920, 1080]"', manifest)
|
|
1306
|
+
self.assertIn('reason: "PPT 背景铺满画布"', manifest)
|
|
1307
|
+
|
|
1308
|
+
def test_sampled_layouts_group_inherited_structured_background(self):
|
|
1309
|
+
layout_part = 'ppt/slideLayouts/slideLayout1.xml'
|
|
1310
|
+
slides = [{
|
|
1311
|
+
'part': 'ppt/slides/slide%d.xml' % index,
|
|
1312
|
+
'layout': layout_part,
|
|
1313
|
+
'background': None,
|
|
1314
|
+
} for index in range(1, 4)]
|
|
1315
|
+
data = {
|
|
1316
|
+
'canvas': {'px': [1920, 1080]},
|
|
1317
|
+
'form_hint': {'form': 1},
|
|
1318
|
+
'layouts': [{
|
|
1319
|
+
'part': layout_part,
|
|
1320
|
+
'name': '内容',
|
|
1321
|
+
'used_by_slides': 3,
|
|
1322
|
+
}],
|
|
1323
|
+
'slides': slides,
|
|
1324
|
+
'images': [],
|
|
1325
|
+
'media': [],
|
|
1326
|
+
'theme_topology': {'themes': ['single'], 'per_master': [], 'default': 'single'},
|
|
1327
|
+
'reference_graph': {
|
|
1328
|
+
'master_of_layout': {},
|
|
1329
|
+
'layout_of_slide': {
|
|
1330
|
+
slide['part']: layout_part for slide in slides
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
'background_composites': {},
|
|
1334
|
+
}
|
|
1335
|
+
shapes = [{
|
|
1336
|
+
'part': slide['part'],
|
|
1337
|
+
'layer': 'slide',
|
|
1338
|
+
'kind': 'sp',
|
|
1339
|
+
'box': {'x': 100, 'y': 100, 'w': 600, 'h': 100},
|
|
1340
|
+
'ph': {'type': 'title', 'idx': '1'},
|
|
1341
|
+
'text': {'paragraphs': [{'runs': [{
|
|
1342
|
+
'text': 'Title %d' % index,
|
|
1343
|
+
'sz_px': 40,
|
|
1344
|
+
}]}]},
|
|
1345
|
+
} for index, slide in enumerate(slides, 1)]
|
|
1346
|
+
|
|
1347
|
+
backgrounds = [{
|
|
1348
|
+
'source': 'bgPr',
|
|
1349
|
+
'type': 'solid',
|
|
1350
|
+
'color': {'resolved': '#FAF9F5'},
|
|
1351
|
+
}, {
|
|
1352
|
+
'type': 'gradient',
|
|
1353
|
+
'stops': [
|
|
1354
|
+
{'pos': 0, 'color': {'resolved': '#4B5CF5'}},
|
|
1355
|
+
{'pos': 100, 'color': {'resolved': '#233AFB'}},
|
|
1356
|
+
],
|
|
1357
|
+
'angle_deg': 0,
|
|
1358
|
+
}]
|
|
1359
|
+
for background in backgrounds:
|
|
1360
|
+
with self.subTest(background=background['type']):
|
|
1361
|
+
data['layouts'][0]['background'] = background
|
|
1362
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
1363
|
+
os.makedirs(os.path.join(output_dir, 'ref'))
|
|
1364
|
+
with open(os.path.join(output_dir, 'ref', 'shapes.json'), 'w',
|
|
1365
|
+
encoding='utf-8') as stream:
|
|
1366
|
+
import json
|
|
1367
|
+
json.dump({'shapes': shapes}, stream)
|
|
1368
|
+
archetypes, pages, _ = draft_layouts(
|
|
1369
|
+
data, output_dir, effective_alpha={})
|
|
1370
|
+
|
|
1371
|
+
self.assertEqual([1, 2, 3], sorted(
|
|
1372
|
+
page for archetype in archetypes for page in archetype['pages']))
|
|
1373
|
+
self.assertTrue(all(page['background'] == background for page in pages))
|
|
1374
|
+
self.assertTrue(all(archetype['bg_raw'] is None
|
|
1375
|
+
for archetype in archetypes))
|
|
1376
|
+
self.assertTrue(all(
|
|
1377
|
+
any(decor.get('trace') == 'canvas-background'
|
|
1378
|
+
for decor in archetype['decor'])
|
|
1379
|
+
for archetype in archetypes
|
|
1380
|
+
))
|
|
1381
|
+
self.assertTrue(all(
|
|
1382
|
+
all(isinstance(value, str)
|
|
1383
|
+
for value in archetype['_source_backgrounds'])
|
|
1384
|
+
for archetype in archetypes
|
|
1385
|
+
))
|
|
1386
|
+
|
|
1090
1387
|
def test_asset_vision_groups_fail_when_page_screenshot_is_unreadable(self):
|
|
1091
1388
|
candidates = [{
|
|
1092
1389
|
'id': 'asset-1',
|
|
@@ -1221,6 +1518,10 @@ layouts:
|
|
|
1221
1518
|
'fullscreen': True,
|
|
1222
1519
|
'probe': {'alpha_mean': 5, 'near_blank': True},
|
|
1223
1520
|
}))
|
|
1521
|
+
self.assertTrue(needs_asset_judgment({
|
|
1522
|
+
'fullscreen': False,
|
|
1523
|
+
'probe': {'alpha_mean': 5, 'near_blank': True},
|
|
1524
|
+
}))
|
|
1224
1525
|
|
|
1225
1526
|
def test_shape_fill_images_are_exposed_as_slide_marks(self):
|
|
1226
1527
|
data = {
|