@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.3c06d32

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.
Files changed (61) hide show
  1. package/package.json +6 -6
  2. package/steering/design-html/skills/charts/SKILL.md +4 -0
  3. package/steering/design-html/skills/pptx-style-extract/SKILL.md +71 -26
  4. package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
  5. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +26 -14
  6. package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
  7. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2810 -292
  8. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +544 -22
  9. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +19 -2
  10. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +991 -165
  11. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +22 -3
  12. package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
  13. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +20 -12
  14. package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
  15. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +364 -0
  16. package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
  17. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +76 -0
  18. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +528 -0
  19. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1694 -0
  20. package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +479 -0
  21. package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
  22. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +315 -0
  23. package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +400 -0
  24. package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
  25. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
  26. package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
  27. package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +1 -0
  28. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
  29. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
  30. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
  31. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +37 -113
  32. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +13 -2
  33. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +7 -7
  34. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +149 -24
  35. package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
  36. package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +255 -0
  37. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +158 -543
  38. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +15 -1
  39. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
  40. package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
  41. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
  42. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
  43. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
  44. package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
  45. package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
  46. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  47. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
  48. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
  49. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
  50. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
  51. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
  52. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
  53. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
  54. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
  55. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
  56. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
  57. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
  58. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
  59. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
  60. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
  61. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env python3
