sf-symbols-svg 6.0.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.
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/dist/index.js +449 -0
- package/package.json +54 -0
- package/sources/6.0/names.txt +6404 -0
- package/sources/6.0/symbols.txt +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Maxime Thirouin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# SF Symbols to SVG
|
|
2
|
+
|
|
3
|
+
<a href="https://github.com/MoOx/sf-symbols-svg?sponsor=1">
|
|
4
|
+
<img width="140" align="right" alt="Sponsoring button" src="https://github.com/moox/.github/raw/main/FUNDING.svg">
|
|
5
|
+
</a>
|
|
6
|
+
|
|
7
|
+
[ ](https://www.npmjs.com/package/sf-symbols-svg)
|
|
8
|
+
[](https://github.com/MoOx/sf-symbols-svg/actions)
|
|
9
|
+
[](https://github.com/MoOx/sf-symbols-svg)
|
|
10
|
+

|
|
11
|
+
[](https://github.com/MoOx)
|
|
12
|
+
[](https://www.linkedin.com/in/maxthirouin/)
|
|
13
|
+
[](https://bsky.app/profile/moox.io)
|
|
14
|
+
[](https://x.com/MoOx)
|
|
15
|
+
|
|
16
|
+
> SF Symbols to SVGs. Period.
|
|
17
|
+
|
|
18
|
+
[Apple SF Symbols](https://developer.apple.com/sf-symbols/) is an icon set with more thant 6,000 symbols.
|
|
19
|
+
Unfortunately, this are not available on the web.
|
|
20
|
+
This tool solve this.
|
|
21
|
+
|
|
22
|
+
> [!WARNING]
|
|
23
|
+
> This tool requires you to have the [_SF Pro Text_ font](https://developer.apple.com/fonts/) installed on your system or in a custom directory.
|
|
24
|
+
|
|
25
|
+
> [!TIP]
|
|
26
|
+
> SF Symbols to SVG can be coupled with [React from SVG](https://github.com/MoOx/react-from-svg) to generate React components from SVGs.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
SF Symbols to SVG is available as a CLI tool:
|
|
31
|
+
|
|
32
|
+
```console
|
|
33
|
+
npx sf-symbols-svg --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### Available Options
|
|
37
|
+
|
|
38
|
+
```console
|
|
39
|
+
--size, -s Font size for symbols (default: 24)
|
|
40
|
+
--padding, -p Padding in pixels (default: 2)
|
|
41
|
+
--weight, -w Font weights to include (default: regular)
|
|
42
|
+
Can specify multiple: -w regular -w bold
|
|
43
|
+
--output, -o Output directory (default: ./sf-symbols-svgs)
|
|
44
|
+
--fonts-dir, -f Directory containing SF Pro Text fonts (default: /Library/Fonts)
|
|
45
|
+
--sf-version SF Symbols version to use (default: latest)
|
|
46
|
+
--sources-dir Directory containing SF Symbols data files (default: ./sources)
|
|
47
|
+
--icons-list Path to a file containing a list of icons to process, one name per line (optional)
|
|
48
|
+
--help, -h Show this help message
|
|
49
|
+
--version Show version of SF Symbols supported by this tool.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> [!NOTE]
|
|
53
|
+
> SF Symbols to SVG will always try to use the latest version of SF Symbols supported by this tool.
|
|
54
|
+
> You can check in the `sources/` directory to see which versions are supported.
|
|
55
|
+
|
|
56
|
+
#### Examples
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
# Generate SVGs for the latest version of SF Symbols, in 24x24 SVGs with 2px padding, in ./svg-symbols-svgs folder
|
|
60
|
+
npx sf-symbols-svg --weight all
|
|
61
|
+
|
|
62
|
+
# Generate SVGs with larger size and padding
|
|
63
|
+
sf-symbols-svg --size 32 --padding 4
|
|
64
|
+
|
|
65
|
+
# Generate SVGs for multiple weights
|
|
66
|
+
sf-symbols-svg --weight bold --weight black
|
|
67
|
+
|
|
68
|
+
# Specify custom output directory
|
|
69
|
+
sf-symbols-svg --output ./my-icons
|
|
70
|
+
|
|
71
|
+
# Specify a different SF Symbols version (if available)
|
|
72
|
+
sf-symbols-svg --sf-version 6.0
|
|
73
|
+
|
|
74
|
+
# Combine options
|
|
75
|
+
sf-symbols-svg --size 48 --padding 8 --weight light --weight regular --weight bold --output ./custom-icons --fonts-dir /Users/moox/Library/Fonts
|
|
76
|
+
|
|
77
|
+
# Process only specific icons
|
|
78
|
+
sf-symbols-svg --icons-list /path/to/your/icons-list.txt
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Creating a new version
|
|
82
|
+
|
|
83
|
+
## About SF Symbols Versions and Font Compatibility
|
|
84
|
+
|
|
85
|
+
### SF Symbols Versions
|
|
86
|
+
|
|
87
|
+
This tool automatically detects supported SF Symbols versions by scanning the `sources/` directory. Each version requires its own data files (`symbols.txt` and `names.txt`) which are already included in the repository for some versions.
|
|
88
|
+
|
|
89
|
+
The tool will automatically use the most recent version as the default, but you can specify a different version using the `--sf-version` option. If no matching versions are detected in the `sources/` directory, the tool will display an error message.
|
|
90
|
+
|
|
91
|
+
If you want to use a different version (when new SF Symbols versions are released):
|
|
92
|
+
|
|
93
|
+
1. Create a new directory in `sources/{version}/` (example: `sources/7.0/`)
|
|
94
|
+
2. Extract the character mappings:
|
|
95
|
+
- Get the [SF Symbols app](https://developer.apple.com/sf-symbols/) and open the app
|
|
96
|
+
- Select all symbols (`cmd + A` or `Edit` > `Select All`)
|
|
97
|
+
- Right click on the selection, and press `Copy all {x} symbols`
|
|
98
|
+
- Paste the symbols into a file at `sources/{version}/symbols.txt`
|
|
99
|
+
- Right click again on the selection, and press `Copy all {x} names`
|
|
100
|
+
- Paste the names into a file at `sources/{version}/names.txt`
|
|
101
|
+
|
|
102
|
+
That's it! The tool will automatically detect the new version and use it as the default (since it's the most recent).
|
|
103
|
+
|
|
104
|
+
### Using a Custom Sources Directory
|
|
105
|
+
|
|
106
|
+
If you want to use a different directory for your SF Symbols data files, you can specify it with the `--sources-dir` option:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
sf-symbols-svg --sources-dir /path/to/your/sources
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The custom sources directory must follow the same structure as the default one:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
sources/
|
|
116
|
+
├── 6.0/
|
|
117
|
+
│ ├── symbols.txt
|
|
118
|
+
│ └── names.txt
|
|
119
|
+
├── 6.1/
|
|
120
|
+
│ ├── symbols.txt
|
|
121
|
+
│ └── names.txt
|
|
122
|
+
└── ...
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The tool will automatically detect available versions from the provided directory and use the most recent one as the default.
|
|
126
|
+
|
|
127
|
+
## Processing Only Specific Icons
|
|
128
|
+
|
|
129
|
+
If you want to process only a limited subset of icons, you can create a text file with one icon name per line and use the `--icons-list` option:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
sf-symbols-svg --icons-list /path/to/your/icons-list.txt
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Example of an icons list file:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
moon.stars.fill
|
|
139
|
+
puzzlepiece
|
|
140
|
+
amplifier
|
|
141
|
+
figure.hiking
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This is particularly useful for:
|
|
145
|
+
|
|
146
|
+
- Testing the tool with a smaller set of icons
|
|
147
|
+
- Generating only the specific icons you need for your project
|
|
148
|
+
- Reducing processing time when you only need a few symbols
|
|
149
|
+
|
|
150
|
+
### Font Compatibility
|
|
151
|
+
|
|
152
|
+
> [!WARNING] SF Symbols requires specific _SF Pro Text_ font versions that match the SF Symbols version you're using. If the font versions don't match, the symbols may not render correctly.
|
|
153
|
+
|
|
154
|
+
> [!CAUTION]
|
|
155
|
+
> The Apple SF Symbols app will display a warning at the top of the application if your installed fonts don't match the expected version. Make sure to check this warning and install the appropriate font version from [Apple's website](https://developer.apple.com/fonts/).
|
|
156
|
+
|
|
157
|
+
### About Font Files
|
|
158
|
+
|
|
159
|
+
To use this script, you need to have the SF Pro Text fonts installed on your system or in a custom directory.
|
|
160
|
+
|
|
161
|
+
#### Using system-installed fonts (recommended)
|
|
162
|
+
|
|
163
|
+
If you have SF Pro Text fonts installed on your system (typically in `/Library/Fonts`), the script will automatically find and use them. This is the default behavior.
|
|
164
|
+
|
|
165
|
+
````console
|
|
166
|
+
# Use fonts from the default location (/Library/Fonts)
|
|
167
|
+
sf-symbols-svg`
|
|
168
|
+
|
|
169
|
+
#### Using fonts from a custom directory
|
|
170
|
+
|
|
171
|
+
If your fonts are installed in a different location, you can specify it with the `--fonts-dir` option:
|
|
172
|
+
|
|
173
|
+
```console
|
|
174
|
+
# Use fonts from a custom location
|
|
175
|
+
sf-symbols-svg --fonts-dir ~/Library/Fonts
|
|
176
|
+
````
|
|
177
|
+
|
|
178
|
+
#### Installing SF Pro Text fonts
|
|
179
|
+
|
|
180
|
+
If you don't have the fonts installed:
|
|
181
|
+
|
|
182
|
+
1. Download SF Pro font from [Apple's website](https://developer.apple.com/fonts/).
|
|
183
|
+
2. Install the font using the provided installer.
|
|
184
|
+
3. The fonts will be installed in `/Library/Fonts` by default.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
> [!NOTE]
|
|
189
|
+
> For legal reasons, this repository does not include the SF Pro Text font files. You must download and install them from Apple's website. Make sure to use font versions that are compatible with the SF Symbols version you are using (check for warnings in the SF Symbols app).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import opentype from "opentype.js";
|
|
6
|
+
import meow from "meow";
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PATH_PRECISION = 2;
|
|
9
|
+
/**
|
|
10
|
+
* Default sources directory path
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_SOURCES_DIR = path.join(__dirname, "../sources");
|
|
13
|
+
/**
|
|
14
|
+
* Detects available versions based on folders in the sources directory
|
|
15
|
+
* @param {string} sourcesDir - Path to the sources directory
|
|
16
|
+
* @throws {Error} If no version is detected
|
|
17
|
+
*/
|
|
18
|
+
function detectAvailableVersions(sourcesDir) {
|
|
19
|
+
try {
|
|
20
|
+
const items = fs.readdirSync(sourcesDir, { withFileTypes: true });
|
|
21
|
+
const versions = items
|
|
22
|
+
.filter((item) => item.isDirectory() && !item.name.startsWith("."))
|
|
23
|
+
.map((item) => item.name)
|
|
24
|
+
// Sort versions in descending order (to have the most recent first)
|
|
25
|
+
.sort((a, b) => {
|
|
26
|
+
// Semantic version comparison (e.g., "6.1" > "6.0")
|
|
27
|
+
const partsA = a.split(".").map(Number);
|
|
28
|
+
const partsB = b.split(".").map(Number);
|
|
29
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
30
|
+
const partA = partsA[i] || 0;
|
|
31
|
+
const partB = partsB[i] || 0;
|
|
32
|
+
if (partA !== partB) {
|
|
33
|
+
return partB - partA; // Descending order
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
});
|
|
38
|
+
if (versions.length === 0) {
|
|
39
|
+
throw new Error(`No SF Symbols version detected in the sources directory. Please add at least one version folder containing symbols.txt and names.txt files.`);
|
|
40
|
+
}
|
|
41
|
+
return versions;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof Error) {
|
|
45
|
+
throw error; // Propagate the error if it's already an Error instance
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
throw new Error(`Error detecting versions: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get available versions from the default sources directory
|
|
54
|
+
* This is used only for CLI default values
|
|
55
|
+
*/
|
|
56
|
+
function getDefaultVersions() {
|
|
57
|
+
try {
|
|
58
|
+
return detectAvailableVersions(DEFAULT_SOURCES_DIR);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Return empty array if no versions are detected in the default directory
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Available SF Symbols versions from default sources (automatically detected)
|
|
67
|
+
*/
|
|
68
|
+
const AVAILABLE_VERSIONS = getDefaultVersions();
|
|
69
|
+
/**
|
|
70
|
+
* Default version (the most recent one, or empty string if no versions are available)
|
|
71
|
+
*/
|
|
72
|
+
const DEFAULT_VERSION = AVAILABLE_VERSIONS.length > 0 ? AVAILABLE_VERSIONS[0] : "";
|
|
73
|
+
/**
|
|
74
|
+
* Builds paths to data files for a specific version
|
|
75
|
+
* @param {string} version - SF Symbols version
|
|
76
|
+
* @param {string} sourcesDir - Path to the sources directory
|
|
77
|
+
*/
|
|
78
|
+
function getVersionPaths(version, sourcesDir) {
|
|
79
|
+
return {
|
|
80
|
+
symbols: path.join(sourcesDir, `${version}/symbols.txt`),
|
|
81
|
+
names: path.join(sourcesDir, `${version}/names.txt`),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Builds file paths based on the output directory, version, and sources directory
|
|
86
|
+
*/
|
|
87
|
+
function buildPaths(outputDir, version, sourcesDir) {
|
|
88
|
+
const versionPaths = getVersionPaths(version, sourcesDir);
|
|
89
|
+
return {
|
|
90
|
+
SYMBOLS: versionPaths.symbols,
|
|
91
|
+
NAMES: versionPaths.names,
|
|
92
|
+
SVG_OUTPUT: outputDir,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Checks if data files exist for a specific version
|
|
97
|
+
*/
|
|
98
|
+
function checkDataFilesExist(version, sourcesDir) {
|
|
99
|
+
const paths = getVersionPaths(version, sourcesDir);
|
|
100
|
+
if (!fileExists(paths.symbols)) {
|
|
101
|
+
console.error(`symbols.txt file not found: ${paths.symbols}`);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (!fileExists(paths.names)) {
|
|
105
|
+
console.error(`names.txt file not found: ${paths.names}`);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Writes a file to the filesystem, creating directories if needed
|
|
112
|
+
*/
|
|
113
|
+
function writeFile(filePath, contents) {
|
|
114
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
115
|
+
fs.writeFileSync(filePath, contents);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Generates an SVG from a font character
|
|
119
|
+
*/
|
|
120
|
+
function generateSymbolSvg(font, char, name, options) {
|
|
121
|
+
try {
|
|
122
|
+
const { fontSize, padding } = options;
|
|
123
|
+
const glyphIndex = font.charToGlyphIndex(char);
|
|
124
|
+
if (glyphIndex === 0)
|
|
125
|
+
return null;
|
|
126
|
+
const glyph = font.glyphs.get(glyphIndex);
|
|
127
|
+
if (!glyph)
|
|
128
|
+
return null;
|
|
129
|
+
const path = font.getPath(char, 0, 0, fontSize);
|
|
130
|
+
if (!path)
|
|
131
|
+
return null;
|
|
132
|
+
const bbox = path.getBoundingBox();
|
|
133
|
+
if (!bbox)
|
|
134
|
+
return null;
|
|
135
|
+
const symbolWidth = bbox.x2 - bbox.x1;
|
|
136
|
+
const symbolHeight = bbox.y2 - bbox.y1;
|
|
137
|
+
const availableWidth = fontSize - padding * 2;
|
|
138
|
+
const availableHeight = fontSize - padding * 2;
|
|
139
|
+
// Calculate the scale needed for the symbol to fit in the available area
|
|
140
|
+
// taking into account padding on all sides
|
|
141
|
+
let scale;
|
|
142
|
+
if (symbolWidth / availableWidth > symbolHeight / availableHeight) {
|
|
143
|
+
scale = availableWidth / symbolWidth;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
scale = availableHeight / symbolHeight;
|
|
147
|
+
}
|
|
148
|
+
const scaledWidth = symbolWidth * scale;
|
|
149
|
+
const scaledHeight = symbolHeight * scale;
|
|
150
|
+
// Calculate translations needed to center the symbol in the viewBox
|
|
151
|
+
const translateX = (fontSize - scaledWidth) / 2 - bbox.x1 * scale;
|
|
152
|
+
const translateY = (fontSize - scaledHeight) / 2 - bbox.y1 * scale;
|
|
153
|
+
const transformMatrix = [scale, 0, 0, scale, translateX, translateY];
|
|
154
|
+
const pathElement = path.toSVG(PATH_PRECISION);
|
|
155
|
+
const dMatch = pathElement.match(/d="([^"]*)"/i);
|
|
156
|
+
if (!dMatch)
|
|
157
|
+
return null;
|
|
158
|
+
const dContent = dMatch[1];
|
|
159
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${fontSize} ${fontSize}" width="${fontSize}" height="${fontSize}">
|
|
160
|
+
<title>${name}</title>
|
|
161
|
+
<path transform="matrix(${transformMatrix.join(", ")})" d="${dContent}" fill="currentColor"/>
|
|
162
|
+
</svg>`;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error(`Error generating SVG for ${name}:`, error);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Creates a safe filename from a symbol name
|
|
171
|
+
*/
|
|
172
|
+
function createSafeFilename(name) {
|
|
173
|
+
return name.replace(/[^a-z0-9\.]/gi, "_").toLowerCase();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Creates a filename for an SVG with appropriate weight suffix
|
|
177
|
+
*/
|
|
178
|
+
function createSvgFilename(name, weight) {
|
|
179
|
+
const safeName = createSafeFilename(name);
|
|
180
|
+
if (weight === "regular") {
|
|
181
|
+
return `${safeName}.svg`;
|
|
182
|
+
}
|
|
183
|
+
return `${safeName}-${weight}.svg`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Checks if a file exists
|
|
187
|
+
*/
|
|
188
|
+
function fileExists(filePath) {
|
|
189
|
+
try {
|
|
190
|
+
return fs.statSync(filePath).isFile();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Finds the font file in the fonts directory
|
|
198
|
+
*/
|
|
199
|
+
function findFontFile(fontsDir, weight) {
|
|
200
|
+
// Expected format for SF Pro Text
|
|
201
|
+
const fontName = `SF-Pro-Text-${weight.charAt(0).toUpperCase() + weight.slice(1)}.otf`;
|
|
202
|
+
const fontPath = path.join(fontsDir, fontName);
|
|
203
|
+
// Check if the file exists
|
|
204
|
+
if (fileExists(fontPath)) {
|
|
205
|
+
return fontPath;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Main function to generate SVG files directly from font files
|
|
211
|
+
*/
|
|
212
|
+
async function makeSvgs(options) {
|
|
213
|
+
const { fontSize = 24, padding = 2, weights = ["regular"], outputDir: outputDirOption = path.join(process.cwd(), "sf-symbols-svgs"), fontsDir: fontsDirOption = "/Library/Fonts", version: versionOption = DEFAULT_VERSION, sourcesDir: sourcesDirOption = DEFAULT_SOURCES_DIR, iconsListFile: iconsListFileOption, } = options || {};
|
|
214
|
+
const fontsDir = String(fontsDirOption);
|
|
215
|
+
const outputDir = String(outputDirOption);
|
|
216
|
+
const sourcesDir = String(sourcesDirOption);
|
|
217
|
+
let availableVersions;
|
|
218
|
+
try {
|
|
219
|
+
availableVersions = detectAvailableVersions(sourcesDir);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
throw new Error(`No SF Symbols versions detected in ${sourcesDir}. Please provide a valid sources directory containing version folders with symbols.txt and names.txt files.`);
|
|
223
|
+
}
|
|
224
|
+
const version = versionOption ? String(versionOption) : availableVersions[0];
|
|
225
|
+
if (!fontsDir) {
|
|
226
|
+
throw new Error("fonts-dir option is required. Please specify the directory containing SF Pro Text fonts.");
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const versionStr = String(version);
|
|
230
|
+
const sourcesStr = String(sourcesDir);
|
|
231
|
+
const sourcesVersionDir = path.join(sourcesStr, versionStr);
|
|
232
|
+
const stats = fs.statSync(sourcesVersionDir);
|
|
233
|
+
if (!stats.isDirectory()) {
|
|
234
|
+
throw new Error(`${sourcesVersionDir} is not a directory`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
throw new Error(`SF Symbols version '${version}' not found in ${sourcesDir}. Please make sure the directory exists and contains the required data files.`);
|
|
239
|
+
}
|
|
240
|
+
if (!checkDataFilesExist(String(version), sourcesDir)) {
|
|
241
|
+
throw new Error(`Data files for SF Symbols ${version} are missing in ${sourcesDir}/${version}/. Please check the README for setup instructions.`);
|
|
242
|
+
}
|
|
243
|
+
const PATHS = buildPaths(outputDir, String(version), sourcesDir);
|
|
244
|
+
console.log("Loading Fonts...");
|
|
245
|
+
const fonts = await Promise.all(weights.map(async (weight) => {
|
|
246
|
+
try {
|
|
247
|
+
const fontPath = findFontFile(fontsDir, weight);
|
|
248
|
+
if (!fontPath) {
|
|
249
|
+
console.error(`Font file for weight '${weight}' not found in ${fontsDir}`);
|
|
250
|
+
console.error(`Please make sure SF-Pro-Text-${weight.charAt(0).toUpperCase() + weight.slice(1)}.otf is installed in the specified fonts directory.`);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
console.log(`Loading font: ${fontPath}`);
|
|
254
|
+
const font = await opentype.load(fontPath);
|
|
255
|
+
return { weight, font };
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error(`Error loading font weight ${weight}:`, error);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}));
|
|
262
|
+
const validFonts = fonts.filter((item) => !!item);
|
|
263
|
+
if (validFonts.length === 0) {
|
|
264
|
+
throw new Error("No valid fonts were loaded. Please check the weights and font files.");
|
|
265
|
+
}
|
|
266
|
+
console.log(`Fonts loaded: ${validFonts.map((f) => f.weight).join(", ")}`);
|
|
267
|
+
console.log("Capturing Paths... This takes a while");
|
|
268
|
+
let symbolsData = fs
|
|
269
|
+
.readFileSync(PATHS.SYMBOLS, { encoding: "utf-8" })
|
|
270
|
+
.match(/.{1,2}/g)
|
|
271
|
+
?.filter((char) => char !== "") || [];
|
|
272
|
+
let names = fs
|
|
273
|
+
.readFileSync(PATHS.NAMES, { encoding: "utf8", flag: "r" })
|
|
274
|
+
.split(/\r?\n/)
|
|
275
|
+
.filter((char) => char !== "");
|
|
276
|
+
if (iconsListFileOption) {
|
|
277
|
+
try {
|
|
278
|
+
const iconsList = fs
|
|
279
|
+
.readFileSync(String(iconsListFileOption), {
|
|
280
|
+
encoding: "utf8",
|
|
281
|
+
flag: "r",
|
|
282
|
+
})
|
|
283
|
+
.split(/\r?\n/)
|
|
284
|
+
.filter((name) => name !== "");
|
|
285
|
+
if (iconsList.length === 0) {
|
|
286
|
+
console.warn(`Icons list file ${iconsListFileOption} is empty. Using all available icons.`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const filteredNamesIndices = [];
|
|
290
|
+
const filteredNames = [];
|
|
291
|
+
const filteredSymbols = [];
|
|
292
|
+
for (const iconName of iconsList) {
|
|
293
|
+
const index = names.indexOf(iconName);
|
|
294
|
+
if (index !== -1) {
|
|
295
|
+
filteredNamesIndices.push(index);
|
|
296
|
+
filteredNames.push(iconName);
|
|
297
|
+
if (index < symbolsData.length) {
|
|
298
|
+
const charItem = symbolsData[index];
|
|
299
|
+
if (charItem) {
|
|
300
|
+
filteredSymbols.push(charItem);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
console.warn(`Icon '${iconName}' not found in available icons.`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (filteredNames.length === 0) {
|
|
309
|
+
console.warn("None of the specified icons were found. Using all available icons.");
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
console.log(`Processing ${filteredNames.length} icons from list file.`);
|
|
313
|
+
names = filteredNames;
|
|
314
|
+
symbolsData = filteredSymbols;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
console.error(`Error reading icons list file: ${error}`);
|
|
320
|
+
console.warn("Using all available icons.");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
console.log(`Output directory: ${outputDir}`);
|
|
324
|
+
fs.mkdirSync(PATHS.SVG_OUTPUT, { recursive: true });
|
|
325
|
+
for (let namesIndex = 0; namesIndex < names.length; namesIndex++) {
|
|
326
|
+
const name = names[namesIndex];
|
|
327
|
+
if (!name)
|
|
328
|
+
continue;
|
|
329
|
+
console.log(`${namesIndex + 1}/${names.length} - ${name}`);
|
|
330
|
+
const char = symbolsData[namesIndex];
|
|
331
|
+
if (!char)
|
|
332
|
+
continue;
|
|
333
|
+
for (const { weight, font } of validFonts) {
|
|
334
|
+
const svgContent = generateSymbolSvg(font, char, name, {
|
|
335
|
+
fontSize,
|
|
336
|
+
padding,
|
|
337
|
+
});
|
|
338
|
+
if (!svgContent)
|
|
339
|
+
continue;
|
|
340
|
+
const svgFilename = createSvgFilename(name, weight);
|
|
341
|
+
const svgPath = path.join(PATHS.SVG_OUTPUT, svgFilename);
|
|
342
|
+
writeFile(svgPath, svgContent);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
console.log("Done! Generated SVG files in", PATHS.SVG_OUTPUT);
|
|
346
|
+
}
|
|
347
|
+
const cli = meow(`
|
|
348
|
+
Usage
|
|
349
|
+
$ sf-symbols-svg [options]
|
|
350
|
+
|
|
351
|
+
Options
|
|
352
|
+
--size, -s Font size for symbols (default: 24)
|
|
353
|
+
--padding, -p Padding in pixels (default: 2)
|
|
354
|
+
--weight, -w Font weights to include (default: regular)
|
|
355
|
+
Can specify multiple: -w regular -w bold
|
|
356
|
+
Special value "all" to include all available weights
|
|
357
|
+
--output, -o Output directory (default: ./sf-symbols-svgs)
|
|
358
|
+
--fonts-dir, -f Directory containing SF Pro Text fonts (default: /Library/Fonts)
|
|
359
|
+
--sf-version SF Symbols version to use (default: latest)
|
|
360
|
+
--sources-dir Directory containing SF Symbols data files (default: ./sources)
|
|
361
|
+
--icons-list Path to a file containing a list of icons to process, one name per line (optional)
|
|
362
|
+
--help, -h Show this help message
|
|
363
|
+
--version Show version
|
|
364
|
+
|
|
365
|
+
Examples
|
|
366
|
+
$ sf-symbols-svg
|
|
367
|
+
$ sf-symbols-svg --size 32 --padding 4
|
|
368
|
+
$ sf-symbols-svg --weight bold --weight black
|
|
369
|
+
$ sf-symbols-svg --weight all
|
|
370
|
+
$ sf-symbols-svg --output ./my-icons
|
|
371
|
+
$ sf-symbols-svg --fonts-dir ~/Library/Fonts
|
|
372
|
+
$ sf-symbols-svg --sf-version 6.0
|
|
373
|
+
`, {
|
|
374
|
+
importMeta: import.meta,
|
|
375
|
+
flags: {
|
|
376
|
+
size: {
|
|
377
|
+
type: "number",
|
|
378
|
+
default: 24,
|
|
379
|
+
shortFlag: "s",
|
|
380
|
+
},
|
|
381
|
+
padding: {
|
|
382
|
+
type: "number",
|
|
383
|
+
default: 2,
|
|
384
|
+
shortFlag: "p",
|
|
385
|
+
},
|
|
386
|
+
weight: {
|
|
387
|
+
type: "string",
|
|
388
|
+
default: ["regular"],
|
|
389
|
+
isMultiple: true,
|
|
390
|
+
shortFlag: "w",
|
|
391
|
+
},
|
|
392
|
+
output: {
|
|
393
|
+
type: "string",
|
|
394
|
+
shortFlag: "o",
|
|
395
|
+
},
|
|
396
|
+
fontsDir: {
|
|
397
|
+
type: "string",
|
|
398
|
+
shortFlag: "f",
|
|
399
|
+
},
|
|
400
|
+
sfVersion: {
|
|
401
|
+
type: "string",
|
|
402
|
+
},
|
|
403
|
+
sourcesDir: {
|
|
404
|
+
type: "string",
|
|
405
|
+
description: "Directory containing SF Symbols data files",
|
|
406
|
+
},
|
|
407
|
+
iconsList: {
|
|
408
|
+
type: "string",
|
|
409
|
+
description: "Path to a file containing a list of icons to process (one per line)",
|
|
410
|
+
},
|
|
411
|
+
help: {
|
|
412
|
+
type: "boolean",
|
|
413
|
+
shortFlag: "h",
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
/**
|
|
418
|
+
* Available font weights
|
|
419
|
+
*/
|
|
420
|
+
const FONT_WEIGHTS = [
|
|
421
|
+
"thin",
|
|
422
|
+
"ultraLight",
|
|
423
|
+
"light",
|
|
424
|
+
"regular",
|
|
425
|
+
"medium",
|
|
426
|
+
"semibold",
|
|
427
|
+
"bold",
|
|
428
|
+
"heavy",
|
|
429
|
+
"black",
|
|
430
|
+
];
|
|
431
|
+
// Process weights option - handle "all" special value
|
|
432
|
+
let weights = cli.flags.weight;
|
|
433
|
+
if (weights.length === 1 && weights[0].toLowerCase() === "all") {
|
|
434
|
+
weights = FONT_WEIGHTS;
|
|
435
|
+
console.log(`Using all available weights: ${weights.join(", ")}`);
|
|
436
|
+
}
|
|
437
|
+
makeSvgs({
|
|
438
|
+
fontSize: cli.flags.size,
|
|
439
|
+
padding: cli.flags.padding,
|
|
440
|
+
weights: weights,
|
|
441
|
+
outputDir: cli.flags.output,
|
|
442
|
+
fontsDir: cli.flags.fontsDir,
|
|
443
|
+
version: cli.flags.sfVersion,
|
|
444
|
+
sourcesDir: cli.flags.sourcesDir,
|
|
445
|
+
iconsListFile: cli.flags.iconsList,
|
|
446
|
+
}).catch((error) => {
|
|
447
|
+
console.error("Error generating SVGs:", error);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
});
|