├── LICENSE
├── main.go
├── README.md
└── embed.go
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Bryan Burke
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.
22 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "path"
9 | "strings"
10 | )
11 |
12 | const (
13 | embedTemplate = `
14 | {{- range .Fonts}}
15 |
23 | {{- end}}
24 |
25 | `
26 | usage = `
27 | Usage:
28 | svg-font-embed input.svg [font1.ttf font2.ttf]
29 |
30 | Required Arguments:
31 | input.svg - The SVG file to embed fonts within
32 |
33 | Optional Arguments:
34 | font.ttf - Specify one or more font files to embed within the SVG document. Fonts do not have to be specified unless it is not obvious which file matches the fonts in the SVG file.
35 |
36 | If no fonts are specified, the current directory and all subdirectories will be walked to look for a matching font file. A match is defined as a font file which has the font-family name in its file name. When multiple font files exists that would match (such as when the font comes in different weights), an error will be thrown unless you specify which font file to use on the command line.
37 | `
38 | )
39 |
40 | func main() {
41 | if (len(os.Args) < 2) || (os.Args[1] == "help") || (os.Args[1] == "--help") {
42 | fmt.Println(usage)
43 | os.Exit(1)
44 | }
45 | svgFile := os.Args[1]
46 |
47 | wd, err := os.Getwd()
48 | if err != nil {
49 | log.Fatal("Could not get current working directory")
50 | }
51 |
52 | if _, err := os.Stat(path.Join(wd, svgFile)); os.IsNotExist(err) {
53 | fmt.Printf("Error: Could not find SVG file %s in current directory\n", svgFile)
54 | os.Exit(1)
55 | }
56 |
57 | if !strings.HasSuffix(os.Args[1], "svg") {
58 | fmt.Printf("Error: Input file %s does not end with a .svg extension\n", os.Args[1])
59 | os.Exit(1)
60 | }
61 | svg, err := ioutil.ReadFile(path.Join(wd, os.Args[1]))
62 |
63 | svgEmbed, err := FindEmbedFonts(svg, wd)
64 | if err != nil {
65 | fmt.Printf("%s\n", err)
66 | os.Exit(1)
67 | }
68 |
69 | svgEmbedFileName := strings.Replace(svgFile, ".", ".embed.", 1)
70 | if err := ioutil.WriteFile(svgEmbedFileName, svgEmbed, 0644); err != nil {
71 | fmt.Printf("Error: %s", err)
72 | os.Exit(1)
73 | }
74 | fmt.Printf("Output saved: %s\n", svgEmbedFileName)
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Embed Fonts in SVG Assets
2 | ===
3 |
4 | SVG is useful for device-independent resolution, but can often be a pain because fonts must be embedded in the file to render properly across all browsers. `svg-embed-font` is a command line tool to easily determine what fonts are used in an SVG file and encode them as Base64 assets within it.
5 |
6 | If your SVG assets look great on your computer and messed up on everyone else's, it's because the fonts aren't embedded properly in the file.
7 |
8 | ### Usage
9 |
10 | ```
11 | svg-embed-font input.svg
12 | ```
13 |
14 | In the default mode, the tool will scan the SVG file for all font-family declarations then attempt to locate matching font files (any font file format). Matches are defined as a case-insensitive substring match for the font family name ignoring any spaces. So if you declare:
15 |
16 | ```
17 | font-family: 'Permanent Marker'
18 |
19 | Matches:
20 | permanentmarker.ttf
21 | PermanentMarker-700.otf
22 | ```
23 |
24 | In this case, there are two possible matches, which can often happen when a font comes in multiple weights. To specify which should be used, list the font on the command line after the input file. Multiple possible matches must be resolved by listing the correct one on the command line.
25 |
26 | ```
27 | svg-embed-font input.svg permanentmarker.ttf
28 | ```
29 |
30 | One or more preferred font files can be listed on the command line and it will use those files instead of any other matches it finds.
31 |
32 | ### Font File Path Search
33 |
34 | If you don't specify the exact font files, it will look in the current directory and all subdirectories for a match, so you can lay out your files in a logical hierarchy and it will find them. If it exhausts all possible files without finding a match to every font in the SVG file, it will return an error.
35 |
36 | ### How It Works
37 |
38 | The font file is Base64 encoded and included as a stylesheet asset directly in the SVG file. If you open the file in a text editor, right before the closing `` tag you will see something like the following for each font:
39 |
40 | ```
41 |
49 | ```
50 |
51 | ### Installation
52 |
53 | Download the release appropriate for your operating system on the [releases page](https://github.com/BTBurke/svg-embed-font/releases).
54 |
55 | ### License
56 |
57 | MIT
58 |
--------------------------------------------------------------------------------
/embed.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "fmt"
7 | "html/template"
8 | "io/ioutil"
9 | "os"
10 | "path"
11 | "path/filepath"
12 | "regexp"
13 | "strings"
14 | )
15 |
16 | // Font is a single font family and associated Base64 encoded font file.
17 | type Font struct {
18 | Family string
19 | //CmdLineSet is set when the user specifies a particular font file on the command line
20 | CmdLineSet bool
21 | EncodedFont string
22 | File string
23 | }
24 |
25 | // Document represents an SVG document containing one or more fonts
26 | type Document struct {
27 | Fonts []Font
28 | }
29 |
30 | // Add will add a new font definition to the document
31 | func (d *Document) Add(f Font) {
32 | d.Fonts = append(d.Fonts, f)
33 | return
34 | }
35 |
36 | // FontMap is an alias type for a map of the font-family to a font specification
37 | type FontMap map[string]Font
38 |
39 | // FindEmbedFonts will analyze the SVG file looking for all unique fonts used. It will then walk the directory tree
40 | // starting from the working directory looking for fonts that match the font-family names. It will then Base64 encode
41 | // the font and embed it in the SVG file.
42 | func FindEmbedFonts(svg []byte, dir string) ([]byte, error) {
43 | fonts := make(FontMap)
44 | re := regexp.MustCompile("font-family:(.*?);")
45 | matches := re.FindAllStringSubmatch(string(svg), -1)
46 | for _, match := range matches {
47 | name := strings.Trim(match[1], " '\"\t\n\r")
48 | if (name == "sans-serif") || (name == "serif") {
49 | continue
50 | }
51 | fonts[name] = Font{Family: name}
52 | }
53 | if len(os.Args) > 2 {
54 | err := ProcessCmdLineFonts(fonts, os.Args[2:], dir)
55 | if err != nil {
56 | return svg, fmt.Errorf("Error processing command line font: %s", err)
57 | }
58 | }
59 | if CheckAllFontsSet(fonts) {
60 | svgEmbed, err := Embed(fonts, svg)
61 | if err != nil {
62 | return svg, err
63 | }
64 | PrintResults(fonts)
65 | return svgEmbed, nil
66 | }
67 | if err := Walk(fonts, dir); err != nil {
68 | return svg, err
69 | }
70 | if !CheckAllFontsSet(fonts) {
71 | for family, f := range fonts {
72 | if len(f.EncodedFont) == 0 {
73 | return svg, fmt.Errorf("No matching font file found for font-family: %s", family)
74 | }
75 | }
76 | }
77 | svgEmbed, err := Embed(fonts, svg)
78 | if err != nil {
79 | return svg, err
80 | }
81 | PrintResults(fonts)
82 | return svgEmbed, nil
83 | }
84 |
85 | // ProcessCmdLineFonts will embed any fonts specified on the command line in the SVG before looking for others
86 | // in the file system
87 | func ProcessCmdLineFonts(fm FontMap, fonts []string, baseDir string) error {
88 | for _, fontFile := range fonts {
89 | p := path.Join(baseDir, fontFile)
90 | if _, err := os.Stat(p); os.IsNotExist(err) {
91 | return err
92 | }
93 | for family := range fm {
94 | familyT := strings.Replace(family, " ", "", -1)
95 | if strings.Contains(strings.ToLower(fontFile), strings.ToLower(familyT)) {
96 | data, err := ioutil.ReadFile(p)
97 | if err != nil {
98 | return err
99 | }
100 | fm[family] = Font{
101 | Family: family,
102 | CmdLineSet: true,
103 | File: fontFile,
104 | EncodedFont: base64.StdEncoding.EncodeToString(data),
105 | }
106 | }
107 | }
108 | }
109 | return nil
110 | }
111 |
112 | // CheckAllFontsSet will determine if all fonts have been resolved to associated font files
113 | func CheckAllFontsSet(fm FontMap) bool {
114 | for _, f := range fm {
115 | if len(f.EncodedFont) == 0 {
116 | return false
117 | }
118 | }
119 | return true
120 | }
121 |
122 | // Embed puts the encoded fonts within the section of the SVG
123 | func Embed(fm FontMap, svg []byte) ([]byte, error) {
124 | doc := new(Document)
125 | for family := range fm {
126 | doc.Add(fm[family])
127 | }
128 | t := template.Must(template.New("embed").Parse(embedTemplate))
129 |
130 | buf := new(bytes.Buffer)
131 | if err := t.Execute(buf, doc); err != nil {
132 | return svg, err
133 | }
134 | svgEmbed := strings.Replace(string(svg), "", buf.String(), 1)
135 | return []byte(svgEmbed), nil
136 | }
137 |
138 | func genWalkFunc(fm FontMap) filepath.WalkFunc {
139 | return func(p string, info os.FileInfo, err error) error {
140 | if (info.IsDir()) || (err != nil) {
141 | return nil
142 | }
143 | _, f := path.Split(p)
144 | for family := range fm {
145 | familyT := strings.Replace(family, " ", "", -1)
146 | if strings.Contains(strings.ToLower(f), strings.ToLower(familyT)) {
147 | if fm[family].CmdLineSet {
148 | return nil
149 | }
150 | data, err := ioutil.ReadFile(p)
151 | if err != nil {
152 | return err
153 | }
154 | if len(fm[family].EncodedFont) > 0 {
155 | return fmt.Errorf("Multiple font files found as a match to font-family: %s (%s, %s)", family, fm[family].File, f)
156 | }
157 | fm[family] = Font{
158 | Family: family,
159 | CmdLineSet: true,
160 | File: f,
161 | EncodedFont: base64.StdEncoding.EncodeToString(data),
162 | }
163 | }
164 | }
165 | return nil
166 | }
167 | }
168 |
169 | // Walk walks the file system looking for font files that match the font-families in the document
170 | func Walk(fm FontMap, baseDir string) error {
171 | if err := filepath.Walk(baseDir, genWalkFunc(fm)); err != nil {
172 | return err
173 | }
174 | return nil
175 | }
176 |
177 | //PrintResults generates a report
178 | func PrintResults(fm FontMap) {
179 | fmt.Printf("Found %d fonts to be embedded. Using the following font files:\n", len(fm))
180 | for family, f := range fm {
181 | fmt.Printf("%s: %s\n", family, f.File)
182 | }
183 | return
184 | }
185 |
--------------------------------------------------------------------------------