2
+ """Integration regression tests for model-decided image roles."""
3
+ import json
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ import unittest
8
+
9
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10
+
11
+ from check_v2 import Pack, RULES # noqa: E402
12
+ from package import (Fail, apply_asset_decisions, bind_asset_vision_group_scopes,
13
+ load_layout_blocks, main as package_main, # noqa: E402
14
+ validate_asset_vision_groups)
15
+
16
+
17
+ class AssetJudgmentPackageTest(unittest.TestCase):
18
+ def test_asset_vision_index_requires_selected_candidates(self):
19
+ with tempfile.TemporaryDirectory() as lout:
20
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
21
+ 'w', encoding='utf-8') as stream:
22
+ json.dump({'version': 2}, stream)
23
+
24
+ with self.assertRaisesRegex(Fail, '缺少 selected'):
25
+ validate_asset_vision_groups(lout, {'asset_vision_groups': []})
26
+
27
+ def test_asset_vision_groups_must_keep_every_extracted_candidate(self):
28
+ with tempfile.TemporaryDirectory() as lout:
29
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
30
+ 'w', encoding='utf-8') as stream:
31
+ json.dump({
32
+ 'version': 2,
33
+ 'selected': [{
34
+ 'candidates': [{
35
+ 'id': 'asset-cover',
36
+ 'source_media': 'cover.png',
37
+ 'placements': [{
38
+ 'box': [100, 200, 300, 400],
39
+ 'archetype': 'cover',
40
+ }],
41
+ }, {
42
+ 'id': 'asset-ornament',
43
+ 'source_media': 'ornament.png',
44
+ 'placements': [{
45
+ 'box': [10, 20, 30, 40],
46
+ 'archetype': 'content',
47
+ }],
48
+ }],
49
+ }],
50
+ }, stream)
51
+ manifest = {
52
+ 'asset_vision_groups': [{
53
+ 'id': 'asset-cover',
54
+ 'source_media': 'cover.png',
55
+ 'layout': 'cover',
56
+ 'box': '[100, 200, 300, 400]',
57
+ 'visual_kind': 'illustration',
58
+ }, {
59
+ 'id': 'asset-ornament',
60
+ 'source_media': 'ornament.png',
61
+ 'layout': 'content',
62
+ 'box': '[10, 20, 30, 40]',
63
+ 'visual_kind': 'decorative',
64
+ }],
65
+ }
66
+
67
+ validate_asset_vision_groups(lout, manifest)
68
+ manifest['asset_vision_groups'].pop()
69
+
70
+ with self.assertRaisesRegex(Fail, '逐项保留抽取候选.*asset-ornament'):
71
+ validate_asset_vision_groups(lout, manifest)
72
+
73
+ def test_asset_vision_groups_reject_rewritten_extracted_candidate_scope(self):
74
+ with tempfile.TemporaryDirectory() as lout:
75
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
76
+ 'w', encoding='utf-8') as stream:
77
+ json.dump({
78
+ 'version': 2,
79
+ 'selected': [{
80
+ 'candidates': [{
81
+ 'id': 'asset-ornament',
82
+ 'source_media': 'ornament.png',
83
+ 'placements': [{
84
+ 'box': [10, 20, 30, 40],
85
+ 'archetype': None,
86
+ }],
87
+ }],
88
+ }],
89
+ }, stream)
90
+ manifest = {
91
+ 'asset_vision_groups': [{
92
+ 'id': 'asset-ornament',
93
+ 'source_media': 'other.png',
94
+ 'box': '[10, 20, 30, 40]',
95
+ 'visual_kind': 'decorative',
96
+ }],
97
+ }
98
+
99
+ with self.assertRaisesRegex(Fail, '改写了抽取候选的图片或位置'):
100
+ validate_asset_vision_groups(lout, manifest)
101
+
102
+ def test_v2_candidate_ids_resolve_same_box_on_different_layouts(self):
103
+ with tempfile.TemporaryDirectory() as lout:
104
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
105
+ 'w', encoding='utf-8') as stream:
106
+ json.dump({
107
+ 'version': 2,
108
+ 'selected': [{
109
+ 'candidates': [{
110
+ 'id': 'asset-cover-logo',
111
+ 'source_media': 'shared.png',
112
+ 'placements': [{
113
+ 'box': [100, 200, 80, 80],
114
+ 'archetype': 'cover',
115
+ }],
116
+ }, {
117
+ 'id': 'asset-content-ornament',
118
+ 'source_media': 'shared.png',
119
+ 'placements': [{
120
+ 'box': [100, 200, 80, 80],
121
+ 'archetype': 'content',
122
+ }],
123
+ }],
124
+ }],
125
+ }, stream)
126
+ manifest = {
127
+ 'assets': [],
128
+ 'asset_vision_groups': [{
129
+ 'id': 'asset-cover-logo',
130
+ 'source_media': 'shared.png',
131
+ 'box': '[100, 200, 80, 80]',
132
+ 'visual_kind': 'logo',
133
+ }, {
134
+ 'id': 'asset-content-ornament',
135
+ 'source_media': 'shared.png',
136
+ 'box': '[100, 200, 80, 80]',
137
+ 'visual_kind': 'decorative',
138
+ }],
139
+ }
140
+
141
+ bind_asset_vision_group_scopes(lout, manifest)
142
+ decisions, _ = apply_asset_decisions(
143
+ manifest,
144
+ bound_sources={'shared.png'},
145
+ bound_slots={
146
+ 'shared.png': {
147
+ ('cover', (100, 200, 80, 80)),
148
+ ('content', (100, 200, 80, 80)),
149
+ },
150
+ },
151
+ )
152
+
153
+ self.assertEqual(
154
+ decisions.for_scope('shared.png', 'cover', (100, 200, 80, 80)),
155
+ 'logo',
156
+ )
157
+ self.assertEqual(
158
+ decisions.for_scope('shared.png', 'content', (100, 200, 80, 80)),
159
+ 'texture',
160
+ )
161
+
162
+ def test_legacy_asset_vision_index_accepts_a_group_default(self):
163
+ with tempfile.TemporaryDirectory() as lout:
164
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
165
+ 'w', encoding='utf-8') as stream:
166
+ json.dump({
167
+ 'version': 1,
168
+ 'selected': [{
169
+ 'candidates': [{
170
+ 'id': 'asset-cover',
171
+ 'source_media': 'cover.png',
172
+ 'placements': [{'box': [0, 0, 100, 100]}],
173
+ }, {
174
+ 'id': 'asset-ornament',
175
+ 'source_media': 'ornament.png',
176
+ 'placements': [{'box': [10, 20, 30, 40]}],
177
+ }],
178
+ }],
179
+ }, stream)
180
+ manifest = {
181
+ 'asset_vision_groups': [{
182
+ 'id': 'vision-1',
183
+ 'source_media': '[cover.png, ornament.png]',
184
+ 'visual_kind': 'decorative',
185
+ }],
186
+ }
187
+
188
+ validate_asset_vision_groups(lout, manifest)
189
+
190
+ def test_layout_controls_override_legacy_judgment_without_reading_layout_todos(self):
191
+ with tempfile.TemporaryDirectory() as root:
192
+ lout = os.path.join(root, 'l-out')
193
+ os.makedirs(lout)
194
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
195
+ stream.write(
196
+ 'names:\n'
197
+ ' content: TODO中文名\n'
198
+ 'roles:\n'
199
+ ' content: TODO角色\n'
200
+ 'layouts:\n'
201
+ ' content:\n'
202
+ ' slots:\n'
203
+ ' - {role: body, box: [10, 20, 30, 40], type: body}\n'
204
+ ' confidence: high\n'
205
+ )
206
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w', encoding='utf-8') as stream:
207
+ stream.write(
208
+ 'names:\n'
209
+ ' content: "内容页"\n'
210
+ 'roles:\n'
211
+ ' content: content\n'
212
+ )
213
+
214
+ blocks = load_layout_blocks(lout)
215
+
216
+ self.assertEqual([key for key, _, _ in blocks],
217
+ ['names', 'roles', 'layouts'])
218
+ self.assertIn('内容页', blocks[0][2][0])
219
+ self.assertIn('content', blocks[1][2][0])
220
+ self.assertNotIn('TODO', '\n'.join(
221
+ line for _, _, lines in blocks for line in lines
222
+ ))
223
+
224
+ def test_layout_controls_reject_non_judgment_block(self):
225
+ with tempfile.TemporaryDirectory() as root:
226
+ lout = os.path.join(root, 'l-out')
227
+ os.makedirs(lout)
228
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
229
+ stream.write('layouts:\n content:\n role: content\n')
230
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w', encoding='utf-8') as stream:
231
+ stream.write('layouts:\n cover:\n role: cover\n')
232
+
233
+ with self.assertRaisesRegex(Fail, '只能包含'):
234
+ load_layout_blocks(lout)
235
+
236
+ def test_layout_controls_must_cover_all_legacy_judgment_blocks(self):
237
+ with tempfile.TemporaryDirectory() as root:
238
+ lout = os.path.join(root, 'l-out')
239
+ os.makedirs(lout)
240
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
241
+ stream.write(
242
+ 'names:\n'
243
+ ' content: "内容页"\n'
244
+ 'roles:\n'
245
+ ' content: content\n'
246
+ 'layouts:\n'
247
+ ' content:\n'
248
+ ' role: content\n'
249
+ )
250
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
251
+ encoding='utf-8') as stream:
252
+ stream.write('roles:\n content: content\n')
253
+
254
+ with self.assertRaisesRegex(Fail, '缺:names'):
255
+ load_layout_blocks(lout)
256
+
257
+ def test_layout_controls_must_keep_each_legacy_judgment_entry(self):
258
+ with tempfile.TemporaryDirectory() as root:
259
+ lout = os.path.join(root, 'l-out')
260
+ os.makedirs(lout)
261
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
262
+ stream.write(
263
+ 'names:\n'
264
+ ' cover: "封面"\n'
265
+ ' content: "内容页"\n'
266
+ 'roles:\n'
267
+ ' cover: cover\n'
268
+ ' content: content\n'
269
+ 'layouts:\n'
270
+ ' cover:\n'
271
+ ' role: cover\n'
272
+ ' content:\n'
273
+ ' role: content\n'
274
+ )
275
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
276
+ encoding='utf-8') as stream:
277
+ stream.write(
278
+ 'names:\n'
279
+ ' cover: "封面"\n'
280
+ 'roles:\n'
281
+ ' cover: cover\n'
282
+ ' content: content\n'
283
+ )
284
+
285
+ with self.assertRaisesRegex(Fail, 'names 段缺少旧判断条目:content'):
286
+ load_layout_blocks(lout)
287
+
288
+ def test_package_strips_judgment_fields_and_preserves_content_slot(self):
289
+ try:
290
+ from PIL import Image
291
+ except ImportError:
292
+ self.skipTest('Pillow unavailable')
293
+
294
+ with tempfile.TemporaryDirectory() as root:
295
+ stage1 = os.path.join(root, 'stage1')
296
+ lout = os.path.join(stage1, 'l-out')
297
+ pack = os.path.join(root, 'pack')
298
+ media = os.path.join(stage1, 'media-out')
299
+ ref = os.path.join(stage1, 'ref')
300
+ os.makedirs(lout)
301
+ os.makedirs(media)
302
+ os.makedirs(ref)
303
+
304
+ Image.new('RGBA', (40, 20), (20, 40, 60, 255)).save(
305
+ os.path.join(media, 'partner-logo.png'))
306
+ Image.new('RGBA', (24, 24), (60, 40, 20, 255)).save(
307
+ os.path.join(media, 'ornament.png'))
308
+
309
+ box_rows = [
310
+ ('ppt/media/image1.png', 'partner-logo.png', [100, 200, 400, 200]),
311
+ ('ppt/media/image2.png', 'ornament.png', [40, 40, 120, 120]),
312
+ ]
313
+ extract = {
314
+ 'source': {'filename': 'fixture.pptx'},
315
+ 'canvas': {
316
+ 'px': [1920, 1080],
317
+ 'source': {'cx': 12192000, 'cy': 6858000, 'unit': 'EMU'},
318
+ },
319
+ 'media': [
320
+ {
321
+ 'media': source,
322
+ 'out': 'media-out/' + output,
323
+ 'source_px': [40, 20] if output == 'partner-logo.png' else [24, 24],
324
+ }
325
+ for source, output, _ in box_rows
326
+ ],
327
+ 'images': [
328
+ {
329
+ 'media': source,
330
+ 'boxes': [{
331
+ 'count': 1,
332
+ 'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
333
+ 'parts': ['ppt/slides/slide1.xml'],
334
+ }],
335
+ }
336
+ for source, _, box in box_rows
337
+ ],
338
+ 'color_freq': [],
339
+ 'text_scale': [],
340
+ }
341
+ with open(os.path.join(stage1, 'extract.json'), 'w', encoding='utf-8') as stream:
342
+ json.dump(extract, stream)
343
+ with open(os.path.join(ref, 'shapes.json'), 'w', encoding='utf-8') as stream:
344
+ json.dump({
345
+ 'shapes': [
346
+ {
347
+ 'part': 'ppt/slides/slide1.xml',
348
+ 'kind': 'pic',
349
+ 'name': output,
350
+ 'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
351
+ }
352
+ for _, output, box in box_rows
353
+ ],
354
+ }, stream)
355
+
356
+ with open(os.path.join(lout, 'manifest.yaml'), 'w', encoding='utf-8') as stream:
357
+ stream.write(
358
+ 'version: alpha\n'
359
+ 'name: asset-judgment-fixture\n'
360
+ 'description: >\n'
361
+ ' A restrained fixture with explicit image-role judgments and traced layout slots.\n'
362
+ 'assets:\n'
363
+ ' - id: logo-primary\n'
364
+ ' source_media: partner-logo.png\n'
365
+ ' kind: logo\n'
366
+ ' on-bg: light\n'
367
+ 'asset_vision_groups:\n'
368
+ ' - id: vision-1\n'
369
+ ' source_media: [partner-logo.png, ornament.png]\n'
370
+ ' visual_kind: content-image\n'
371
+ 'asset_decisions:\n'
372
+ ' - source_media: ornament.png\n'
373
+ ' visual_kind: decorative\n'
374
+ )
375
+ with open(os.path.join(lout, 'frontmatter.yaml'), 'w', encoding='utf-8') as stream:
376
+ stream.write('')
377
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
378
+ stream.write(
379
+ 'roles:\n'
380
+ ' content: TODO角色\n'
381
+ 'layouts:\n'
382
+ ' content:\n'
383
+ ' name: "内容页"\n'
384
+ ' role: content\n'
385
+ ' slots:\n'
386
+ ' - {role: logo, box: [100, 200, 400, 200], type: pic, '
387
+ 'asset: logo-primary, source_media: partner-logo.png}\n'
388
+ ' - {role: asset-candidate, box: [40, 40, 120, 120], type: pic, '
389
+ 'source_media: ornament.png}\n'
390
+ ' confidence: high\n'
391
+ )
392
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
393
+ encoding='utf-8') as stream:
394
+ stream.write('roles:\n content: content\n')
395
+ with open(os.path.join(lout, 'body.md'), 'w', encoding='utf-8') as stream:
396
+ stream.write(
397
+ '## Usage\n\n'
398
+ 'Read `layouts.md` before composing a slide.\n\n'
399
+ '## Assets\n\n'
400
+ '{{ASSET_TABLE}}\n\n'
401
+ '## Hard Rules\n\n'
402
+ '{{LOGO_RULES}}\n'
403
+ )
404
+ check_v1 = os.path.join(root, 'check_v1.py')
405
+ with open(check_v1, 'w', encoding='utf-8') as stream:
406
+ stream.write('raise SystemExit(0)\n')
407
+
408
+ rc = package_main([stage1, lout, pack, '--check-v1', check_v1])
409
+
410
+ self.assertEqual(rc, 0)
411
+ with open(os.path.join(pack, 'design.md'), encoding='utf-8') as stream:
412
+ design = stream.read()
413
+ with open(os.path.join(pack, 'layouts.md'), encoding='utf-8') as stream:
414
+ layouts = stream.read()
415
+ self.assertNotIn('asset_decisions', design)
416
+ self.assertNotIn('asset_vision_groups', design)
417
+ self.assertNotIn('visual_kind', design)
418
+ self.assertNotIn('source_media', design)
419
+ self.assertNotIn('source_media', layouts)
420
+ self.assertNotIn('TODO', layouts)
421
+ self.assertNotIn('{{LOGO_RULES}}', design)
422
+ self.assertNotIn('logo-primary', design)
423
+ self.assertNotIn('logo-primary', layouts)
424
+ self.assertIn(
425
+ '- {role: pic, box: [100, 200, 400, 200], type: pic}',
426
+ layouts,
427
+ )
428
+ self.assertIn('texture-1:', design)
429
+ self.assertIn('kind: texture', design)
430
+ self.assertIn(
431
+ '- {role: texture, box: [40, 40, 120, 120], type: pic, asset: texture-1}',
432
+ layouts,
433
+ )
434
+ gate = Pack(pack)
435
+ self.assertFalse([
436
+ result
437
+ for result in (rule(gate) for rule in RULES)
438
+ if result.fails
439
+ ])
440
+
441
+ def test_package_places_a_rendered_picture_instance_as_a_logo_asset(self):
442
+ try:
443
+ from PIL import Image
444
+ except ImportError:
445
+ self.skipTest('Pillow unavailable')
446
+
447
+ with tempfile.TemporaryDirectory() as root:
448
+ stage1 = os.path.join(root, 'stage1')
449
+ lout = os.path.join(stage1, 'l-out')
450
+ pack = os.path.join(root, 'pack')
451
+ media = os.path.join(stage1, 'media-out')
452
+ ref = os.path.join(stage1, 'ref')
453
+ os.makedirs(lout)
454
+ os.makedirs(media)
455
+ os.makedirs(ref)
456
+
457
+ source = 'generated/instance/picture-directory-logo.webp'
458
+ output = 'picture-directory-logo.webp'
459
+ box = [1813, 18, 59, 228]
460
+ Image.new('RGBA', (59, 228), (20, 60, 100, 255)).save(
461
+ os.path.join(media, output), 'WEBP')
462
+ extract = {
463
+ 'source': {'filename': 'fixture.pptx'},
464
+ 'canvas': {
465
+ 'px': [1920, 1080],
466
+ 'source': {'cx': 12192000, 'cy': 6858000, 'unit': 'EMU'},
467
+ },
468
+ 'media': [{
469
+ 'media': source,
470
+ 'out': 'media-out/' + output,
471
+ 'generated': True,
472
+ 'rendered_from': 'ppt/media/original-logo.png',
473
+ }],
474
+ 'images': [{
475
+ 'media': source,
476
+ 'boxes': [{
477
+ 'count': 1,
478
+ 'box': dict(zip(('x', 'y', 'w', 'h'), box)),
479
+ 'parts': ['ppt/slides/slide2.xml'],
480
+ }],
481
+ }],
482
+ 'color_freq': [],
483
+ 'text_scale': [],
484
+ }
485
+ with open(os.path.join(stage1, 'extract.json'), 'w', encoding='utf-8') as stream:
486
+ json.dump(extract, stream)
487
+ with open(os.path.join(ref, 'shapes.json'), 'w', encoding='utf-8') as stream:
488
+ json.dump({'shapes': [{
489
+ 'part': 'ppt/slides/slide2.xml',
490
+ 'kind': 'pic',
491
+ 'box': dict(zip(('x', 'y', 'w', 'h'), box)),
492
+ }]}, stream)
493
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
494
+ 'w', encoding='utf-8') as stream:
495
+ json.dump({
496
+ 'version': 2,
497
+ 'selected': [{
498
+ 'candidates': [{
499
+ 'id': 'asset-directory-logo',
500
+ 'source_media': output,
501
+ 'placements': [{
502
+ 'slide': 2,
503
+ 'archetype': 'content',
504
+ 'box': box,
505
+ }],
506
+ }],
507
+ }],
508
+ 'omitted': [],
509
+ }, stream)
510
+ with open(os.path.join(lout, 'manifest.yaml'), 'w', encoding='utf-8') as stream:
511
+ stream.write(
512
+ 'version: alpha\n'
513
+ 'name: rendered-instance-fixture\n'
514
+ 'description: Rendered local PPT image instances remain reusable assets.\n'
515
+ 'asset_vision_groups:\n'
516
+ ' - id: asset-directory-logo\n'
517
+ ' source_media: picture-directory-logo.webp\n'
518
+ ' box: [1813, 18, 59, 228]\n'
519
+ ' visual_kind: logo\n'
520
+ )
521
+ for name, content in (
522
+ ('frontmatter.yaml', ''),
523
+ ('body.md', 'Read `layouts.md` before composing a slide.\n\n{{ASSET_TABLE}}\n'),
524
+ ('layouts.yaml',
525
+ 'layouts:\n'
526
+ ' content:\n'
527
+ ' name: "目录"\n'
528
+ ' role: content\n'
529
+ ' slots:\n'
530
+ ' - {role: asset-candidate, box: [1813, 18, 59, 228], type: pic, '
531
+ 'source_media: picture-directory-logo.webp}\n'
532
+ ' confidence: high\n'),
533
+ ):
534
+ with open(os.path.join(lout, name), 'w', encoding='utf-8') as stream:
535
+ stream.write(content)
536
+ check_v1 = os.path.join(root, 'check_v1.py')
537
+ with open(check_v1, 'w', encoding='utf-8') as stream:
538
+ stream.write('raise SystemExit(0)\n')
539
+
540
+ self.assertEqual(package_main([stage1, lout, pack, '--check-v1', check_v1]), 0)
541
+
542
+ with open(os.path.join(pack, 'design.md'), encoding='utf-8') as stream:
543
+ design = stream.read()
544
+ with open(os.path.join(pack, 'layouts.md'), encoding='utf-8') as stream:
545
+ layouts = stream.read()
546
+ self.assertTrue(os.path.isfile(os.path.join(pack, 'assets', 'logos', '1.webp')))
547
+ self.assertIn('logo-1:', design)
548
+ self.assertIn('assets/logos/1.webp', design)
549
+ self.assertIn(
550
+ '- {role: logo, box: [1813, 18, 59, 228], type: pic, asset: logo-1}',
551
+ layouts,
552
+ )
553
+
554
+
555
+ if __name__ == '__main__':
556
+ unittest.main()