forked from niklasf/python-chess
-
Notifications
You must be signed in to change notification settings - Fork 0
/
svg.py
517 lines (432 loc) · 32.1 KB
/
svg.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
from __future__ import annotations
import math
import xml.etree.ElementTree as ET
import chess
from typing import Dict, Iterable, Optional, Tuple, Union
from chess import Color, IntoSquareSet, Square
SQUARE_SIZE = 45
MARGIN = 20
PIECES = {
"b": """<g id="black-bishop" class="black bishop" fill="none" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 36c3.39-.97 10.11.43 13.5-2 3.39 2.43 10.11 1.03 13.5 2 0 0 1.65.54 3 2-.68.97-1.65.99-3 .5-3.39-.97-10.11.46-13.5-1-3.39 1.46-10.11.03-13.5 1-1.354.49-2.323.47-3-.5 1.354-1.94 3-2 3-2zm6-4c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2zM25 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z" fill="#000" stroke-linecap="butt"/><path d="M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5" stroke="#fff" stroke-linejoin="miter"/></g>""", # noqa: E501
"k": """<g id="black-king" class="black king" fill="none" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22.5 11.63V6" stroke-linejoin="miter"/><path d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5" fill="#000" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M11.5 37c5.5 3.5 15.5 3.5 21 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-3.5-7.5-13-10.5-16-4-3 6 5 10 5 10V37z" fill="#000"/><path d="M20 8h5" stroke-linejoin="miter"/><path d="M32 29.5s8.5-4 6.03-9.65C34.15 14 25 18 22.5 24.5l.01 2.1-.01-2.1C20 18 9.906 14 6.997 19.85c-2.497 5.65 4.853 9 4.853 9M11.5 30c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0" stroke="#fff"/></g>""", # noqa: E501
"n": """<g id="black-knight" class="black knight" fill="none" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18" style="fill:#000000; stroke:#000000;"/><path d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10" style="fill:#000000; stroke:#000000;"/><path d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z" style="fill:#ececec; stroke:#ececec;"/><path d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z" transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)" style="fill:#ececec; stroke:#ececec;"/><path d="M 24.55,10.4 L 24.1,11.85 L 24.6,12 C 27.75,13 30.25,14.49 32.5,18.75 C 34.75,23.01 35.75,29.06 35.25,39 L 35.2,39.5 L 37.45,39.5 L 37.5,39 C 38,28.94 36.62,22.15 34.25,17.66 C 31.88,13.17 28.46,11.02 25.06,10.5 L 24.55,10.4 z " style="fill:#ececec; stroke:none;"/></g>""", # noqa: E501
"p": """<g id="black-pawn" class="black pawn"><path d="M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z" fill="#000" stroke="#000" stroke-width="1.5" stroke-linecap="round"/></g>""", # noqa: E501
"q": """<g id="black-queen" class="black queen" fill="#000" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><g fill="#000" stroke="none"><circle cx="6" cy="12" r="2.75"/><circle cx="14" cy="9" r="2.75"/><circle cx="22.5" cy="8" r="2.75"/><circle cx="31" cy="9" r="2.75"/><circle cx="39" cy="12" r="2.75"/></g><path d="M9 26c8.5-1.5 21-1.5 27 0l2.5-12.5L31 25l-.3-14.1-5.2 13.6-3-14.5-3 14.5-5.2-13.6L14 25 6.5 13.5 9 26zM9 26c0 2 1.5 2 2.5 4 1 1.5 1 1 .5 3.5-1.5 1-1.5 2.5-1.5 2.5-1.5 1.5.5 2.5.5 2.5 6.5 1 16.5 1 23 0 0 0 1.5-1 0-2.5 0 0 .5-1.5-1-2.5-.5-2.5-.5-2 .5-3.5 1-2 2.5-2 2.5-4-8.5-1.5-18.5-1.5-27 0z" stroke-linecap="butt"/><path d="M11 38.5a35 35 1 0 0 23 0" fill="none" stroke-linecap="butt"/><path d="M11 29a35 35 1 0 1 23 0M12.5 31.5h20M11.5 34.5a35 35 1 0 0 22 0M10.5 37.5a35 35 1 0 0 24 0" fill="none" stroke="#fff"/></g>""", # noqa: E501
"r": """<g id="black-rook" class="black rook" fill="#000" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 39h27v-3H9v3zM12.5 32l1.5-2.5h17l1.5 2.5h-20zM12 36v-4h21v4H12z" stroke-linecap="butt"/><path d="M14 29.5v-13h17v13H14z" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M14 16.5L11 14h23l-3 2.5H14zM11 14V9h4v2h5V9h5v2h5V9h4v5H11z" stroke-linecap="butt"/><path d="M12 35.5h21M13 31.5h19M14 29.5h17M14 16.5h17M11 14h23" fill="none" stroke="#fff" stroke-width="1" stroke-linejoin="miter"/></g>""", # noqa: E501
"B": """<g id="white-bishop" class="white bishop" fill="none" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><g fill="#fff" stroke-linecap="butt"><path d="M9 36c3.39-.97 10.11.43 13.5-2 3.39 2.43 10.11 1.03 13.5 2 0 0 1.65.54 3 2-.68.97-1.65.99-3 .5-3.39-.97-10.11.46-13.5-1-3.39 1.46-10.11.03-13.5 1-1.354.49-2.323.47-3-.5 1.354-1.94 3-2 3-2zM15 32c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2zM25 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z"/></g><path d="M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5" stroke-linejoin="miter"/></g>""", # noqa: E501
"K": """<g id="white-king" class="white king" fill="none" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22.5 11.63V6M20 8h5" stroke-linejoin="miter"/><path d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5" fill="#fff" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M11.5 37c5.5 3.5 15.5 3.5 21 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-3.5-7.5-13-10.5-16-4-3 6 5 10 5 10V37z" fill="#fff"/><path d="M11.5 30c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0"/></g>""", # noqa: E501
"N": """<g id="white-knight" class="white knight" fill="none" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18" style="fill:#ffffff; stroke:#000000;"/><path d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10" style="fill:#ffffff; stroke:#000000;"/><path d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z" style="fill:#000000; stroke:#000000;"/><path d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z" transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)" style="fill:#000000; stroke:#000000;"/></g>""", # noqa: E501
"P": """<g id="white-pawn" class="white pawn"><path d="M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z" fill="#fff" stroke="#000" stroke-width="1.5" stroke-linecap="round"/></g>""", # noqa: E501
"Q": """<g id="white-queen" class="white queen" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 12a2 2 0 1 1-4 0 2 2 0 1 1 4 0zM24.5 7.5a2 2 0 1 1-4 0 2 2 0 1 1 4 0zM41 12a2 2 0 1 1-4 0 2 2 0 1 1 4 0zM16 8.5a2 2 0 1 1-4 0 2 2 0 1 1 4 0zM33 9a2 2 0 1 1-4 0 2 2 0 1 1 4 0z"/><path d="M9 26c8.5-1.5 21-1.5 27 0l2-12-7 11V11l-5.5 13.5-3-15-3 15-5.5-14V25L7 14l2 12zM9 26c0 2 1.5 2 2.5 4 1 1.5 1 1 .5 3.5-1.5 1-1.5 2.5-1.5 2.5-1.5 1.5.5 2.5.5 2.5 6.5 1 16.5 1 23 0 0 0 1.5-1 0-2.5 0 0 .5-1.5-1-2.5-.5-2.5-.5-2 .5-3.5 1-2 2.5-2 2.5-4-8.5-1.5-18.5-1.5-27 0z" stroke-linecap="butt"/><path d="M11.5 30c3.5-1 18.5-1 22 0M12 33.5c6-1 15-1 21 0" fill="none"/></g>""", # noqa: E501
"R": """<g id="white-rook" class="white rook" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 39h27v-3H9v3zM12 36v-4h21v4H12zM11 14V9h4v2h5V9h5v2h5V9h4v5" stroke-linecap="butt"/><path d="M34 14l-3 3H14l-3-3"/><path d="M31 17v12.5H14V17" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M31 29.5l1.5 2.5h-20l1.5-2.5"/><path d="M11 14h23" fill="none" stroke-linejoin="miter"/></g>""", # noqa: E501
}
COORDS = {
"1": """<path d="M6.754 26.996h2.578v-8.898l-2.805.562v-1.437l2.79-.563h1.578v10.336h2.578v1.328h-6.72z"/>""", # noqa: E501
"2": """<path d="M8.195 26.996h5.508v1.328H6.297v-1.328q.898-.93 2.445-2.492 1.555-1.57 1.953-2.024.758-.851 1.055-1.437.305-.594.305-1.164 0-.93-.657-1.516-.648-.586-1.695-.586-.742 0-1.57.258-.82.258-1.758.781v-1.593q.953-.383 1.781-.578.828-.196 1.516-.196 1.812 0 2.89.906 1.079.907 1.079 2.422 0 .72-.274 1.368-.265.64-.976 1.515-.196.227-1.243 1.313-1.046 1.078-2.953 3.023z"/>""", # noqa: E501
"3": """<path d="M11.434 22.035q1.132.242 1.765 1.008.64.766.64 1.89 0 1.727-1.187 2.672-1.187.946-3.375.946-.734 0-1.515-.149-.774-.14-1.602-.43V26.45q.656.383 1.438.578.78.196 1.632.196 1.485 0 2.258-.586.782-.586.782-1.703 0-1.032-.727-1.61-.719-.586-2.008-.586h-1.36v-1.297h1.423q1.164 0 1.78-.46.618-.47.618-1.344 0-.899-.64-1.375-.633-.485-1.82-.485-.65 0-1.391.141-.743.14-1.633.437V16.95q.898-.25 1.68-.375.788-.125 1.484-.125 1.797 0 2.844.82 1.046.813 1.046 2.204 0 .968-.554 1.64-.555.664-1.578.922z"/>""", # noqa: E501
"4": """<path d="M11.016 18.035L7.03 24.262h3.985zm-.414-1.375h1.984v7.602h1.664v1.312h-1.664v2.75h-1.57v-2.75H5.75v-1.523z"/>""", # noqa: E501
"5": """<path d="M6.719 16.66h6.195v1.328h-4.75v2.86q.344-.118.688-.172.343-.063.687-.063 1.953 0 3.094 1.07 1.14 1.07 1.14 2.899 0 1.883-1.171 2.93-1.172 1.039-3.305 1.039-.735 0-1.5-.125-.758-.125-1.57-.375v-1.586q.703.383 1.453.57.75.188 1.586.188 1.351 0 2.14-.711.79-.711.79-1.93 0-1.219-.79-1.93-.789-.71-2.14-.71-.633 0-1.266.14-.625.14-1.281.438z"/>""", # noqa: E501
"6": """<path d="M10.137 21.863q-1.063 0-1.688.727-.617.726-.617 1.992 0 1.258.617 1.992.625.727 1.688.727 1.062 0 1.68-.727.624-.734.624-1.992 0-1.266-.625-1.992-.617-.727-1.68-.727zm3.133-4.945v1.437q-.594-.28-1.204-.43-.601-.148-1.195-.148-1.562 0-2.39 1.055-.82 1.055-.938 3.188.46-.68 1.156-1.04.696-.367 1.531-.367 1.758 0 2.774 1.07 1.023 1.063 1.023 2.899 0 1.797-1.062 2.883-1.063 1.086-2.828 1.086-2.024 0-3.094-1.547-1.07-1.555-1.07-4.5 0-2.766 1.312-4.406 1.313-1.649 3.524-1.649.593 0 1.195.117.61.118 1.266.352z"/>""", # noqa: E501
"7": """<path d="M6.25 16.66h7.5v.672L9.516 28.324H7.867l3.985-10.336H6.25z"/>""", # noqa: E501
"8": """<path d="M10 22.785q-1.125 0-1.773.602-.641.601-.641 1.656t.64 1.656q.649.602 1.774.602t1.773-.602q.649-.61.649-1.656 0-1.055-.649-1.656-.64-.602-1.773-.602zm-1.578-.672q-1.016-.25-1.586-.945-.563-.695-.563-1.695 0-1.399.993-2.211 1-.813 2.734-.813 1.742 0 2.734.813.993.812.993 2.21 0 1-.57 1.696-.563.695-1.571.945 1.14.266 1.773 1.04.641.773.641 1.89 0 1.695-1.04 2.602-1.03.906-2.96.906t-2.969-.906Q6 26.738 6 25.043q0-1.117.64-1.89.641-.774 1.782-1.04zm-.578-2.492q0 .906.562 1.414.57.508 1.594.508 1.016 0 1.586-.508.578-.508.578-1.414 0-.906-.578-1.414-.57-.508-1.586-.508-1.023 0-1.594.508-.562.508-.562 1.414z"/>""", # noqa: E501
"a": """<path d="M23.328 10.016q-1.742 0-2.414.398-.672.398-.672 1.36 0 .765.5 1.218.508.445 1.375.445 1.196 0 1.914-.843.727-.852.727-2.258v-.32zm2.867-.594v4.992h-1.437v-1.328q-.492.797-1.227 1.18-.734.375-1.797.375-1.343 0-2.14-.75-.79-.758-.79-2.024 0-1.476.985-2.226.992-.75 2.953-.75h2.016V8.75q0-.992-.656-1.531-.649-.547-1.829-.547-.75 0-1.46.18-.711.18-1.368.539V6.062q.79-.304 1.532-.453.742-.156 1.445-.156 1.898 0 2.836.984.937.985.937 2.985z"/>""", # noqa: E501
"b": """<path d="M24.922 10.047q0-1.586-.656-2.485-.649-.906-1.79-.906-1.14 0-1.796.906-.649.899-.649 2.485 0 1.586.649 2.492.656.898 1.797.898 1.14 0 1.789-.898.656-.906.656-2.492zm-4.89-3.055q.452-.781 1.14-1.156.695-.383 1.656-.383 1.594 0 2.586 1.266 1 1.265 1 3.328 0 2.062-1 3.328-.992 1.266-2.586 1.266-.96 0-1.656-.375-.688-.383-1.14-1.164v1.312h-1.446V2.258h1.445z"/>""", # noqa: E501
"c": """<path d="M25.96 6v1.344q-.608-.336-1.226-.5-.609-.172-1.234-.172-1.398 0-2.172.89-.773.883-.773 2.485 0 1.601.773 2.492.774.883 2.172.883.625 0 1.234-.164.618-.172 1.227-.508v1.328q-.602.281-1.25.422-.64.14-1.367.14-1.977 0-3.14-1.242-1.165-1.242-1.165-3.351 0-2.14 1.172-3.367 1.18-1.227 3.227-1.227.664 0 1.296.14.633.134 1.227.407z"/>""", # noqa: E501
"d": """<path d="M24.973 6.992V2.258h1.437v12.156h-1.437v-1.312q-.453.78-1.149 1.164-.687.375-1.656.375-1.586 0-2.586-1.266-.992-1.266-.992-3.328 0-2.063.992-3.328 1-1.266 2.586-1.266.969 0 1.656.383.696.375 1.149 1.156zm-4.899 3.055q0 1.586.649 2.492.656.898 1.797.898 1.14 0 1.796-.898.657-.906.657-2.492 0-1.586-.657-2.485-.656-.906-1.796-.906-1.141 0-1.797.906-.649.899-.649 2.485z"/>""", # noqa: E501
"e": """<path d="M26.555 9.68v.703h-6.61q.094 1.484.89 2.265.806.774 2.235.774.828 0 1.602-.203.781-.203 1.547-.61v1.36q-.774.328-1.586.5-.813.172-1.649.172-2.093 0-3.32-1.22-1.219-1.218-1.219-3.296 0-2.148 1.157-3.406 1.164-1.266 3.132-1.266 1.766 0 2.79 1.14 1.03 1.134 1.03 3.087zm-1.438-.422q-.015-1.18-.664-1.883-.64-.703-1.703-.703-1.203 0-1.93.68-.718.68-.828 1.914z"/>""", # noqa: E501
"f": """<path d="M25.285 2.258v1.195H23.91q-.773 0-1.078.313-.297.312-.297 1.125v.773h2.367v1.117h-2.367v7.633H21.09V6.781h-1.375V5.664h1.375v-.61q0-1.46.68-2.124.68-.672 2.156-.672z"/>""", # noqa: E501
"g": """<path d="M24.973 9.937q0-1.562-.649-2.421-.64-.86-1.804-.86-1.157 0-1.805.86-.64.859-.64 2.421 0 1.555.64 2.415.648.859 1.805.859 1.164 0 1.804-.86.649-.859.649-2.414zm1.437 3.391q0 2.234-.992 3.32-.992 1.094-3.04 1.094-.757 0-1.429-.117-.672-.11-1.304-.344v-1.398q.632.344 1.25.508.617.164 1.257.164 1.414 0 2.118-.743.703-.734.703-2.226v-.711q-.446.773-1.141 1.156-.695.383-1.664.383-1.61 0-2.594-1.227-.984-1.226-.984-3.25 0-2.03.984-3.257.985-1.227 2.594-1.227.969 0 1.664.383t1.14 1.156V5.664h1.438z"/>""", # noqa: E501
"h": """<path d="M26.164 9.133v5.281h-1.437V9.18q0-1.243-.485-1.86-.484-.617-1.453-.617-1.164 0-1.836.742-.672.742-.672 2.024v4.945h-1.445V2.258h1.445v4.765q.516-.789 1.211-1.18.703-.39 1.617-.39 1.508 0 2.282.938.773.93.773 2.742z"/>""", # noqa: E501
}
XX = """<g id="xx"><path d="M35.865 9.135a1.89 1.89 0 0 1 0 2.673L25.173 22.5l10.692 10.692a1.89 1.89 0 0 1 0 2.673 1.89 1.89 0 0 1-2.673 0L22.5 25.173 11.808 35.865a1.89 1.89 0 0 1-2.673 0 1.89 1.89 0 0 1 0-2.673L19.827 22.5 9.135 11.808a1.89 1.89 0 0 1 0-2.673 1.89 1.89 0 0 1 2.673 0L22.5 19.827 33.192 9.135a1.89 1.89 0 0 1 2.673 0z" fill="#000" stroke="#fff" stroke-width="1.688"/></g>""" # noqa: E501
CHECK_GRADIENT = """<radialGradient id="check_gradient" r="0.5"><stop offset="0%" stop-color="#ff0000" stop-opacity="1.0" /><stop offset="50%" stop-color="#e70000" stop-opacity="1.0" /><stop offset="100%" stop-color="#9e0000" stop-opacity="0.0" /></radialGradient>""" # noqa: E501
DEFAULT_COLORS = {
"square light": "#ffce9e",
"square dark": "#d18b47",
"square dark lastmove": "#aaa23b",
"square light lastmove": "#cdd16a",
"margin": "#212121",
"inner border": "#111",
"outer border": "#111",
"coord": "#e5e5e5",
"arrow green": "#15781B80",
"arrow red": "#88202080",
"arrow yellow": "#e68f00b3",
"arrow blue": "#00308880",
}
class Arrow:
"""Details of an arrow to be drawn."""
tail: Square
"""Start square of the arrow."""
head: Square
"""End square of the arrow."""
color: str
"""Arrow color."""
def __init__(self, tail: Square, head: Square, *, color: str = "green") -> None:
self.tail = tail
self.head = head
self.color = color
def pgn(self) -> str:
"""
Returns the arrow in the format used by ``[%csl ...]`` and
``[%cal ...]`` PGN annotations, e.g., ``Ga1`` or ``Ya2h2``.
Colors other than ``red``, ``yellow``, and ``blue`` default to green.
"""
if self.color == "red":
color = "R"
elif self.color == "yellow":
color = "Y"
elif self.color == "blue":
color = "B"
else:
color = "G"
if self.tail == self.head:
return f"{color}{chess.SQUARE_NAMES[self.tail]}"
else:
return f"{color}{chess.SQUARE_NAMES[self.tail]}{chess.SQUARE_NAMES[self.head]}"
def __str__(self) -> str:
return self.pgn()
def __repr__(self) -> str:
return f"Arrow({chess.SQUARE_NAMES[self.tail].upper()}, {chess.SQUARE_NAMES[self.head].upper()}, color={self.color!r})"
@classmethod
def from_pgn(cls, pgn: str) -> Arrow:
"""
Parses an arrow from the format used by ``[%csl ...]`` and
``[%cal ...]`` PGN annotations, e.g., ``Ga1`` or ``Ya2h2``.
Also allows skipping the color prefix, defaulting to green.
:raises: :exc:`ValueError` if the format is invalid.
"""
if pgn.startswith("G"):
color = "green"
pgn = pgn[1:]
elif pgn.startswith("R"):
color = "red"
pgn = pgn[1:]
elif pgn.startswith("Y"):
color = "yellow"
pgn = pgn[1:]
elif pgn.startswith("B"):
color = "blue"
pgn = pgn[1:]
else:
color = "green"
tail = chess.parse_square(pgn[:2])
head = chess.parse_square(pgn[2:]) if len(pgn) > 2 else tail
return cls(tail, head, color=color)
class SvgWrapper(str):
def _repr_svg_(self) -> SvgWrapper:
return self
def _svg(viewbox: int, size: Optional[int]) -> ET.Element:
svg = ET.Element("svg", {
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink",
"viewBox": f"0 0 {viewbox:d} {viewbox:d}",
})
if size is not None:
svg.set("width", str(size))
svg.set("height", str(size))
return svg
def _attrs(attrs: Dict[str, Union[str, int, float, None]]) -> Dict[str, str]:
return {k: str(v) for k, v in attrs.items() if v is not None}
def _select_color(colors: Dict[str, str], color: str) -> Tuple[str, float]:
return _color(colors.get(color, DEFAULT_COLORS[color]))
def _color(color: str) -> Tuple[str, float]:
if color.startswith("#"):
try:
if len(color) == 5:
return color[:4], int(color[4], 16) / 0xf
elif len(color) == 9:
return color[:7], int(color[7:], 16) / 0xff
except ValueError:
pass # Ignore invalid hex value
return color, 1.0
def _coord(text: str, x: int, y: int, width: int, height: int, horizontal: bool, margin: int, *, color: str, opacity: float) -> ET.Element:
scale = margin / MARGIN
if horizontal:
x += int(width - scale * width) // 2
else:
y += int(height - scale * height) // 2
t = ET.Element("g", _attrs({
"transform": f"translate({x}, {y}) scale({scale}, {scale})",
"fill": color,
"stroke": color,
"opacity": opacity if opacity < 1.0 else None,
}))
t.append(ET.fromstring(COORDS[text]))
return t
def piece(piece: chess.Piece, size: Optional[int] = None) -> str:
"""
Renders the given :class:`chess.Piece` as an SVG image.
>>> import chess
>>> import chess.svg
>>>
>>> chess.svg.piece(chess.Piece.from_symbol("R")) # doctest: +SKIP
.. image:: ../docs/wR.svg
:alt: R
"""
svg = _svg(SQUARE_SIZE, size)
svg.append(ET.fromstring(PIECES[piece.symbol()]))
return SvgWrapper(ET.tostring(svg).decode("utf-8"))
def board(board: Optional[chess.BaseBoard] = None, *,
orientation: Color = chess.WHITE,
lastmove: Optional[chess.Move] = None,
check: Optional[Square] = None,
arrows: Iterable[Union[Arrow, Tuple[Square, Square]]] = [],
fill: Dict[Square, str] = {},
squares: Optional[IntoSquareSet] = None,
size: Optional[int] = None,
coordinates: bool = True,
colors: Dict[str, str] = {},
flipped: bool = False,
borders: bool = False,
style: Optional[str] = None) -> str:
"""
Renders a board with pieces and/or selected squares as an SVG image.
:param board: A :class:`chess.BaseBoard` for a chessboard with pieces, or
``None`` (the default) for a chessboard without pieces.
:param orientation: The point of view, defaulting to ``chess.WHITE``.
:param lastmove: A :class:`chess.Move` to be highlighted.
:param check: A square to be marked indicating a check.
:param arrows: A list of :class:`~chess.svg.Arrow` objects, like
``[chess.svg.Arrow(chess.E2, chess.E4)]``, or a list of tuples, like
``[(chess.E2, chess.E4)]``. An arrow from a square pointing to the same
square is drawn as a circle, like ``[(chess.E2, chess.E2)]``.
:param fill: A dictionary mapping squares to a colors that they should be
filled with.
:param squares: A :class:`chess.SquareSet` with selected squares to mark
with an X.
:param size: The size of the image in pixels (e.g., ``400`` for a 400 by
400 board), or ``None`` (the default) for no size limit.
:param coordinates: Pass ``False`` to disable the coordinate margin.
:param colors: A dictionary to override default colors. Possible keys are
``square light``, ``square dark``, ``square light lastmove``,
``square dark lastmove``, ``margin``, ``coord``, ``inner border``,
``outer border``, ``arrow green``, ``arrow blue``, ``arrow red``,
and ``arrow yellow``. Values should look like ``#ffce9e`` (opaque),
or ``#15781B80`` (transparent).
:param flipped: Pass ``True`` to flip the board.
:param borders: Pass ``True`` to enable a border around the board and,
(if *coordinates* is enabled) the coordinate margin.
:param style: A CSS stylesheet to include in the SVG image.
>>> import chess
>>> import chess.svg
>>>
>>> board = chess.Board("8/8/8/8/4N3/8/8/8 w - - 0 1")
>>>
>>> chess.svg.board(
... board,
... fill=dict.fromkeys(board.attacks(chess.E4), "#cc0000cc"),
... arrows=[chess.svg.Arrow(chess.E4, chess.F6, color="#0000cccc")],
... squares=chess.SquareSet(chess.BB_DARK_SQUARES & chess.BB_FILE_B),
... size=350,
... ) # doctest: +SKIP
.. image:: ../docs/Ne4.svg
:alt: 8/8/8/8/4N3/8/8/8
.. deprecated:: 1.1
Use *orientation* with a color instead of the *flipped* toggle.
"""
orientation ^= flipped
inner_border = 1 if borders and coordinates else 0
outer_border = 1 if borders else 0
margin = 15 if coordinates else 0
full_size = 2 * outer_border + 2 * margin + 2 * inner_border + 8 * SQUARE_SIZE
svg = _svg(full_size, size)
if style:
ET.SubElement(svg, "style").text = style
if board:
desc = ET.SubElement(svg, "desc")
asciiboard = ET.SubElement(desc, "pre")
asciiboard.text = str(board)
defs = ET.SubElement(svg, "defs")
if board:
for piece_color in chess.COLORS:
for piece_type in chess.PIECE_TYPES:
if board.pieces_mask(piece_type, piece_color):
defs.append(ET.fromstring(PIECES[chess.Piece(piece_type, piece_color).symbol()]))
squares = chess.SquareSet(squares) if squares else chess.SquareSet()
if squares:
defs.append(ET.fromstring(XX))
if check is not None:
defs.append(ET.fromstring(CHECK_GRADIENT))
if outer_border:
outer_border_color, outer_border_opacity = _select_color(colors, "outer border")
ET.SubElement(svg, "rect", _attrs({
"x": outer_border / 2,
"y": outer_border / 2,
"width": full_size - outer_border,
"height": full_size - outer_border,
"fill": "none",
"stroke": outer_border_color,
"stroke-width": outer_border,
"opacity": outer_border_opacity if outer_border_opacity < 1.0 else None,
}))
if margin:
margin_color, margin_opacity = _select_color(colors, "margin")
ET.SubElement(svg, "rect", _attrs({
"x": outer_border + margin / 2,
"y": outer_border + margin / 2,
"width": full_size - 2 * outer_border - margin,
"height": full_size - 2 * outer_border - margin,
"fill": "none",
"stroke": margin_color,
"stroke-width": margin,
"opacity": margin_opacity if margin_opacity < 1.0 else None,
}))
if inner_border:
inner_border_color, inner_border_opacity = _select_color(colors, "inner border")
ET.SubElement(svg, "rect", _attrs({
"x": outer_border + margin + inner_border / 2,
"y": outer_border + margin + inner_border / 2,
"width": full_size - 2 * outer_border - 2 * margin - inner_border,
"height": full_size - 2 * outer_border - 2 * margin - inner_border,
"fill": "none",
"stroke": inner_border_color,
"stroke-width": inner_border,
"opacity": inner_border_opacity if inner_border_opacity < 1.0 else None,
}))
# Render coordinates.
if coordinates:
coord_color, coord_opacity = _select_color(colors, "coord")
for file_index, file_name in enumerate(chess.FILE_NAMES):
x = (file_index if orientation else 7 - file_index) * SQUARE_SIZE + inner_border + margin + outer_border
# Keep some padding here to separate the ascender from the border
svg.append(_coord(file_name, x, 1, SQUARE_SIZE, margin, True, margin, color=coord_color, opacity=coord_opacity))
svg.append(_coord(file_name, x, full_size - outer_border - margin, SQUARE_SIZE, margin, True, margin, color=coord_color, opacity=coord_opacity))
for rank_index, rank_name in enumerate(chess.RANK_NAMES):
y = (7 - rank_index if orientation else rank_index) * SQUARE_SIZE + inner_border + margin + outer_border
svg.append(_coord(rank_name, 0, y, margin, SQUARE_SIZE, False, margin, color=coord_color, opacity=coord_opacity))
svg.append(_coord(rank_name, full_size - outer_border - margin, y, margin, SQUARE_SIZE, False, margin, color=coord_color, opacity=coord_opacity))
# Render board.
for square, bb in enumerate(chess.BB_SQUARES):
file_index = chess.square_file(square)
rank_index = chess.square_rank(square)
x = (file_index if orientation else 7 - file_index) * SQUARE_SIZE + inner_border + margin + outer_border
y = (7 - rank_index if orientation else rank_index) * SQUARE_SIZE + inner_border + margin + outer_border
cls = ["square", "light" if chess.BB_LIGHT_SQUARES & bb else "dark"]
if lastmove and square in [lastmove.from_square, lastmove.to_square]:
cls.append("lastmove")
square_color, square_opacity = _select_color(colors, " ".join(cls))
cls.append(chess.SQUARE_NAMES[square])
ET.SubElement(svg, "rect", _attrs({
"x": x,
"y": y,
"width": SQUARE_SIZE,
"height": SQUARE_SIZE,
"class": " ".join(cls),
"stroke": "none",
"fill": square_color,
"opacity": square_opacity if square_opacity < 1.0 else None,
}))
try:
fill_color, fill_opacity = _color(fill[square])
except KeyError:
pass
else:
ET.SubElement(svg, "rect", _attrs({
"x": x,
"y": y,
"width": SQUARE_SIZE,
"height": SQUARE_SIZE,
"stroke": "none",
"fill": fill_color,
"opacity": fill_opacity if fill_opacity < 1.0 else None,
}))
# Render check mark.
if check is not None:
file_index = chess.square_file(check)
rank_index = chess.square_rank(check)
x = (file_index if orientation else 7 - file_index) * SQUARE_SIZE + margin
y = (7 - rank_index if orientation else rank_index) * SQUARE_SIZE + margin
ET.SubElement(svg, "rect", _attrs({
"x": x,
"y": y,
"width": SQUARE_SIZE,
"height": SQUARE_SIZE,
"class": "check",
"fill": "url(#check_gradient)",
}))
# Render pieces and selected squares.
for square, bb in enumerate(chess.BB_SQUARES):
file_index = chess.square_file(square)
rank_index = chess.square_rank(square)
x = (file_index if orientation else 7 - file_index) * SQUARE_SIZE + margin
y = (7 - rank_index if orientation else rank_index) * SQUARE_SIZE + margin
if board is not None:
piece = board.piece_at(square)
if piece:
href = f"#{chess.COLOR_NAMES[piece.color]}-{chess.PIECE_NAMES[piece.piece_type]}"
ET.SubElement(svg, "use", {
"href": href,
"xlink:href": href,
"transform": f"translate({x:d}, {y:d})",
})
# Render selected squares.
if square in squares:
ET.SubElement(svg, "use", _attrs({
"href": "#xx",
"xlink:href": "#xx",
"x": x,
"y": y,
}))
# Render arrows.
for arrow in arrows:
try:
tail, head, color = arrow.tail, arrow.head, arrow.color # type: ignore
except AttributeError:
tail, head = arrow # type: ignore
color = "green"
try:
color, opacity = _select_color(colors, " ".join(["arrow", color]))
except KeyError:
opacity = 1.0
tail_file = chess.square_file(tail)
tail_rank = chess.square_rank(tail)
head_file = chess.square_file(head)
head_rank = chess.square_rank(head)
xtail = outer_border + margin + inner_border + (tail_file + 0.5 if orientation else 7.5 - tail_file) * SQUARE_SIZE
ytail = outer_border + margin + inner_border + (7.5 - tail_rank if orientation else tail_rank + 0.5) * SQUARE_SIZE
xhead = outer_border + margin + inner_border + (head_file + 0.5 if orientation else 7.5 - head_file) * SQUARE_SIZE
yhead = outer_border + margin + inner_border + (7.5 - head_rank if orientation else head_rank + 0.5) * SQUARE_SIZE
if (head_file, head_rank) == (tail_file, tail_rank):
ET.SubElement(svg, "circle", _attrs({
"cx": xhead,
"cy": yhead,
"r": SQUARE_SIZE * 0.9 / 2,
"stroke-width": SQUARE_SIZE * 0.1,
"stroke": color,
"opacity": opacity if opacity < 1.0 else None,
"fill": "none",
"class": "circle",
}))
else:
marker_size = 0.75 * SQUARE_SIZE
marker_margin = 0.1 * SQUARE_SIZE
dx, dy = xhead - xtail, yhead - ytail
hypot = math.hypot(dx, dy)
shaft_x = xhead - dx * (marker_size + marker_margin) / hypot
shaft_y = yhead - dy * (marker_size + marker_margin) / hypot
xtip = xhead - dx * marker_margin / hypot
ytip = yhead - dy * marker_margin / hypot
ET.SubElement(svg, "line", _attrs({
"x1": xtail,
"y1": ytail,
"x2": shaft_x,
"y2": shaft_y,
"stroke": color,
"opacity": opacity if opacity < 1.0 else None,
"stroke-width": SQUARE_SIZE * 0.2,
"stroke-linecap": "butt",
"class": "arrow",
}))
marker = [(xtip, ytip),
(shaft_x + dy * 0.5 * marker_size / hypot,
shaft_y - dx * 0.5 * marker_size / hypot),
(shaft_x - dy * 0.5 * marker_size / hypot,
shaft_y + dx * 0.5 * marker_size / hypot)]
ET.SubElement(svg, "polygon", _attrs({
"points": " ".join(f"{x},{y}" for x, y in marker),
"fill": color,
"opacity": opacity if opacity < 1.0 else None,
"class": "arrow",
}))
return SvgWrapper(ET.tostring(svg).decode("utf-8"))