@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.3598eb3
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 +6 -6
- package/steering/design-html/skills/charts/SKILL.md +4 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +71 -26
- package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +26 -14
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2831 -293
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +544 -22
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +19 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +991 -165
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +22 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +20 -12
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +364 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +76 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +528 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1814 -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/test_rounded_contract.py +112 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +315 -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
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
- package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
- package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +37 -113
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +13 -2
- package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +7 -7
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +149 -24
- package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
- package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +255 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +158 -543
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +15 -1
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
- package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
- package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Focused regression tests for deterministic picture compositing."""
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest import mock
|
|
9
|
+
from xml.etree import ElementTree as ET
|
|
10
|
+
|
|
11
|
+
from PIL import Image
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
14
|
+
|
|
15
|
+
import extract # noqa: E402
|
|
16
|
+
from extract import ( # noqa: E402
|
|
17
|
+
_draw_picture,
|
|
18
|
+
_is_full_canvas_fill_overlay,
|
|
19
|
+
compose_backgrounds,
|
|
20
|
+
export_media,
|
|
21
|
+
export_visible_picture_instances,
|
|
22
|
+
)
|
|
23
|
+
from census import image_census # noqa: E402
|
|
24
|
+
from ooxml import Units # noqa: E402
|
|
25
|
+
from parts import walk_tree # noqa: E402
|
|
26
|
+
from render_pages import background_css # noqa: E402
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Archive:
|
|
30
|
+
def __init__(self, media, raw):
|
|
31
|
+
self.names = {media}
|
|
32
|
+
self.media = [media]
|
|
33
|
+
self.zip = self
|
|
34
|
+
self.raw = raw
|
|
35
|
+
|
|
36
|
+
def read(self, media):
|
|
37
|
+
if media not in self.names:
|
|
38
|
+
raise KeyError(media)
|
|
39
|
+
return self.raw
|
|
40
|
+
|
|
41
|
+
def size_of(self, media):
|
|
42
|
+
if media not in self.names:
|
|
43
|
+
raise KeyError(media)
|
|
44
|
+
return len(self.raw)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _CompositeArchive(_Archive):
|
|
48
|
+
def __init__(self, media, raw, slide):
|
|
49
|
+
super().__init__(media, raw)
|
|
50
|
+
self.layouts = []
|
|
51
|
+
self.slides = [slide]
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def xml(_part):
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _ShapeContext:
|
|
59
|
+
def __init__(self, media):
|
|
60
|
+
self.part = 'ppt/slides/slide1.xml'
|
|
61
|
+
self.layer = 'slide'
|
|
62
|
+
self.units = Units(1920, 1080)
|
|
63
|
+
self.clrmap = {}
|
|
64
|
+
self.clrscheme = {}
|
|
65
|
+
self.media = media
|
|
66
|
+
|
|
67
|
+
def media_of(self, _relationship_id):
|
|
68
|
+
return self.media
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _png(pixels):
|
|
72
|
+
image = Image.new('RGBA', (len(pixels), 1))
|
|
73
|
+
image.putdata(pixels)
|
|
74
|
+
output = io.BytesIO()
|
|
75
|
+
image.save(output, 'PNG')
|
|
76
|
+
return output.getvalue()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BackgroundCompositeTest(unittest.TestCase):
|
|
80
|
+
def test_applies_flip_and_opacity_once(self):
|
|
81
|
+
media = 'ppt/media/source.png'
|
|
82
|
+
package = _Archive(media, _png([(255, 0, 0, 255), (0, 0, 255, 255)]))
|
|
83
|
+
canvas = Image.new('RGBA', (2, 1), (255, 255, 255, 255))
|
|
84
|
+
|
|
85
|
+
drawn = _draw_picture(canvas, {
|
|
86
|
+
'media': media,
|
|
87
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
88
|
+
'flipH': True,
|
|
89
|
+
'opacity': 0.5,
|
|
90
|
+
}, package)
|
|
91
|
+
|
|
92
|
+
self.assertTrue(drawn)
|
|
93
|
+
left, right = canvas.getpixel((0, 0)), canvas.getpixel((1, 0))
|
|
94
|
+
self.assertEqual(left[:3], (128, 128, 255))
|
|
95
|
+
self.assertEqual(right[:3], (255, 128, 128))
|
|
96
|
+
self.assertEqual(left[3], 255)
|
|
97
|
+
self.assertEqual(right[3], 255)
|
|
98
|
+
|
|
99
|
+
def test_exports_a_cropped_rotated_visible_instance_from_a_near_blank_source(self):
|
|
100
|
+
media = 'ppt/media/source.png'
|
|
101
|
+
source = Image.new('RGBA', (1000, 1000))
|
|
102
|
+
for x in range(460, 540):
|
|
103
|
+
for y in range(460, 540):
|
|
104
|
+
source.putpixel((x, y), (15, 80, 160, 255))
|
|
105
|
+
raw = io.BytesIO()
|
|
106
|
+
source.save(raw, 'PNG')
|
|
107
|
+
package = _Archive(media, raw.getvalue())
|
|
108
|
+
shape = {
|
|
109
|
+
'kind': 'pic',
|
|
110
|
+
'media': media,
|
|
111
|
+
'part': 'ppt/slides/slide2.xml',
|
|
112
|
+
'box_unrotated': {'x': 100, 'y': 200, 'w': 20, 'h': 100},
|
|
113
|
+
'box': {'x': 50, 'y': 240, 'w': 120, 'h': 20},
|
|
114
|
+
'crop': {'l': 40, 't': 40, 'r': 40, 'b': 40},
|
|
115
|
+
'rot': 90,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
119
|
+
rows = export_visible_picture_instances(package, [shape], outdir, True)
|
|
120
|
+
self.assertEqual(len(rows), 1)
|
|
121
|
+
self.assertTrue(rows[0]['generated'])
|
|
122
|
+
self.assertEqual(rows[0]['rendered_from'], media)
|
|
123
|
+
self.assertTrue(shape['media'].startswith('generated/instance/'))
|
|
124
|
+
|
|
125
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGBA')
|
|
126
|
+
self.assertGreater(rendered.width, rendered.height)
|
|
127
|
+
self.assertGreater(sum(pixel[3] for pixel in rendered.getdata()) / rendered.width
|
|
128
|
+
/ rendered.height, 13)
|
|
129
|
+
|
|
130
|
+
def test_instance_webp_writer_removes_partial_output_after_a_save_error(self):
|
|
131
|
+
canvas = Image.new('RGBA', (1, 1), (15, 80, 160, 255))
|
|
132
|
+
|
|
133
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
134
|
+
path = os.path.join(outdir, 'instance.webp')
|
|
135
|
+
with mock.patch.object(Image.Image, 'save', side_effect=OSError('webp unavailable')):
|
|
136
|
+
reason = extract._save_picture_instance_webp(canvas, path)
|
|
137
|
+
|
|
138
|
+
self.assertEqual('pillow-error: OSError: webp unavailable', reason)
|
|
139
|
+
self.assertFalse(os.path.exists(path))
|
|
140
|
+
self.assertFalse(os.path.exists(path + '.tmp'))
|
|
141
|
+
|
|
142
|
+
def test_instance_webp_failure_keeps_the_source_media_and_records_the_reason(self):
|
|
143
|
+
media = 'ppt/media/source.png'
|
|
144
|
+
package = _Archive(media, _png([(15, 80, 160, 255)]))
|
|
145
|
+
shape = {
|
|
146
|
+
'kind': 'pic',
|
|
147
|
+
'media': media,
|
|
148
|
+
'part': 'ppt/slides/slide2.xml',
|
|
149
|
+
'layer': 'slide',
|
|
150
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 100, 'h': 100},
|
|
151
|
+
'box': {'x': 0, 'y': 0, 'w': 100, 'h': 100},
|
|
152
|
+
'rot': 90,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
156
|
+
blocked = {}
|
|
157
|
+
with mock.patch.object(
|
|
158
|
+
extract, '_save_picture_instance_webp',
|
|
159
|
+
return_value='pillow-error: OSError: webp unavailable'):
|
|
160
|
+
rows = export_visible_picture_instances(
|
|
161
|
+
package, [shape], outdir, True, blocked)
|
|
162
|
+
images, _ = image_census([shape], [], Units(1920, 1080))
|
|
163
|
+
media_rows = export_media(package, images, outdir, True)
|
|
164
|
+
|
|
165
|
+
self.assertEqual([], rows)
|
|
166
|
+
self.assertEqual(media, shape['media'])
|
|
167
|
+
self.assertTrue(images[0]['visible_instance_blocked'])
|
|
168
|
+
self.assertTrue(media_rows[0]['candidate'])
|
|
169
|
+
self.assertIn('visible_instance_fallback', media_rows[0]['reasons'])
|
|
170
|
+
self.assertEqual({
|
|
171
|
+
media: {
|
|
172
|
+
'reason': 'pillow-error: OSError: webp unavailable',
|
|
173
|
+
'attempted_specs': 1,
|
|
174
|
+
'part_refs': ['ppt/slides/slide2.xml'],
|
|
175
|
+
},
|
|
176
|
+
}, blocked)
|
|
177
|
+
|
|
178
|
+
def test_alpha_modulations_combine_for_a_visible_instance(self):
|
|
179
|
+
media = 'ppt/media/source.png'
|
|
180
|
+
package = _Archive(media, _png([(20, 40, 60, 255)]))
|
|
181
|
+
picture_tree = ET.fromstring('''
|
|
182
|
+
<p:spTree xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
|
183
|
+
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
|
|
184
|
+
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
185
|
+
<p:pic>
|
|
186
|
+
<p:nvPicPr><p:cNvPr id="1" name="half-alpha"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
|
|
187
|
+
<p:blipFill>
|
|
188
|
+
<a:blip r:embed="rId1">
|
|
189
|
+
<a:alphaMod amt="50000"/>
|
|
190
|
+
<a:alphaModFix amt="50000"/>
|
|
191
|
+
</a:blip>
|
|
192
|
+
<a:stretch><a:fillRect/></a:stretch>
|
|
193
|
+
</p:blipFill>
|
|
194
|
+
<p:spPr>
|
|
195
|
+
<a:xfrm><a:off x="0" y="0"/><a:ext cx="100" cy="100"/></a:xfrm>
|
|
196
|
+
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
|
197
|
+
</p:spPr>
|
|
198
|
+
</p:pic>
|
|
199
|
+
</p:spTree>
|
|
200
|
+
''')
|
|
201
|
+
shapes = []
|
|
202
|
+
walk_tree(picture_tree, _ShapeContext(media), shapes)
|
|
203
|
+
|
|
204
|
+
self.assertEqual(shapes[0]['opacity'], 0.25)
|
|
205
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
206
|
+
rows = export_visible_picture_instances(package, shapes, outdir, True)
|
|
207
|
+
self.assertEqual(len(rows), 1)
|
|
208
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGBA')
|
|
209
|
+
self.assertAlmostEqual(rendered.getpixel((50, 50))[3], 64, delta=2)
|
|
210
|
+
|
|
211
|
+
def test_composites_full_canvas_gradient_and_solid_fill_overlays(self):
|
|
212
|
+
media = 'ppt/media/source.png'
|
|
213
|
+
slide = 'ppt/slides/slide1.xml'
|
|
214
|
+
package = _CompositeArchive(media, _png([(0, 0, 255, 255)]), slide)
|
|
215
|
+
picture = {
|
|
216
|
+
'part': slide,
|
|
217
|
+
'kind': 'pic',
|
|
218
|
+
'media': media,
|
|
219
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
220
|
+
'w_pct': 100,
|
|
221
|
+
'h_pct': 100,
|
|
222
|
+
}
|
|
223
|
+
shape_base = {
|
|
224
|
+
'geom': {'prst': 'rect'},
|
|
225
|
+
'line': {'none': True},
|
|
226
|
+
}
|
|
227
|
+
gradient_overlay = {
|
|
228
|
+
**shape_base,
|
|
229
|
+
'part': slide,
|
|
230
|
+
'kind': 'sp',
|
|
231
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
232
|
+
'w_pct': 100,
|
|
233
|
+
'h_pct': 100,
|
|
234
|
+
'fill': {
|
|
235
|
+
'type': 'gradient',
|
|
236
|
+
'stops': [
|
|
237
|
+
{'pos': 0, 'color': {'resolved': 'rgba(255,0,0,0.5)'}},
|
|
238
|
+
{'pos': 100, 'color': {'resolved': 'rgba(255,0,0,0.5)'}},
|
|
239
|
+
],
|
|
240
|
+
'angle_deg': 0,
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
solid_overlay = {
|
|
244
|
+
**shape_base,
|
|
245
|
+
'part': slide,
|
|
246
|
+
'kind': 'sp',
|
|
247
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
248
|
+
'w_pct': 100,
|
|
249
|
+
'h_pct': 100,
|
|
250
|
+
'fill': {
|
|
251
|
+
'type': 'solid',
|
|
252
|
+
'color': {'resolved': 'rgba(0,255,0,0.5)'},
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
text_overlay = dict(solid_overlay, text={
|
|
256
|
+
'paragraphs': [{'runs': [{'text': 'Do not flatten me'}]}],
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
self.assertTrue(_is_full_canvas_fill_overlay(gradient_overlay))
|
|
260
|
+
self.assertFalse(_is_full_canvas_fill_overlay(text_overlay))
|
|
261
|
+
self.assertFalse(_is_full_canvas_fill_overlay({
|
|
262
|
+
**gradient_overlay,
|
|
263
|
+
'fill': {**gradient_overlay['fill'], 'path': 'circle'},
|
|
264
|
+
}))
|
|
265
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
266
|
+
part_map, rows, _ = compose_backgrounds(
|
|
267
|
+
package,
|
|
268
|
+
{'layout_of_slide': {}, 'master_of_layout': {}},
|
|
269
|
+
[picture, gradient_overlay, solid_overlay, text_overlay],
|
|
270
|
+
{},
|
|
271
|
+
Units(2, 1),
|
|
272
|
+
outdir,
|
|
273
|
+
True,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
self.assertEqual(set(part_map), {slide})
|
|
277
|
+
self.assertEqual(len(rows), 1)
|
|
278
|
+
self.assertEqual(
|
|
279
|
+
[layer['kind'] for layer in rows[0]['composited_from']],
|
|
280
|
+
['picture', 'fill_overlay', 'fill_overlay'],
|
|
281
|
+
)
|
|
282
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGB')
|
|
283
|
+
red, green, blue = rendered.getpixel((0, 0))
|
|
284
|
+
self.assertGreater(red, 50)
|
|
285
|
+
self.assertGreater(green, 100)
|
|
286
|
+
self.assertGreater(blue, 50)
|
|
287
|
+
|
|
288
|
+
def test_preserves_fill_and_picture_draw_order(self):
|
|
289
|
+
media = 'ppt/media/source.png'
|
|
290
|
+
slide = 'ppt/slides/slide1.xml'
|
|
291
|
+
package = _CompositeArchive(media, _png([(0, 0, 255, 255)]), slide)
|
|
292
|
+
overlay = {
|
|
293
|
+
'part': slide,
|
|
294
|
+
'kind': 'sp',
|
|
295
|
+
'geom': {'prst': 'rect'},
|
|
296
|
+
'line': {'none': True},
|
|
297
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
298
|
+
'w_pct': 100,
|
|
299
|
+
'h_pct': 100,
|
|
300
|
+
'fill': {'type': 'solid', 'color': {'resolved': 'rgba(255,0,0,0.5)'}},
|
|
301
|
+
}
|
|
302
|
+
picture = {
|
|
303
|
+
'part': slide,
|
|
304
|
+
'kind': 'pic',
|
|
305
|
+
'media': media,
|
|
306
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
307
|
+
'w_pct': 100,
|
|
308
|
+
'h_pct': 100,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
312
|
+
_, rows, _ = compose_backgrounds(
|
|
313
|
+
package,
|
|
314
|
+
{'layout_of_slide': {}, 'master_of_layout': {}},
|
|
315
|
+
[overlay, picture],
|
|
316
|
+
{},
|
|
317
|
+
Units(2, 1),
|
|
318
|
+
outdir,
|
|
319
|
+
True,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGB')
|
|
323
|
+
self.assertEqual(rendered.getpixel((0, 0)), (0, 0, 255))
|
|
324
|
+
|
|
325
|
+
def test_path_gradient_background_is_not_linearized_for_compositing(self):
|
|
326
|
+
media = 'ppt/media/source.png'
|
|
327
|
+
slide = 'ppt/slides/slide1.xml'
|
|
328
|
+
package = _CompositeArchive(media, _png([(0, 0, 255, 255)]), slide)
|
|
329
|
+
picture = {
|
|
330
|
+
'part': slide,
|
|
331
|
+
'kind': 'pic',
|
|
332
|
+
'media': media,
|
|
333
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
334
|
+
'w_pct': 100,
|
|
335
|
+
'h_pct': 100,
|
|
336
|
+
'rot': 90,
|
|
337
|
+
}
|
|
338
|
+
background = {
|
|
339
|
+
'type': 'gradient',
|
|
340
|
+
'path': 'circle',
|
|
341
|
+
'stops': [
|
|
342
|
+
{'pos': 0, 'color': {'resolved': '#FF0000'}},
|
|
343
|
+
{'pos': 100, 'color': {'resolved': '#00FF00'}},
|
|
344
|
+
],
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
self.assertIsNone(background_css(background))
|
|
348
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
349
|
+
_, rows, _ = compose_backgrounds(
|
|
350
|
+
package,
|
|
351
|
+
{'layout_of_slide': {}, 'master_of_layout': {}},
|
|
352
|
+
[picture],
|
|
353
|
+
{slide: background},
|
|
354
|
+
Units(2, 1),
|
|
355
|
+
outdir,
|
|
356
|
+
True,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGB')
|
|
360
|
+
self.assertEqual(rendered.getpixel((0, 0)), (0, 0, 255))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
if __name__ == '__main__':
|
|
364
|
+
unittest.main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression test for the generated consumer-facing color contract."""
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from draft import emit_body
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ColorContractTest(unittest.TestCase):
|
|
11
|
+
def test_body_allows_supporting_colors_without_a_second_accent_system(self):
|
|
12
|
+
data = {
|
|
13
|
+
'canvas': {'px': [1920, 1080]},
|
|
14
|
+
'counts': {'slides': 1},
|
|
15
|
+
'form_hint': {'form': 3},
|
|
16
|
+
}
|
|
17
|
+
tokens = [
|
|
18
|
+
('primary', {'hex': '#123456'}),
|
|
19
|
+
('surface', {'hex': '#FFFFFF'}),
|
|
20
|
+
]
|
|
21
|
+
fonts = [
|
|
22
|
+
{
|
|
23
|
+
'names': ['Example Sans'],
|
|
24
|
+
'stack': ['Example Sans', 'Noto Sans SC'],
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
roles = {'body': {'sz_px': 36}}
|
|
28
|
+
archetypes = [
|
|
29
|
+
{
|
|
30
|
+
'name': 'content',
|
|
31
|
+
'slots': [],
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
36
|
+
emit_body(
|
|
37
|
+
data,
|
|
38
|
+
tokens,
|
|
39
|
+
fonts,
|
|
40
|
+
roles,
|
|
41
|
+
[],
|
|
42
|
+
archetypes,
|
|
43
|
+
[],
|
|
44
|
+
{},
|
|
45
|
+
output_dir,
|
|
46
|
+
)
|
|
47
|
+
with open(os.path.join(output_dir, 'body.md'), encoding='utf-8') as stream:
|
|
48
|
+
body = stream.read()
|
|
49
|
+
|
|
50
|
+
self.assertIn('允许新增中性色、低彩度辅助色或局部语义色', body)
|
|
51
|
+
self.assertRegex(body, r'正负.*风险.*警告.*状态.*图表序列')
|
|
52
|
+
self.assertIn('必要时可以使用 Colors 之外的颜色', body)
|
|
53
|
+
self.assertIn('不能形成与模板主色竞争的第二强调色', body)
|
|
54
|
+
self.assertIn('色相、明度和饱和度关系', body)
|
|
55
|
+
self.assertIn('高饱和、高对比、大面积或跨页重复', body)
|
|
56
|
+
self.assertIn('标题、关键数字、图表主序列、卡片底色或渐变', body)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == '__main__':
|
|
60
|
+
unittest.main()
|
package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for consumer rules emitted into design.md."""
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from draft import emit_body
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DesignConsumerContractTest(unittest.TestCase):
|
|
11
|
+
def test_generated_design_owns_template_consumption_rules(self):
|
|
12
|
+
data = {
|
|
13
|
+
'canvas': {'px': [1920, 1080]},
|
|
14
|
+
'counts': {'slides': 1},
|
|
15
|
+
'form_hint': {'form': 3},
|
|
16
|
+
}
|
|
17
|
+
tokens = [('primary', {'hex': '#123456'})]
|
|
18
|
+
fonts = [
|
|
19
|
+
{
|
|
20
|
+
'names': ['Example Sans'],
|
|
21
|
+
'stack': ['Example Sans', 'Noto Sans SC'],
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
roles = {'body': {'sz_px': 36}}
|
|
25
|
+
assets = [
|
|
26
|
+
{
|
|
27
|
+
'id': 'bg-content',
|
|
28
|
+
'kind': 'background',
|
|
29
|
+
'role': 'content',
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
archetypes = [{'name': 'content', 'slots': []}]
|
|
33
|
+
|
|
34
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
35
|
+
emit_body(
|
|
36
|
+
data,
|
|
37
|
+
tokens,
|
|
38
|
+
fonts,
|
|
39
|
+
roles,
|
|
40
|
+
assets,
|
|
41
|
+
archetypes,
|
|
42
|
+
[],
|
|
43
|
+
{},
|
|
44
|
+
output_dir,
|
|
45
|
+
)
|
|
46
|
+
with open(os.path.join(output_dir, 'body.md'), encoding='utf-8') as stream:
|
|
47
|
+
body = stream.read()
|
|
48
|
+
|
|
49
|
+
self.assertIn('是全局 token', body)
|
|
50
|
+
self.assertIn('局部 slot / decor 的 `css` 优先', body)
|
|
51
|
+
self.assertIn('字体使用 Typography 的完整栈与降级', body)
|
|
52
|
+
self.assertIn('将包内 `assets/` 复制到项目内相对目录', body)
|
|
53
|
+
self.assertIn('本机绝对路径', body)
|
|
54
|
+
self.assertIn('背景、资产和本段规则均来自本风格包', body)
|
|
55
|
+
self.assertIn('页面无资源加载失败、内容溢出或画幅裁切', body)
|
|
56
|
+
self.assertIn('沿用该页型已有的标题层级与局部 `css`', body)
|
|
57
|
+
self.assertIn('背景中已经可见的固定标题不再创建文本', body)
|
|
58
|
+
self.assertIn('没有 `subtitle` 槽就不新增副标题', body)
|
|
59
|
+
self.assertIn('区带自带 `margin: [左, 右]` 时用它的', body)
|
|
60
|
+
self.assertIn('必须完整阅读本 `design.md` 和 `layouts.md`', body)
|
|
61
|
+
self.assertIn('确认全部页型后再开始搭页', body)
|
|
62
|
+
self.assertIn('不能只看摘要、前几个页型', body)
|
|
63
|
+
self.assertIn('data-pptx-layout="<页型名>"', body)
|
|
64
|
+
self.assertIn('data-pptx-asset="<asset id>"', body)
|
|
65
|
+
self.assertIn(
|
|
66
|
+
'position:absolute;left:<x>px;top:<y>px;width:<w>px;height:<h>px',
|
|
67
|
+
body,
|
|
68
|
+
)
|
|
69
|
+
self.assertIn('元素必须可见', body)
|
|
70
|
+
self.assertIn('`box` 使用全画布 `[0, 0, 1920, 1080]`', body)
|
|
71
|
+
self.assertIn('该页型绑定的背景与图片资产均已使用', body)
|
|
72
|
+
self.assertIn('不得省略或换图', body)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == '__main__':
|
|
76
|
+
unittest.main()
|