sf_symbol_converter 0.1.0
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.
- checksums.yaml +7 -0
- data/assets/template.svg +95 -0
- data/bin/sf-symbol-converter +41 -0
- data/lib/sf_symbol_converter.rb +122 -0
- data/lib/trimmer/template_trimmer.rb +31 -0
- data/lib/utils.rb +12 -0
- data/lib/validators/icon_validator.rb +28 -0
- data/lib/validators/template_validator.rb +62 -0
- metadata +94 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a73ca5114d60907080d27034713195e7dfd459aae8f332f8d9ca9dcdb0ff03da
|
|
4
|
+
data.tar.gz: f9336910f51849ebca69a10561dacfcdefac5a0c0de792a75a74af9f5e76158f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6922fe44fefd4ac87ad6a2f76a9245d63c19eead6338ea959049725da965b10792066204df14e3a60362b6e31c361dc964dc952c7cd74728d68682bab0ccf4a3
|
|
7
|
+
data.tar.gz: d2caa2b4f7bb4fa1c4c1b20b26a7e37d56acddd222042ad1d0409fa7c6e9ccab71b86ce6b56745deaa44e804551e8320e8ae79481dd9b52108b4db7009343ae9
|
data/assets/template.svg
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!--Generator: Apple Native CoreSVG 232.5-->
|
|
3
|
+
<!DOCTYPE svg
|
|
4
|
+
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
|
5
|
+
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
6
|
+
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="3300" height="2200">
|
|
7
|
+
<!--glyph: "uni100000.medium", point size: 100.0, font version: "19.2d2e1", template writer version: "128"-->
|
|
8
|
+
<style>.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
|
|
9
|
+
</style>
|
|
10
|
+
<g id="Notes">
|
|
11
|
+
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
|
|
12
|
+
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
|
|
13
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
|
|
14
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
|
|
15
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
|
|
16
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
|
|
17
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
|
|
18
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
|
|
19
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
|
|
20
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
|
|
21
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
|
|
22
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
|
|
23
|
+
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
|
|
24
|
+
<g transform="matrix(0.2 0 0 0.2 263 1933)">
|
|
25
|
+
<path d="m46.2402 4.15039c21.5332 0 39.4531-17.8711 39.4531-39.4043s-17.9688-39.4043-39.502-39.4043c-21.4844 0-39.3555 17.8711-39.3555 39.4043s17.9199 39.4043 39.4043 39.4043Zm0-7.42188c-17.7246 0-31.8848-14.209-31.8848-31.9824s14.1113-31.9824 31.8359-31.9824c17.7734 0 32.0312 14.209 32.0312 31.9824s-14.209 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
|
|
26
|
+
</g>
|
|
27
|
+
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
|
|
28
|
+
<path d="m58.5449 14.5508c27.2461 0 49.8047-22.6074 49.8047-49.8047 0-27.2461-22.6074-49.8047-49.8535-49.8047-27.1973 0-49.7559 22.5586-49.7559 49.8047 0 27.1973 22.6074 49.8047 49.8047 49.8047Zm0-8.30078c-23.0469 0-41.4551-18.457-41.4551-41.5039s18.3594-41.5039 41.4062-41.5039 41.5527 18.457 41.5527 41.5039-18.457 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
|
|
29
|
+
</g>
|
|
30
|
+
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
|
|
31
|
+
<path d="m74.8535 28.3203c34.8145 0 63.623-28.8086 63.623-63.5742 0-34.8145-28.8574-63.623-63.6719-63.623-34.7656 0-63.5254 28.8086-63.5254 63.623 0 34.7656 28.8086 63.5742 63.5742 63.5742Zm0-9.08203c-30.1758 0-54.4434-24.3164-54.4434-54.4922 0-30.2246 24.2188-54.4922 54.3945-54.4922 30.2246 0 54.541 24.2676 54.541 54.4922 0 30.1758-24.2676 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
|
|
32
|
+
</g>
|
|
33
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
|
|
34
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
|
|
35
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
|
|
36
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
|
|
37
|
+
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
|
|
38
|
+
<g transform="matrix(0.2 0 0 0.2 776 1933)">
|
|
39
|
+
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
|
|
40
|
+
</g>
|
|
41
|
+
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
|
|
42
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
|
|
43
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
|
|
44
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
|
|
45
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
|
|
46
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
|
|
47
|
+
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
|
|
48
|
+
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
|
|
49
|
+
</g>
|
|
50
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
|
|
51
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
|
|
52
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
|
|
53
|
+
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.5.0</text>
|
|
54
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 15 or greater</text>
|
|
55
|
+
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from circle</text>
|
|
56
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
|
|
57
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
|
|
58
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
|
|
59
|
+
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
|
|
60
|
+
</g>
|
|
61
|
+
<g id="Guides">
|
|
62
|
+
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
|
|
63
|
+
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
|
64
|
+
</g>
|
|
65
|
+
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
|
|
66
|
+
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
|
|
67
|
+
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
|
|
68
|
+
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
|
69
|
+
</g>
|
|
70
|
+
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
|
|
71
|
+
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
|
|
72
|
+
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
|
|
73
|
+
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
|
74
|
+
</g>
|
|
75
|
+
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
|
|
76
|
+
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
|
|
77
|
+
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="515.649" x2="515.649" y1="600.785" y2="720.121"/>
|
|
78
|
+
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="603.773" x2="603.773" y1="600.785" y2="720.121"/>
|
|
79
|
+
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1403.58" x2="1403.58" y1="600.785" y2="720.121"/>
|
|
80
|
+
<line id="right-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1496.11" x2="1496.11" y1="600.785" y2="720.121"/>
|
|
81
|
+
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2884.57" x2="2884.57" y1="600.785" y2="720.121"/>
|
|
82
|
+
<line id="right-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2982.23" x2="2982.23" y1="600.785" y2="720.121"/>
|
|
83
|
+
</g>
|
|
84
|
+
<g id="Symbols">
|
|
85
|
+
<g id="Black-S" transform="matrix(1 0 0 1 2884.57 696)">
|
|
86
|
+
<path class="SFSymbolsPreviewWireframe" d="M48.8281 6.78711C71.9727 6.78711 90.8203-12.0605 90.8203-35.2051C90.8203-58.3496 71.9727-77.1973 48.8281-77.1973C25.6836-77.1973 6.83594-58.3496 6.83594-35.2051C6.83594-12.0605 25.6836 6.78711 48.8281 6.78711ZM48.8281-7.37305C33.4473-7.37305 20.9961-19.8242 20.9961-35.2051C20.9961-50.5859 33.4473-63.0371 48.8281-63.0371C64.209-63.0371 76.6602-50.5859 76.6602-35.2051C76.6602-19.8242 64.209-7.37305 48.8281-7.37305Z"/>
|
|
87
|
+
</g>
|
|
88
|
+
<g id="Regular-S" transform="matrix(1 0 0 1 1403.58 696)">
|
|
89
|
+
<path class="SFSymbolsPreviewWireframe" d="M46.2402 4.15039C67.7734 4.15039 85.6934-13.7207 85.6934-35.2539C85.6934-56.7871 67.7246-74.6582 46.1914-74.6582C24.707-74.6582 6.83594-56.7871 6.83594-35.2539C6.83594-13.7207 24.7559 4.15039 46.2402 4.15039ZM46.2402-3.27148C28.5156-3.27148 14.3555-17.4805 14.3555-35.2539C14.3555-53.0273 28.4668-67.2363 46.1914-67.2363C63.9648-67.2363 78.2227-53.0273 78.2227-35.2539C78.2227-17.4805 64.0137-3.27148 46.2402-3.27148Z"/>
|
|
90
|
+
</g>
|
|
91
|
+
<g id="Ultralight-S" transform="matrix(1 0 0 1 515.649 696)">
|
|
92
|
+
<path class="SFSymbolsPreviewWireframe" d="M44.0606 1.97072C64.5039 1.97072 81.2886-14.8105 81.2886-35.2539C81.2886-55.6973 64.5005-72.4785 44.0571-72.4785C23.5718-72.4785 6.83594-55.6973 6.83594-35.2539C6.83594-14.8105 23.5752 1.97072 44.0606 1.97072ZM44.0606-0.274438C24.7466-0.274438 9.04252-15.9365 9.04252-35.2539C9.04252-54.5713 24.7432-70.2334 44.0571-70.2334C63.3745-70.2334 79.04-54.5713 79.04-35.2539C79.04-15.9365 63.3779-0.274438 44.0606-0.274438Z"/>
|
|
93
|
+
</g>
|
|
94
|
+
</g>
|
|
95
|
+
</svg>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#! /usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'thor'
|
|
5
|
+
require 'nokogiri'
|
|
6
|
+
require './lib/sf_symbol_converter'
|
|
7
|
+
|
|
8
|
+
# CLI for converting SF Symbols
|
|
9
|
+
class SFSymbolCli < Thor
|
|
10
|
+
include Thor::Actions
|
|
11
|
+
|
|
12
|
+
TEMPLATE_PATH = 'assets/template.svg'
|
|
13
|
+
|
|
14
|
+
desc 'convert ICON_SVG [OUTPUT_SVG]', 'Converts an SF Symbol SVG to a template SVG'
|
|
15
|
+
def convert(icon_svg_path, output_path = 'output.svg')
|
|
16
|
+
template_svg = Nokogiri::XML(File.open(TEMPLATE_PATH))
|
|
17
|
+
icon_svg = Nokogiri::XML(File.open(icon_svg_path))
|
|
18
|
+
|
|
19
|
+
output_path = output_path || 'output.svg'
|
|
20
|
+
|
|
21
|
+
converter = SFSymbolConverter.new(template_svg, icon_svg)
|
|
22
|
+
converted_svg = converter.convert
|
|
23
|
+
|
|
24
|
+
pretty_printed_svg = Nokogiri::XML(converted_svg.to_s, &:noblanks).to_xml(indent: 2)
|
|
25
|
+
|
|
26
|
+
File.open(output_path, 'w') { |file| file.write(pretty_printed_svg) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc 'batch_convert INPUT_DIR OUTPUT_DIR', 'Converts all SVGs in a directory to SFSymbols'
|
|
30
|
+
def batch_convert(input_dir, output_dir)
|
|
31
|
+
Dir.glob("#{input_dir}/*.svg").each do |icon_svg_path|
|
|
32
|
+
icon_svg = Nokogiri::XML(File.open(icon_svg_path))
|
|
33
|
+
|
|
34
|
+
output_path = "#{output_dir}/#{File.basename(icon_svg_path)}"
|
|
35
|
+
convert(icon_svg_path, output_path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
SFSymbolCli.start(ARGV)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
require './lib/utils'
|
|
5
|
+
|
|
6
|
+
require './lib/validators/icon_validator'
|
|
7
|
+
require './lib/validators/template_validator'
|
|
8
|
+
require './lib/trimmer/template_trimmer'
|
|
9
|
+
|
|
10
|
+
# Given a source SVG and a SFSymbol template, generate a SFSymbol
|
|
11
|
+
class SFSymbolConverter
|
|
12
|
+
SOURCE_ICON_VIEWBOX_SIZE = 24
|
|
13
|
+
|
|
14
|
+
REFERENCE_SFSYMBOL_FONT_CAPS_HEIGHT = 14
|
|
15
|
+
TARGET_SYMBOL_HEIGHT_MEDIUM = 16
|
|
16
|
+
|
|
17
|
+
SFSYMBOL_MEDIUM_TO_SMALL_SCALE = 0.783
|
|
18
|
+
|
|
19
|
+
MARGIN_LINE_WIDTH = 0.5
|
|
20
|
+
|
|
21
|
+
attr_accessor :template_svg
|
|
22
|
+
attr_reader :icon_svg, :icon_validator, :template_validator
|
|
23
|
+
|
|
24
|
+
def initialize(template_svg, icon_svg)
|
|
25
|
+
@template_svg = template_svg
|
|
26
|
+
@icon_svg = icon_svg
|
|
27
|
+
|
|
28
|
+
@icon_validator = IconValidator.new(SOURCE_ICON_VIEWBOX_SIZE)
|
|
29
|
+
@template_validator = TemplateValidator.new
|
|
30
|
+
|
|
31
|
+
@template_trimmer = TemplateTrimmer.new
|
|
32
|
+
|
|
33
|
+
@icon_validator.validate(icon_svg)
|
|
34
|
+
@template_validator.validate(template_svg)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def convert
|
|
38
|
+
@template_svg = @template_trimmer.trim(template_svg)
|
|
39
|
+
replace_template_symbols_with_source_icons
|
|
40
|
+
adjust_guidelines
|
|
41
|
+
@template_svg
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def cap_height_small
|
|
47
|
+
baseline_y = get_guide_value(template_svg, :y, 'Baseline-S')
|
|
48
|
+
capline_y = get_guide_value(template_svg, :y, 'Capline-S')
|
|
49
|
+
|
|
50
|
+
(baseline_y - capline_y).abs
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def scale_factor_small
|
|
54
|
+
cap_height_small / REFERENCE_SFSYMBOL_FONT_CAPS_HEIGHT * SFSYMBOL_MEDIUM_TO_SMALL_SCALE
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def scaled_size_small
|
|
58
|
+
SOURCE_ICON_VIEWBOX_SIZE * scale_factor_small
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def vertical_center_small
|
|
62
|
+
baseline_y = get_guide_value(template_svg, :y, 'Baseline-S')
|
|
63
|
+
capline_y = get_guide_value(template_svg, :y, 'Capline-S')
|
|
64
|
+
|
|
65
|
+
(baseline_y + capline_y) / 2
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def horizontal_center(symbol)
|
|
69
|
+
left_margin = get_guide_value(template_svg, :x, "left-margin-#{symbol}")
|
|
70
|
+
right_margin = get_guide_value(template_svg, :x, "right-margin-#{symbol}")
|
|
71
|
+
|
|
72
|
+
(left_margin + right_margin) / 2
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def adjusted_left_margin(symbol)
|
|
76
|
+
# TODO: is it really (MARGIN_LINE_WIDTH / 2) or just MARGIN_LINE_WIDTH
|
|
77
|
+
horizontal_center(symbol) - scaled_size_small / 2 - (MARGIN_LINE_WIDTH / 2)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def adjusted_right_margin(symbol)
|
|
81
|
+
# TODO: is it really (MARGIN_LINE_WIDTH / 2) or just MARGIN_LINE_WIDTH
|
|
82
|
+
horizontal_center(symbol) + scaled_size_small / 2 + (MARGIN_LINE_WIDTH / 2)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def transform_matrix(symbol)
|
|
86
|
+
translation_x = horizontal_center(symbol) - scaled_size_small / 2
|
|
87
|
+
translation_y = vertical_center_small - scaled_size_small / 2
|
|
88
|
+
|
|
89
|
+
matrix_text = '%<scale>.6f 0 0 %<scale>.6f %<trans_x>.6f %<trans_y>.6f'
|
|
90
|
+
parameters = { scale: scale_factor_small, trans_x: translation_x, trans_y: translation_y }
|
|
91
|
+
|
|
92
|
+
format(matrix_text, parameters)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
IDS_TO_REPLACE = %w[Ultralight-S Regular-S Black-S].freeze
|
|
96
|
+
|
|
97
|
+
def replace_template_symbols_with_source_icons
|
|
98
|
+
IDS_TO_REPLACE.each do |symbol|
|
|
99
|
+
template_symbol_node = template_svg.at_css("##{symbol}")
|
|
100
|
+
|
|
101
|
+
template_symbol_node['transform'] = "matrix(#{transform_matrix(symbol)})"
|
|
102
|
+
|
|
103
|
+
paths = icon_svg.dup.root.children
|
|
104
|
+
paths.each do |node|
|
|
105
|
+
node.delete('fill')
|
|
106
|
+
node['class'] = 'SFSymbolsPreviewWireframe'
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
template_symbol_node.children = paths
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def adjust_guidelines
|
|
114
|
+
IDS_TO_REPLACE.each do |symbol|
|
|
115
|
+
left_margin_node = template_svg.at_css("#left-margin-#{symbol}")
|
|
116
|
+
left_margin_node['x1'] = left_margin_node['x2'] = adjusted_left_margin(symbol).to_s
|
|
117
|
+
|
|
118
|
+
right_margin_node = template_svg.at_css("#right-margin-#{symbol}")
|
|
119
|
+
right_margin_node['x1'] = right_margin_node['x2'] = adjusted_right_margin(symbol).to_s
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# given a template SVG, remove unused icons
|
|
4
|
+
class TemplateTrimmer
|
|
5
|
+
def trim(template_svg)
|
|
6
|
+
remove_unused_template_icons(template_svg)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
ICON_SIZE = %w[S M L].freeze
|
|
12
|
+
ICON_WEIGHTS = %w[Black Heavy Bold Semibold Medium Regular Light Thin Ultralight].freeze
|
|
13
|
+
|
|
14
|
+
IDS_TO_KEEP = %w[Ultralight-S Regular-S Black-S].freeze
|
|
15
|
+
|
|
16
|
+
def remove_unused_template_icons(template_svg)
|
|
17
|
+
ICON_SIZE.each do |size|
|
|
18
|
+
ICON_WEIGHTS.each do |weight|
|
|
19
|
+
id = "#{weight}-#{size}"
|
|
20
|
+
next if IDS_TO_KEEP.include?(id)
|
|
21
|
+
|
|
22
|
+
template_svg.at_css("##{id}")&.remove
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
template_svg
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# TODO: should we also clean up margin lines ?
|
|
30
|
+
# ... But it is complicated as the static version provides only M margin (and not S like the variable version)
|
|
31
|
+
end
|
data/lib/utils.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def get_guide_value(template_svg, axis, xml_id)
|
|
4
|
+
guide_node = template_svg.at_css("##{xml_id}")
|
|
5
|
+
raise 'invalid axis' unless %i[x y].include?(axis)
|
|
6
|
+
|
|
7
|
+
val1 = guide_node["#{axis}1"]
|
|
8
|
+
val2 = guide_node["#{axis}2"]
|
|
9
|
+
raise "invalid #{xml_id} guide" if val1.nil? || val1 != val2
|
|
10
|
+
|
|
11
|
+
val1.to_f # Convert the value from string to float.
|
|
12
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# given an icon SVG, validate its dimensions and viewbox
|
|
4
|
+
class IconValidator
|
|
5
|
+
attr_reader :viewbox_size
|
|
6
|
+
|
|
7
|
+
def initialize(viewbox_size)
|
|
8
|
+
@viewbox_size = viewbox_size
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def validate(icon_svg)
|
|
12
|
+
raise "expected icon size to be (#{viewbox_size}, #{viewbox_size})" unless icon_dimension_valid?(icon_svg)
|
|
13
|
+
raise "expected icon viewbox to be (0, 0, #{viewbox_size}, #{viewbox_size})" unless icon_viewbox_valid?(icon_svg)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def icon_dimension_valid?(icon_svg)
|
|
19
|
+
is_width_ok = icon_svg.root['width'] == viewbox_size.to_s
|
|
20
|
+
is_height_ok = icon_svg.root['height'] == viewbox_size.to_s
|
|
21
|
+
|
|
22
|
+
is_width_ok && is_height_ok
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def icon_viewbox_valid?(icon_svg)
|
|
26
|
+
icon_svg.root['viewBox'] == "0 0 #{viewbox_size} #{viewbox_size}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# given a template SVG, validate its structure and guidelines
|
|
4
|
+
class TemplateValidator
|
|
5
|
+
def validate(template_svg)
|
|
6
|
+
validate_required_sections(template_svg)
|
|
7
|
+
validate_required_symbols(template_svg)
|
|
8
|
+
validate_guidelines(template_svg)
|
|
9
|
+
validate_margin_lines(template_svg)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
REQUIRED_SECTIONS = %w[Notes Symbols Guides].freeze
|
|
15
|
+
|
|
16
|
+
def validate_required_sections(template_svg)
|
|
17
|
+
REQUIRED_SECTIONS.each do |section_id|
|
|
18
|
+
validate_presence(template_svg, "g##{section_id}", "Invalid template: Missing required section #{section_id}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
REQUIRED_SYMBOLS = %w[Ultralight-S Regular-S Black-S].freeze
|
|
23
|
+
|
|
24
|
+
def validate_required_symbols(template_svg)
|
|
25
|
+
symbols_section = template_svg.at_css('g#Symbols')
|
|
26
|
+
REQUIRED_SYMBOLS.each do |symbol_id|
|
|
27
|
+
validate_presence_within(symbols_section, "g##{symbol_id}", "Invalid template: Missing symbol #{symbol_id}")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
REQUIRED_GUIDELINE_SCALES = %w[S L M].freeze
|
|
32
|
+
REQUIRED_GUIDELINE_TYPES = %w[Baseline Capline].freeze
|
|
33
|
+
|
|
34
|
+
def validate_guidelines(template_svg)
|
|
35
|
+
REQUIRED_GUIDELINE_SCALES.each do |scale|
|
|
36
|
+
REQUIRED_GUIDELINE_TYPES.each do |line_type|
|
|
37
|
+
validate_presence(template_svg, "line##{line_type}-#{scale}", "Invalid template: Missing #{line_type}-#{scale}")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
REQUIRED_MARGIN_SCALES = %w[S].freeze
|
|
43
|
+
REQUIRED_MARGIN_WEIGHTS = %w[Ultralight Regular Black].freeze
|
|
44
|
+
REQUIRED_MARGIN_TYPES = %w[left-margin right-margin].freeze
|
|
45
|
+
|
|
46
|
+
def validate_margin_lines(template_svg)
|
|
47
|
+
REQUIRED_MARGIN_WEIGHTS.product(REQUIRED_MARGIN_SCALES).each do |weight, scale|
|
|
48
|
+
REQUIRED_MARGIN_TYPES.each do |margin_type|
|
|
49
|
+
full_margin_id = "#{margin_type}-#{weight}-#{scale}"
|
|
50
|
+
validate_presence(template_svg, "line##{full_margin_id}", "Invalid template: Missing #{full_margin_id}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate_presence(root, selector, error_message)
|
|
56
|
+
raise error_message unless root.at_css(selector)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_presence_within(parent, selector, error_message)
|
|
60
|
+
raise error_message unless parent.at_css(selector)
|
|
61
|
+
end
|
|
62
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sf_symbol_converter
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yang Liu
|
|
8
|
+
- Tycho Tatitscheff
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2024-04-10 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: nokogiri
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - "~>"
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: '1.16'
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: 1.16.3
|
|
24
|
+
type: :runtime
|
|
25
|
+
prerelease: false
|
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
27
|
+
requirements:
|
|
28
|
+
- - "~>"
|
|
29
|
+
- !ruby/object:Gem::Version
|
|
30
|
+
version: '1.16'
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 1.16.3
|
|
34
|
+
- !ruby/object:Gem::Dependency
|
|
35
|
+
name: thor
|
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.1'
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 1.1.0
|
|
44
|
+
type: :runtime
|
|
45
|
+
prerelease: false
|
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - "~>"
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '1.1'
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 1.1.0
|
|
54
|
+
description: Given a SVG icon with viewBox that respect [material icon principles](https://m3.material.io/styles/icons/designing-icons),
|
|
55
|
+
generate appropriate SFSymbol
|
|
56
|
+
email:
|
|
57
|
+
- yangl@bam.tech
|
|
58
|
+
- tychot@bam.tech
|
|
59
|
+
executables:
|
|
60
|
+
- sf-symbol-converter
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- assets/template.svg
|
|
65
|
+
- bin/sf-symbol-converter
|
|
66
|
+
- lib/sf_symbol_converter.rb
|
|
67
|
+
- lib/trimmer/template_trimmer.rb
|
|
68
|
+
- lib/utils.rb
|
|
69
|
+
- lib/validators/icon_validator.rb
|
|
70
|
+
- lib/validators/template_validator.rb
|
|
71
|
+
homepage: https://github.com/tychota/SfSymbolExporter
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata: {}
|
|
75
|
+
post_install_message:
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: 2.7.0
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 3.5.7
|
|
91
|
+
signing_key:
|
|
92
|
+
specification_version: 4
|
|
93
|
+
summary: Convert SVG icon to SFSymbol
|
|
94
|
+
test_files: []
|