prawn-icon 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Binary file
data/lib/prawn/icon.rb ADDED
@@ -0,0 +1,156 @@
1
+ # encoding: utf-8
2
+ #
3
+ # icon.rb: Prawn icon functionality.
4
+ #
5
+ # Copyright October 2014, Jesse Doyle. All rights reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ require_relative 'icon/font_data'
10
+ require_relative 'icon/parser'
11
+
12
+ module Prawn
13
+ module Errors
14
+ # Error raised when an icon glyph is not found
15
+ #
16
+ IconNotFound = Class.new(StandardError)
17
+
18
+ # Error raised when an icon key is not provided
19
+ #
20
+ IconKeyEmpty = Class.new(StandardError)
21
+ end
22
+
23
+ # Easy icon font usage within Prawn. Currently
24
+ # supported icon fonts include: FontAwesome,
25
+ # Zurb Foundicons, and GitHub Octicons.
26
+ #
27
+ # = Icon Keys
28
+ #
29
+ # Icon keys must be supplied to most +Prawn::Icon+
30
+ # methods. Keys map directly to a unicode character
31
+ # within the font that produces a given icon. As a
32
+ # rule, included icon keys should match the keys from
33
+ # the font provider. The icon key mapping is specified
34
+ # in the font's +legend_file+, which is a +YAML+ file
35
+ # located in Prawn::Icon::FONTDIR/font/font.yml.
36
+ #
37
+ # Prawn::Icon::
38
+ # Houses the methods and interfaces necessary for
39
+ # rendering icons to the Prawn::Document.
40
+ #
41
+ # Prawn::Icon::FontData::
42
+ # Used to store various information about an icon font,
43
+ # including the key-to-unicode mapping information.
44
+ # Also houses methods to cache and lazily load the
45
+ # requested font data on a document basis.
46
+ #
47
+ # Prawn::Icon::Parser::
48
+ # Used to initially parse icons that are used with the
49
+ # inline_format: true option. The input string is parsed
50
+ # once for <icon></icon> tags, then the output is provided
51
+ # to Prawn's internal formatted text parser.
52
+ #
53
+ class Icon
54
+ FONTDIR = File.join \
55
+ File.expand_path('../../..', __FILE__), 'fonts'
56
+
57
+ module Interface
58
+ # Set up and draw an icon on this document. This
59
+ # method operates much like +Prawn::Text::Box+.
60
+ #
61
+ # == Parameters:
62
+ # key::
63
+ # Contains the key to a particular icon within
64
+ # a font family. If :inline_format is true,
65
+ # then key may contain formatted text marked
66
+ # with <icon></icon> tags and any tag supported
67
+ # by Prawn's parser.
68
+ #
69
+ # opts::
70
+ # A hash of options that may be supplied to
71
+ # the underlying +text+ method call.
72
+ #
73
+ # == Examples:
74
+ # pdf.icon 'fa-beer'
75
+ # pdf.icon '<icon color="0099FF">fa-arrows</icon>',
76
+ # inline_format: true
77
+ #
78
+ def icon(key, opts = {})
79
+ i = make_icon(key, opts)
80
+ i.render
81
+ i
82
+ end
83
+
84
+ # Initialize a new icon object, but do
85
+ # not render it to the document.
86
+ #
87
+ # == Parameters:
88
+ # key::
89
+ # Contains the key to a particular icon within
90
+ # a font family. If :inline_format is true,
91
+ # then key may contain formatted text marked
92
+ # with <icon></icon> tags and any tag supported
93
+ # by Prawn's parser.
94
+ #
95
+ # opts::
96
+ # A hash of options that may be supplied to
97
+ # the underlying text method call.
98
+ #
99
+ def make_icon(key, opts = {})
100
+ if opts[:inline_format]
101
+ inline_icon(key, opts)
102
+ else
103
+ Icon.new(key, self, opts)
104
+ end
105
+ end
106
+
107
+ # Initialize a new formatted text box containing
108
+ # icon information, but don't render it to the
109
+ # document.
110
+ #
111
+ # == Parameters:
112
+ # text::
113
+ # Input text to be parsed initially for <icon>
114
+ # tags, then passed to Prawn's formatted text
115
+ # parser.
116
+ #
117
+ # opts::
118
+ # A hash of options that may be supplied to the
119
+ # underlying text call.
120
+ #
121
+ def inline_icon(text, opts = {})
122
+ parsed = Icon::Parser.format(self, text)
123
+ content = Text::Formatted::Parser.format(parsed)
124
+ opts.merge!(inline_format: true, document: self)
125
+ Text::Formatted::Box.new(content, opts)
126
+ end
127
+ end
128
+
129
+ attr_reader :set, :unicode
130
+
131
+ def initialize(key, document, opts = {})
132
+ @pdf = document
133
+ @set = opts[:set] ||
134
+ FontData.specifier_from_key(key)
135
+ @data = FontData.load(document, @set)
136
+ @key = strip_specifier_from_key(key)
137
+ @unicode = @data.unicode(@key)
138
+ @options = opts
139
+ end
140
+
141
+ def render
142
+ @pdf.font(@data.path) do
143
+ @pdf.text @unicode, @options
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def strip_specifier_from_key(key)
150
+ reg = Regexp.new "#{@data.specifier}-"
151
+ key.sub(reg, '') # Only one specifier
152
+ end
153
+ end
154
+ end
155
+
156
+ Prawn::Document.extensions << Prawn::Icon::Interface
@@ -0,0 +1,106 @@
1
+ # encoding: utf-8
2
+ #
3
+ # font_data.rb: Icon font metadata container/cache.
4
+ #
5
+ # Copyright October 2014, Jesse Doyle. All rights reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ require 'yaml'
10
+
11
+ module Prawn
12
+ class Icon
13
+ class FontData
14
+ class << self
15
+ # Make a simple cache that contains font
16
+ # data that has been lazily loaded.
17
+ def load(document, set)
18
+ set = set.to_sym
19
+ @data ||= {}
20
+ @data[set] ||= FontData.new(document, set: set)
21
+ end
22
+
23
+ # Release all font references if requested.
24
+ def release_data
25
+ @data = {}
26
+ end
27
+
28
+ def unicode_from_key(document, key)
29
+ set = specifier_from_key(key)
30
+ key = key.sub(Regexp.new("#{set}-"), '')
31
+ load(document, set).unicode(key)
32
+ end
33
+
34
+ def specifier_from_key(key)
35
+ if key.nil? || key == ''
36
+ raise Prawn::Errors::IconKeyEmpty,
37
+ 'Icon key provided was nil.'
38
+ end
39
+
40
+ key.split('-')[0].to_sym
41
+ end
42
+ end
43
+
44
+ attr_reader :set
45
+
46
+ def initialize(document, opts = {})
47
+ @pdf = document
48
+ @set = opts[:set] || :fa
49
+ update_document_fonts!
50
+ end
51
+
52
+ def font_version
53
+ yaml[specifier]['__font_version__']
54
+ end
55
+
56
+ def legend_path
57
+ File.join(File.dirname(path), "#{@set}.yml")
58
+ end
59
+
60
+ def path
61
+ ttf = File.join(Icon::FONTDIR, @set.to_s, '*.ttf')
62
+ fonts = Dir[ttf]
63
+
64
+ if fonts.empty?
65
+ raise Prawn::Errors::UnknownFont,
66
+ "Icon font not found for set: #{@set}"
67
+ end
68
+
69
+ @path ||= fonts.first
70
+ end
71
+
72
+ def specifier
73
+ @specifier ||= yaml.keys[0]
74
+ end
75
+
76
+ def unicode(key)
77
+ char = yaml[specifier][key]
78
+
79
+ unless char
80
+ raise Prawn::Errors::IconNotFound,
81
+ "Key: #{specifier}-#{key} not found"
82
+ end
83
+
84
+ char
85
+ end
86
+
87
+ def keys
88
+ # Strip the first element: __font_version__
89
+ yaml[specifier].keys.map { |k| "#{specifier}-#{k}" }[1..-1]
90
+ end
91
+
92
+ def yaml
93
+ @yaml ||= YAML.load_file legend_path
94
+ end
95
+
96
+ private
97
+
98
+ def update_document_fonts!
99
+ @pdf.font_families.update(
100
+ @set.to_s => {
101
+ normal: path
102
+ })
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,126 @@
1
+ # encoding: utf-8
2
+ #
3
+ # parser.rb: Prawn icon tag text parser (pseudo-HTML).
4
+ #
5
+ # Copyright October 2014, Jesse Doyle. All rights reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ module Prawn
10
+ class Icon
11
+ # Provides the necessary methods to enable the parsing
12
+ # of <icon> tags from input text.
13
+ #
14
+ # = Supported Tags:
15
+ # <icon></icon>::
16
+ # Place an icon key between the tags and the output
17
+ # will be translated into: <font name="fa">unicode</font>.
18
+ #
19
+ # = Supported Attributes:
20
+ #
21
+ # Various attributes will be extracted from +<icon>+ tags:
22
+ #
23
+ # color::
24
+ # The hex representation of a color that the icon should
25
+ # be rendered as. If left nil, the document's fill color
26
+ # will be used.
27
+ #
28
+ # size::
29
+ # The font size of a particular icon. If left nil, the
30
+ # document's font size will be used.
31
+ #
32
+ class Parser
33
+ PARSER_REGEX = Regexp.new \
34
+ '<icon[^>]*>|</icon>',
35
+ Regexp::IGNORECASE |
36
+ Regexp::MULTILINE
37
+
38
+ CONTENT_REGEX = /<icon[^>]*>(?<content>[^<]*)<\/icon>/mi
39
+
40
+ TAG_REGEX = /<icon[^>]*>[^<]*<\/icon>/mi
41
+
42
+ class << self
43
+ def format(document, string)
44
+ tokens = string.scan(PARSER_REGEX)
45
+ config = config_from_tokens(tokens)
46
+ content = string.scan(CONTENT_REGEX).flatten
47
+ icons = keys_to_unicode(document, content, config)
48
+ tags = icon_tags(icons)
49
+
50
+ string.gsub(TAG_REGEX).with_index do |_, i|
51
+ tags[i]
52
+ end
53
+ end
54
+
55
+ def config_from_tokens(tokens)
56
+ array = []
57
+
58
+ tokens.each do |token|
59
+ # Skip the closing tag
60
+ next if token =~ /<\/icon>/i
61
+
62
+ icon = {}
63
+
64
+ # TODO: Only support double quotes?
65
+ size = /size="([^"]*)"/i.match(token)
66
+ color = /color="([^"]*)"/i.match(token)
67
+
68
+ icon[:size] = size[1].to_f if size
69
+ icon[:color] = color[1] if color
70
+
71
+ array << icon
72
+ end
73
+
74
+ array
75
+ end
76
+
77
+ def icon_tags(icons)
78
+ tags = []
79
+
80
+ icons.each do |icon|
81
+ # Mandatory
82
+ content = icon[:content]
83
+ set = icon[:set]
84
+
85
+ # Optional
86
+ color = icon[:color]
87
+ size = icon[:size]
88
+
89
+ opening = "<font name=\"#{set}\""
90
+
91
+ unless color || size
92
+ tags << "#{opening}>#{content}</font>"
93
+ next
94
+ end
95
+
96
+ opening += " size=\"#{size}\"" if size
97
+ content = "<color rgb=\"#{color}\">#{content}</color>" if color
98
+
99
+ opening += '>'
100
+ tags << "#{opening}#{content}</font>"
101
+ end
102
+
103
+ tags
104
+ end
105
+
106
+ def keys_to_unicode(document, content, config)
107
+ icons = []
108
+
109
+ content.each_with_index do |icon, index|
110
+ options ||= {}
111
+ options = config[index] if config.any?
112
+ info = {
113
+ set: FontData.specifier_from_key(icon),
114
+ size: options[:size],
115
+ color: options[:color],
116
+ content: FontData.unicode_from_key(document, icon)
117
+ }
118
+ icons << info
119
+ end
120
+
121
+ icons
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ #
3
+ # version.rb: Prawn icon versioning.
4
+ #
5
+ # Copyright October 2014, Jesse Doyle. All rights reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ module Prawn
10
+ class Icon
11
+ VERSION = '0.5.0'.freeze
12
+ end
13
+ end
@@ -0,0 +1,36 @@
1
+ basedir = File.expand_path(File.dirname(__FILE__))
2
+ require "#{basedir}/lib/prawn/icon/version"
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'prawn-icon'
6
+ spec.version = Prawn::Icon::VERSION
7
+ spec.platform = Gem::Platform::RUBY
8
+ spec.summary = 'Provides icon fonts for PrawnPDF'
9
+ spec.files = Dir.glob('{lib,spec,fonts,examples}/**/**/*') +
10
+ ['prawn-icon.gemspec', 'Gemfile', 'Rakefile',
11
+ 'COPYING', 'LICENSE', 'GPLv2', 'GPLv3']
12
+ spec.require_path = 'lib'
13
+ spec.required_ruby_version = '>= 1.9.3'
14
+ spec.required_rubygems_version = '>= 1.3.6'
15
+
16
+ spec.homepage = "https://github.com/jessedoyle/prawn-icon/"
17
+
18
+ spec.test_files = Dir['spec/*_spec.rb']
19
+ spec.authors = ['Jesse Doyle']
20
+ spec.email = ['jdoyle@ualberta.ca']
21
+ spec.licenses = ['RUBY', 'GPL-2', 'GPL-3']
22
+
23
+ spec.add_development_dependency('prawn', '~> 1.3')
24
+ spec.add_development_dependency('pdf-inspector', '~> 1.1.0')
25
+ spec.add_development_dependency('rspec', '2.14.1')
26
+ spec.add_development_dependency('mocha')
27
+ spec.add_development_dependency('rake')
28
+ spec.add_development_dependency('simplecov')
29
+ spec.add_development_dependency('pdf-reader', '~>1.2')
30
+
31
+ spec.description = <<-END_DESC
32
+ Prawn::Icon provides various icon fonts including
33
+ FontAwesome, Foundation Icons and GitHub Octicons
34
+ for use with the Prawn PDF toolkit.
35
+ END_DESC
36
+ end
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Copyright October 2014, Jesse Doyle. All rights reserved.
4
+ #
5
+ # This is free software. Please see the LICENSE and COPYING files for details.
6
+
7
+ require 'spec_helper'
8
+
9
+ describe Prawn::Icon::Interface do
10
+ describe '::icon' do
11
+ context 'valid icon key' do
12
+ context 'with options' do
13
+ it 'should handle text options (size)' do
14
+ pdf = create_pdf
15
+ pdf.icon 'fa-arrows', size: 60
16
+ text = PDF::Inspector::Text.analyze(pdf.render)
17
+
18
+ expect(text.font_settings.first[:size]).to eq(60)
19
+ end
20
+ end
21
+
22
+ context 'inline_format: true' do
23
+ it 'should handle text options (size)' do
24
+ pdf = create_pdf
25
+ # We need to flush the font cache here...
26
+ Prawn::Icon::FontData.release_data
27
+ pdf.icon '<icon size="60">fa-arrows</icon>', inline_format: true
28
+ text = PDF::Inspector::Text.analyze(pdf.render)
29
+
30
+ expect(text.strings.first).to eq("\uf047")
31
+ expect(text.font_settings.first[:size]).to eq(60.0)
32
+ end
33
+ end
34
+
35
+ context 'without options' do
36
+ it 'should render an icon to document' do
37
+ pdf = create_pdf
38
+ pdf.icon 'fa-arrows'
39
+ text = PDF::Inspector::Text.analyze(pdf.render)
40
+
41
+ expect(text.strings.first).to eq("\uf047")
42
+ end
43
+ end
44
+ end
45
+
46
+ context 'invalid icon key' do
47
+ it 'should raise IconNotFound' do
48
+ pdf = create_pdf
49
+ proc = Proc.new { pdf.icon 'fa-__INVALID' }
50
+
51
+ expect(proc).to raise_error(Prawn::Errors::IconNotFound)
52
+ end
53
+ end
54
+
55
+ context 'invalid specifier' do
56
+ it 'should raise UnknownFont' do
57
+ pdf = create_pdf
58
+ proc = Proc.new { pdf.icon '__INVALID__' }
59
+
60
+ expect(proc).to raise_error(Prawn::Errors::UnknownFont)
61
+ end
62
+ end
63
+ end
64
+
65
+ describe '::make_icon' do
66
+ context ':inline_format => false (default)' do
67
+ it 'should return a Prawn::Icon instance' do
68
+ pdf = create_pdf
69
+ icon = pdf.make_icon 'fa-arrows'
70
+
71
+ expect(icon.class).to eq(Prawn::Icon)
72
+ end
73
+ end
74
+
75
+ context ':inline_format => true' do
76
+ it 'should return a Prawn::::Text::Formatted::Box instance' do
77
+ pdf = create_pdf
78
+ icon = pdf.make_icon '<icon>fa-arrows</icon>', inline_format: true
79
+
80
+ expect(icon.class).to eq(Prawn::Text::Formatted::Box)
81
+ end
82
+ end
83
+ end
84
+
85
+ describe '::inline_icon' do
86
+ it 'should return a Prawn::Text::Formatted::Box instance' do
87
+ pdf = create_pdf
88
+ icon = pdf.inline_icon '<icon>fa-arrows</icon>'
89
+
90
+ expect(icon.class).to eq(Prawn::Text::Formatted::Box)
91
+ end
92
+ end
93
+ end