@lark-apaas/coding-steering 0.1.32-dev.5abff3b → 0.1.32-dev.6ef2b6f

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.
@@ -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 = {