favicon_factory 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/lib/favicon_factory/version.rb +1 -1
- data/lib/favicon_factory.rb +137 -60
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1105c6329d7279577f8d478e3be7e80984c3b374b38f66686b293299fa1dc5d8
|
4
|
+
data.tar.gz: 58300ea64d615ccd2876493467916a47fa6150708e45bbe0e0183ac5b151997d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d673025100bef413b41b4026f60e1b435cd8b8f5343a5d928771a6aa33aa72b2b942b8333e92f9923ac2c90798352482b0b221a3dfc38054d74f8649336a4f2f
|
7
|
+
data.tar.gz: 76ff8e945a1fe543bcb04ed2198fa3e2ebaedcc03511666b497f88a90b6972ec660f428c236904465238b53a2101d6e0965ee02bdca63c31389e23680d83743c
|
data/lib/favicon_factory.rb
CHANGED
@@ -1,29 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "favicon_factory/version"
|
4
|
-
require "mini_magick"
|
5
4
|
require "tty/which"
|
6
5
|
require "tty/option"
|
7
6
|
|
8
|
-
|
9
|
-
|
7
|
+
autoload(:MiniMagick, "mini_magick")
|
8
|
+
autoload(:Vips, "vips")
|
10
9
|
|
10
|
+
module FaviconFactory
|
11
11
|
Params = Data.define(:favicon_svg, :background) do
|
12
12
|
def dir
|
13
13
|
File.dirname(favicon_svg)
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
PngParams = Data.define(:favicon_svg, :background, :size) do
|
18
|
-
def self.from_params(size, params)
|
19
|
-
new(**params.to_h, size: size)
|
20
|
-
end
|
21
|
-
|
22
|
-
def dir
|
23
|
-
File.dirname(favicon_svg)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
17
|
class Command
|
28
18
|
include TTY::Option
|
29
19
|
|
@@ -56,8 +46,8 @@ module FaviconFactory
|
|
56
46
|
option :background do
|
57
47
|
short "-b"
|
58
48
|
long "--background string"
|
59
|
-
default "
|
60
|
-
desc "Background color for apple-touch-icon.png"
|
49
|
+
default "#ffffff"
|
50
|
+
desc "Background hex color for apple-touch-icon.png"
|
61
51
|
end
|
62
52
|
|
63
53
|
flag :help do
|
@@ -69,46 +59,33 @@ module FaviconFactory
|
|
69
59
|
|
70
60
|
class Cli
|
71
61
|
def self.call
|
72
|
-
|
62
|
+
adapter = BaseAdapter.find
|
63
|
+
if adapter.nil?
|
64
|
+
stderr.puts "Error: Neither vips or imagemagick found, install one"
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
exit new(adapter: adapter, argv: ARGV, file: File, stderr: $stderr).call
|
73
68
|
end
|
74
69
|
|
75
|
-
|
76
|
-
|
77
|
-
def initialize(argv, stderr)
|
70
|
+
def initialize(adapter:, argv:, file:, stderr:)
|
71
|
+
@adapter = adapter
|
78
72
|
@argv = argv
|
73
|
+
@file = file
|
79
74
|
@stderr = stderr
|
80
75
|
end
|
81
76
|
|
82
77
|
def call
|
83
|
-
|
84
|
-
stderr.puts "inkscape not found, install inkscape for best results" unless TTY::Which.which("inkscape")
|
85
|
-
|
86
|
-
params, status = parse(@argv)
|
78
|
+
params, status = parse(argv)
|
87
79
|
return status if status >= 0
|
88
80
|
|
89
|
-
|
90
|
-
Thread.new { create("favicon.ico", params) },
|
91
|
-
Thread.new { create("icon-192.png", PngParams.from_params(192, params)) },
|
92
|
-
Thread.new { create("icon-512.png", PngParams.from_params(512, params)) },
|
93
|
-
Thread.new { create("apple-touch-icon.png", params) },
|
94
|
-
Thread.new { create("manifest.webmanifest", params) }
|
95
|
-
]
|
96
|
-
.each(&:join)
|
97
|
-
|
98
|
-
stderr.puts <<~TEXT
|
99
|
-
Info: Add the following to the `<head>`
|
100
|
-
<!-- favicons generated with the favicon_factory gem -->
|
101
|
-
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
102
|
-
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
103
|
-
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
104
|
-
<link rel="manifest" href="/manifest.webmanifest">
|
105
|
-
TEXT
|
106
|
-
|
81
|
+
adapter.new(file: file, params: params, stderr: stderr).create_icons
|
107
82
|
0
|
108
83
|
end
|
109
84
|
|
110
85
|
private
|
111
86
|
|
87
|
+
attr_reader :stderr, :file, :adapter, :argv
|
88
|
+
|
112
89
|
def parse(argv)
|
113
90
|
command = Command.new.parse(argv)
|
114
91
|
params = command.params
|
@@ -118,19 +95,67 @@ module FaviconFactory
|
|
118
95
|
params = params.to_h
|
119
96
|
favicon_svg = params.fetch(:favicon_svg)
|
120
97
|
return exit_message(1, "Error: #{favicon_svg} does not end with .svg") unless favicon_svg.end_with?(".svg")
|
121
|
-
return exit_message(1, "Error: #{favicon_svg} does not exist") unless
|
98
|
+
return exit_message(1, "Error: #{favicon_svg} does not exist") unless file.exist?(favicon_svg)
|
122
99
|
|
123
|
-
|
100
|
+
background = params.fetch(:background)
|
101
|
+
unless hex?(background)
|
102
|
+
return exit_message(1, "Error: #{background} is not a valid color, use a hex value like #0099ff")
|
103
|
+
end
|
104
|
+
|
105
|
+
[Params.new(favicon_svg: favicon_svg, background: background), -1]
|
106
|
+
end
|
107
|
+
|
108
|
+
def hex?(string)
|
109
|
+
string = string.delete_prefix("#")
|
110
|
+
string.split("").all? { |char| char.match?(/^[0-9a-fA-F]$/) }
|
124
111
|
end
|
125
112
|
|
126
113
|
def exit_message(status, message)
|
127
114
|
stderr.puts message
|
128
115
|
[nil, status]
|
129
116
|
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class BaseAdapter
|
120
|
+
class << self
|
121
|
+
def find
|
122
|
+
if TTY::Which.which("vips") || TTY::Which.which("libvips")
|
123
|
+
VipsAdapter
|
124
|
+
elsif TTY::Which.which("magick") || TTY::Which.which("convert")
|
125
|
+
ImageMagickAdapter
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def initialize(file:, params:, stderr:)
|
131
|
+
@file = file
|
132
|
+
@params = params
|
133
|
+
@stderr = stderr
|
134
|
+
end
|
135
|
+
|
136
|
+
def create_icons
|
137
|
+
create_by_name
|
138
|
+
.keys
|
139
|
+
.map { |name| Thread.new { create(name, params) } }
|
140
|
+
.each(&:join)
|
141
|
+
|
142
|
+
stderr.puts <<~TEXT
|
143
|
+
Info: Add the following to the `<head>`
|
144
|
+
<!-- favicons generated with the favicon_factory gem -->
|
145
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
146
|
+
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
147
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
148
|
+
<link rel="manifest" href="/manifest.webmanifest">
|
149
|
+
TEXT
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
attr_reader :params, :stderr, :file
|
130
155
|
|
131
156
|
def create(name, params)
|
132
|
-
path =
|
133
|
-
if
|
157
|
+
path = file.join(params.dir, name)
|
158
|
+
if file.exist?(path)
|
134
159
|
stderr.puts "Info: Skipping #{path} because it already exists"
|
135
160
|
return
|
136
161
|
end
|
@@ -142,13 +167,76 @@ module FaviconFactory
|
|
142
167
|
def create_by_name
|
143
168
|
{
|
144
169
|
"favicon.ico" => method(:ico!),
|
145
|
-
"icon-192.png" => method(:
|
146
|
-
"icon-512.png" => method(:
|
170
|
+
"icon-192.png" => method(:png_192!),
|
171
|
+
"icon-512.png" => method(:png_512!),
|
147
172
|
"apple-touch-icon.png" => method(:touch!),
|
148
173
|
"manifest.webmanifest" => method(:manifest!)
|
149
174
|
}
|
150
175
|
end
|
151
176
|
|
177
|
+
def png_192!(path, params)
|
178
|
+
png!(path, params, 192)
|
179
|
+
end
|
180
|
+
|
181
|
+
def png_512!(path, params)
|
182
|
+
png!(path, params, 512)
|
183
|
+
end
|
184
|
+
|
185
|
+
def manifest!(path, _params)
|
186
|
+
file.write(path, <<~MANIFEST)
|
187
|
+
{
|
188
|
+
"icons": [
|
189
|
+
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
|
190
|
+
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
|
191
|
+
]
|
192
|
+
}
|
193
|
+
MANIFEST
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
class VipsAdapter < BaseAdapter
|
198
|
+
def ico!(path, params)
|
199
|
+
png = Vips::Image.thumbnail(params.favicon_svg, 32).write_to_buffer(".png")
|
200
|
+
# https://www.meziantou.net/creating-ico-files-from-multiple-images-in-dotnet.htm
|
201
|
+
ico = [0, 1, 1].pack("S<*") + [32, 32, 0, 0].pack("C*") + [1, 32].pack("S<*") + [png.size, 22].pack("L<*") + png
|
202
|
+
file.write(path, ico)
|
203
|
+
end
|
204
|
+
|
205
|
+
def png!(path, params, size)
|
206
|
+
Vips::Image.thumbnail(params.favicon_svg, size).write_to_file(path)
|
207
|
+
end
|
208
|
+
|
209
|
+
def touch!(path, params)
|
210
|
+
svg = Vips::Image.thumbnail(params.favicon_svg, 160).gravity("centre", 180, 180)
|
211
|
+
image = square(180, params.background).composite(svg, :over)
|
212
|
+
image.write_to_file(path)
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
def square(size, hex)
|
218
|
+
pixel = (Vips::Image.black(1, 1) + hex2rgb(hex)).cast(:uchar)
|
219
|
+
pixel.embed 0, 0, size, size, extend: :copy
|
220
|
+
end
|
221
|
+
|
222
|
+
def hex2rgb(hex)
|
223
|
+
hex = hex.delete_prefix("#")
|
224
|
+
r = hex[0..1].to_i(16)
|
225
|
+
g = hex[2..3].to_i(16)
|
226
|
+
b = hex[4..5].to_i(16)
|
227
|
+
[r, g, b]
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
class ImageMagickAdapter < BaseAdapter
|
232
|
+
SVG_DENSITY = 1_000
|
233
|
+
|
234
|
+
def initialize(**)
|
235
|
+
super
|
236
|
+
stderr.puts "Warn: Install imagemagick v7 for best results, using v6" unless MiniMagick.imagemagick7?
|
237
|
+
stderr.puts "Warn: Inkscape not found, install it for best results" unless TTY::Which.which("inkscape")
|
238
|
+
end
|
239
|
+
|
152
240
|
def ico!(path, params)
|
153
241
|
MiniMagick::Tool::Convert.new do |convert|
|
154
242
|
convert.density(SVG_DENSITY).background("none")
|
@@ -158,11 +246,11 @@ module FaviconFactory
|
|
158
246
|
end
|
159
247
|
end
|
160
248
|
|
161
|
-
def png!(path, params)
|
249
|
+
def png!(path, params, size)
|
162
250
|
MiniMagick::Tool::Convert.new do |convert|
|
163
251
|
convert.density(SVG_DENSITY).background("none")
|
164
252
|
convert << params.favicon_svg
|
165
|
-
convert.resize("#{
|
253
|
+
convert.resize("#{size}x#{size}")
|
166
254
|
convert << path
|
167
255
|
end
|
168
256
|
end
|
@@ -175,16 +263,5 @@ module FaviconFactory
|
|
175
263
|
convert << path
|
176
264
|
end
|
177
265
|
end
|
178
|
-
|
179
|
-
def manifest!(path, _params)
|
180
|
-
File.write(path, <<~MANIFEST)
|
181
|
-
{
|
182
|
-
"icons": [
|
183
|
-
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
|
184
|
-
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
|
185
|
-
]
|
186
|
-
}
|
187
|
-
MANIFEST
|
188
|
-
end
|
189
266
|
end
|
190
267
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: favicon_factory
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 3v0k4
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mini_magick
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ruby-vips
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.2'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: tty-option
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|