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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4c019833e1b1397a11142ee88f5ea2c9924aeb6c101316898babdd09ec526bb
4
- data.tar.gz: b242b7a4575f87eb36047f9986481d8064b523fad955693613c85e41e618057a
3
+ metadata.gz: 1105c6329d7279577f8d478e3be7e80984c3b374b38f66686b293299fa1dc5d8
4
+ data.tar.gz: 58300ea64d615ccd2876493467916a47fa6150708e45bbe0e0183ac5b151997d
5
5
  SHA512:
6
- metadata.gz: 91e5b2c1db12ebdada6679e6d07452ad53acf26970de15e9ee3120806fc1bd6468b54e31a9146913f9c40b23e6569decdfbe8ef220d4f40a3cac5c9f9e239259
7
- data.tar.gz: afa68fb6c33b90cca3a358872f48c77574ca3ac6570ebd46faedd6f527f5c83600919dbf4f61c77264a4d4ec8efa833f7da08259b5745777a72d1a646f4b01ab
6
+ metadata.gz: d673025100bef413b41b4026f60e1b435cd8b8f5343a5d928771a6aa33aa72b2b942b8333e92f9923ac2c90798352482b0b221a3dfc38054d74f8649336a4f2f
7
+ data.tar.gz: 76ff8e945a1fe543bcb04ed2198fa3e2ebaedcc03511666b497f88a90b6972ec660f428c236904465238b53a2101d6e0965ee02bdca63c31389e23680d83743c
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FaviconFactory
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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
- module FaviconFactory
9
- SVG_DENSITY = 1_000
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 "white"
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
- exit new(ARGV, $stderr).call
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
- attr_reader :stderr
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
- stderr.puts "imagemagick v7 not found, please install for best results" unless MiniMagick.imagemagick7?
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 File.exist?(favicon_svg)
98
+ return exit_message(1, "Error: #{favicon_svg} does not exist") unless file.exist?(favicon_svg)
122
99
 
123
- [Params.new(favicon_svg: favicon_svg, background: params.fetch(:background)), -1]
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 = File.join(params.dir, name)
133
- if File.exist?(path)
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(:png!),
146
- "icon-512.png" => method(:png!),
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("#{params.size}x#{params.size}")
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.1.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-05-27 00:00:00.000000000 Z
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