woff 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ python: "3.4"
3
+ before_install:
4
+ - curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
5
+ - sudo python get-pip.py
6
+ - python -V
7
+ - pip -V
8
+ install:
9
+ - bundle install
10
+ - sudo pip install -r requirements.txt
11
+ rvm:
12
+ - 2.2
13
+ - 2.3
data/README.md CHANGED
@@ -1,12 +1,17 @@
1
- # Woff
1
+ # WOFF
2
2
 
3
- TODO: Write a gem description
3
+ [![Gem Version](https://badge.fury.io/rb/woff.svg)](https://badge.fury.io/rb/woff)
4
+ [![Build Status](https://travis-ci.org/friendsoftheweb/woff-rb.svg?branch=master)](https://travis-ci.org/friendsoftheweb/woff-rb)
5
+ [![Code Climate](https://codeclimate.com/github/friendsoftheweb/woff-rb/badges/gpa.svg)](https://codeclimate.com/github/friendsoftheweb/woff-rb)
6
+
7
+ This reads binary data from WOFF files in pure Ruby and allows limited
8
+ modification of metadata.
4
9
 
5
10
  ## Installation
6
11
 
7
12
  Add this line to your application's Gemfile:
8
13
 
9
- gem 'woff'
14
+ gem 'woff', '~> 1.1.0'
10
15
 
11
16
  And then execute:
12
17
 
@@ -16,9 +21,25 @@ Or install it yourself as:
16
21
 
17
22
  $ gem install woff
18
23
 
24
+ ## WOFF2 Support
25
+
26
+ The gem can currently read, but not write to WOFF2 files. Both reading and writing
27
+ WOFF2 files should be considered in development, with API changes likely.
28
+
19
29
  ## Usage
20
30
 
21
- TODO: Write usage instructions here
31
+ Used in generation of WOFF files. Returns the data to be written to a file or
32
+ sent to the Zip gem of your choice.
33
+
34
+ ```ruby
35
+ woff = WOFF::Builder.new("/Users/Desktop/sample.woff")
36
+
37
+ # This will set or update the metadata's licensee name to `The Friends` and the
38
+ # metadata's license id to `L012356093901`.
39
+ data = woff.font_with_licensee_and_id("The Friends", "L012356093901")
40
+
41
+ File.binwrite("/Users/Desktop/sample-with-metadata.woff", data)
42
+ ```
22
43
 
23
44
  ## Contributing
24
45
 
data/Rakefile CHANGED
@@ -1 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new("spec")
5
+
6
+ task :default => :spec
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rake' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rake", "rake")
@@ -1,9 +1,10 @@
1
1
  require "woff/version"
2
2
  require "zlib"
3
+ require "brotli"
3
4
  require "bindata"
4
5
  require "rexml/document"
6
+ require "woff/file"
5
7
  require "woff/builder"
6
- require "woff/data"
7
8
 
8
9
  module WOFF
9
10
  class FontNotFoundError < StandardError
@@ -11,4 +12,10 @@ module WOFF
11
12
  super(msg)
12
13
  end
13
14
  end
15
+
16
+ class InvalidSignatureError < StandardError
17
+ def initialize(msg = "The WOFF file contains an invalid WOFF or WOFF2 signature.")
18
+ super(msg)
19
+ end
20
+ end
14
21
  end
@@ -1,10 +1,9 @@
1
1
  module WOFF
2
- # Used in generation of WOFF files. Currently only modifies licensee
3
- # information and writes to a file. In the future it should stream
4
- # the output for use in one time use downloads.
2
+ # Used in generation of WOFF files with modified metadata for licensee and
3
+ # license id information.
5
4
  #
6
5
  # woff = WOFF::Builder.new("/Users/Josh/Desktop/sample.woff")
7
- # woff.font_with_licensee("The Friends")
6
+ # woff.font_with_licensee_and_id("The Friends", "L012356093901")
8
7
  #
9
8
  class Builder
10
9
  def initialize(file)
@@ -12,36 +11,88 @@ module WOFF
12
11
  end
13
12
 
14
13
  def font_with_licensee_and_id(name, id)
15
- metadata_xml = ::Zlib::Inflate.inflate(data.metadata)
14
+ font_with_metadata(licensee: name, license_id: id)
15
+ end
16
+
17
+ def font_with_metadata(licensee: nil, license_id: nil, license_text: nil, description: nil)
18
+ metadata_xml = data.metadata.length > 0 ? compressor.inflate(data.metadata) : default_metadata
16
19
  metadata_doc = REXML::Document.new(metadata_xml)
17
20
 
18
- if metadata_doc.root.elements["licensee"]
19
- metadata_doc.root.elements["licensee"].attributes["name"] = name
20
- else
21
- metadata_doc.root.add_element "licensee", { "name" => name }
21
+ if licensee
22
+ if metadata_doc.root.elements["licensee"]
23
+ metadata_doc.root.elements["licensee"].attributes["name"] = licensee
24
+ else
25
+ metadata_doc.root.add_element "licensee", { "name" => licensee }
26
+ end
22
27
  end
23
28
 
24
- if metadata_doc.root.elements["license"]
25
- metadata_doc.root.elements["license"].attributes["id"] = id
26
- else
27
- metadata_doc.root.add_element "license", { "id" => id }
29
+ if license_id
30
+ if metadata_doc.root.elements["license"]
31
+ metadata_doc.root.elements["license"].attributes["id"] = license_id
32
+ else
33
+ metadata_doc.root.add_element "license", { "id" => license_id }
34
+ end
28
35
  end
29
36
 
30
- compressed_metadata = ::Zlib::Deflate.deflate(metadata_doc.to_s)
37
+ if license_text
38
+ license_el = metadata_doc.root.elements["license"]
39
+ unless license_el
40
+ license_el = metadata_doc.root.add_element "license"
41
+ end
42
+
43
+ license_text_el = license_el.elements["text"]
44
+ unless license_text_el
45
+ license_text_el = license_el.add_element("text", { "lang" => "en "})
46
+ end
47
+
48
+ license_text_el.text = license_text
49
+ end
50
+
51
+ if description
52
+ description_el = metadata_doc.root.elements["description"]
53
+ unless description_el
54
+ description_el = metadata_doc.root.add_element "description"
55
+ end
56
+
57
+ description_text_el = description_el.elements["text"]
58
+ unless description_text_el
59
+ description_text_el = description_el.add_element("text", { "lang" => "en "})
60
+ end
61
+
62
+ description_text_el.text = description
63
+ end
64
+
65
+ compressed_metadata = compressor.deflate(metadata_doc.to_s)
31
66
 
32
67
  data.meta_orig_length = metadata_doc.to_s.bytesize
33
68
  data.metadata = compressed_metadata
34
69
  data.meta_length = compressed_metadata.bytesize
70
+ data.meta_offset = data.metadata.abs_offset # "Offset to metadata block, from beginning of WOFF file."
71
+
35
72
  data.data_length = data.num_bytes
36
73
 
37
74
  data.to_binary_s
38
75
  end
39
76
 
77
+
40
78
  private
41
79
  attr_reader :location
42
80
 
43
81
  def data
44
- @data ||= WOFF::Data.read(File.open(location))
82
+ @data ||= WOFF::File.read(::File.open(location))
83
+ end
84
+
85
+ def compressor
86
+ case data
87
+ when WOFF::File::V1
88
+ ::Zlib
89
+ when WOFF::File::V2
90
+ ::Brotli
91
+ end
92
+ end
93
+
94
+ def default_metadata
95
+ %Q{<?xml version="1.0" encoding="UTF-8"?><metadata version="1.0"></metadata>}
45
96
  end
46
97
  end
47
98
  end
@@ -0,0 +1,152 @@
1
+ class UIntBase128 < BinData::BasePrimitive
2
+ # TODO: This should, like actually encode the value. This file might help:
3
+ # https://github.com/khaledhosny/woff2/blob/f43ad222715f58ea62a004b54e4b6a31e589e762/src/variable_length.cc
4
+ def value_to_binary_string(value)
5
+ "0"
6
+ end
7
+
8
+ def read_and_return_value(io)
9
+ value = 0
10
+ offset = 0
11
+
12
+ loop do
13
+ byte = io.readbytes(1).unpack('C')[0]
14
+ value |= (byte & 0x7F) << offset
15
+ offset += 7
16
+ if byte & 0x80 == 0
17
+ break
18
+ end
19
+ end
20
+
21
+ value
22
+ end
23
+
24
+ def sensible_default
25
+ 0
26
+ end
27
+ end
28
+
29
+ module WOFF
30
+ class File
31
+ def self.read(file)
32
+ data = Data.read(file)
33
+ file.rewind
34
+
35
+ if data.signature == 0x774F4646
36
+ V1.read(file)
37
+ elsif data.signature == 0x774F4632
38
+ V2.read(file)
39
+ else
40
+ raise WOFF::InvalidSignatureError
41
+ end
42
+ end
43
+
44
+ class Data < ::BinData::Record
45
+ endian :big
46
+
47
+ uint32 :signature
48
+ end
49
+
50
+ class V1 < ::BinData::Record
51
+ endian :big
52
+ count_bytes_remaining :bytes_remaining
53
+
54
+ uint32 :signature
55
+ uint32 :flavor
56
+ uint32 :data_length
57
+ uint16 :num_tables
58
+ uint16 :reserved
59
+ uint32 :total_s_fnt_size
60
+ uint16 :major_version
61
+ uint16 :minor_version
62
+ uint32 :meta_offset
63
+ uint32 :meta_length
64
+ uint32 :meta_orig_length
65
+ uint32 :priv_offset
66
+ uint32 :priv_length
67
+
68
+ array :table_directory, initial_length: :num_tables do
69
+ uint32 :tag
70
+ uint32 :table_offset
71
+ uint32 :comp_length
72
+ uint32 :orig_length
73
+ uint32 :orig_checksum
74
+ end
75
+
76
+ string :fonts, read_length: lambda {
77
+ dir = table_directory.sort_by { |entry| entry["table_offset"] }
78
+
79
+ first_table_start = dir.first["table_offset"]
80
+ last_table_end = dir.last["table_offset"] + dir.last["comp_length"]
81
+
82
+ table_length = last_table_end - first_table_start
83
+
84
+ # Next largest number divisible by 4
85
+ (table_length / 4.0).ceil * 4
86
+ }
87
+
88
+ string :metadata, read_length: :meta_length
89
+
90
+ rest :private_data
91
+ end
92
+
93
+
94
+ class V2 < ::BinData::Record
95
+ endian :big
96
+ count_bytes_remaining :bytes_remaining
97
+
98
+ uint32 :signature
99
+ uint32 :flavor
100
+ uint32 :data_length
101
+ uint16 :num_tables
102
+ uint16 :reserved
103
+ uint32 :total_s_fnt_size
104
+ uint32 :total_compressed_size
105
+ uint16 :major_version
106
+ uint16 :minor_version
107
+ uint32 :meta_offset
108
+ uint32 :meta_length
109
+ uint32 :meta_orig_length
110
+ uint32 :priv_offset
111
+ uint32 :priv_length
112
+
113
+ array :table_directory, initial_length: :num_tables do
114
+ uint8 :flags
115
+ uint32 :tag, onlyif: -> {
116
+ bits = flags.to_binary_s.unpack('B*')[0]
117
+ tag_index = bits[0..5].to_i(2)
118
+
119
+ tag_index == 63
120
+ }
121
+ u_int_base128 :orig_length
122
+ u_int_base128 :transform_length, onlyif: -> {
123
+ bits = flags.to_binary_s.unpack('B*')[0]
124
+ tag_index = bits[0..5].to_i(2)
125
+ transform_version = bits[6..7].to_i(2)
126
+ is_loca_or_glyf = [10, 11].include?(tag_index)
127
+
128
+ (is_loca_or_glyf && transform_version != 3) || (!is_loca_or_glyf && transform_version != 0)
129
+ }
130
+ end
131
+
132
+ struct :collection_directory, onlyif: :has_collection? do
133
+ uint32 :version
134
+ uint16 :num_fonts # 255UInt16
135
+
136
+ array :collection_font_entries, initial_length: :num_fonts do
137
+ uint16 :num_collection_tables # 255UInt16
138
+ end
139
+ end
140
+
141
+ string :compressed_data, read_length: :total_compressed_size
142
+
143
+ string :metadata, read_length: :meta_length
144
+
145
+ rest :private_data
146
+
147
+ def has_collection?
148
+ flavor == 0x74746366
149
+ end
150
+ end
151
+ end
152
+ end
@@ -1,3 +1,3 @@
1
1
  module WOFF
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -0,0 +1 @@
1
+ fonttools==3.1.1
@@ -0,0 +1,49 @@
1
+ # This uses the forked and embedded woffTools script, it needs python available, as well
2
+ # as woffTools python dependencies.
3
+
4
+ require 'spec_helper.rb'
5
+ require 'tmpdir'
6
+
7
+ describe WOFF::Builder do
8
+ let(:tmpdir_path) { Dir.mktmpdir }
9
+ let(:output_path) { File.join(tmpdir_path, "with_licensee_and_id.woff") }
10
+ let(:no_metadata_woff_path) { File.expand_path("../data/font-with-no-metadata.woff", __FILE__) }
11
+
12
+ let(:validate_script) { File.expand_path("../../woffTools/Lib/woffTools/tools/validate.py", __FILE__) }
13
+
14
+ after do
15
+ FileUtils.rm_r(tmpdir_path)
16
+ end
17
+
18
+ describe "#font_with_licensee_and_id" do
19
+ let(:licensee) { "Some Licensee" }
20
+ let(:id) { "L012356093901" }
21
+
22
+ before do
23
+ woff = WOFF::Builder.new(no_metadata_woff_path)
24
+
25
+ data = woff.font_with_licensee_and_id("The Friends", "L012356093901")
26
+ File.binwrite(output_path, data)
27
+ end
28
+
29
+ it "is valid according to woffTools" do
30
+ expect(system("python", validate_script, output_path, "-q")).to be(true)
31
+ end
32
+ end
33
+
34
+ describe "#font_with_metadata" do
35
+ let(:licensee) { "Some Licensee" }
36
+ let(:license_id) { "L012356093901" }
37
+ let(:license_text) { "Do the right things" }
38
+ let(:description) { "very nice font" }
39
+
40
+ let(:woff) { woff = WOFF::Builder.new(no_metadata_woff_path) }
41
+
42
+ it "can set just license_text and description" do
43
+ data = woff.font_with_metadata(license_text: license_text, description: description)
44
+ File.binwrite(output_path, data)
45
+
46
+ expect(system("python", validate_script, output_path, "-q")).to be(true)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,7 @@
1
+ require "bundler/setup"
2
+ Bundler.setup
3
+
4
+ require "woff"
5
+
6
+ RSpec.configure do |config|
7
+ end