├── spec ├── fixtures │ ├── sandbox │ │ └── .gitkeep │ ├── shared │ │ ├── not-a-dir │ │ ├── vectors-empty │ │ │ └── no_vectors_here.txt │ │ ├── templates │ │ │ ├── custom.css │ │ │ └── regular.css │ │ └── vectors │ │ │ ├── a_R3ally-eXotic f1Le Name.svg │ │ │ ├── D.svg │ │ │ └── C.svg │ ├── options │ │ ├── no-config-here │ │ │ └── .gitkeep │ │ ├── fontcustom-empty.yml │ │ ├── any-file-name.yml │ │ ├── fontcustom.yml │ │ ├── rails-like │ │ │ └── config │ │ │ │ └── fontcustom.yml │ │ ├── config-is-in-dir │ │ │ └── fontcustom.yml │ │ └── fontcustom-malformed.yml │ ├── generators │ │ ├── fontcustom.yml │ │ ├── .fontcustom-manifest-empty.json │ │ ├── mixed-output │ │ │ ├── dont-delete-me.bro │ │ │ ├── another-font.ttf │ │ │ ├── fontcustom_82a59e769bc60192484f2620570bbb59.eot │ │ │ ├── fontcustom_82a59e769bc60192484f2620570bbb59.ttf │ │ │ ├── fontcustom_82a59e769bc60192484f2620570bbb59.woff │ │ │ ├── fontcustom.css │ │ │ └── fontcustom_82a59e769bc60192484f2620570bbb59.svg │ │ ├── .fontcustom-manifest-corrupted.json │ │ └── .fontcustom-manifest.json │ └── example │ │ ├── example.eot │ │ ├── example.ttf │ │ ├── example.woff │ │ ├── example.css │ │ ├── _example-rails.scss │ │ ├── example.svg │ │ └── example-preview.html ├── fontcustom │ ├── manifest_spec.rb │ ├── cli_spec.rb │ ├── base_spec.rb │ ├── utility_spec.rb │ ├── generator │ │ ├── font_spec.rb │ │ └── template_spec.rb │ ├── watcher_spec.rb │ └── options_spec.rb └── spec_helper.rb ├── lib ├── fontcustom │ ├── version.rb │ ├── error.rb │ ├── scripts │ │ ├── sfnt2woff │ │ ├── generate.py │ │ └── eotlitetool.py │ ├── templates │ │ ├── fontcustom.css │ │ ├── _fontcustom-rails.scss │ │ ├── _fontcustom.scss │ │ ├── fontcustom.yml │ │ └── fontcustom-preview.html │ ├── manifest.rb │ ├── base.rb │ ├── watcher.rb │ ├── utility.rb │ ├── generator │ │ ├── font.rb │ │ └── template.rb │ ├── cli.rb │ └── options.rb └── fontcustom.rb ├── bin └── fontcustom ├── Gemfile ├── gemfiles └── Gemfile.listen_1 ├── Rakefile ├── .gitignore ├── .travis.yml ├── fontcustom.gemspec ├── CONTRIBUTING.md ├── LICENSES.txt ├── README.md └── CHANGELOG.md /spec/fixtures/sandbox/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/shared/not-a-dir: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/options/no-config-here/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/generators/fontcustom.yml: -------------------------------------------------------------------------------- 1 | # noop 2 | -------------------------------------------------------------------------------- /spec/fixtures/generators/.fontcustom-manifest-empty.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/generators/mixed-output/dont-delete-me.bro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/options/fontcustom-empty.yml: -------------------------------------------------------------------------------- 1 | # nothing 2 | -------------------------------------------------------------------------------- /spec/fixtures/shared/vectors-empty/no_vectors_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/options/any-file-name.yml: -------------------------------------------------------------------------------- 1 | font_name: Custom Name From Config 2 | -------------------------------------------------------------------------------- /spec/fixtures/options/fontcustom.yml: -------------------------------------------------------------------------------- 1 | font_name: Custom Name From Config 2 | -------------------------------------------------------------------------------- /lib/fontcustom/version.rb: -------------------------------------------------------------------------------- 1 | module Fontcustom 2 | VERSION = "1.3.8" 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/options/rails-like/config/fontcustom.yml: -------------------------------------------------------------------------------- 1 | font_name: Rails Like 2 | -------------------------------------------------------------------------------- /spec/fixtures/options/config-is-in-dir/fontcustom.yml: -------------------------------------------------------------------------------- 1 | font_name: Config Is in Dir 2 | -------------------------------------------------------------------------------- /spec/fixtures/options/fontcustom-malformed.yml: -------------------------------------------------------------------------------- 1 | font_name: Custom: Name From Config 2 | -------------------------------------------------------------------------------- /lib/fontcustom/error.rb: -------------------------------------------------------------------------------- 1 | module Fontcustom 2 | class Error < StandardError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/shared/templates/custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | <%= @glyphs.inspect %> 3 | */ 4 | .foo { color: black; } -------------------------------------------------------------------------------- /bin/fontcustom: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'fontcustom/cli' 4 | 5 | Fontcustom::CLI.start(ARGV) 6 | -------------------------------------------------------------------------------- /spec/fixtures/shared/templates/regular.css: -------------------------------------------------------------------------------- 1 | /* 2 | <%= @glyphs.inspect %> 3 | */ 4 | .foo { color: blue; } 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in fontcustom.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/fontcustom/scripts/sfnt2woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icons8/fontcustom/HEAD/lib/fontcustom/scripts/sfnt2woff -------------------------------------------------------------------------------- /spec/fixtures/example/example.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icons8/fontcustom/HEAD/spec/fixtures/example/example.eot -------------------------------------------------------------------------------- /spec/fixtures/example/example.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icons8/fontcustom/HEAD/spec/fixtures/example/example.ttf -------------------------------------------------------------------------------- /spec/fixtures/example/example.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icons8/fontcustom/HEAD/spec/fixtures/example/example.woff -------------------------------------------------------------------------------- /spec/fixtures/generators/mixed-output/another-font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icons8/fontcustom/HEAD/spec/fixtures/generators/mixed-output/another-font.ttf -------------------------------------------------------------------------------- /gemfiles/Gemfile.listen_1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem"s dependencies in fontcustom.gemspec 4 | gemspec :path => ".." 5 | 6 | gem "listen", "~>1.0" 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new "spec" do |s| 5 | s.rspec_opts = "--color --format documentation" 6 | end 7 | 8 | task :default => :spec 9 | -------------------------------------------------------------------------------- /spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icons8/fontcustom/HEAD/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.eot -------------------------------------------------------------------------------- /spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icons8/fontcustom/HEAD/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.ttf -------------------------------------------------------------------------------- /spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icons8/fontcustom/HEAD/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.woff -------------------------------------------------------------------------------- /lib/fontcustom/templates/fontcustom.css: -------------------------------------------------------------------------------- 1 | /* 2 | Icon Font: <%= font_name %> 3 | */ 4 | 5 | <%= font_face %> 6 | 7 | [data-icon]:before { content: attr(data-icon); } 8 | 9 | [data-icon]:before, 10 | <%= glyph_selectors %> { 11 | <%= glyph_properties %> 12 | } 13 | 14 | <%= glyphs %> 15 | -------------------------------------------------------------------------------- /lib/fontcustom/templates/_fontcustom-rails.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Icon Font: <%= font_name %> 3 | // 4 | 5 | <%= font_face(url: "font-url", path: @font_path_alt) %> 6 | 7 | [data-icon]:before { content: attr(data-icon); } 8 | 9 | [data-icon]:before, 10 | <%= glyph_selectors %> { 11 | <%= glyph_properties %> 12 | } 13 | 14 | <%= glyphs %> 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | spec/fixtures/sandbox/* 19 | spec/fixtures/.fontcustom-manifest.json 20 | .DS_Store 21 | .ruby-version 22 | -------------------------------------------------------------------------------- /lib/fontcustom/templates/_fontcustom.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Icon Font: <%= font_name %> 3 | // 4 | 5 | <%= font_face(path: @font_path_alt) %> 6 | 7 | [data-icon]:before { content: attr(data-icon); } 8 | 9 | [data-icon]:before, 10 | <%= glyph_selectors %> { 11 | <%= glyph_properties %> 12 | } 13 | 14 | <%= glyphs %> 15 | <% @glyphs.each do |name, value| %> 16 | $font-<%= font_name.gsub(/[^\w\d_]/, '-') %>-<%= name.to_s %>: "\<%= value[:codepoint].to_s(16) %>";<% end %> 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_install: 3 | - sudo apt-get update -qq 4 | - sudo apt-get install -qq fontforge 5 | - wget http://people.mozilla.com/~jkew/woff/woff-code-latest.zip 6 | - unzip woff-code-latest.zip -d sfnt2woff && cd sfnt2woff && make && sudo mv sfnt2woff /usr/local/bin/ 7 | - bundle 8 | rvm: 9 | - 2.1.1 10 | - 2.0.0 11 | - 1.9.3 12 | - 1.9.2 13 | gemfile: 14 | - Gemfile 15 | - gemfiles/Gemfile.listen_1 16 | matrix: 17 | exclude: 18 | - gemfile: Gemfile 19 | rvm: 1.9.2 20 | script: bundle exec rake 21 | -------------------------------------------------------------------------------- /spec/fontcustom/manifest_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Fontcustom::Manifest do 4 | context "#initialize" do 5 | it "should create a manifest file and assign :options", :integration => true do 6 | live_test do |testdir| 7 | capture(:stdout) do 8 | manifest = File.join testdir, ".fontcustom-manifest.json" 9 | options = Fontcustom::Options.new(:manifest => manifest, :input => "vectors").options 10 | Fontcustom::Manifest.new manifest, options 11 | end 12 | content = File.read File.join(testdir, ".fontcustom-manifest.json") 13 | expect(content).to match(/"options":.+"input":/m) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/fixtures/generators/.fontcustom-manifest-corrupted.json: -------------------------------------------------------------------------------- 1 | 'Totally invalid JSON.' 2 | { 3 | "checksum": { 4 | "current": "82a59e769bc60192484f2620570bbb59e225db97c1aac3f242a2e49d6060a19c", 5 | "previous": "82a59e769bc60192484f2620570bbb59e225db97c1aac3f242a2e49d6060a19c" 6 | }, 7 | "fonts": [ 8 | "fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.woff", 9 | "fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.ttf", 10 | "fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.eot", 11 | "fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.svg" 12 | ], 13 | "glyphs": [ 14 | "a_r3ally-exotic-f1le-name", 15 | "c", 16 | "d" 17 | ], 18 | "options": { 19 | "foo": "bar", 20 | "baz": "bum" 21 | }, 22 | "templates": [ 23 | "fontcustom.css" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /lib/fontcustom.rb: -------------------------------------------------------------------------------- 1 | require "fontcustom/version" 2 | require "fontcustom/error" 3 | require "fontcustom/utility" 4 | require "fontcustom/base" 5 | require "fontcustom/manifest" 6 | require "fontcustom/options" 7 | require "fontcustom/generator/font" 8 | require "fontcustom/generator/template" 9 | 10 | module Fontcustom 11 | def gem_lib 12 | File.expand_path(File.join(File.dirname(__FILE__), "fontcustom")) 13 | end 14 | module_function :gem_lib 15 | 16 | ## 17 | # Hack to get Thor to show more helpful defaults in `fontcustom help`. These 18 | # are overwritten in Fontcustom::Options. 19 | EXAMPLE_OPTIONS = { 20 | :output => "./FONT_NAME", 21 | :config => "./fontcustom.yml -or- ./config/fontcustom.yml", 22 | :templates => "css preview" 23 | } 24 | 25 | DEFAULT_OPTIONS = { 26 | :input => nil, 27 | :output => nil, 28 | :config => nil, 29 | :templates => %w|css preview|, 30 | :font_name => "fontcustom", 31 | :font_design_size => 16, 32 | :font_em => 512, 33 | :font_ascent => 448, 34 | :font_descent => 64, 35 | :css_selector => ".icon-{{glyph}}", 36 | :preprocessor_path => nil, 37 | :autowidth => false, 38 | :no_hash => false, 39 | :debug => false, 40 | :force => false, 41 | :quiet => false 42 | } 43 | end 44 | -------------------------------------------------------------------------------- /fontcustom.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "fontcustom/version" 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "fontcustom" 8 | gem.version = Fontcustom::VERSION 9 | gem.authors = ["Kai Zau", "Joshua Gross"] 10 | gem.email = ["kai@kaizau.com", "joshua@gross.is"] 11 | gem.summary = "Generate icon fonts from the command line." 12 | gem.description = "Font Custom makes using vector icons easy. Generate icon fonts and supporting templates (e.g. @font-face CSS) from a collection of SVGs." 13 | gem.homepage = "http://fontcustom.com" 14 | gem.post_install_message = ">> Thanks for installing Font Custom! Please ensure that fontforge is installed before compiling any icons. Visit for instructions." 15 | 16 | gem.files = `git ls-files`.split($/) 17 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 18 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 19 | gem.require_paths = ["lib"] 20 | 21 | gem.add_dependency "json", "~>1.4" 22 | gem.add_dependency "thor", "~>0.14" 23 | gem.add_dependency "listen", ">=1.0","<3.0" 24 | 25 | gem.add_development_dependency "rake" 26 | gem.add_development_dependency "bundler" 27 | gem.add_development_dependency "rspec", "~>3.1.0" 28 | end 29 | -------------------------------------------------------------------------------- /spec/fontcustom/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "fontcustom/cli" 3 | 4 | describe Fontcustom::CLI do 5 | context "#compile" do 6 | it "should generate fonts and templates (integration)", :integration => true do 7 | live_test do |testdir| 8 | Fontcustom::CLI.start ["compile", "vectors", "--quiet"] 9 | manifest = File.join testdir, ".fontcustom-manifest.json" 10 | preview = File.join testdir, "fontcustom", "fontcustom-preview.html" 11 | 12 | expect(Dir.glob(File.join(testdir, "fontcustom", "fontcustom_*\.{ttf,svg,woff,eot}")).length).to eq(4) 13 | expect(File.read(manifest)).to match(/"fonts":.+fontcustom\/fontcustom_.+\.ttf"/m) 14 | expect(File.exists?(preview)).to be_truthy 15 | end 16 | end 17 | 18 | it "should generate fonts and templates according to passed options (integration)", :integration => true do 19 | live_test do |testdir| 20 | Fontcustom::CLI.start ["compile", "vectors", "--font-name", "example", "--preprocessor-path", "../foo/bar", "--templates", "css", "scss-rails", "preview", "--no-hash", "--base64", "--quiet"] 21 | manifest = File.join testdir, ".fontcustom-manifest.json" 22 | css = Dir.glob(File.join("example", "*.scss")).first 23 | 24 | expect(File.read(manifest)).to match(/"fonts":.+example\/example\.ttf"/m) 25 | expect(File.read(css)).to match("x-font-woff") 26 | expect(File.read(css)).to match("../foo/bar/") 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/fixtures/shared/vectors/a_R3ally-eXotic f1Le Name.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Help make Font Custom better! 2 | 3 | This project was born out of an overheard conversation between two devs in a 4 | NYC coffee shop — it's come a long ways thanks to your support. Here's what's 5 | on the menu: 6 | 7 | * **Ruby on Rails integration** 8 | * **Compass integration** 9 | * Templates for LESS, stylus, etc. 10 | * Ligature support 11 | * Windows support 12 | * Make better use of Thor 13 | 14 | Just [file an issue](https://github.com/FontCustom/fontcustom/issues) if you 15 | have an idea or would like to claim one. 16 | 17 | ### Rules of Thumb 18 | 19 | If you catch a typo or a block of code that could be more elegant — please let 20 | us know. No such thing as too small of an improvement. 21 | 22 | * Spaces instead of tabs, please. 23 | * Develop in a topic branch. 24 | * Include passing tests if applicable. 25 | * Follow the [Github ruby styleguide](https://github.com/styleguide/ruby) as 26 | much as possible. 27 | 28 | ### Getting Started 29 | 30 | You'll need: 31 | 32 | * Fontforge with Python scripting (easiest via [Homebrew](http://brew.sh/) on Mac) 33 | * Ruby 1.9.2+ (via [rbenv](https://github.com/sstephenson/rbenv), [RVM](https://rvm.io/), etc.) 34 | * Rubygems 35 | * Bundler 36 | * Rake 37 | * Rspec 38 | 39 | Some helpful links: 40 | 41 | * http://createdbypete.com/articles/ruby-on-rails-development-with-mac-os-x-mountain-lion/ 42 | * http://guides.rubygems.org/make-your-own-gem/ 43 | 44 | --- 45 | 46 | That's all there is to it. Thanks again, and please don't hesitate to reach out: 47 | 48 | [Github Issues](https://github.com/FontCustom/fontcustom/issues)
49 | [@kaizau](https://twitter.com/kaizau)
50 | [@endtwist](https://twitter.com/endtwist) 51 | -------------------------------------------------------------------------------- /spec/fontcustom/base_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Fontcustom::Base do 4 | before(:each) { allow(Fontcustom::Manifest).to receive(:write_file) } 5 | 6 | context "#compile" do 7 | context "when [:checksum][:current] equals [:checksum][:previous]" do 8 | it "should show 'no change' message" do 9 | allow(Fontcustom::Base).to receive(:check_fontforge) 10 | options = double("options") 11 | allow(options).to receive(:options).and_return({}) 12 | allow(Fontcustom::Options).to receive(:new).and_return options 13 | 14 | output = capture(:stdout) do 15 | base = Fontcustom::Base.new({}) 16 | manifest = base.instance_variable_get :@manifest 17 | expect(manifest).to receive(:get).and_return :previous => "abc" 18 | expect(base).to receive(:checksum).and_return "abc" 19 | base.compile 20 | end 21 | expect(output).to match(/No changes/) 22 | end 23 | end 24 | end 25 | 26 | context ".check_fontforge" do 27 | it "should raise error if fontforge isn't installed" do 28 | allow_any_instance_of(Fontcustom::Base).to receive(:"`").and_return("") 29 | expect { Fontcustom::Base.new(:option => "foo") }.to raise_error Fontcustom::Error, /fontforge/ 30 | end 31 | end 32 | 33 | context ".checksum" do 34 | it "should return hash of all vectors and templates" do 35 | pending "SHA2 is different on CI servers. Why?" 36 | allow(Fontcustom::Base).to receive(:check_fontforge) 37 | base = Fontcustom::Base.new(:input => {:vectors => fixture("shared/vectors")}) 38 | base.instance_variable_set :@options, { 39 | :templates => Dir.glob(File.join(fixture("shared/templates"), "*")) 40 | } 41 | hash = base.send :checksum 42 | hash.should == "81ffd2f72877be02aad673fdf59c6f9dbfee4cc37ad0b121b9486bc2923b4b36" 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/fontcustom/manifest.rb: -------------------------------------------------------------------------------- 1 | module Fontcustom 2 | class Manifest 3 | include Utility 4 | 5 | attr_reader :manifest 6 | 7 | def initialize(manifest, cli_options = {}) 8 | @manifest = manifest 9 | @cli_options = symbolize_hash cli_options 10 | if File.exists? @manifest 11 | reload 12 | if ! @cli_options.empty? && get(:options) != @cli_options 13 | set :options, @cli_options 14 | end 15 | else 16 | create_manifest @cli_options 17 | end 18 | end 19 | 20 | # TODO convert paths to absolute 21 | def get(key) 22 | @data[key] 23 | end 24 | 25 | # TODO convert paths to relative 26 | def set(key, value, status = nil) 27 | if key == :all 28 | @data = value 29 | else 30 | @data[key] = value 31 | end 32 | json = JSON.pretty_generate @data 33 | write_file @manifest, json, status 34 | end 35 | 36 | def reload 37 | begin 38 | json = File.read @manifest 39 | @data = JSON.parse json, :symbolize_names => true 40 | rescue JSON::ParserError 41 | raise Fontcustom::Error, 42 | "Couldn't parse `#{@manifest}`. Fix any invalid "\ 43 | "JSON or delete the file to start from scratch." 44 | end 45 | end 46 | 47 | def delete(key) 48 | files = get(key) 49 | return if files.empty? 50 | begin 51 | deleted = [] 52 | files.each do |file| 53 | remove_file file, :verbose => false 54 | deleted << file 55 | end 56 | ensure 57 | set key, files - deleted 58 | say_changed :delete, deleted 59 | end 60 | end 61 | 62 | private 63 | 64 | def create_manifest(options) 65 | defaults = { 66 | :checksum => { :current => "", :previous => "" }, 67 | :fonts => [], 68 | :glyphs => {}, 69 | :options => options, 70 | :templates => [] 71 | } 72 | set :all, defaults, :create 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/fontcustom/base.rb: -------------------------------------------------------------------------------- 1 | require "digest/sha2" 2 | 3 | module Fontcustom 4 | class Base 5 | include Utility 6 | 7 | def initialize(raw_options) 8 | check_fontforge 9 | check_woff2 10 | manifest = '.fontcustom-manifest.json' 11 | raw_options[:manifest] = manifest 12 | @options = Fontcustom::Options.new(raw_options).options 13 | @manifest = Fontcustom::Manifest.new(manifest, @options) 14 | end 15 | 16 | def compile 17 | current = checksum 18 | previous = @manifest.get(:checksum)[:previous] 19 | 20 | say_message :status, "Forcing compile." if @options[:force] 21 | if @options[:force] || current != previous 22 | @manifest.set :checksum, {:previous => previous, :current => current} 23 | start_generators 24 | @manifest.reload 25 | @manifest.set :checksum, {:previous => current, :current => current} 26 | else 27 | say_message :status, "No changes detected. Skipping compile." 28 | end 29 | end 30 | 31 | private 32 | 33 | def check_fontforge 34 | fontforge = `which fontforge` 35 | if fontforge == "" || fontforge == "fontforge not found" 36 | raise Fontcustom::Error, "Please install fontforge first. Visit for instructions." 37 | end 38 | end 39 | 40 | def check_woff2 41 | woff2 = `which woff2_compress` 42 | if woff2 == "" || woff2 == "woff2_compress not found" 43 | fail Fontcustom::Error, "Please install woff2 first. Visit for instructions." 44 | end 45 | end 46 | 47 | # Calculates a hash of vectors, options, and templates (content and filenames) 48 | def checksum 49 | files = Dir.glob(File.join(@options[:input][:vectors], "*.svg")).select { |fn| File.file?(fn) } 50 | files += Dir.glob(File.join(@options[:input][:templates], "*")).select { |fn| File.file?(fn) } 51 | content = files.map { |file| File.read(file) }.join 52 | content << files.join 53 | content << @options.flatten(2).join 54 | Digest::SHA2.hexdigest(content).to_s 55 | end 56 | 57 | def start_generators 58 | Fontcustom::Generator::Font.new(@manifest.manifest).generate 59 | Fontcustom::Generator::Template.new(@manifest.manifest).generate 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/fixtures/generators/.fontcustom-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "checksum": { 3 | "current": "82a59e769bc60192484f2620570bbb59e225db97c1aac3f242a2e49d6060a19c", 4 | "previous": "82a59e769bc60192484f2620570bbb59e225db97c1aac3f242a2e49d6060a19c" 5 | }, 6 | "fonts": [ 7 | "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/fontcustom/fontcustom_82a59e769bc60192484f2620570bbb59.ttf", 8 | "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/fontcustom/fontcustom_82a59e769bc60192484f2620570bbb59.svg", 9 | "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/fontcustom/fontcustom_82a59e769bc60192484f2620570bbb59.woff", 10 | "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/fontcustom/fontcustom_82a59e769bc60192484f2620570bbb59.eot" 11 | ], 12 | "glyphs": { 13 | "a_r3ally-exotic-f1le-name": { 14 | "codepoint": 61696, 15 | "source": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/vectors/a_R3ally-eXotic f1Le Name.svg" 16 | }, 17 | "c": { 18 | "codepoint": 61697, 19 | "source": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/vectors/C.svg" 20 | }, 21 | "d": { 22 | "codepoint": 61698, 23 | "source": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/vectors/D.svg" 24 | } 25 | }, 26 | "options": { 27 | "autowidth": false, 28 | "config": false, 29 | "css_prefix": "test-", 30 | "debug": false, 31 | "font_name": "fontcustom", 32 | "input": { 33 | "templates": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/vectors", 34 | "vectors": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/vectors" 35 | }, 36 | "manifest": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/.fontcustom-manifest-fonts.json", 37 | "no_hash": false, 38 | "output": { 39 | "css": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/fontcustom", 40 | "fonts": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/fontcustom", 41 | "preview": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test/fontcustom" 42 | }, 43 | "preprocessor_path": null, 44 | "project_root": "/Users/kz/Projects/fontcustom/spec/fixtures/sandbox/test", 45 | "quiet": true, 46 | "templates": [ 47 | "/Users/kz/Projects/fontcustom/lib/fontcustom/templates/fontcustom.css", 48 | "/Users/kz/Projects/fontcustom/lib/fontcustom/templates/fontcustom-preview.html" 49 | ] 50 | }, 51 | "templates": [] 52 | } 53 | -------------------------------------------------------------------------------- /spec/fontcustom/utility_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Fontcustom::Utility do 4 | class Generator 5 | include Fontcustom::Utility 6 | attr_accessor :options, :manifest 7 | 8 | def initialize 9 | @options = { :quiet => false } 10 | @manifest = fixture ".fontcustom-manifest.json" 11 | end 12 | end 13 | 14 | it "should include Thor::Action methods" do 15 | gen = Generator.new 16 | %w|template add_file remove_file|.each do |method| 17 | expect(gen).to respond_to(method.to_sym) 18 | end 19 | end 20 | 21 | context "#symbolize_hash" do 22 | it "should turn string keys into symbols" do 23 | gen = Generator.new 24 | hash = gen.symbolize_hash "foo" => "bar" 25 | expect(hash).to eq({ :foo => "bar" }) 26 | end 27 | end 28 | 29 | context "#methodize_hash" do 30 | it "should define getter method" do 31 | gen = Generator.new 32 | hash = gen.methodize_hash :foo => "bar" 33 | expect(hash.foo).to eq("bar") 34 | end 35 | 36 | it "should define setter method" do 37 | gen = Generator.new 38 | hash = gen.methodize_hash :foo => "bar" 39 | hash.foo = "baz" 40 | expect(hash.foo).to eq("baz") 41 | end 42 | end 43 | 44 | context "#write_file" do 45 | it "should replace the contents of a file" do 46 | gen = Generator.new 47 | file = double "file" 48 | expect(File).to receive(:open).with(fixture("shared/test"), "w").and_yield file 49 | expect(file).to receive(:write).with("testing") 50 | gen.write_file fixture("shared/test"), "testing" 51 | end 52 | end 53 | 54 | #context "#say_message" do 55 | #it "should not respond if :quiet is true" do 56 | #pending 57 | #gen = Generator.new 58 | #gen.options[:quiet] = true 59 | #output = capture(:stdout) { gen.say_message(:test, "Hello") } 60 | #output.should == "" 61 | #end 62 | #end 63 | 64 | #context "#say_changed" do 65 | #it "should strip :project_root from changed paths" do 66 | #pending 67 | #changed = %w|a b c|.map { |file| fixture(file) } 68 | #gen = Generator.new 69 | #output = capture(:stdout) { gen.say_changed(:success, changed) } 70 | #output.should_not match(fixture) 71 | #end 72 | 73 | #it "should not respond if :quiet is true " do 74 | #pending 75 | #changed = %w|a b c|.map { |file| fixture(file) } 76 | #gen = Generator.new 77 | #gen.options[:quiet] = true 78 | #output = capture(:stdout) { gen.say_changed(:success, changed) } 79 | #output.should == "" 80 | #end 81 | #end 82 | end 83 | -------------------------------------------------------------------------------- /lib/fontcustom/watcher.rb: -------------------------------------------------------------------------------- 1 | require "fontcustom" 2 | require "listen" 3 | 4 | module Fontcustom 5 | class Watcher 6 | include Utility 7 | 8 | def initialize(options, is_test = false) 9 | @base = Fontcustom::Base.new options 10 | @options = @base.options 11 | @is_test = is_test 12 | 13 | templates = @options[:templates].dup.map { |template| File.basename(template) } 14 | packaged = %w|preview css scss scss-rails| 15 | templates.delete_if { |template| packaged.include?(template) } 16 | 17 | create_listener(templates) 18 | end 19 | 20 | def watch 21 | compile unless @options[:skip_first] 22 | start 23 | rescue SignalException # Catches Ctrl + C 24 | stop 25 | end 26 | 27 | private 28 | 29 | def create_listener(templates) 30 | listen_options = {} 31 | listen_options[:polling_fallback_message] = false if @is_test 32 | 33 | listen_dirs = [@options[:input][:vectors]] 34 | listen_dirs << @options[:input][:templates] unless templates.empty? 35 | 36 | if listen_eq2 37 | listen_options[:only] = /(#{templates.join("|")}|.+\.svg)$/ 38 | @listener = Listen.to(listen_dirs, listen_options, &callback) 39 | else 40 | listen_options[:filter] = /(#{templates.join("|")}|.+\.svg)$/ 41 | listen_options[:relative_paths] = true 42 | @listener = Listen::Listener.new(listen_dirs, listen_options, &callback) 43 | end 44 | end 45 | 46 | def start 47 | if @is_test # Non-blocking listener 48 | @listener.start 49 | else 50 | if listen_eq2 51 | @listener.start 52 | sleep 53 | else 54 | @listener.start! 55 | end 56 | end 57 | end 58 | 59 | def stop 60 | @listener.stop 61 | shell.say "\nFont Custom is signing off. Good night and good luck.", :yellow 62 | end 63 | 64 | def callback 65 | Proc.new do |modified, added, removed| 66 | begin 67 | say_message :changed, modified.join(", ") unless modified.empty? 68 | say_message :added, added.join(", ") unless added.empty? 69 | say_message :removed, removed.join(", ") unless removed.empty? 70 | changed = modified + added + removed 71 | compile unless changed.empty? 72 | rescue Fontcustom::Error => e 73 | say_message :error, e.message 74 | end 75 | end 76 | end 77 | 78 | def compile 79 | @base.compile 80 | end 81 | 82 | def listen_eq2 83 | begin 84 | require 'listen/version' 85 | ::Listen::VERSION =~ /^2\./ 86 | rescue LoadError 87 | false 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/fontcustom/utility.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "thor/actions" 3 | require "thor/shell" 4 | require "thor/shell/basic" 5 | require "thor/shell/color" 6 | 7 | # Requires access to: 8 | # @options or @cli_options 9 | # @manifest 10 | module Fontcustom 11 | module Utility 12 | include Thor::Actions 13 | 14 | # 15 | # Hacks that allow Thor::Actions and Thor::Shell to be used in Fontcustom classes. 16 | # 17 | 18 | def self.shell 19 | @shell || Thor::Shell::Color.new 20 | end 21 | 22 | def shell 23 | Fontcustom::Utility.shell 24 | end 25 | 26 | def behavior 27 | :invoke 28 | end 29 | 30 | def say_status(*args) 31 | shell.say_status *args 32 | end 33 | 34 | def destination_root 35 | @destination_stack ||= [project_root] 36 | @destination_stack.last 37 | end 38 | 39 | def source_paths 40 | @source_paths ||= [File.join(Fontcustom.gem_lib, "templates"), Dir.pwd] 41 | end 42 | 43 | # 44 | # Options 45 | # 46 | 47 | module HashWithMethodAccess 48 | def method_missing(method, arg = nil) 49 | if method[-1, 1] == "=" 50 | self[method[0...-1].to_sym] = arg 51 | else 52 | self[method.to_sym] 53 | end 54 | end 55 | end 56 | 57 | def symbolize_hash(hash) 58 | hash.inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo } 59 | end 60 | 61 | def methodize_hash(hash) 62 | hash.extend HashWithMethodAccess 63 | end 64 | 65 | # 66 | # Paths 67 | # 68 | 69 | def project_root 70 | if @manifest.is_a? String 71 | File.dirname @manifest 72 | else 73 | File.dirname @manifest.manifest 74 | end 75 | end 76 | 77 | # 78 | # File Manipulation 79 | # 80 | 81 | def write_file(file, content = "", message = nil, message_body = nil) 82 | File.open(file, "w") { |f| f.write(content) } 83 | if message 84 | body = message_body || file 85 | say_message message, body 86 | end 87 | end 88 | 89 | # 90 | # Messages 91 | # 92 | 93 | def say_message(status, message, color = nil) 94 | return if options[:quiet] && status != :error && status != :debug 95 | color = :red if [:error, :debug, :warn].include?(status) 96 | say_status status, message, color 97 | end 98 | 99 | def say_changed(status, changed) 100 | return if options[:quiet] || ! options[:debug] && status == :delete 101 | say_status status, changed.join(line_break) 102 | end 103 | 104 | # magic number for Thor say_status line breaks 105 | def line_break(n = 14) 106 | "\n#{" " * n}" 107 | end 108 | 109 | def options 110 | if @data 111 | @data[:options] 112 | else 113 | @options || @cli_options || @config_options || {} 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /LICENSES.txt: -------------------------------------------------------------------------------- 1 | fontcustom 2 | 3 | Copyright (c) 2013 Kai Zau, Joshua Gross 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | 27 | sfnt2woff 28 | 29 | Version: MPL 1.1/GPL 2.0/LGPL 2.1 30 | 31 | The contents of this file are subject to the Mozilla Public License Version 32 | 1.1 (the "License"); you may not use this file except in compliance with 33 | the License. You may obtain a copy of the License at 34 | http://www.mozilla.org/MPL/ 35 | 36 | Software distributed under the License is distributed on an "AS IS" basis, 37 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 38 | for the specific language governing rights and limitations under the 39 | License. 40 | 41 | The Original Code is WOFF font packaging code. 42 | 43 | The Initial Developer of the Original Code is Mozilla Corporation. 44 | Portions created by the Initial Developer are Copyright (C) 2009 45 | the Initial Developer. All Rights Reserved. 46 | 47 | Contributor(s): 48 | Jonathan Kew 49 | 50 | Alternatively, the contents of this file may be used under the terms of 51 | either the GNU General Public License Version 2 or later (the "GPL"), or 52 | the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 53 | in which case the provisions of the GPL or the LGPL are applicable instead 54 | of those above. If you wish to allow use of your version of this file only 55 | under the terms of either the GPL or the LGPL, and not to allow others to 56 | use your version of this file under the terms of the MPL, indicate your 57 | decision by deleting the provisions above and replace them with the notice 58 | and other provisions required by the GPL or the LGPL. If you do not delete 59 | the provisions above, a recipient may use your version of this file under 60 | the terms of any one of the MPL, the GPL or the LGPL. 61 | -------------------------------------------------------------------------------- /spec/fontcustom/generator/font_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Fontcustom::Generator::Font do 4 | def generator 5 | allow_any_instance_of(Fontcustom::Manifest).to receive(:write_file) 6 | Fontcustom::Generator::Font.new("") 7 | end 8 | 9 | context "#generate" do 10 | it "should set manifest[:glyphs] (integration)", :integration => true do 11 | live_test do |testdir| 12 | test_manifest 13 | manifest = File.join Dir.pwd, ".fontcustom-manifest.json" 14 | gen = Fontcustom::Generator::Font.new manifest 15 | allow(gen).to receive(:create_fonts) 16 | gen.generate 17 | expect(File.read(manifest)).to match(/"glyphs":.+"C":/m) 18 | end 19 | end 20 | 21 | it "should generate fonts (integration)", :integration => true do 22 | live_test do |testdir| 23 | test_manifest 24 | manifest = File.join Dir.pwd, ".fontcustom-manifest.json" 25 | Fontcustom::Generator::Font.new(manifest).generate 26 | expect(Dir.glob(File.join(testdir, "fontcustom", "fontcustom_*\.{ttf,svg,woff,eot}")).length).to eq(4) 27 | expect(File.read(manifest)).to match(/"fonts":.*fontcustom\/fontcustom_.+\.ttf"/m) 28 | end 29 | end 30 | end 31 | 32 | context ".create_output_dirs" do 33 | it "should create empty dirs if they don't exist" do 34 | gen = generator 35 | options = { 36 | :output => {:fonts => "path/fonts", :vectors => "path/vectors"}, 37 | :quiet => true 38 | } 39 | gen.instance_variable_set :@options, options 40 | expect(gen).to receive(:empty_directory).with("path/fonts", :verbose => false) 41 | expect(gen).to receive(:empty_directory).with("path/vectors", :verbose => false) 42 | gen.send :create_output_dirs 43 | end 44 | end 45 | 46 | context ".set_glyph_info" do 47 | it "should set :glyphs in manifest" do 48 | gen = generator 49 | gen.instance_variable_set :@options, :input => {:vectors => fixture("shared/vectors")} 50 | manifest = gen.instance_variable_get(:@manifest) 51 | 52 | gen.send :set_glyph_info 53 | data = manifest.instance_variable_get(:@data) 54 | expect(data[:glyphs][:C]).to include(:codepoint => 61696) 55 | expect(data[:glyphs][:D]).to include(:codepoint => 61697) 56 | expect(data[:glyphs][:"a_R3ally-eXotic-f1Le-Name"]).to include(:codepoint => 61698) 57 | end 58 | 59 | it "should not change codepoints of existing glyphs" do 60 | gen = generator 61 | gen.instance_variable_set :@options, :input => {:vectors => fixture("shared/vectors")} 62 | manifest = gen.instance_variable_get(:@manifest) 63 | manifest.set :glyphs, {:C => {:source => "foo", :codepoint => 61699}} 64 | 65 | gen.send :set_glyph_info 66 | data = manifest.instance_variable_get(:@data) 67 | expect(data[:glyphs][:C]).to include(:codepoint => 61699) 68 | expect(data[:glyphs][:D]).to include(:codepoint => 61700) 69 | expect(data[:glyphs][:"a_R3ally-eXotic-f1Le-Name"]).to include(:codepoint => 61701) 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/fontcustom/generator/font.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "open3" 3 | 4 | module Fontcustom 5 | module Generator 6 | class Font 7 | include Utility 8 | 9 | attr_reader :manifest 10 | 11 | def initialize(manifest) 12 | @manifest = Fontcustom::Manifest.new manifest 13 | @options = @manifest.get :options 14 | end 15 | 16 | def generate 17 | create_output_dirs 18 | delete_old_fonts 19 | set_glyph_info 20 | create_fonts 21 | end 22 | 23 | private 24 | 25 | def create_output_dirs 26 | dirs = @options[:output].values.uniq 27 | dirs.each do |dir| 28 | unless File.directory? dir 29 | empty_directory dir, :verbose => false 30 | say_message :create, dir 31 | end 32 | end 33 | end 34 | 35 | def delete_old_fonts 36 | @manifest.delete :fonts 37 | end 38 | 39 | def set_glyph_info 40 | manifest_glyphs = @manifest.get :glyphs 41 | codepoint = if ! manifest_glyphs.empty? 42 | codepoints = manifest_glyphs.values.map { |data| data[:codepoint] } 43 | codepoints.max + 1 44 | else 45 | # Offset to work around Chrome Windows bug 46 | # https://github.com/FontCustom/fontcustom/issues/1 47 | 0xf100 48 | end 49 | 50 | files = Dir.glob File.join(@options[:input][:vectors], "*.svg") 51 | glyphs = {} 52 | files.each do |file| 53 | name = File.basename file, ".svg" 54 | name = name.strip.gsub(/\W/, "-") 55 | glyphs[name.to_sym] = { :source => file } 56 | if File.read(file).include? "rgba" 57 | say_message :warn, "`#{file}` contains transparency and will be skipped." 58 | end 59 | end 60 | 61 | # Dir.glob returns a different order depending on ruby 62 | # version/platform, so we have to sort it first 63 | glyphs = Hash[glyphs.sort_by { |key, val| key.to_s }] 64 | glyphs.each do |name, data| 65 | if manifest_glyphs.has_key? name 66 | data[:codepoint] = manifest_glyphs[name][:codepoint] 67 | else 68 | data[:codepoint] = codepoint 69 | codepoint = codepoint + 1 70 | end 71 | end 72 | @manifest.set :glyphs, glyphs 73 | end 74 | 75 | def create_fonts 76 | cmd = "fontforge -script #{Fontcustom.gem_lib}/scripts/generate.py #{@manifest.manifest}" 77 | stdout, stderr, status = Open3::capture3(cmd) 78 | stdout = stdout.split("\n") 79 | stdout = stdout[1..-1] if stdout[0] == "CreateAllPyModules()" 80 | 81 | debug_msg = " Try again with --debug for more details." 82 | if @options[:debug] 83 | messages = stderr.split("\n") + stdout 84 | say_message :debug, messages.join(line_break) 85 | debug_msg = "" 86 | end 87 | 88 | if status.success? 89 | @manifest.reload 90 | say_changed :create, @manifest.get(:fonts) 91 | else 92 | raise Fontcustom::Error, "`fontforge` compilation failed.#{debug_msg}" 93 | end 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/fixtures/generators/mixed-output/fontcustom.css: -------------------------------------------------------------------------------- 1 | /* 2 | Font Custom - icon webfonts made simple 3 | */ 4 | 5 | @font-face { 6 | font-family: "fontcustom"; 7 | src: url("fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.eot?#iefix") format("embedded-opentype"), 8 | url("fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.woff") format("woff"), 9 | url("fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.ttf") format("truetype"), 10 | url("fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.svg#fontcustom") format("svg"); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | 15 | /* 16 | Bootstrap Overrides 17 | */ 18 | 19 | [class^="icon-"], 20 | [class*=" icon-"] { 21 | display: inherit; 22 | width: auto; 23 | height: auto; 24 | margin-top: auto; 25 | *margin-right: auto; 26 | vertical-align: text-top; 27 | background-image: none; 28 | } 29 | 30 | [class^="icon-"]:before, [class*=" icon-"]:before { 31 | font-family: "fontcustom"; 32 | font-weight: normal; 33 | font-style: normal; 34 | display: inline-block; 35 | text-decoration: inherit; 36 | } 37 | 38 | a [class^="icon-"], a [class*=" icon-"] { 39 | display: inline-block; 40 | text-decoration: inherit; 41 | } 42 | 43 | [class^="icon-"]:before, [class*=" icon-"]:before { 44 | font-family: "fontcustom"; 45 | font-weight: normal; 46 | font-style: normal; 47 | display: inline-block; 48 | text-decoration: inherit; 49 | } 50 | 51 | a [class^="icon-"], a [class*=" icon-"] { 52 | display: inline-block; 53 | text-decoration: inherit; 54 | } 55 | 56 | /* makes the font 33% larger relative to the icon container */ 57 | .icon-large:before { 58 | vertical-align: top; 59 | font-size: 1.333em; 60 | } 61 | 62 | /* keeps button heights with and without icons the same */ 63 | .btn [class^="icon-"], .btn [class*=" icon-"] { 64 | line-height: 0.9em; 65 | } 66 | 67 | li [class^="icon-"], li [class*=" icon-"] { 68 | display: inline-block; 69 | width: 1.25em; 70 | text-align: center; 71 | } 72 | 73 | /* 1.5 increased font size for icon-large * 1.25 width */ 74 | li .icon-large[class^="icon-"], li .icon-large[class*=" icon-"] { 75 | width: 1.875em; 76 | } 77 | 78 | li[class^="icon-"], li[class*=" icon-"] { 79 | margin-left: 0; 80 | list-style-type: none; 81 | } 82 | 83 | li[class^="icon-"]:before, li[class*=" icon-"]:before { 84 | text-indent: -2em; 85 | text-align: center; 86 | } 87 | 88 | li[class^="icon-"].icon-large:before, li[class*=" icon-"].icon-large:before { 89 | text-indent: -1.333em; 90 | } 91 | 92 | /* 93 | Icon Classes 94 | */ 95 | 96 | .icon-browser:before { content: "\f100"; } 97 | .icon-customize:before { content: "\f101"; } 98 | .icon-factory:before { content: "\f102"; } 99 | .icon-glasses:before { content: "\f103"; } 100 | .icon-graffiti:before { content: "\f104"; } 101 | .icon-lightning:before { content: "\f105"; } 102 | .icon-outlet:before { content: "\f106"; } 103 | .icon-pen:before { content: "\f107"; } 104 | .icon-pinch:before { content: "\f108"; } 105 | .icon-scale:before { content: "\f109"; } 106 | .icon-star:before { content: "\f10a"; } 107 | .icon-walk:before { content: "\f10b"; } 108 | .icon-windmill:before { content: "\f10c"; } 109 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rspec" 2 | require "json" 3 | require "fileutils" 4 | require File.expand_path("../../lib/fontcustom.rb", __FILE__) 5 | 6 | RSpec.configure do |c| 7 | c.before(:all) do 8 | FileUtils.cd fixture 9 | puts "Running `cd #{Dir.pwd}`" 10 | end 11 | 12 | def fixture(path = "") 13 | File.join(File.expand_path("../fixtures", __FILE__), path) 14 | end 15 | 16 | def manifest_contents 17 | { 18 | :checksum => { 19 | :current => "82a59e769bc60192484f2620570bbb59e225db97c1aac3f242a2e49d6060a19c", 20 | :previous => "82a59e769bc60192484f2620570bbb59e225db97c1aac3f242a2e49d6060a19c" 21 | }, 22 | :fonts => [ 23 | "fontcustom/fontcustom_82a59e769bc60192484f2620570bbb59.ttf", 24 | "fontcustom/fontcustom_82a59e769bc60192484f2620570bbb59.svg", 25 | "fontcustom/fontcustom_82a59e769bc60192484f2620570bbb59.woff", 26 | "fontcustom/fontcustom_82a59e769bc60192484f2620570bbb59.eot" 27 | ], 28 | :glyphs => { 29 | :"a_r3ally-exotic-f1le-name" => { 30 | :codepoint => 61696, 31 | :source => "vectors/a_R3ally-eXotic f1Le Name.svg" 32 | }, 33 | :c => { 34 | :codepoint => 61697, 35 | :source => "vectors/C.svg" 36 | }, 37 | :d => { 38 | :codepoint => 61698, 39 | :source => "vectors/D.svg"} 40 | }, 41 | :options => { 42 | :autowidth => false, 43 | :config => false, 44 | :css_selector => ".icon-{{glyph}}", 45 | :debug => false, 46 | :font_name => "fontcustom", 47 | :force => true, 48 | :input => { 49 | :templates => "vectors", 50 | :vectors => "vectors" 51 | }, 52 | :no_hash => false, 53 | :output => { 54 | :css => "fontcustom", 55 | :fonts => "fontcustom", 56 | :preview => "fontcustom" 57 | }, 58 | :preprocessor_path => nil, 59 | :quiet => true, 60 | :templates => [ 61 | "css", 62 | "scss", 63 | "preview" 64 | ] 65 | }, 66 | :templates => [] 67 | } 68 | end 69 | 70 | def fontforge_stderr 71 | "Copyright (c) 2000-2012 by George Williams.\n Executable based on sources from 14:57 GMT 31-Jul-2012-D.\n Library based on sources from 14:57 GMT 31-Jul-2012.\n" 72 | end 73 | 74 | def capture(stream) 75 | begin 76 | stream = stream.to_s 77 | eval "$#{stream} = StringIO.new" 78 | yield 79 | result = eval("$#{stream}").string 80 | ensure 81 | eval("$#{stream} = #{stream.upcase}") 82 | end 83 | result 84 | end 85 | 86 | def live_test 87 | testdir = fixture File.join("sandbox", "test") 88 | FileUtils.rm_r testdir if File.directory?(testdir) 89 | FileUtils.mkdir testdir 90 | FileUtils.cp_r fixture("shared/vectors"), testdir 91 | FileUtils.cd testdir do 92 | yield(testdir) 93 | end 94 | end 95 | 96 | def test_manifest(options = { :input => "vectors", :quiet => true }) 97 | base = Fontcustom::Base.new options 98 | manifest = base.instance_variable_get :@manifest 99 | checksum = base.send :checksum 100 | manifest.set :checksum, { :current => checksum, :previous => "" } 101 | manifest 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/fontcustom/templates/fontcustom.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================= 2 | # Font Custom Configuration 3 | # This file should live in the directory where you run `fontcustom compile`. 4 | # For more info, visit . 5 | # ============================================================================= 6 | 7 | 8 | # ----------------------------------------------------------------------------- 9 | # Project Info 10 | # ----------------------------------------------------------------------------- 11 | 12 | # The font's name. Also determines the file names of generated templates. 13 | #font_name: icons 14 | 15 | # Format of CSS selectors. {{glyph}} is substituted for the glyph name. 16 | #css_selector: .i-{{glyph}} 17 | 18 | # Generate fonts without asset-busting hashes. 19 | #no_hash: true 20 | 21 | # Encode WOFF fonts into the generated CSS. 22 | #base64: true 23 | 24 | # Forces compilation, even if inputs have not changed 25 | #force: true 26 | 27 | # Display (possibly useful) debugging messages. 28 | #debug: true 29 | 30 | # Hide status messages. 31 | #quiet: true 32 | 33 | 34 | # ----------------------------------------------------------------------------- 35 | # Input / Output Locations 36 | # You can save generated fonts, CSS, and other files to different locations 37 | # here. Font Custom can also read input vectors and templates from different 38 | # places. 39 | # 40 | # NOTE: 41 | # - Be sure to preserve the whitespace in these YAML hashes. 42 | # - INPUT[:vectors] and OUTPUT[:fonts] are required. Everything else is 43 | # optional. 44 | # - Specify output locations for custom templates by including their file 45 | # names as the key. 46 | # ----------------------------------------------------------------------------- 47 | 48 | #input: 49 | # vectors: my/vectors 50 | # templates: my/templates 51 | 52 | #output: 53 | # fonts: app/assets/fonts 54 | # css: app/assets/stylesheets 55 | # preview: app/views/styleguide 56 | # my-custom-template.yml: path/to/template/output 57 | 58 | 59 | # ----------------------------------------------------------------------------- 60 | # Templates 61 | # A YAML array of templates and files to generate alongside fonts. Custom 62 | # templates should be saved in the INPUT[:templates] directory and referenced 63 | # by their base file name. 64 | # 65 | # For Rails and Compass templates, set `preprocessor_path` as the relative 66 | # path from OUTPUT[:css] to OUTPUT[:fonts]. By default, these are the same 67 | # directory. 68 | # 69 | # Included in Font Custom: preview, css, scss, scss-rails 70 | # Default: css, preview 71 | # ----------------------------------------------------------------------------- 72 | 73 | #templates: 74 | #- scss-rails 75 | #- preview 76 | #- my-custom-template.yml 77 | 78 | #preprocessor_path: ../fonts/ 79 | 80 | 81 | # ----------------------------------------------------------------------------- 82 | # Font Settings (defaults shown) 83 | # ----------------------------------------------------------------------------- 84 | 85 | # Size (in pica points) for which your font is designed. 86 | #font_design_size: 16 87 | 88 | # The em size. Setting this will scale the entire font to the given size. 89 | #font_em: 512 90 | 91 | # The font's ascent and descent. Used to calculate the baseline. 92 | #font_ascent: 448 93 | #font_descent: 64 94 | 95 | # Horizontally fit glyphs to their individual vector widths. 96 | #autowidth: false 97 | -------------------------------------------------------------------------------- /spec/fixtures/shared/vectors/D.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fontcustom/watcher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "fileutils" 3 | require "fontcustom/watcher" 4 | 5 | describe Fontcustom::Watcher do 6 | # Silence messages without passing :quiet => true to everything 7 | #before(:each) do 8 | #Fontcustom::Options.any_instance.stub :say_message 9 | #end 10 | 11 | def watcher(options) 12 | allow_any_instance_of(Fontcustom::Manifest).to receive(:write_file) 13 | allow_any_instance_of(Fontcustom::Base).to receive(:compile) 14 | 15 | # undocumented — non-blocking use of watcher for testing 16 | Fontcustom::Watcher.new options, true 17 | end 18 | 19 | context "#watch" do 20 | it "should compile on init" do 21 | expect_any_instance_of(Fontcustom::Base).to receive(:compile).once 22 | 23 | w = watcher( 24 | :input => "shared/vectors", 25 | :output => "output" 26 | ) 27 | 28 | # silence output 29 | capture(:stdout) do 30 | w.watch 31 | w.send :stop 32 | end 33 | end 34 | 35 | it "should not call generators on init if options[:skip_first] is passed" do 36 | expect_any_instance_of(Fontcustom::Base).to_not receive(:compile).once 37 | 38 | w = watcher( 39 | :input => "shared/vectors", 40 | :output => "output", 41 | :skip_first => true 42 | ) 43 | 44 | capture(:stdout) do 45 | w.watch 46 | w.send :stop 47 | end 48 | end 49 | 50 | it "should call generators when vectors change" do 51 | expect_any_instance_of(Fontcustom::Base).to receive(:compile).once 52 | 53 | w = watcher( 54 | :input => "shared/vectors", 55 | :output => "output", 56 | :skip_first => true 57 | ) 58 | 59 | capture(:stdout) do 60 | begin 61 | w.watch 62 | FileUtils.cp fixture("shared/vectors/C.svg"), fixture("shared/vectors/test.svg") 63 | sleep 1 64 | ensure 65 | w.send :stop 66 | new = fixture("shared/vectors/test.svg") 67 | FileUtils.rm(new) if File.exists?(new) 68 | end 69 | end 70 | end 71 | 72 | it "should call generators when custom templates change" do 73 | expect_any_instance_of(Fontcustom::Base).to receive(:compile) 74 | 75 | w = watcher( 76 | :input => {:vectors => "shared/vectors", :templates => "shared/templates"}, 77 | :templates => %w|css preview custom.css|, 78 | :output => "output", 79 | :skip_first => false 80 | ) 81 | 82 | capture(:stdout) do 83 | begin 84 | template = fixture "shared/templates/custom.css" 85 | content = File.read template 86 | new = content + "\n.bar { color: red; }" 87 | 88 | w.watch 89 | sleep 1 90 | File.open(template, "w") { |file| file.write(new) } 91 | sleep 1 92 | ensure 93 | w.send :stop 94 | File.open(template, "w") { |file| file.write(content) } 95 | end 96 | end 97 | 98 | end 99 | 100 | it "should do nothing when non-vectors change" do 101 | expect_any_instance_of(Fontcustom::Base).to_not receive(:compile).once 102 | 103 | w = watcher( 104 | :input => "shared/vectors", 105 | :output => "output", 106 | :skip_first => true 107 | ) 108 | 109 | capture(:stdout) do 110 | begin 111 | w.watch 112 | FileUtils.touch fixture("shared/vectors/non-vector-file") 113 | ensure 114 | w.send :stop 115 | new = fixture("shared/vectors/non-vector-file") 116 | FileUtils.rm(new) if File.exists?(new) 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/fixtures/shared/vectors/C.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/fixtures/example/example.css: -------------------------------------------------------------------------------- 1 | /* 2 | Icon Font: example 3 | */ 4 | 5 | @font-face { 6 | font-family: "example"; 7 | src: url("./example.eot?") format("embedded-opentype"); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: "example"; 14 | src: url("data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAgAAA0AAAAAC4QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAH5AAAABkAAAAccaH0x09TLzIAAAGgAAAASgAAAGBAoF1jY21hcAAAAgAAAABCAAABQgAP9K1jdnQgAAACRAAAAAQAAAAEABEBRGdhc3AAAAfcAAAACAAAAAj//wADZ2x5ZgAAAlgAAAQzAAAF/Pd0+9ZoZWFkAAABMAAAAC8AAAA2Ak7J+WhoZWEAAAFgAAAAHQAAACQD1AH6aG10eAAAAewAAAAUAAAAFAYSAKVsb2NhAAACSAAAAA4AAAAOBHwC1m1heHAAAAGAAAAAIAAAACAAUwD/bmFtZQAABowAAAEdAAAB+O5hv99wb3N0AAAHrAAAAC0AAABG3Z3qEXjaY2BkYGAA4raDayfE89t8ZeBmYgCBC7OOLIPTgv+/Mr5k3A3kcjCApQFxtQ2QAHjaY2BkYGDc/f8rgx4TAwgwvmRgZEAFLABnMQPRAAAAAAEAAAAGAM4ACwAAAAAAAgAAAAEAAQAAAEAALgAAAAB42mNgYWJgnMDAysDA6MOYxsDA4A6lvzJIMrQwMDAxsHIywIEAgskQkOaawnDgI8NHJsYD/w8w6DHuZuAGCjMiKVFgYAQADZcLmAAAAgAAEQAAAAACAAAAAgAAWQASADt42mNgYGBmgGAZBkYGELAB8hjBfBYGBSDNAoRA/kem//+BJMP///zMUJUMjGwMMCYDIxOQYGJABYwMwx4AAD6CBq4AAAARAUQAAAAqACoAKgFUAoIC/gAAeNpdVM2LHEUUr1ef3VVd3dOftTPuzLg9u92LibNJ90z3YrIbFZQISkTMF6whii6I5BDE5CAejAdz8eAhiHfxoiBZvIgk4DEB/wP/gRw8KqKysTqbjdlAV9f7vV/V772q9yiEUYgQ+hpOI4IEmt4EtHZ8R1D0e3WTs9+O7xBsTXSTdG7WuXcEh3+P70Dnr8M6LOtwEl6/ur0Np3e/C6G2agqdv/8HfAvfW+0l9BZ6H32EPkHX0OfoK3Qb/YpQk89nxST3IU2s1VRZmnWenD/CPJ/isiin0LSbMIaMrwE3WduYEa02ybzp7FLw4gQ0mRkBF1z4uMjLwjJV22SCVCOcJjwvZk1WNZ12YiaiqcvaFBMzxT4R5SbJzIrdcwI3Y2j3Bu5CFWsg+P4IIDd2kQ1mU8lMJvifocuJoGH0VHQucjwnigfx2olq6FPP9QTFuMdKN3YT5lPhYSBBVlaraUCJElSTnpu4Ex5gTLldToPF8syQe8KcXOAeP+n2PDFYHgyUr5Qv0/TtrYxmF7eyD8yRouwvuCSgYT1/bXn3H5cwvRwzpxgLMS4cFq9qxztMcbjM2OIi5cshJqv4G4LDnlBCmp69JHzNZS4VxHV1FGnXgu7r7BvZyqKPpd7ItcPEIRnJrO1RAI/pkYljM9LMA6DBLLJUyamjnz6mJfYXV0Y2e26M/cG7QvXcaDBY7m/IQMogvpxuXU4v+AFhjmI4JTLmvTSwp/YMBydmLHZsXT3apxpI6gCJIgJOirG+irVmmjuYU84pQi6KUAL34Iqdc1ShF9Eb6B30MfoU3UWo6wvsQwBd7WZdFW0bJP9DW9gn8BOseIIvD+4ULOOTvW6YHFmDvGjj+axeqoZQLqVLVR23JT8YbV/AGofgoXJp++/xVGZHhWmbyrqK/Uhb9koJAMF9wH3GlCYXtWKsjx+iF7wO7XEe/bJjwNpSE6KfO8Dd2BcgnRtWgdJbHOdD4HCLUlh34KzT2/0JXgJwp5FdvB+CaBVDJypcjPGbdGA5Cj+G0u2ovVzGP+SgcPmyqx5EVvwzJTFjlGGDsWGaSYWNUtSnCwALdlKKXFLSEgbgIT98DElC5BUpD3jee6SEH+y8S3gC7NxZDgkn6P6G42zMp7t/s9cJeVxGnt4DHCvgtEuH8ldtx5NH8vBFvM3YtqPknjZHDB27/xfcgZ/REK2iZ213nUdoxSy187bcxG0zgrraBDNiY9tiRlj7wWOVZO3cvlgmbeazsk4rI+ybdtTOI5ImIp3Mj0zyss3qqp5P8XzWwvXndz985vwGUZMIc4dS6VG5kKWhTyg+c+7q4PBg/dT6+oobydgNNPY0ABaOxyYu9mLs+xg8qSe/3Llw+xUjkoJDFEe2dEI4nAM2YXuvf3hQrq+fai/1qRTjPnWcs4rGbASYOsoZUu2xRIwo89WQov8ADu6qvgB42nWPwWrCQBRF72hUupHuup1CFwomTAbduKwQCu5cSJcdZIiBmIQxgv5UP6I/00/osr0Tx0UXBibvvJeXe+8AGOMTAtfnCSqwwAjvgXvkJnAfL/gKHHH+E3iAR/EceIiReOWmiB7YJd1fngW93gL3yB+B+5yeA0ecfwceQOI38BBjobFCzSQXOBTIsUfLjQl2mLJqZk8xx4y8huEGVnVzcUW+b+VkN5VapfOZXBt+sPQzOFCrJMOezaEpCRt2OU6cGnpgY/NTaQgZfSu6+eq4YTu/hI4SS57/eteZxgIxE6V837Ihq6s2q11upU6UXMrgTNKLOE1jn/F+vC07hyOv5uNIinrZpKs+CrbWHYu6kkqliVJK3pX6A9Z1SrEAAAB42mNgYgCD/wcYJIEUIwM6YAOLMjEyMTIzsrCX5mW6GRoYQGlDKG0EAMecCHIAAAAAAAAB//8AAnjaY2BgYGQAggs52VVgetaRZTAaAE0xB8sAAAA=") format("woff"), 15 | url("./example.ttf") format("truetype"), 16 | url("./example.svg#example") format("svg"); 17 | font-weight: normal; 18 | font-style: normal; 19 | } 20 | 21 | @media screen and (-webkit-min-device-pixel-ratio:0) { 22 | @font-face { 23 | font-family: "example"; 24 | src: url("./example.svg#example") format("svg"); 25 | } 26 | } 27 | 28 | [data-icon]:before { content: attr(data-icon); } 29 | 30 | [data-icon]:before, 31 | .icon-C:before, 32 | .icon-D:before, 33 | .icon-a_R3ally-eXotic-f1Le-Name:before { 34 | display: inline-block; 35 | font-family: "example"; 36 | font-style: normal; 37 | font-weight: normal; 38 | font-variant: normal; 39 | line-height: 1; 40 | text-decoration: inherit; 41 | text-rendering: optimizeLegibility; 42 | text-transform: none; 43 | -moz-osx-font-smoothing: grayscale; 44 | -webkit-font-smoothing: antialiased; 45 | font-smoothing: antialiased; 46 | } 47 | 48 | .icon-C:before { content: "\f100"; } 49 | .icon-D:before { content: "\f101"; } 50 | .icon-a_R3ally-eXotic-f1Le-Name:before { content: "\f102"; } 51 | -------------------------------------------------------------------------------- /spec/fixtures/example/_example-rails.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Icon Font: example 3 | // 4 | 5 | @font-face { 6 | font-family: "example"; 7 | src: font-url("../foo/bar/example.eot?") format("embedded-opentype"); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: "example"; 14 | src: url("data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAgAAA0AAAAAC4QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAH5AAAABkAAAAccaH0x09TLzIAAAGgAAAASgAAAGBAoF1jY21hcAAAAgAAAABCAAABQgAP9K1jdnQgAAACRAAAAAQAAAAEABEBRGdhc3AAAAfcAAAACAAAAAj//wADZ2x5ZgAAAlgAAAQzAAAF/Pd0+9ZoZWFkAAABMAAAAC8AAAA2Ak7J+WhoZWEAAAFgAAAAHQAAACQD1AH6aG10eAAAAewAAAAUAAAAFAYSAKVsb2NhAAACSAAAAA4AAAAOBHwC1m1heHAAAAGAAAAAIAAAACAAUwD/bmFtZQAABowAAAEdAAAB+O5hv99wb3N0AAAHrAAAAC0AAABG3Z3qEXjaY2BkYGAA4raDayfE89t8ZeBmYgCBC7OOLIPTgv+/Mr5k3A3kcjCApQFxtQ2QAHjaY2BkYGDc/f8rgx4TAwgwvmRgZEAFLABnMQPRAAAAAAEAAAAGAM4ACwAAAAAAAgAAAAEAAQAAAEAALgAAAAB42mNgYWJgnMDAysDA6MOYxsDA4A6lvzJIMrQwMDAxsHIywIEAgskQkOaawnDgI8NHJsYD/w8w6DHuZuAGCjMiKVFgYAQADZcLmAAAAgAAEQAAAAACAAAAAgAAWQASADt42mNgYGBmgGAZBkYGELAB8hjBfBYGBSDNAoRA/kem//+BJMP///zMUJUMjGwMMCYDIxOQYGJABYwMwx4AAD6CBq4AAAARAUQAAAAqACoAKgFUAoIC/gAAeNpdVM2LHEUUr1ef3VVd3dOftTPuzLg9u92LibNJ90z3YrIbFZQISkTMF6whii6I5BDE5CAejAdz8eAhiHfxoiBZvIgk4DEB/wP/gRw8KqKysTqbjdlAV9f7vV/V772q9yiEUYgQ+hpOI4IEmt4EtHZ8R1D0e3WTs9+O7xBsTXSTdG7WuXcEh3+P70Dnr8M6LOtwEl6/ur0Np3e/C6G2agqdv/8HfAvfW+0l9BZ6H32EPkHX0OfoK3Qb/YpQk89nxST3IU2s1VRZmnWenD/CPJ/isiin0LSbMIaMrwE3WduYEa02ybzp7FLw4gQ0mRkBF1z4uMjLwjJV22SCVCOcJjwvZk1WNZ12YiaiqcvaFBMzxT4R5SbJzIrdcwI3Y2j3Bu5CFWsg+P4IIDd2kQ1mU8lMJvifocuJoGH0VHQucjwnigfx2olq6FPP9QTFuMdKN3YT5lPhYSBBVlaraUCJElSTnpu4Ex5gTLldToPF8syQe8KcXOAeP+n2PDFYHgyUr5Qv0/TtrYxmF7eyD8yRouwvuCSgYT1/bXn3H5cwvRwzpxgLMS4cFq9qxztMcbjM2OIi5cshJqv4G4LDnlBCmp69JHzNZS4VxHV1FGnXgu7r7BvZyqKPpd7ItcPEIRnJrO1RAI/pkYljM9LMA6DBLLJUyamjnz6mJfYXV0Y2e26M/cG7QvXcaDBY7m/IQMogvpxuXU4v+AFhjmI4JTLmvTSwp/YMBydmLHZsXT3apxpI6gCJIgJOirG+irVmmjuYU84pQi6KUAL34Iqdc1ShF9Eb6B30MfoU3UWo6wvsQwBd7WZdFW0bJP9DW9gn8BOseIIvD+4ULOOTvW6YHFmDvGjj+axeqoZQLqVLVR23JT8YbV/AGofgoXJp++/xVGZHhWmbyrqK/Uhb9koJAMF9wH3GlCYXtWKsjx+iF7wO7XEe/bJjwNpSE6KfO8Dd2BcgnRtWgdJbHOdD4HCLUlh34KzT2/0JXgJwp5FdvB+CaBVDJypcjPGbdGA5Cj+G0u2ovVzGP+SgcPmyqx5EVvwzJTFjlGGDsWGaSYWNUtSnCwALdlKKXFLSEgbgIT98DElC5BUpD3jee6SEH+y8S3gC7NxZDgkn6P6G42zMp7t/s9cJeVxGnt4DHCvgtEuH8ldtx5NH8vBFvM3YtqPknjZHDB27/xfcgZ/REK2iZ213nUdoxSy187bcxG0zgrraBDNiY9tiRlj7wWOVZO3cvlgmbeazsk4rI+ybdtTOI5ImIp3Mj0zyss3qqp5P8XzWwvXndz985vwGUZMIc4dS6VG5kKWhTyg+c+7q4PBg/dT6+oobydgNNPY0ABaOxyYu9mLs+xg8qSe/3Llw+xUjkoJDFEe2dEI4nAM2YXuvf3hQrq+fai/1qRTjPnWcs4rGbASYOsoZUu2xRIwo89WQov8ADu6qvgB42nWPwWrCQBRF72hUupHuup1CFwomTAbduKwQCu5cSJcdZIiBmIQxgv5UP6I/00/osr0Tx0UXBibvvJeXe+8AGOMTAtfnCSqwwAjvgXvkJnAfL/gKHHH+E3iAR/EceIiReOWmiB7YJd1fngW93gL3yB+B+5yeA0ecfwceQOI38BBjobFCzSQXOBTIsUfLjQl2mLJqZk8xx4y8huEGVnVzcUW+b+VkN5VapfOZXBt+sPQzOFCrJMOezaEpCRt2OU6cGnpgY/NTaQgZfSu6+eq4YTu/hI4SS57/eteZxgIxE6V837Ihq6s2q11upU6UXMrgTNKLOE1jn/F+vC07hyOv5uNIinrZpKs+CrbWHYu6kkqliVJK3pX6A9Z1SrEAAAB42mNgYgCD/wcYJIEUIwM6YAOLMjEyMTIzsrCX5mW6GRoYQGlDKG0EAMecCHIAAAAAAAAB//8AAnjaY2BgYGQAggs52VVgetaRZTAaAE0xB8sAAAA=") format("woff"), 15 | font-url("../foo/bar/example.ttf") format("truetype"), 16 | font-url("../foo/bar/example.svg#example") format("svg"); 17 | font-weight: normal; 18 | font-style: normal; 19 | } 20 | 21 | @media screen and (-webkit-min-device-pixel-ratio:0) { 22 | @font-face { 23 | font-family: "example"; 24 | src: font-url("../foo/bar/example.svg#example") format("svg"); 25 | } 26 | } 27 | 28 | [data-icon]:before { content: attr(data-icon); } 29 | 30 | [data-icon]:before, 31 | .icon-C:before, 32 | .icon-D:before, 33 | .icon-a_R3ally-eXotic-f1Le-Name:before { 34 | display: inline-block; 35 | font-family: "example"; 36 | font-style: normal; 37 | font-weight: normal; 38 | font-variant: normal; 39 | line-height: 1; 40 | text-decoration: inherit; 41 | text-rendering: optimizeLegibility; 42 | text-transform: none; 43 | -moz-osx-font-smoothing: grayscale; 44 | -webkit-font-smoothing: antialiased; 45 | font-smoothing: antialiased; 46 | } 47 | 48 | .icon-C:before { content: "\f100"; } 49 | .icon-D:before { content: "\f101"; } 50 | .icon-a_R3ally-eXotic-f1Le-Name:before { content: "\f102"; } 51 | -------------------------------------------------------------------------------- /lib/fontcustom/scripts/generate.py: -------------------------------------------------------------------------------- 1 | import fontforge 2 | import os 3 | import subprocess 4 | import tempfile 5 | import json 6 | 7 | # 8 | # Manifest / Options 9 | # Older Pythons don't have argparse, so we use optparse instead 10 | # 11 | 12 | try: 13 | import argparse 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument('manifest', help='Path to .fontcustom-manifest.json') 16 | args = parser.parse_args() 17 | manifestfile = open(args.manifest, 'r+') 18 | except ImportError: 19 | import optparse 20 | parser = optparse.OptionParser() 21 | (nothing, args) = parser.parse_args() 22 | manifestfile = open(args[0], 'r+') 23 | 24 | manifest = json.load(manifestfile) 25 | options = manifest['options'] 26 | 27 | # 28 | # Font 29 | # 30 | 31 | design_px = options['font_em'] / options['font_design_size'] 32 | 33 | font = fontforge.font() 34 | font.encoding = 'UnicodeFull' 35 | font.design_size = options['font_design_size'] 36 | font.em = options['font_em'] 37 | font.ascent = options['font_ascent'] 38 | font.descent = options['font_descent'] 39 | font.fontname = options['font_name'] 40 | font.familyname = options['font_name'] 41 | font.fullname = options['font_name'] 42 | if options['autowidth']: 43 | font.autoWidth(0, 0, options['font_em']) 44 | 45 | # 46 | # Glyphs 47 | # 48 | 49 | def removeSwitchFromSvg( file ): 50 | svgfile = open(file, 'r') 51 | svgtext = svgfile.read() 52 | svgfile.close() 53 | tmpsvgfile = tempfile.NamedTemporaryFile(suffix=".svg", delete=False) 54 | svgtext = svgtext.replace('', '') 55 | svgtext = svgtext.replace('', '') 56 | tmpsvgfile.file.write(svgtext) 57 | tmpsvgfile.file.close() 58 | 59 | return tmpsvgfile.name 60 | 61 | def createGlyph( name, source, code ): 62 | frag, ext = os.path.splitext(source) 63 | 64 | if ext == '.svg': 65 | temp = removeSwitchFromSvg(source) 66 | glyph = font.createChar(code) 67 | glyph.importOutlines(temp) 68 | os.unlink(temp) 69 | 70 | if options['autowidth']: 71 | glyph.left_side_bearing = glyph.right_side_bearing = 0 72 | glyph.round() 73 | else: 74 | glyph.width = options['font_em'] 75 | width = glyph.width - glyph.left_side_bearing - glyph.right_side_bearing 76 | aligned_to_pixel_grid = (width % design_px == 0) 77 | if (aligned_to_pixel_grid): 78 | shift = glyph.left_side_bearing % design_px 79 | glyph.left_side_bearing = glyph.left_side_bearing - shift 80 | glyph.right_side_bearing = glyph.right_side_bearing + shift 81 | 82 | # Add valid space glyph to avoid "unknown character" box on IE11 83 | glyph = font.createChar(32) 84 | glyph.width = 200 85 | 86 | for glyph, data in manifest['glyphs'].iteritems(): 87 | name = createGlyph(glyph, data['source'], data['codepoint']) 88 | 89 | # 90 | # Generate Files 91 | # 92 | 93 | try: 94 | fontfile = options['output']['fonts'] + '/' + options['font_name'] 95 | if not options['no_hash']: 96 | fontfile += '_' + manifest['checksum']['current'][:32] 97 | 98 | # Generate TTF and SVG 99 | font.generate(fontfile + '.ttf') 100 | font.generate(fontfile + '.svg') 101 | manifest['fonts'].append(fontfile + '.ttf') 102 | manifest['fonts'].append(fontfile + '.svg') 103 | 104 | # Fix SVG header for webkit 105 | # from: https://github.com/fontello/font-builder/blob/master/bin/fontconvert.py 106 | svgfile = open(fontfile + '.svg', 'r+') 107 | svgtext = svgfile.read() 108 | svgfile.seek(0) 109 | svgfile.write(svgtext.replace('''''', '''''')) 110 | svgfile.close() 111 | 112 | # Convert WOFF 113 | scriptPath = os.path.dirname(os.path.realpath(__file__)) 114 | try: 115 | subprocess.Popen([scriptPath + '/sfnt2woff', fontfile + '.ttf'], stdout=subprocess.PIPE) 116 | except OSError: 117 | # If the local version of sfnt2woff fails (i.e., on Linux), try to use the 118 | # global version. This allows us to avoid forcing OS X users to compile 119 | # sfnt2woff from source, simplifying install. 120 | subprocess.call(['sfnt2woff', fontfile + '.ttf']) 121 | manifest['fonts'].append(fontfile + '.woff') 122 | 123 | # Convert EOT for IE7 124 | subprocess.call('python ' + scriptPath + '/eotlitetool.py ' + fontfile + '.ttf -o ' + fontfile + '.eot', shell=True) 125 | subprocess.call('mv ' + fontfile + '.eotlite ' + fontfile + '.eot', shell=True) 126 | manifest['fonts'].append(fontfile + '.eot') 127 | 128 | # Convert TTF to WOFF2 129 | subprocess.call('woff2_compress \'' + fontfile + '.ttf\'', shell=True) 130 | manifest['fonts'].append(fontfile + '.woff2') 131 | 132 | finally: 133 | manifestfile.seek(0) 134 | manifestfile.write(json.dumps(manifest, indent=2, sort_keys=True)) 135 | manifestfile.truncate() 136 | manifestfile.close() 137 | -------------------------------------------------------------------------------- /lib/fontcustom/cli.rb: -------------------------------------------------------------------------------- 1 | require "thor" 2 | require "thor/actions" 3 | require "fontcustom" 4 | require "fontcustom/watcher" 5 | 6 | module Fontcustom 7 | class CLI < Thor 8 | include Utility 9 | 10 | default_task :show_help 11 | 12 | class_option :output, :aliases => "-o", :type => :string, 13 | :desc => "Where generated files are saved. Set different locations for different file types via a configuration file.", 14 | :default => EXAMPLE_OPTIONS[:output] 15 | 16 | class_option :config, :aliases => "-c", :type => :string, 17 | :desc => "Optional path to a configuration file.", 18 | :default => EXAMPLE_OPTIONS[:config] 19 | 20 | class_option :templates, :aliases => "-t", :type => :array, 21 | :desc => "Space-delinated list of files to generate alongside fonts. Use stock templates or choose your own.", 22 | :enum => %w|preview css scss scss-rails|, 23 | :default => EXAMPLE_OPTIONS[:templates] 24 | 25 | class_option :font_name, :aliases => %w|--name -n|, :type => :string, 26 | :desc => "The font's name. Also determines the file names of generated templates.", 27 | :default => DEFAULT_OPTIONS[:font_name] 28 | 29 | class_option :font_design_size, :aliases => %s|--size -s|, :type => :numeric, 30 | :desc => "Size (in pica points) for which this font is designed.", 31 | :default => DEFAULT_OPTIONS[:font_design_size] 32 | 33 | class_option :font_em, :aliases => %w|--em -e|, :type => :numeric, 34 | :desc => "The em size. Setting this will scale the entire font to the given size.", 35 | :default => DEFAULT_OPTIONS[:font_em] 36 | 37 | class_option :font_ascent, :aliases => %w|--ascent -a|, :type => :numeric, 38 | :desc => "The font's ascent. Used to calculate the baseline.", 39 | :default => DEFAULT_OPTIONS[:font_ascent] 40 | 41 | class_option :font_descent, :aliases => %w|--descent -d|, :type => :numeric, 42 | :desc => "The font's descent. Used to calculate the baseline.", 43 | :default => DEFAULT_OPTIONS[:font_descent] 44 | 45 | class_option :css_selector, :aliases => %w|--selector -S|, :type => :string, 46 | :desc => "Format of CSS selectors. \"{{glyph}}\" is substituted for the glyph name.", 47 | :default => DEFAULT_OPTIONS[:css_selector] 48 | 49 | class_option :preprocessor_path, :aliases => %w|--prepath -p|, :type => :string, 50 | :desc => "For Rails and Compass templates, set this as the relative path from your compiled CSS to your font output directory." 51 | 52 | class_option :autowidth, :aliases => "-A", :type => :boolean, 53 | :desc => "Horizontally fit glyphs to their individual vector widths." 54 | 55 | class_option :no_hash, :aliases => "-h", :type => :boolean, 56 | :desc => "Generate fonts without asset-busting hashes." 57 | 58 | class_option :base64, :aliases => "-b", :type => :boolean, 59 | :desc => "Encode WOFF fonts into the generated CSS." 60 | 61 | class_option :debug, :aliases => "-D", :type => :boolean, 62 | :desc => "Display (possibly useful) debugging messages." 63 | 64 | class_option :force, :aliases => "-F", :type => :boolean, 65 | :desc => "Forces compilation, even if inputs have not changed." 66 | 67 | class_option :quiet, :aliases => "-q", :type => :boolean, 68 | :desc => "Hide status messages." 69 | 70 | # Required for Thor::Actions#template 71 | def self.source_root 72 | File.join Fontcustom.gem_lib, "templates" 73 | end 74 | 75 | desc "compile [INPUT] [OPTIONS]", "Generates webfonts and templates from *.svg files in INPUT. Default: `pwd`" 76 | def compile(input = nil) 77 | Base.new(options.merge(:input => input)).compile 78 | rescue Fontcustom::Error => e 79 | say_status :error, e.message, :red 80 | puts e.backtrace.join("\n") if options[:debug] 81 | end 82 | 83 | desc "watch [INPUT] [OPTIONS]", "Watches INPUT for changes and regenerates files automatically. Ctrl + C to stop. Default: `pwd`" 84 | method_option :skip_first, :type => :boolean, 85 | :desc => "Skip the initial compile upon watching.", 86 | :default => false 87 | def watch(input = nil) 88 | say "Font Custom is watching your icons. Press Ctrl + C to stop.", :yellow unless options[:quiet] 89 | opts = options.merge :input => input, :skip_first => !! options[:skip_first] 90 | Watcher.new(opts).watch 91 | rescue Fontcustom::Error => e 92 | say_status :error, e.message, :red 93 | end 94 | 95 | desc "config [DIR]", "Generates a starter configuration file (fontcustom.yml) in DIR. Default: `pwd`" 96 | def config(dir = Dir.pwd) 97 | template "fontcustom.yml", File.join(dir, "fontcustom.yml") 98 | end 99 | 100 | desc "hidden", "hidden", :hide => true 101 | method_option :version, :aliases => "-v", :type => :boolean, :default => false 102 | def show_help 103 | if options[:version] 104 | puts "fontcustom-#{VERSION}" 105 | else 106 | help 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/fontcustom/templates/fontcustom-preview.html: -------------------------------------------------------------------------------- 1 | <% scale = %w|12 14 16 18 21 24 36 48 60 72| %> 2 | 3 | 4 | <%= font_name %> glyphs preview 5 | 6 | 135 | 136 | 137 | 138 | 144 | 145 | 146 | 147 |
148 |
149 |

<%= font_name %> contains <%= @glyphs.length %> glyphs:

150 | Toggle Preview Characters 151 |
152 | 153 | <% @glyphs.each do |name, value| 154 | selector = @options[:css_selector].sub('{{glyph}}', name.to_s) %> 155 |
156 |
157 | <% scale.each do |n| %>Pp<% end %> 158 |
159 |
160 | <% scale.each do |n| %><%= n %><% end %> 161 |
162 |
163 | 164 | 165 |
166 |
167 | <% end %> 168 | 169 | 172 |
173 | 174 | 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/fontcustom.png)](http://badge.fury.io/rb/fontcustom) 2 | [![Build Status](https://api.travis-ci.org/FontCustom/fontcustom.png)](https://travis-ci.org/FontCustom/fontcustom) 3 | [![Code Quality](https://codeclimate.com/github/FontCustom/fontcustom.png)](https://codeclimate.com/github/FontCustom/fontcustom) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=32953)](https://www.bountysource.com/trackers/32953-endtwist-fontcustom?utm_source=32953&utm_medium=shield&utm_campaign=TRACKER_BADGE) 4 | 5 | ## Font Custom 6 | 7 | **Icon fonts from the command line.** 8 | 9 | Generate cross-browser icon fonts and supporting files (@font-face CSS, etc.) 10 | from a collection of SVGs 11 | ([example](https://rawgit.com/FontCustom/fontcustom/master/spec/fixtures/example/example-preview.html)). 12 | 13 | [Changelog](https://github.com/FontCustom/fontcustom/blob/master/CHANGELOG.md)
14 | [Bugs/Support](https://github.com/FontCustom/fontcustom/issues)
15 | [Contribute!](https://github.com/FontCustom/fontcustom/blob/master/CONTRIBUTING.md) 16 | 17 | ### Installation 18 | 19 | Requires **Ruby 1.9.2+**, **WOFF2**, **FontForge** with Python scripting. 20 | 21 | ```sh 22 | # On Mac 23 | brew tap bramstein/webfonttools 24 | brew update 25 | brew install woff2 26 | 27 | brew install fontforge --with-python 28 | brew install eot-utils 29 | gem install fontcustom 30 | 31 | # On Linux 32 | sudo apt-get install fontforge 33 | wget http://people.mozilla.com/~jkew/woff/woff-code-latest.zip 34 | unzip woff-code-latest.zip -d sfnt2woff && cd sfnt2woff && make && sudo mv sfnt2woff /usr/local/bin/ 35 | git clone --recursive https://github.com/google/woff2.git && cd woff2 && make clean all && sudo mv woff2_compress /usr/local/bin/ && sudo mv woff2_decompress /usr/local/bin/ 36 | gem install fontcustom 37 | ``` 38 | 39 | ### Quick Start 40 | 41 | ```sh 42 | fontcustom compile my/vectors # Compiles icons into `fontcustom/` 43 | fontcustom watch my/vectors # Compiles when vectors are changed/added/removed 44 | fontcustom compile # Uses options from `./fontcustom.yml` or `config/fontcustom.yml` 45 | fontcustom config # Generate a blank a config file 46 | fontcustom help # See all options 47 | ``` 48 | 49 | ### Configuration 50 | 51 | To manage settings between compiles, run `fontcustom config` to generate a 52 | config file. Inside, you'll find a list of [**all possible options**](https://github.com/FontCustom/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.yml). 53 | Each option is also available as a dash-case command line flag (e.g. 54 | `--css-selector`) that overrides the config file. 55 | 56 | ### SVG Guidelines 57 | 58 | * All colors will be rendered identically. Watch out for white fills! 59 | * Use only solid colors. SVGs with transparency will be skipped. 60 | * For greater precision in curved icons, use fills instead strokes and [try 61 | these solutions](https://github.com/FontCustom/fontcustom/issues/85). 62 | * Activating `autowidth` trims horizontal white space from each glyph. This 63 | can be much easier than centering dozens of SVGs by hand. 64 | 65 | ### Advanced 66 | 67 | **For use with Compass and/or Rails** 68 | 69 | Set `templates` to include `scss-rails` to generate a SCSS partial with the 70 | compatible font-url() helper. You'll most likely also need to set 71 | `preprocessor_path` as the relative path from your compiled CSS to your output 72 | directory. 73 | 74 | **Save CSS and fonts to different locations** 75 | 76 | You can save generated fonts, CSS, and other files to different locations by 77 | using `fontcustom.yml`. Font Custom can also read input vectors and templates 78 | from different places. 79 | 80 | Just edit the `input` and `output` YAML hashes and their corresponding keys. 81 | 82 | **Tweak font settings** 83 | 84 | By default, Font Custom assumes a square viewBox, 512 by 512, and 16 pica 85 | points. Change `font_design_size`, `font_em`, `font_ascent`, `font_descent`, 86 | and `autowidth` to suit your own needs. 87 | 88 | **Generate LESS, Stylus, and other text files** 89 | 90 | Custom templates give you the flexibility to generate just about anything you 91 | want with Font Custom's output data. 92 | 93 | Any non-SVG file in your input directory (or input:templates directory if you 94 | set it in `fontcustom.yml`) will be available as a custom template to copy into 95 | the output directory after compilation. You just need to specify the file name 96 | under the `templates` hash. 97 | 98 | Any embedded ruby in the templates will be processed, along with the following 99 | helpers: 100 | 101 | * `font_name` 102 | * `font_face`: [FontSpring's Bulletproof @Font-Face Syntax](http://www.fontspring.com/blog/further-hardening-of-the-bulletproof-syntax) 103 | * `glyph_selectors`: comma-separated list of all icon CSS selectors 104 | * `glyphs`: all selectors and their codepoint assignments (`.icon-example:before { content: "\f103"; }`) 105 | * `@options`: a hash of options used during compilation 106 | * `@manifest`: a hash of options, generated file paths, code points, and just about everything else Font Custom knows. 107 | * `@font_path`: the path from CSS to font files (without an extension) 108 | * `@font_path_alt`: if `preprocessor_path` was set, this is the modified path 109 | 110 | `font_face` accepts a hash that modifies the CSS url() function and the path of 111 | the font files (`font_face(url: "font-url", path: @font_path_alt)`). 112 | 113 | --- 114 | 115 | [Licenses](https://github.com/FontCustom/fontcustom/blob/master/LICENSES.txt) 116 | 117 | Brought to you by [@endtwist](https://github.com/endtwist) and [@kaizau](https://github.com/kaizau) 118 | -------------------------------------------------------------------------------- /spec/fontcustom/generator/template_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Fontcustom::Generator::Template do 4 | context "#generate" do 5 | it "should generate templates (integration)", :integration => true do 6 | live_test do |testdir| 7 | FileUtils.cp_r fixture("generators/mixed-output"), "fontcustom" 8 | test_manifest( 9 | :input => "vectors", 10 | :quiet => true, 11 | :templates => %w|preview css scss scss-rails| 12 | ) 13 | manifest = File.join testdir, ".fontcustom-manifest.json" 14 | Fontcustom::Generator::Font.new(manifest).generate 15 | Fontcustom::Generator::Template.new(manifest).generate 16 | 17 | content = File.read manifest 18 | expect(content).to match(/fontcustom\/fontcustom-preview.html/) 19 | end 20 | end 21 | end 22 | 23 | context ".set_relative_paths" do 24 | it "should assign @font_path, @font_path_alt, and @font_path_preview" do 25 | gen = Fontcustom::Generator::Template.new fixture("generators/.fontcustom-manifest.json") 26 | options = gen.instance_variable_get :@options 27 | options[:output] = {:fonts => fixture("sandbox/test/fonts"), :css => fixture("sandbox/test/css"), :preview => fixture("sandbox/test")} 28 | 29 | gen.send :set_relative_paths 30 | expect(gen.instance_variable_get(:@font_path)).to match("../fonts") 31 | expect(gen.instance_variable_get(:@font_path_alt)).to match("../fonts") 32 | expect(gen.instance_variable_get(:@font_path_preview)).to match(".") 33 | end 34 | 35 | it "should assign @font_path_alt if :preprocessor_path is set" do 36 | gen = Fontcustom::Generator::Template.new fixture("generators/.fontcustom-manifest.json") 37 | options = gen.instance_variable_get :@options 38 | options[:preprocessor_path] = "fonts/fontcustom" 39 | options[:output] = {:fonts => fixture("sandbox/test/fonts"), :css => fixture("sandbox/test/css"), :preview => fixture("sandbox/test")} 40 | 41 | gen.send :set_relative_paths 42 | expect(gen.instance_variable_get(:@font_path_alt)).to match("fonts/fontcustom") 43 | end 44 | 45 | it "should assign @font_path_alt as bare font name if :preprocessor_path is false" do 46 | gen = Fontcustom::Generator::Template.new fixture("generators/.fontcustom-manifest.json") 47 | options = gen.instance_variable_get :@options 48 | options[:preprocessor_path] = false 49 | options[:output] = {:fonts => fixture("sandbox/test/fonts"), :css => fixture("sandbox/test/css"), :preview => fixture("sandbox/test")} 50 | 51 | gen.send :set_relative_paths 52 | expect(gen.instance_variable_get(:@font_path_alt)).to_not match("../fonts") 53 | end 54 | 55 | it "should assign '.' when paths are the same" do 56 | gen = Fontcustom::Generator::Template.new fixture("generators/.fontcustom-manifest.json") 57 | options = gen.instance_variable_get :@options 58 | options[:output] = {:fonts => fixture("sandbox/test/fonts"), :css => fixture("sandbox/test/fonts"), :preview => fixture("sandbox/test/fonts")} 59 | 60 | gen.send :set_relative_paths 61 | expect(gen.instance_variable_get(:@font_path)).to match("./") 62 | end 63 | end 64 | 65 | context ".create_files" do 66 | it "should not include the template path in custom output file paths" do 67 | gen = Fontcustom::Generator::Template.new fixture("generators/.fontcustom-manifest.json") 68 | 69 | # Set options to specify a custom CSS template with a custom output location. 70 | options = gen.instance_variable_get :@options 71 | options[:input][:templates] = fixture("shared/templates") 72 | options[:templates] = ['custom.css'] 73 | options[:output][:'custom.css'] = fixture("sandbox/test") 74 | 75 | # Don't update the manifest based on these options. 76 | manifest = gen.instance_variable_get :@manifest 77 | expect(manifest).to receive(:set) 78 | 79 | # Confirm that the output file doesn't include the template path. 80 | expect(gen).to receive(:template).once do |source, target| 81 | expect(source).to match("shared/templates") 82 | expect(target).to_not match("shared/templates") 83 | end 84 | 85 | gen.send :create_files 86 | end 87 | end 88 | 89 | context ".font_face" do 90 | it "should return base64 when options are set" do 91 | gen = Fontcustom::Generator::Template.new fixture("generators/.fontcustom-manifest.json") 92 | allow(gen).to receive(:woff_base64).and_return("3xampled4ta") 93 | options = gen.instance_variable_get :@options 94 | options[:base64] = true 95 | 96 | expect(gen.send(:font_face)).to match("x-font-woff") 97 | end 98 | end 99 | 100 | context ".get_target_path" do 101 | it "should generate the correct preview target when using default font_name" do 102 | gen = Fontcustom::Generator::Template.new fixture("generators/.fontcustom-manifest.json") 103 | options = gen.instance_variable_get :@options 104 | options[:output] = {:fonts => fixture("sandbox/test/fonts"), :preview => fixture("sandbox/test")} 105 | expect(gen.send(:get_target_path, "sandbox/test/fontcustom-preview.html")).to match("/sandbox/test/fontcustom-preview.html") 106 | end 107 | it "should generate the correct preview target when using custom font_name with output directory containing 'fontcustom'" do 108 | gen = Fontcustom::Generator::Template.new fixture("generators/.fontcustom-manifest.json") 109 | options = gen.instance_variable_get :@options 110 | options[:font_name] = 'custom' 111 | options[:output] = {:fonts => fixture("sandbox/test-fontcustom/fonts"), :preview => fixture("sandbox/test-fontcustom")} 112 | expect(gen.send(:get_target_path, "sandbox/test/fontcustom-preview.html")).to match("/sandbox/test-fontcustom/custom-preview.html") 113 | end 114 | end 115 | 116 | end 117 | -------------------------------------------------------------------------------- /spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by FontForge 20120731 at Sun Nov 10 12:45:07 2013 9 | By Kai Zau 10 | Created by Kai Zau with FontForge 2.0 (http://fontforge.sf.net) 11 | 12 | 13 | 14 | 27 | 28 | 47 | 50 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/fontcustom/options.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require "pp" 3 | 4 | module Fontcustom 5 | class Options 6 | include Utility 7 | 8 | def initialize(cli_options = {}) 9 | @manifest = cli_options[:manifest] 10 | @cli_options = symbolize_hash(cli_options) 11 | parse_options 12 | end 13 | 14 | private 15 | 16 | def parse_options 17 | overwrite_examples 18 | set_config_path 19 | load_config 20 | merge_options 21 | clean_font_name 22 | clean_css_selector 23 | set_input_paths 24 | set_output_paths 25 | check_template_paths 26 | print_debug if @options[:debug] 27 | end 28 | 29 | # We give Thor fake defaults to generate more useful help messages. 30 | # Here, we delete any CLI options that match those examples. 31 | # TODO There's *got* a be a cleaner way to customize Thor help messages. 32 | def overwrite_examples 33 | EXAMPLE_OPTIONS.keys.each do |key| 34 | @cli_options.delete(key) if @cli_options[key] == EXAMPLE_OPTIONS[key] 35 | end 36 | @cli_options = DEFAULT_OPTIONS.dup.merge @cli_options 37 | end 38 | 39 | def set_config_path 40 | @cli_options[:config] = if @cli_options[:config] 41 | path = @cli_options[:config] 42 | if File.exists?(path) && ! File.directory?(path) 43 | path 44 | elsif File.exists? File.join(path, "fontcustom.yml") 45 | File.join path, "fontcustom.yml" 46 | else 47 | raise Fontcustom::Error, "No configuration file found at `#{path}`." 48 | end 49 | else 50 | if File.exists? "fontcustom.yml" 51 | "fontcustom.yml" 52 | elsif File.exists? File.join("config", "fontcustom.yml") 53 | File.join "config", "fontcustom.yml" 54 | else 55 | false 56 | end 57 | end 58 | end 59 | 60 | def load_config 61 | @config_options = {} 62 | if @cli_options[:config] 63 | begin 64 | config = YAML.load File.open(@cli_options[:config]) 65 | if config # empty YAML returns false 66 | @config_options = symbolize_hash(config) 67 | say_message :debug, "Using settings from `#{@cli_options[:config]}`." if @cli_options[:debug] || @config_options[:debug] 68 | else 69 | say_message :warn, "`#{@cli_options[:config]}` was empty. Using defaults." 70 | end 71 | rescue Exception => e 72 | raise Fontcustom::Error, "Error parsing `#{@cli_options[:config]}`:\n#{e.message}" 73 | end 74 | end 75 | end 76 | 77 | # TODO validate keys 78 | def merge_options 79 | @cli_options.delete_if { |key, val| val == DEFAULT_OPTIONS[key] } 80 | @options = DEFAULT_OPTIONS.merge(@config_options).merge(@cli_options) 81 | @options.delete :manifest 82 | end 83 | 84 | def clean_font_name 85 | @options[:font_name] = @options[:font_name].strip.gsub(/\W/, "-") 86 | end 87 | 88 | def clean_css_selector 89 | unless @options[:css_selector].include? "{{glyph}}" 90 | raise Fontcustom::Error, 91 | "CSS selector `#{@options[:css_selector]}` should contain the \"{{glyph}}\" placeholder." 92 | end 93 | @options[:css_selector] = @options[:css_selector].strip.gsub(/[^&%=\[\]\.#\{\}""\d\w]/, "-") 94 | end 95 | 96 | def set_input_paths 97 | if @options[:input].is_a? Hash 98 | @options[:input] = symbolize_hash(@options[:input]) 99 | if @options[:input].has_key? :vectors 100 | check_input @options[:input][:vectors] 101 | else 102 | raise Fontcustom::Error, 103 | "Input paths (assigned as a hash) should have a :vectors key. Check your options." 104 | end 105 | 106 | if @options[:input].has_key? :templates 107 | check_input @options[:input][:templates] 108 | else 109 | @options[:input][:templates] = @options[:input][:vectors] 110 | end 111 | else 112 | if @options[:input] 113 | input = @options[:input] 114 | else 115 | input = "." 116 | say_message :warn, "No input directory given. Using present working directory." 117 | end 118 | check_input input 119 | @options[:input] = { :vectors => input, :templates => input } 120 | end 121 | 122 | if Dir[File.join(@options[:input][:vectors], "*.svg")].empty? 123 | raise Fontcustom::Error, "`#{@options[:input][:vectors]}` doesn't contain any SVGs." 124 | end 125 | end 126 | 127 | def set_output_paths 128 | if @options[:output].is_a? Hash 129 | @options[:output] = symbolize_hash(@options[:output]) 130 | unless @options[:output].has_key? :fonts 131 | raise Fontcustom::Error, 132 | "Output paths (assigned as a hash) should have a :fonts key. Check your options." 133 | end 134 | 135 | @options[:output].each do |key, val| 136 | @options[:output][key] = val 137 | if File.exists?(val) && ! File.directory?(val) 138 | raise Fontcustom::Error, 139 | "Output `#{@options[:output][key]}` exists but isn't a directory. Check your options." 140 | end 141 | end 142 | 143 | @options[:output][:css] ||= @options[:output][:fonts] 144 | @options[:output][:preview] ||= @options[:output][:fonts] 145 | else 146 | if @options[:output].is_a? String 147 | output = @options[:output] 148 | if File.exists?(output) && ! File.directory?(output) 149 | raise Fontcustom::Error, 150 | "Output `#{output}` exists but isn't a directory. Check your options." 151 | end 152 | else 153 | output = @options[:font_name] 154 | say_message :debug, "Generated files will be saved to `#{output}/`." if @options[:debug] 155 | end 156 | 157 | @options[:output] = { 158 | :fonts => output, 159 | :css => output, 160 | :preview => output 161 | } 162 | end 163 | end 164 | 165 | def check_template_paths 166 | @options[:templates].each do |template| 167 | next if %w|preview css scss scss-rails|.include? template 168 | path = File.expand_path File.join(@options[:input][:templates], template) unless template[0] == "/" 169 | unless File.exists? path 170 | raise Fontcustom::Error, 171 | "Custom template `#{template}` wasn't found in `#{@options[:input][:templates]}/`. Check your options." 172 | end 173 | end 174 | end 175 | 176 | def check_input(dir) 177 | if ! File.exists? dir 178 | raise Fontcustom::Error, 179 | "Input `#{dir}` doesn't exist. Check your options." 180 | elsif ! File.directory? dir 181 | raise Fontcustom::Error, 182 | "Input `#{dir}` isn't a directory. Check your options." 183 | end 184 | end 185 | 186 | def print_debug 187 | message = line_break(16) 188 | message << @options.pretty_inspect.split("\n ").join(line_break(16)) 189 | say_message :debug, "Using options:#{message}" 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /lib/fontcustom/generator/template.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "pathname" 3 | require "base64" 4 | 5 | module Fontcustom 6 | module Generator 7 | class Template 8 | include Utility 9 | 10 | attr_reader :manifest 11 | 12 | def initialize(manifest) 13 | @manifest = Fontcustom::Manifest.new manifest 14 | @options = @manifest.get :options 15 | end 16 | 17 | def generate 18 | if ! @manifest.get(:fonts).empty? 19 | delete_old_templates 20 | set_relative_paths 21 | create_files 22 | else 23 | raise Fontcustom::Error, "No generated fonts were detected - aborting template generation." 24 | end 25 | end 26 | 27 | private 28 | 29 | def delete_old_templates 30 | @manifest.delete :templates 31 | end 32 | 33 | def set_relative_paths 34 | fonts = @manifest.get :fonts 35 | name = File.basename fonts.first, File.extname(fonts.first) 36 | fonts_path = Pathname.new(@options[:output][:fonts]).realdirpath 37 | css_path = Pathname.new(@options[:output][:css]).realdirpath 38 | preview_path = Pathname.new(@options[:output][:preview]).realdirpath 39 | @font_path = File.join fonts_path.relative_path_from(css_path).to_s, name 40 | @font_path_alt = if @options[:preprocessor_path].nil? 41 | @font_path 42 | elsif ! @options[:preprocessor_path] || @options[:preprocessor_path].empty? 43 | name 44 | else 45 | File.join(@options[:preprocessor_path], name) 46 | end 47 | @font_path_preview = File.join fonts_path.relative_path_from(preview_path).to_s, name 48 | end 49 | 50 | def create_files 51 | @glyphs = @manifest.get :glyphs 52 | existing = @manifest.get :templates 53 | created = [] 54 | begin 55 | @options[:templates].each do |template_name| 56 | begin 57 | source = get_source_path(template_name) 58 | target = get_target_path(source) 59 | template source, target, :verbose => false, :force => true 60 | end 61 | created << target 62 | end 63 | ensure 64 | say_changed :create, created 65 | @manifest.set :templates, (existing + created).uniq 66 | end 67 | end 68 | 69 | def get_source_path(template) 70 | template_path = File.join Fontcustom.gem_lib, "templates" 71 | 72 | case template 73 | when "preview" 74 | File.join template_path, "fontcustom-preview.html" 75 | when "css" 76 | File.join template_path, "fontcustom.css" 77 | when "scss" 78 | File.join template_path, "_fontcustom.scss" 79 | when "scss-rails" 80 | File.join template_path, "_fontcustom-rails.scss" 81 | else 82 | File.join @options[:input][:templates], template 83 | end 84 | end 85 | 86 | def get_target_path(source) 87 | ext = File.extname source 88 | base = File.basename source 89 | css_exts = %w|.css .scss .sass .less .stylus| 90 | packaged = %w|fontcustom-preview.html fontcustom.css _fontcustom.scss _fontcustom-rails.scss| 91 | 92 | target = if @options[:output].keys.include? base.to_sym 93 | File.join @options[:output][base.to_sym], base 94 | elsif ext && css_exts.include?(ext) 95 | File.join @options[:output][:css], base 96 | elsif source.match(/fontcustom-preview\.html/) 97 | File.join @options[:output][:preview], base 98 | else 99 | File.join @options[:output][:fonts], base 100 | end 101 | 102 | if packaged.include?(base) && @options[:font_name] != DEFAULT_OPTIONS[:font_name] 103 | target = File.join(File.dirname(target), File.basename(target).sub(DEFAULT_OPTIONS[:font_name], @options[:font_name])) 104 | end 105 | 106 | target 107 | end 108 | 109 | # 110 | # Template Helpers 111 | # 112 | 113 | def font_name 114 | @options[:font_name] 115 | end 116 | 117 | def font_face(style = {}) 118 | if style.is_a?(Symbol) 119 | if style == :preprocessor 120 | url = "font-url" 121 | path = @font_path_alt 122 | elsif style == :preview 123 | url = "url" 124 | path = @font_path_preview 125 | else 126 | url = "url" 127 | path = @font_path 128 | end 129 | say_message :warn, "`font_face(:#{style})` is deprecated. Use `font_face(url:'url', path:'path')` instead." 130 | else 131 | style = {:url => "url", :path => @font_path}.merge(style) 132 | url = style[:url] 133 | path = style[:path] 134 | end 135 | 136 | # Bulletproof @Font-Face 137 | # With and without Base64 138 | if @options[:base64] 139 | string = %Q|@font-face { 140 | font-family: "#{font_name}"; 141 | src: #{url}("#{path}.eot?") format("embedded-opentype"); 142 | font-weight: normal; 143 | font-style: normal; 144 | } 145 | 146 | @font-face { 147 | font-family: "#{font_name}"; 148 | src: url("data:application/x-font-woff;charset=utf-8;base64,#{woff_base64}") format("woff"), 149 | #{url}("#{path}.woff2") format("woff2"), 150 | #{url}("#{path}.ttf") format("truetype"), 151 | #{url}("#{path}.svg##{font_name}") format("svg"); 152 | font-weight: normal; 153 | font-style: normal; 154 | }| 155 | else 156 | string = %Q|@font-face { 157 | font-family: "#{font_name}"; 158 | src: #{url}("#{path}.eot"); 159 | src: #{url}("#{path}.eot?#iefix") format("embedded-opentype"), 160 | #{url}("#{path}.woff2") format("woff2"), 161 | #{url}("#{path}.woff") format("woff"), 162 | #{url}("#{path}.ttf") format("truetype"), 163 | #{url}("#{path}.svg##{font_name}") format("svg"); 164 | font-weight: normal; 165 | font-style: normal; 166 | }| 167 | end 168 | 169 | # For Windows/Chrome 170 | string << %Q| 171 | 172 | @media screen and (-webkit-min-device-pixel-ratio:0) { 173 | @font-face { 174 | font-family: "#{font_name}"; 175 | src: #{url}("#{path}.svg##{font_name}") format("svg"); 176 | } 177 | }| 178 | string 179 | end 180 | 181 | def woff_base64 182 | woff_path = File.join(@options[:output][:fonts], "#{@font_path}.woff") 183 | Base64.encode64(File.binread(File.join(woff_path))).gsub("\n", "") 184 | end 185 | 186 | def glyph_selectors 187 | output = @glyphs.map do |name, value| 188 | @options[:css_selector].sub("{{glyph}}", name.to_s) + ":before" 189 | end 190 | output.join ",\n" 191 | end 192 | 193 | def glyph_properties 194 | %Q| display: inline-block; 195 | font-family: "#{font_name}"; 196 | font-style: normal; 197 | font-weight: normal; 198 | font-variant: normal; 199 | line-height: 1; 200 | text-decoration: inherit; 201 | text-rendering: optimizeLegibility; 202 | text-transform: none; 203 | -moz-osx-font-smoothing: grayscale; 204 | -webkit-font-smoothing: antialiased; 205 | font-smoothing: antialiased;| 206 | end 207 | 208 | def glyphs 209 | output = @glyphs.map do |name, value| 210 | %Q|#{@options[:css_selector].sub('{{glyph}}', name.to_s)}:before { content: "\\#{value[:codepoint].to_s(16)}"; }| 211 | end 212 | output.join "\n" 213 | end 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.4 (10/11/2014) 2 | 3 | * Updates rspec tests to be compatible with rspec v3.1.6 4 | * Add additional metrics to make it easier to have different size icon fonts ([#175](https://github.com/FontCustom/fontcustom/pull/175)) 5 | * Add woff data uri to generated CSS + template helper ([#182](https://github.com/FontCustom/fontcustom/pull/182)) 6 | * Support listen v1 and v2 ([#191](https://github.com/FontCustom/fontcustom/pull/191)) 7 | * Add multiple classes to config file ([#174](https://github.com/FontCustom/fontcustom/issues/174)) 8 | * Don't strip "%" symbol (and other potentially valid characters) from CSS selector ([#173](https://github.com/FontCustom/fontcustom/issues/173)) 9 | * Fix bug where custom template path appears in output filenames ([#198](https://github.com/FontCustom/fontcustom/pull/198), [#172](https://github.com/FontCustom/fontcustom/issues/172)) 10 | * SCSS content variables like Font Awesome ([#151](https://github.com/FontCustom/fontcustom/issues/151)) 11 | * Running compile on a folder containing directories shouldn't throw an error 12 | 13 | ## 1.3.3 (2/20/2014) 14 | 15 | * Removes ttfautohint ([#160c](https://github.com/FontCustom/fontcustom/pull/160#issuecomment-34593191)) 16 | * Fixes rails-scss template helper ([#185](https://github.com/FontCustom/fontcustom/issues/185)) 17 | * Adds `text-rendering: optimizeLegibility` ([#181](https://github.com/FontCustom/fontcustom/pull/181)) 18 | 19 | ## 1.3.2 (1/31/2014) 20 | 21 | * Fixes `preprocessor_path` for Rails asset pipeline / Sprockets ([#162](https://github.com/FontCustom/fontcustom/pull/162), [#167](https://github.com/FontCustom/fontcustom/pull/167)) 22 | * Fixes bug where `preprocessor_path` was ignored by the scss template ([#171](https://github.com/FontCustom/fontcustom/issues/171)) 23 | * Fixes bug where relative output paths containing ".." would fail to compile 24 | 25 | ## 1.3.1 (12/28/2013) 26 | 27 | * Fixes syntax error in generate.py that affects Python 2.6 28 | 29 | ## 1.3.0 (12/24/2013) 30 | 31 | **If upgrading from 1.2.0, delete your old `.fontcustom-manifest.json` and output directories first.** 32 | 33 | The big news: fixed glyph code points. Automatically assigned for now, but changing them by hand is just a matter of modifying the generated `.fontcustom-manifest.json`. A few breaking changes (`css_prefix`, custom template syntax, possibly others). 34 | 35 | * Adds fixed glyph code points ([#56](https://github.com/FontCustom/fontcustom/issues/56)) 36 | * Drops bootstrap templates (maintenance overhead, unsure if anyone was using them) 37 | * Stores relative paths for collaborative editing ([#149](https://github.com/FontCustom/fontcustom/pull/149)) 38 | * Changes `css_prefix` to `css_selector` to allow greater flexibility ([#126](https://github.com/FontCustom/fontcustom/pull/126)) 39 | * Skips compilation if inputs have not changed (and `force` option to bypass checks) 40 | * Adds CSS template helpers for convenience and DRYness 41 | * Improves rendering on Chrome Windows ([#143](https://github.com/FontCustom/fontcustom/pull/143)) 42 | * Improves Windows hinting ([#160](https://github.com/FontCustom/fontcustom/pull/160)) 43 | * Fixes Python 2.6 optsparse syntax ([#159](https://github.com/FontCustom/fontcustom/issues/159)) 44 | * Fixes bug where changes in custom templates were not detected by `watch` 45 | * Improves error and debuggging messages 46 | 47 | ## 1.2.0 (11/2/2013) 48 | 49 | * Preparation for fixed glyph code points. 50 | * Tweaks command line options (more semantic aliases) 51 | * Renames :data_cache to :manifest 52 | * Sets the stage for a more streamlined, predictable workflow 53 | * Drops EPS support (was buggy and unused) 54 | * Turns glyph width adjustment into an option (off by default) ([#137](https://github.com/FontCustom/fontcustom/pull/137)) 55 | * Relaxes all dependency version requirements ([#127](https://github.com/FontCustom/fontcustom/issues/127)) 56 | 57 | ## 1.1.1 (10/16/2013) 58 | 59 | * Preview characters are turned off by default in the preview template. 60 | * Relaxes JSON version requirement ([#125](https://github.com/FontCustom/fontcustom/pull/125)) 61 | * Fixes ttf hinting ([#124](https://github.com/FontCustom/fontcustom/pull/124)) 62 | * Cleans up README, fontcustom.yml template, .gitignore ([#123](https://github.com/FontCustom/fontcustom/pull/123), [#128](https://github.com/FontCustom/fontcustom/pull/128)) 63 | 64 | ## 1.1.0 (9/22/2013) 65 | 66 | More customizable interface for vastly improved workflow. 67 | 68 | * Specify where input vectors/templates are stored ([#89](https://github.com/FontCustom/fontcustom/issues/89)) 69 | * Specify where output fonts/templates are saved ([#89](https://github.com/FontCustom/fontcustom/issues/89)) 70 | * Stock templates are saved as `#{font_name}.css` instead of `_fontcustom.css` 71 | * More robust path handling (relative paths, customizable `project_root`) 72 | * User-friendly variables for usage in custom templates 73 | * Rails-friendly template 74 | * Enable HTML data-attributes usage ([#118](https://github.com/FontCustom/fontcustom/pull/118)) 75 | * Helper characters in preview ([#107](https://github.com/FontCustom/fontcustom/pull/107)) 76 | * More robust execution of fontforge command ([#114](https://github.com/FontCustom/fontcustom/pull/114)) 77 | * Allow captial letters in font names ([#92](https://github.com/FontCustom/fontcustom/issues/92)) 78 | * More helpful, colorful messages 79 | * More intuitive flags (`--verbose=false` => `--quiet`, `--file-hash=false` => `--no-hash`) 80 | * More intuitive version (`fontcustom version` => `fontcustom --version`) ([#115](https://github.com/FontCustom/fontcustom/issues/115)) 81 | 82 | ## 1.0.1 (7/21/2013) 83 | 84 | Various bugfixes. 85 | 86 | * Set glyph widths automatically ([#95](https://github.com/FontCustom/fontcustom/issues/95)) 87 | * Fixes Ruby 1.8.7 syntax error ([#94](https://github.com/FontCustom/fontcustom/issues/94)) 88 | * More robust fontforge error handling ([#99](https://github.com/FontCustom/fontcustom/issues/99)) 89 | 90 | ## 1.0.0 (4/18/2013) 91 | 92 | Big changes, more flexibility, better workflow. Be sure to check out the [docs](http://fontcustom.com) to see how it all ties together. 93 | 94 | * Improved preview html to show glyphs at various sizes 95 | * Added support for fontcustom.yml config file ([#49](https://github.com/FontCustom/fontcustom/issues/49)) 96 | * Added support for .fontcustom-data file ([#55](https://github.com/FontCustom/fontcustom/pull/55)) 97 | * Added support for custom templates ([#39](https://github.com/FontCustom/fontcustom/pull/39), [#48](https://github.com/FontCustom/fontcustom/issues/48)) 98 | * Added support for custom CSS selector namespaces ([#32](https://github.com/FontCustom/fontcustom/issues/32)) 99 | * Added support for --verbose=false ([#54](https://github.com/FontCustom/fontcustom/pull/54)) 100 | * Improved ascent/decent heights ([#33](https://github.com/FontCustom/fontcustom/issues/33)) 101 | * Added clean Ruby API ([#62](https://github.com/FontCustom/fontcustom/issues/62)) 102 | * Workaround for Sprockets compatibility ([#61](https://github.com/FontCustom/fontcustom/pull/61)) 103 | * Added clean (bootstrap free) CSS and made it the default choice ([#59](https://github.com/FontCustom/fontcustom/pull/59)) 104 | * Added option to pass different path to @font-face for SCSS partials ([#64](https://github.com/FontCustom/fontcustom/issues/64)) 105 | * Addes SCSS versions of Bootstrap and IE7 stylesheets 106 | * Fixed CSS bug on IE8 and IE9's compatibility mode 107 | * Fixed gem bug where watcher could fall into an infinite loop 108 | * Added error messages for faulty input 109 | * Refactored gem internals to use Thor more sanely 110 | * Refactored tests 111 | 112 | ## 0.1.4 (2/19/2013) 113 | 114 | * Instructions for stopping watcher ([#46](https://github.com/FontCustom/fontcustom/issues/46)) 115 | * Dev/contribution instructions ([#45](https://github.com/FontCustom/fontcustom/issues/45)) 116 | 117 | ## 0.1.3 (2/2/2013) 118 | 119 | * Add --debug CLI option, which shows fontforge output ([#37](https://github.com/FontCustom/fontcustom/issues/37)) 120 | * Patch for Illustrator CS6 SVG output ([#42](https://github.com/FontCustom/fontcustom/pull/42)) 121 | * Generate IE7 stylesheet ([#43](https://github.com/FontCustom/fontcustom/pull/43)) 122 | * Option to set custom font path for @font-face ([#43](https://github.com/FontCustom/fontcustom/pull/43)) 123 | * Option to generate test HTML file showing all glyphs ([#43](https://github.com/FontCustom/fontcustom/pull/43)) 124 | * Use eotlite.py instead of mkeot ([#43](https://github.com/FontCustom/fontcustom/pull/43)) 125 | 126 | ## 0.1.0 (12/2/2012) 127 | 128 | * Changed API to use Thor `class_option`s 129 | * Added option to change the name of the font and generated files ([#6](https://github.com/FontCustom/fontcustom/issues/6)) 130 | * Added option to disable the file name hash ([#13](https://github.com/FontCustom/fontcustom/issues/13)) 131 | * `fontcustom watch` compiles automatically on the first run 132 | * Better help messages 133 | 134 | ## 0.0.2 (11/26/2012) 135 | 136 | * Fixed gemspec dependency bug ([#2](https://github.com/FontCustom/fontcustom/pull/2)) 137 | * Fixed Windows Chrome PUA bug ([#1](https://github.com/FontCustom/fontcustom/issues/1)) 138 | -------------------------------------------------------------------------------- /spec/fixtures/example/example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by FontForge 20141021 at Tue Nov 25 18:43:34 2014 9 | By Kai 10 | Copyright (c) 2014, Kai 11 | 12 | 13 | 14 | 27 | 28 | 48 | 55 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /spec/fontcustom/options_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "spec_helper" 3 | 4 | describe Fontcustom::Options do 5 | def options(args = {}) 6 | args[:manifest] = fixture(".fontcustom-manifest.json") if args[:manifest].nil? 7 | Fontcustom::Options.new(args) 8 | end 9 | 10 | before(:each) do 11 | allow_any_instance_of(Fontcustom::Options).to receive(:say_message) 12 | allow_any_instance_of(Fontcustom::Options).to receive(:parse_options) 13 | end 14 | 15 | context ".overwrite_examples" do 16 | it "should overwite example defaults with real defaults" do 17 | o = options Fontcustom::EXAMPLE_OPTIONS.dup 18 | o.send :overwrite_examples 19 | cli = o.instance_variable_get(:@cli_options) 20 | Fontcustom::EXAMPLE_OPTIONS.keys.each do |key| 21 | expect(cli[key]).to eq(Fontcustom::DEFAULT_OPTIONS[key]) 22 | end 23 | end 24 | end 25 | 26 | context ".set_config_path" do 27 | context "when :config is set" do 28 | it "should use options[:config] if it's a file" do 29 | o = options :config => "options/any-file-name.yml" 30 | o.send :set_config_path 31 | expect(o.instance_variable_get(:@cli_options)[:config]).to eq("options/any-file-name.yml") 32 | end 33 | 34 | it "should search for fontcustom.yml if options[:config] is a dir" do 35 | o = options :config => "options/config-is-in-dir" 36 | o.send :set_config_path 37 | expect(o.instance_variable_get(:@cli_options)[:config]).to eq("options/config-is-in-dir/fontcustom.yml") 38 | end 39 | 40 | it "should raise error if :config doesn't exist" do 41 | o = options :config => "does-not-exist" 42 | expect { o.send :set_config_path }.to raise_error Fontcustom::Error, /configuration file/ 43 | end 44 | end 45 | 46 | context "when :config is not set" do 47 | it "should find fontcustom.yml in the same dir as the manifest" do 48 | FileUtils.cd fixture("options") do 49 | o = options 50 | o.send :set_config_path 51 | expect(o.instance_variable_get(:@cli_options)[:config]).to eq("fontcustom.yml") 52 | end 53 | end 54 | 55 | it "should find fontcustom.yml at config/fontcustom.yml" do 56 | FileUtils.cd fixture("options/rails-like") do 57 | o = options 58 | o.send :set_config_path 59 | expect(o.instance_variable_get(:@cli_options)[:config]).to eq("config/fontcustom.yml") 60 | end 61 | end 62 | 63 | it "should be false if nothing is found" do 64 | o = options :manifest => "options/no-config-here/.fontcustom-manifest.json" 65 | o.send :set_config_path 66 | expect(o.instance_variable_get(:@cli_options)[:config]).to eq(false) 67 | end 68 | end 69 | end 70 | 71 | context ".load_config" do 72 | it "should warn if fontcustom.yml is blank" do 73 | o = options 74 | o.instance_variable_set :@cli_options, {:config => fixture("options/fontcustom-empty.yml")} 75 | expect(o).to receive(:say_message).with :warn, /was empty/ 76 | o.send :load_config 77 | end 78 | 79 | it "should raise error if fontcustom.yml isn't valid" do 80 | o = options 81 | o.instance_variable_set :@cli_options, {:config => fixture("options/fontcustom-malformed.yml")} 82 | expect { o.send :load_config }.to raise_error Fontcustom::Error, /Error parsing/ 83 | end 84 | 85 | it "should assign empty hash :config is false" do 86 | o = options 87 | o.instance_variable_set :@cli_options, {:config => false} 88 | o.send :load_config 89 | expect(o.instance_variable_get(:@config_options)).to eq({}) 90 | end 91 | 92 | context "when :debug is true" do 93 | it "should report which configuration file it's using" do 94 | o = options 95 | o.instance_variable_set :@cli_options, { 96 | :config => fixture("options/any-file-name.yml"), 97 | :debug => true 98 | } 99 | expect(o).to receive(:say_message).with :debug, /Using settings/ 100 | o.send :load_config 101 | end 102 | end 103 | end 104 | 105 | context ".merge_options" do 106 | it "should overwrite defaults with config options" do 107 | o = options 108 | o.instance_variable_set :@config_options, { :input => "config" } 109 | o.send :merge_options 110 | expect(o.options[:input]).to eq("config") 111 | end 112 | 113 | it "should overwrite config file and defaults with CLI options" do 114 | o = options 115 | o.instance_variable_set :@config_options, { :input => "config", :output => "output" } 116 | o.instance_variable_set :@cli_options, { :input => "cli" } 117 | o.send :merge_options 118 | expect(o.options[:input]).to eq("cli") 119 | expect(o.options[:output]).to eq("output") 120 | end 121 | end 122 | 123 | context ".clean_font_name" do 124 | it "should normalize the font name" do 125 | o = options 126 | o.instance_variable_set :@options, { :font_name => " A_stR4nG3 nAm3 Ø& " } 127 | o.send :clean_font_name 128 | expect(o.options[:font_name]).to eq("A_stR4nG3--nAm3---") 129 | end 130 | end 131 | 132 | context ".set_input_paths" do 133 | it "should raise error if input[:vectors] doesn't contain SVGs" do 134 | FileUtils.cd fixture("shared") do 135 | o = options 136 | o.instance_variable_set :@options, { :input => "vectors-empty" } 137 | expect { o.send :set_input_paths }.to raise_error Fontcustom::Error, /doesn't contain any SVGs/ 138 | end 139 | end 140 | 141 | context "when :input is a hash" do 142 | it "should set :templates as :vectors if :templates isn't set" do 143 | FileUtils.cd fixture("shared") do 144 | o = options 145 | o.instance_variable_set :@options, { :input => { :vectors => "vectors" } } 146 | o.send :set_input_paths 147 | expect(o.options[:input][:templates]).to eq("vectors") 148 | end 149 | end 150 | 151 | it "should preserve :templates if it's set" do 152 | FileUtils.cd fixture("shared") do 153 | o = options 154 | o.instance_variable_set :@options, { :input => { :vectors => "vectors", :templates => "templates" } } 155 | o.send :set_input_paths 156 | expect(o.options[:input][:templates]).to eq("templates") 157 | end 158 | end 159 | 160 | it "should raise an error if :vectors isn't set" do 161 | FileUtils.cd fixture("shared") do 162 | o = options 163 | o.instance_variable_set :@options, { :input => { :templates => "templates" } } 164 | expect { o.send :set_input_paths }.to raise_error Fontcustom::Error, /have a :vectors key/ 165 | end 166 | end 167 | 168 | it "should raise an error if :vectors doesn't point to an existing directory" do 169 | FileUtils.cd fixture("shared") do 170 | o = options 171 | o.instance_variable_set :@options, { 172 | :config => "fontcustom.yml", 173 | :input => { :vectors => "not-a-dir" } 174 | } 175 | expect { o.send :set_input_paths }.to raise_error Fontcustom::Error, /isn't a directory/ 176 | end 177 | end 178 | end 179 | 180 | context "when :input is a string" do 181 | it "should return a hash of locations" do 182 | FileUtils.cd fixture("shared") do 183 | o = options 184 | o.instance_variable_set :@options, { :input => "vectors" } 185 | o.send :set_input_paths 186 | expect(o.options[:input]).to have_key(:vectors) 187 | expect(o.options[:input]).to have_key(:templates) 188 | end 189 | end 190 | 191 | it "should set :templates to match :vectors" do 192 | FileUtils.cd fixture("shared") do 193 | o = options 194 | o.instance_variable_set :@options, { :input => "vectors" } 195 | o.send :set_input_paths 196 | expect(o.options[:input][:templates]).to eq("vectors") 197 | end 198 | end 199 | 200 | it "should raise an error if :vectors doesn't point to a directory" do 201 | FileUtils.cd fixture("shared") do 202 | o = options 203 | o.instance_variable_set :@options, { 204 | :config => "fontcustom.yml", 205 | :input => "not-a-dir" 206 | } 207 | expect { o.send :set_input_paths }.to raise_error Fontcustom::Error, /isn't a directory/ 208 | end 209 | end 210 | end 211 | end 212 | 213 | context ".set_output_paths" do 214 | context "when :output is nil" do 215 | context "when :debug is true" do 216 | it "should print a warning" do 217 | o = options 218 | o.instance_variable_set :@options, { 219 | :debug => true, 220 | :font_name => "Test-Font" 221 | } 222 | expect(o).to receive(:say_message).with :debug, /Test-Font/ 223 | o.send :set_output_paths 224 | end 225 | end 226 | end 227 | 228 | context "when :output is a hash" do 229 | it "should set :css and :preview to match :fonts if either aren't set" do 230 | o = options 231 | o.instance_variable_set :@options, { :output => { :fonts => "output/fonts" } } 232 | o.send :set_output_paths 233 | expect(o.options[:output][:css]).to eq("output/fonts") 234 | expect(o.options[:output][:preview]).to eq("output/fonts") 235 | end 236 | 237 | it "should preserve :css and :preview if they do exist" do 238 | o = options 239 | o.instance_variable_set :@options, { 240 | :output => { 241 | :fonts => "output/fonts", 242 | :css => "output/styles", 243 | :preview => "output/preview" 244 | } 245 | } 246 | o.send :set_output_paths 247 | expect(o.options[:output][:css]).to eq("output/styles") 248 | expect(o.options[:output][:preview]).to eq("output/preview") 249 | end 250 | 251 | it "should create additional paths if they are given" do 252 | o = options 253 | o.instance_variable_set :@options, { 254 | :output => { 255 | :fonts => "output/fonts", 256 | "special.js" => "assets/javascripts" 257 | } 258 | } 259 | o.send :set_output_paths 260 | expect(o.options[:output][:"special.js"]).to eq("assets/javascripts") 261 | end 262 | 263 | it "should raise an error if :fonts isn't set" do 264 | o = options 265 | o.instance_variable_set :@options, { 266 | :config => "fontcustom.yml", 267 | :output => { :css => "output/styles" } 268 | } 269 | expect { o.send :set_output_paths }.to raise_error Fontcustom::Error, /have a :fonts key/ 270 | end 271 | end 272 | 273 | context "when :output is a string" do 274 | it "should return a hash of output locations" do 275 | o = options 276 | o.instance_variable_set :@options, { :output => "output/fonts" } 277 | o.send :set_output_paths 278 | expect(o.options[:output]).to be_a(Hash) 279 | expect(o.options[:output]).to have_key(:fonts) 280 | expect(o.options[:output]).to have_key(:css) 281 | expect(o.options[:output]).to have_key(:preview) 282 | end 283 | 284 | it "should set :css and :preview to match :fonts" do 285 | o = options 286 | o.instance_variable_set :@options, { :output => "output/fonts" } 287 | o.send :set_output_paths 288 | expect(o.options[:output][:css]).to eq("output/fonts") 289 | expect(o.options[:output][:preview]).to eq("output/fonts") 290 | end 291 | 292 | it "should raise an error if :fonts exists but isn't a directory" do 293 | FileUtils.cd fixture("shared") do 294 | o = options 295 | o.instance_variable_set :@options, { 296 | :config => "fontcustom.yml", 297 | :output => "not-a-dir" 298 | } 299 | expect { o.send :set_output_paths }.to raise_error Fontcustom::Error, /isn't a directory/ 300 | end 301 | end 302 | end 303 | end 304 | 305 | context ".check_template_paths" do 306 | it "should raise an error if a template does not exist" do 307 | o = options 308 | o.instance_variable_set :@options, { 309 | :input => { :templates => fixture("shared/templates") }, 310 | :templates => %w|fake-template.txt| 311 | } 312 | expect { o.send :check_template_paths }.to raise_error Fontcustom::Error, /wasn't found/ 313 | end 314 | end 315 | end 316 | -------------------------------------------------------------------------------- /spec/fixtures/example/example-preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | example glyphs preview 5 | 6 | 189 | 190 | 191 | 192 | 198 | 199 | 200 | 201 |
202 |
203 |

example contains 3 glyphs:

204 | Toggle Preview Characters 205 |
206 | 207 | 208 |
209 |
210 | PpPpPpPpPpPpPpPpPpPp 211 |
212 |
213 | 12141618212436486072 214 |
215 |
216 | 217 | 218 |
219 |
220 | 221 |
222 |
223 | PpPpPpPpPpPpPpPpPpPp 224 |
225 |
226 | 12141618212436486072 227 |
228 |
229 | 230 | 231 |
232 |
233 | 234 |
235 |
236 | PpPpPpPpPpPpPpPpPpPp 237 |
238 |
239 | 12141618212436486072 240 |
241 |
242 | 243 | 244 |
245 |
246 | 247 | 248 | 251 |
252 | 253 | 254 | -------------------------------------------------------------------------------- /lib/fontcustom/scripts/eotlitetool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***** BEGIN LICENSE BLOCK ***** 3 | # Version: MPL 1.1/GPL 2.0/LGPL 2.1 4 | # 5 | # The contents of this file are subject to the Mozilla Public License Version 6 | # 1.1 (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # http://www.mozilla.org/MPL/ 9 | # 10 | # Software distributed under the License is distributed on an "AS IS" basis, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 12 | # for the specific language governing rights and limitations under the 13 | # License. 14 | # 15 | # The Original Code is font utility code. 16 | # 17 | # The Initial Developer of the Original Code is Mozilla Corporation. 18 | # Portions created by the Initial Developer are Copyright (C) 2009 19 | # the Initial Developer. All Rights Reserved. 20 | # 21 | # Contributor(s): 22 | # John Daggett 23 | # 24 | # Alternatively, the contents of this file may be used under the terms of 25 | # either the GNU General Public License Version 2 or later (the "GPL"), or 26 | # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 27 | # in which case the provisions of the GPL or the LGPL are applicable instead 28 | # of those above. If you wish to allow use of your version of this file only 29 | # under the terms of either the GPL or the LGPL, and not to allow others to 30 | # use your version of this file under the terms of the MPL, indicate your 31 | # decision by deleting the provisions above and replace them with the notice 32 | # and other provisions required by the GPL or the LGPL. If you do not delete 33 | # the provisions above, a recipient may use your version of this file under 34 | # the terms of any one of the MPL, the GPL or the LGPL. 35 | # 36 | # ***** END LICENSE BLOCK ***** */ 37 | 38 | # eotlitetool.py - create EOT version of OpenType font for use with IE 39 | # 40 | # Usage: eotlitetool.py [-o output-filename] font1 [font2 ...] 41 | # 42 | 43 | # OpenType file structure 44 | # http://www.microsoft.com/typography/otspec/otff.htm 45 | # 46 | # Types: 47 | # 48 | # BYTE 8-bit unsigned integer. 49 | # CHAR 8-bit signed integer. 50 | # USHORT 16-bit unsigned integer. 51 | # SHORT 16-bit signed integer. 52 | # ULONG 32-bit unsigned integer. 53 | # Fixed 32-bit signed fixed-point number (16.16) 54 | # LONGDATETIME Date represented in number of seconds since 12:00 midnight, January 1, 1904. The value is represented as a signed 64-bit integer. 55 | # 56 | # SFNT Header 57 | # 58 | # Fixed sfnt version // 0x00010000 for version 1.0. 59 | # USHORT numTables // Number of tables. 60 | # USHORT searchRange // (Maximum power of 2 <= numTables) x 16. 61 | # USHORT entrySelector // Log2(maximum power of 2 <= numTables). 62 | # USHORT rangeShift // NumTables x 16-searchRange. 63 | # 64 | # Table Directory 65 | # 66 | # ULONG tag // 4-byte identifier. 67 | # ULONG checkSum // CheckSum for this table. 68 | # ULONG offset // Offset from beginning of TrueType font file. 69 | # ULONG length // Length of this table. 70 | # 71 | # OS/2 Table (Version 4) 72 | # 73 | # USHORT version // 0x0004 74 | # SHORT xAvgCharWidth 75 | # USHORT usWeightClass 76 | # USHORT usWidthClass 77 | # USHORT fsType 78 | # SHORT ySubscriptXSize 79 | # SHORT ySubscriptYSize 80 | # SHORT ySubscriptXOffset 81 | # SHORT ySubscriptYOffset 82 | # SHORT ySuperscriptXSize 83 | # SHORT ySuperscriptYSize 84 | # SHORT ySuperscriptXOffset 85 | # SHORT ySuperscriptYOffset 86 | # SHORT yStrikeoutSize 87 | # SHORT yStrikeoutPosition 88 | # SHORT sFamilyClass 89 | # BYTE panose[10] 90 | # ULONG ulUnicodeRange1 // Bits 0-31 91 | # ULONG ulUnicodeRange2 // Bits 32-63 92 | # ULONG ulUnicodeRange3 // Bits 64-95 93 | # ULONG ulUnicodeRange4 // Bits 96-127 94 | # CHAR achVendID[4] 95 | # USHORT fsSelection 96 | # USHORT usFirstCharIndex 97 | # USHORT usLastCharIndex 98 | # SHORT sTypoAscender 99 | # SHORT sTypoDescender 100 | # SHORT sTypoLineGap 101 | # USHORT usWinAscent 102 | # USHORT usWinDescent 103 | # ULONG ulCodePageRange1 // Bits 0-31 104 | # ULONG ulCodePageRange2 // Bits 32-63 105 | # SHORT sxHeight 106 | # SHORT sCapHeight 107 | # USHORT usDefaultChar 108 | # USHORT usBreakChar 109 | # USHORT usMaxContext 110 | # 111 | # 112 | # The Naming Table is organized as follows: 113 | # 114 | # [name table header] 115 | # [name records] 116 | # [string data] 117 | # 118 | # Name Table Header 119 | # 120 | # USHORT format // Format selector (=0). 121 | # USHORT count // Number of name records. 122 | # USHORT stringOffset // Offset to start of string storage (from start of table). 123 | # 124 | # Name Record 125 | # 126 | # USHORT platformID // Platform ID. 127 | # USHORT encodingID // Platform-specific encoding ID. 128 | # USHORT languageID // Language ID. 129 | # USHORT nameID // Name ID. 130 | # USHORT length // String length (in bytes). 131 | # USHORT offset // String offset from start of storage area (in bytes). 132 | # 133 | # head Table 134 | # 135 | # Fixed tableVersion // Table version number 0x00010000 for version 1.0. 136 | # Fixed fontRevision // Set by font manufacturer. 137 | # ULONG checkSumAdjustment // To compute: set it to 0, sum the entire font as ULONG, then store 0xB1B0AFBA - sum. 138 | # ULONG magicNumber // Set to 0x5F0F3CF5. 139 | # USHORT flags 140 | # USHORT unitsPerEm // Valid range is from 16 to 16384. This value should be a power of 2 for fonts that have TrueType outlines. 141 | # LONGDATETIME created // Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer 142 | # LONGDATETIME modified // Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer 143 | # SHORT xMin // For all glyph bounding boxes. 144 | # SHORT yMin 145 | # SHORT xMax 146 | # SHORT yMax 147 | # USHORT macStyle 148 | # USHORT lowestRecPPEM // Smallest readable size in pixels. 149 | # SHORT fontDirectionHint 150 | # SHORT indexToLocFormat // 0 for short offsets, 1 for long. 151 | # SHORT glyphDataFormat // 0 for current format. 152 | # 153 | # 154 | # 155 | # Embedded OpenType (EOT) file format 156 | # http://www.w3.org/Submission/EOT/ 157 | # 158 | # EOT version 0x00020001 159 | # 160 | # An EOT font consists of a header with the original OpenType font 161 | # appended at the end. Most of the data in the EOT header is simply a 162 | # copy of data from specific tables within the font data. The exceptions 163 | # are the 'Flags' field and the root string name field. The root string 164 | # is a set of names indicating domains for which the font data can be 165 | # used. A null root string implies the font data can be used anywhere. 166 | # The EOT header is in little-endian byte order but the font data remains 167 | # in big-endian order as specified by the OpenType spec. 168 | # 169 | # Overall structure: 170 | # 171 | # [EOT header] 172 | # [EOT name records] 173 | # [font data] 174 | # 175 | # EOT header 176 | # 177 | # ULONG eotSize // Total structure length in bytes (including string and font data) 178 | # ULONG fontDataSize // Length of the OpenType font (FontData) in bytes 179 | # ULONG version // Version number of this format - 0x00020001 180 | # ULONG flags // Processing Flags (0 == no special processing) 181 | # BYTE fontPANOSE[10] // OS/2 Table panose 182 | # BYTE charset // DEFAULT_CHARSET (0x01) 183 | # BYTE italic // 0x01 if ITALIC in OS/2 Table fsSelection is set, 0 otherwise 184 | # ULONG weight // OS/2 Table usWeightClass 185 | # USHORT fsType // OS/2 Table fsType (specifies embedding permission flags) 186 | # USHORT magicNumber // Magic number for EOT file - 0x504C. 187 | # ULONG unicodeRange1 // OS/2 Table ulUnicodeRange1 188 | # ULONG unicodeRange2 // OS/2 Table ulUnicodeRange2 189 | # ULONG unicodeRange3 // OS/2 Table ulUnicodeRange3 190 | # ULONG unicodeRange4 // OS/2 Table ulUnicodeRange4 191 | # ULONG codePageRange1 // OS/2 Table ulCodePageRange1 192 | # ULONG codePageRange2 // OS/2 Table ulCodePageRange2 193 | # ULONG checkSumAdjustment // head Table CheckSumAdjustment 194 | # ULONG reserved[4] // Reserved - must be 0 195 | # USHORT padding1 // Padding - must be 0 196 | # 197 | # EOT name records 198 | # 199 | # USHORT FamilyNameSize // Font family name size in bytes 200 | # BYTE FamilyName[FamilyNameSize] // Font family name (name ID = 1), little-endian UTF-16 201 | # USHORT Padding2 // Padding - must be 0 202 | # 203 | # USHORT StyleNameSize // Style name size in bytes 204 | # BYTE StyleName[StyleNameSize] // Style name (name ID = 2), little-endian UTF-16 205 | # USHORT Padding3 // Padding - must be 0 206 | # 207 | # USHORT VersionNameSize // Version name size in bytes 208 | # bytes VersionName[VersionNameSize] // Version name (name ID = 5), little-endian UTF-16 209 | # USHORT Padding4 // Padding - must be 0 210 | # 211 | # USHORT FullNameSize // Full name size in bytes 212 | # BYTE FullName[FullNameSize] // Full name (name ID = 4), little-endian UTF-16 213 | # USHORT Padding5 // Padding - must be 0 214 | # 215 | # USHORT RootStringSize // Root string size in bytes 216 | # BYTE RootString[RootStringSize] // Root string, little-endian UTF-16 217 | 218 | 219 | 220 | import optparse 221 | import struct 222 | 223 | class FontError(Exception): 224 | """Error related to font handling""" 225 | pass 226 | 227 | def multichar(str): 228 | vals = struct.unpack('4B', str[:4]) 229 | return (vals[0] << 24) + (vals[1] << 16) + (vals[2] << 8) + vals[3] 230 | 231 | def multicharval(v): 232 | return struct.pack('4B', (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF) 233 | 234 | class EOT: 235 | EOT_VERSION = 0x00020001 236 | EOT_MAGIC_NUMBER = 0x504c 237 | EOT_DEFAULT_CHARSET = 0x01 238 | EOT_FAMILY_NAME_INDEX = 0 # order of names in variable portion of EOT header 239 | EOT_STYLE_NAME_INDEX = 1 240 | EOT_VERSION_NAME_INDEX = 2 241 | EOT_FULL_NAME_INDEX = 3 242 | EOT_NUM_NAMES = 4 243 | 244 | EOT_HEADER_PACK = '<4L10B2BL2H7L18x' 245 | 246 | class OpenType: 247 | SFNT_CFF = multichar('OTTO') # Postscript CFF SFNT version 248 | SFNT_TRUE = 0x10000 # Standard TrueType version 249 | SFNT_APPLE = multichar('true') # Apple TrueType version 250 | 251 | SFNT_UNPACK = '>I4H' 252 | TABLE_DIR_UNPACK = '>4I' 253 | 254 | TABLE_HEAD = multichar('head') # TrueType table tags 255 | TABLE_NAME = multichar('name') 256 | TABLE_OS2 = multichar('OS/2') 257 | TABLE_GLYF = multichar('glyf') 258 | TABLE_CFF = multichar('CFF ') 259 | 260 | OS2_FSSELECTION_ITALIC = 0x1 261 | OS2_UNPACK = '>4xH2xH22x10B4L4xH14x2L' 262 | 263 | HEAD_UNPACK = '>8xL' 264 | 265 | NAME_RECORD_UNPACK = '>6H' 266 | NAME_ID_FAMILY = 1 267 | NAME_ID_STYLE = 2 268 | NAME_ID_UNIQUE = 3 269 | NAME_ID_FULL = 4 270 | NAME_ID_VERSION = 5 271 | NAME_ID_POSTSCRIPT = 6 272 | PLATFORM_ID_UNICODE = 0 # Mac OS uses this typically 273 | PLATFORM_ID_MICROSOFT = 3 274 | ENCODING_ID_MICROSOFT_UNICODEBMP = 1 # with Microsoft platformID BMP-only Unicode encoding 275 | LANG_ID_MICROSOFT_EN_US = 0x0409 # with Microsoft platformID EN US lang code 276 | 277 | def eotname(ttf): 278 | i = ttf.rfind('.') 279 | if i != -1: 280 | ttf = ttf[:i] 281 | return ttf + '.eotlite' 282 | 283 | def readfont(f): 284 | data = open(f, 'rb').read() 285 | return data 286 | 287 | def get_table_directory(data): 288 | """read the SFNT header and table directory""" 289 | datalen = len(data) 290 | sfntsize = struct.calcsize(OpenType.SFNT_UNPACK) 291 | if sfntsize > datalen: 292 | raise FontError, 'truncated font data' 293 | sfntvers, numTables = struct.unpack(OpenType.SFNT_UNPACK, data[:sfntsize])[:2] 294 | if sfntvers != OpenType.SFNT_CFF and sfntvers != OpenType.SFNT_TRUE: 295 | raise FontError, 'invalid font type'; 296 | 297 | font = {} 298 | font['version'] = sfntvers 299 | font['numTables'] = numTables 300 | 301 | # create set of offsets, lengths for tables 302 | table_dir_size = struct.calcsize(OpenType.TABLE_DIR_UNPACK) 303 | if sfntsize + table_dir_size * numTables > datalen: 304 | raise FontError, 'truncated font data, table directory extends past end of data' 305 | table_dir = {} 306 | for i in range(0, numTables): 307 | start = sfntsize + i * table_dir_size 308 | end = start + table_dir_size 309 | tag, check, bongo, dirlen = struct.unpack(OpenType.TABLE_DIR_UNPACK, data[start:end]) 310 | table_dir[tag] = {'offset': bongo, 'length': dirlen, 'checksum': check} 311 | 312 | font['tableDir'] = table_dir 313 | 314 | return font 315 | 316 | def get_name_records(nametable): 317 | """reads through the name records within name table""" 318 | name = {} 319 | # read the header 320 | headersize = 6 321 | count, strOffset = struct.unpack('>2H', nametable[2:6]) 322 | namerecsize = struct.calcsize(OpenType.NAME_RECORD_UNPACK) 323 | if count * namerecsize + headersize > len(nametable): 324 | raise FontError, 'names exceed size of name table' 325 | name['count'] = count 326 | name['strOffset'] = strOffset 327 | 328 | # read through the name records 329 | namerecs = {} 330 | for i in range(0, count): 331 | start = headersize + i * namerecsize 332 | end = start + namerecsize 333 | platformID, encodingID, languageID, nameID, namelen, offset = struct.unpack(OpenType.NAME_RECORD_UNPACK, nametable[start:end]) 334 | if platformID != OpenType.PLATFORM_ID_MICROSOFT or \ 335 | encodingID != OpenType.ENCODING_ID_MICROSOFT_UNICODEBMP or \ 336 | languageID != OpenType.LANG_ID_MICROSOFT_EN_US: 337 | continue 338 | namerecs[nameID] = {'offset': offset, 'length': namelen} 339 | 340 | name['namerecords'] = namerecs 341 | return name 342 | 343 | def make_eot_name_headers(fontdata, nameTableDir): 344 | """extracts names from the name table and generates the names header portion of the EOT header""" 345 | nameoffset = nameTableDir['offset'] 346 | namelen = nameTableDir['length'] 347 | name = get_name_records(fontdata[nameoffset : nameoffset + namelen]) 348 | namestroffset = name['strOffset'] 349 | namerecs = name['namerecords'] 350 | 351 | eotnames = (OpenType.NAME_ID_FAMILY, OpenType.NAME_ID_STYLE, OpenType.NAME_ID_VERSION, OpenType.NAME_ID_FULL) 352 | nameheaders = [] 353 | for nameid in eotnames: 354 | if nameid in namerecs: 355 | namerecord = namerecs[nameid] 356 | noffset = namerecord['offset'] 357 | nlen = namerecord['length'] 358 | nformat = '%dH' % (nlen / 2) # length is in number of bytes 359 | start = nameoffset + namestroffset + noffset 360 | end = start + nlen 361 | nstr = struct.unpack('>' + nformat, fontdata[start:end]) 362 | nameheaders.append(struct.pack(' os2Dir['length']: 401 | raise FontError, 'OS/2 table invalid length' 402 | 403 | os2fields = struct.unpack(OpenType.OS2_UNPACK, fontdata[os2offset : os2offset + os2size]) 404 | 405 | panose = [] 406 | urange = [] 407 | codepage = [] 408 | 409 | weight, fsType = os2fields[:2] 410 | panose[:10] = os2fields[2:12] 411 | urange[:4] = os2fields[12:16] 412 | fsSelection = os2fields[16] 413 | codepage[:2] = os2fields[17:19] 414 | 415 | italic = fsSelection & OpenType.OS2_FSSELECTION_ITALIC 416 | 417 | # read in values from head table 418 | headDir = tableDir[OpenType.TABLE_HEAD] 419 | headoffset = headDir['offset'] 420 | headsize = struct.calcsize(OpenType.HEAD_UNPACK) 421 | 422 | if headsize > headDir['length']: 423 | raise FontError, 'head table invalid length' 424 | 425 | headfields = struct.unpack(OpenType.HEAD_UNPACK, fontdata[headoffset : headoffset + headsize]) 426 | checkSumAdjustment = headfields[0] 427 | 428 | # make name headers 429 | nameheaders = make_eot_name_headers(fontdata, tableDir[OpenType.TABLE_NAME]) 430 | rootstring = make_root_string() 431 | 432 | # calculate the total eot size 433 | eotSize = struct.calcsize(EOT.EOT_HEADER_PACK) + len(nameheaders) + len(rootstring) + fontDataSize 434 | fixed = struct.pack(EOT.EOT_HEADER_PACK, 435 | *([eotSize, fontDataSize, version, flags] + panose + [charset, italic] + 436 | [weight, fsType, magicNumber] + urange + codepage + [checkSumAdjustment])) 437 | 438 | return ''.join((fixed, nameheaders, rootstring)) 439 | 440 | 441 | def write_eot_font(eot, header, data): 442 | open(eot,'wb').write(''.join((header, data))) 443 | return 444 | 445 | def main(): 446 | 447 | # deal with options 448 | p = optparse.OptionParser() 449 | p.add_option('--output', '-o', default="world") 450 | options, args = p.parse_args() 451 | 452 | # iterate over font files 453 | for f in args: 454 | data = readfont(f) 455 | if len(data) == 0: 456 | print 'Error reading %s' % f 457 | else: 458 | eot = eotname(f) 459 | header = make_eot_header(data) 460 | write_eot_font(eot, header, data) 461 | 462 | 463 | if __name__ == '__main__': 464 | main() 465 | 466 | --------------------------------------------------------------------------------