├── 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 | --------------------------------------------------------------------------------