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 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
@@ -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: []