woff 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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