svg_optimizer 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of svg_optimizer might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +29 -0
- data/lib/svg_optimizer.rb +7 -10
- data/lib/svg_optimizer/plugins/base.rb +1 -0
- data/lib/svg_optimizer/plugins/cleanup_attribute.rb +1 -0
- data/lib/svg_optimizer/plugins/cleanup_id.rb +1 -0
- data/lib/svg_optimizer/plugins/collapse_groups.rb +15 -11
- data/lib/svg_optimizer/plugins/remove_comment.rb +1 -0
- data/lib/svg_optimizer/plugins/remove_editor_namespace.rb +14 -13
- data/lib/svg_optimizer/plugins/remove_empty_attribute.rb +2 -0
- data/lib/svg_optimizer/plugins/remove_empty_container.rb +2 -0
- data/lib/svg_optimizer/plugins/remove_empty_text_node.rb +2 -0
- data/lib/svg_optimizer/plugins/remove_hidden_element.rb +1 -0
- data/lib/svg_optimizer/plugins/remove_metadata.rb +1 -0
- data/lib/svg_optimizer/plugins/remove_raster_image.rb +1 -0
- data/lib/svg_optimizer/plugins/remove_unused_namespace.rb +1 -0
- data/lib/svg_optimizer/plugins/remove_useless_stroke_and_fill.rb +18 -17
- data/lib/svg_optimizer/version.rb +2 -1
- data/spec/spec_helper.rb +13 -6
- data/spec/svg_optimizer/plugins/cleanup_attribute_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/cleanup_id_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/collapse_groups_spec.rb +2 -1
- data/spec/svg_optimizer/plugins/remove_comment_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/remove_editor_namespace_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/remove_empty_attribute_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/remove_empty_container_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/remove_empty_text_node_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/remove_hidden_element_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/remove_metadata_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/remove_raster_image_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/remove_unused_namespace_spec.rb +1 -0
- data/spec/svg_optimizer/plugins/remove_useless_stroke_and_fill_spec.rb +2 -1
- data/spec/svg_optimizer/svg_optimizer_spec.rb +54 -0
- metadata +17 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e80d8684aae3105745cd84db8c935a332bc58b02
|
4
|
+
data.tar.gz: 698bc37343bf931cea862afb939cd0a45547208b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d4bf32d76d719e774137c9a00231de35caaed49d4a7f970606f9f6e58b2729bbae67708df72719ac31a008c29bdbd3b0c3548fe6d54e6dd8a9f7bb888eacddf
|
7
|
+
data.tar.gz: 28a362b536013a61c426e0ccc3e3fc6d1f9674e9e011cfe50772e22d5aab954904925fb9209714e950556aef3700ab91cee876c0b634fae207b36ed3cd201a80
|
data/README.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# SvgOptimizer
|
2
2
|
|
3
|
+
[![Travis-CI](https://travis-ci.org/fnando/svg_optimizer.png)](https://travis-ci.org/fnando/svg_optimizer)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/fnando/svg_optimizer/badges/gpa.svg)](https://codeclimate.com/github/fnando/svg_optimizer)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/fnando/svg_optimizer/badges/coverage.svg)](https://codeclimate.com/github/fnando/svg_optimizer/coverage)
|
6
|
+
[![Gem](https://img.shields.io/gem/v/svg_optimizer.svg)](https://rubygems.org/gems/svg_optimizer)
|
7
|
+
[![Gem](https://img.shields.io/gem/dt/svg_optimizer.svg)](https://rubygems.org/gems/svg_optimizer)
|
8
|
+
|
3
9
|
Some small optimizations for SVG files.
|
4
10
|
|
5
11
|
## Installation
|
@@ -30,6 +36,29 @@ SvgOptimizer.optimize_file("file.svg")
|
|
30
36
|
SvgOptimizer.optimize_file("file.svg", "optimized/file.svg")
|
31
37
|
```
|
32
38
|
|
39
|
+
You can specify the plugins you want to enable. The method signature is:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
SvgOptimizer.optimize(xml, plugins)
|
43
|
+
SvgOptimizer.optimize_file(input, output, plugins)
|
44
|
+
```
|
45
|
+
|
46
|
+
where `plugins` is an array of classes that implement the following contract:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class MyPlugin < SvgOptimizer::Plugins::Base
|
50
|
+
def process
|
51
|
+
xml.xpath("//comment()").remove
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
The default list of plugins is stored at `SvgOptimizer::DEFAULT_PLUGINS`. To use your new plugin, just do something like this:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
SvgOptimizer.optimize(xml, SvgOptimizer::DEFAULT_PLUGINS + [MyPlugin])
|
60
|
+
```
|
61
|
+
|
33
62
|
## Contributing
|
34
63
|
|
35
64
|
1. Fork it
|
data/lib/svg_optimizer.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "nokogiri"
|
2
3
|
|
3
4
|
require "svg_optimizer/version"
|
@@ -17,7 +18,7 @@ require "svg_optimizer/plugins/remove_unused_namespace"
|
|
17
18
|
require "svg_optimizer/plugins/remove_useless_stroke_and_fill"
|
18
19
|
|
19
20
|
module SvgOptimizer
|
20
|
-
|
21
|
+
DEFAULT_PLUGINS = %w[
|
21
22
|
CleanupAttribute
|
22
23
|
CleanupId
|
23
24
|
RemoveComment
|
@@ -31,20 +32,16 @@ module SvgOptimizer
|
|
31
32
|
RemoveUselessStrokeAndFill
|
32
33
|
RemoveEmptyTextNode
|
33
34
|
RemoveEmptyContainer
|
34
|
-
]
|
35
|
+
].map {|name| Plugins.const_get(name) }
|
35
36
|
|
36
|
-
def self.optimize(contents)
|
37
|
+
def self.optimize(contents, plugins = DEFAULT_PLUGINS)
|
37
38
|
xml = Nokogiri::XML(contents)
|
38
|
-
|
39
|
-
Plugins.const_get(plugin_name).new(xml).process
|
40
|
-
end
|
41
|
-
|
39
|
+
plugins.each {|plugin| plugin.new(xml).process }
|
42
40
|
xml.root.to_xml
|
43
41
|
end
|
44
42
|
|
45
|
-
def self.optimize_file(path, target = path)
|
46
|
-
contents = optimize(File.read(path))
|
47
|
-
|
43
|
+
def self.optimize_file(path, target = path, plugins = DEFAULT_PLUGINS)
|
44
|
+
contents = optimize(File.read(path), plugins)
|
48
45
|
File.open(target, "w") {|file| file << contents }
|
49
46
|
true
|
50
47
|
end
|
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module SvgOptimizer
|
2
3
|
module Plugins
|
3
4
|
class CollapseGroups < Base
|
5
|
+
COLLAPSABLE_ATTRS = %w[transform class]
|
6
|
+
|
4
7
|
def process
|
5
|
-
xml.css(
|
8
|
+
xml.css("g:not(:empty)").each do |group|
|
6
9
|
collapse(group)
|
7
10
|
end
|
8
11
|
end
|
@@ -19,28 +22,29 @@ module SvgOptimizer
|
|
19
22
|
return unless collapse_attributes?(group, child)
|
20
23
|
|
21
24
|
group.attributes.each do |attr, val|
|
22
|
-
if
|
23
|
-
child[attr] = [val, child[attr]].compact.join(
|
25
|
+
if COLLAPSABLE_ATTRS.include?(attr)
|
26
|
+
child[attr] = [val, child[attr]].compact.join(" ")
|
24
27
|
else
|
25
28
|
child[attr] ||= val
|
26
29
|
end
|
30
|
+
|
27
31
|
group.delete(attr)
|
28
32
|
end
|
29
33
|
end
|
30
34
|
|
31
35
|
def collapse?(group)
|
32
36
|
group.attributes.empty? &&
|
33
|
-
!group.element_children.map(&:name).include?(
|
37
|
+
!group.element_children.map(&:name).include?("animate")
|
34
38
|
end
|
35
39
|
|
36
40
|
def collapse_attributes?(group, child)
|
37
|
-
return
|
38
|
-
return
|
39
|
-
return
|
40
|
-
return
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
return unless group.attributes.any?
|
42
|
+
return unless group.element_children.length == 1
|
43
|
+
return if child.has_attribute?("id")
|
44
|
+
return unless !group.has_attribute?("clip-path") ||
|
45
|
+
child.name == "g" &&
|
46
|
+
!group.has_attribute?("transform") &&
|
47
|
+
!child.has_attribute?("transform")
|
44
48
|
true
|
45
49
|
end
|
46
50
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module SvgOptimizer
|
2
3
|
module Plugins
|
3
4
|
class RemoveEditorNamespace < Base
|
@@ -17,6 +18,19 @@ module SvgOptimizer
|
|
17
18
|
http://www.bohemiancoding.com/sketch/ns
|
18
19
|
]
|
19
20
|
|
21
|
+
def process
|
22
|
+
namespaces = xml.namespaces
|
23
|
+
remove_namespaced_attributes
|
24
|
+
xml.remove_namespaces!
|
25
|
+
|
26
|
+
namespaces.each do |name, value|
|
27
|
+
next if NAMESPACES.include?(value)
|
28
|
+
|
29
|
+
_, name = name.split(":")
|
30
|
+
xml.root.add_namespace name, value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
20
34
|
def namespaces_to_be_removed
|
21
35
|
xml.namespaces.map do |name, value|
|
22
36
|
_, name = name.split(":")
|
@@ -34,19 +48,6 @@ module SvgOptimizer
|
|
34
48
|
end
|
35
49
|
end
|
36
50
|
end
|
37
|
-
|
38
|
-
def process
|
39
|
-
namespaces = xml.namespaces
|
40
|
-
remove_namespaced_attributes
|
41
|
-
xml.remove_namespaces!
|
42
|
-
|
43
|
-
namespaces.each do |name, value|
|
44
|
-
next if NAMESPACES.include?(value)
|
45
|
-
|
46
|
-
_, name = name.split(":")
|
47
|
-
xml.root.add_namespace name, value
|
48
|
-
end
|
49
|
-
end
|
50
51
|
end
|
51
52
|
end
|
52
53
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module SvgOptimizer
|
2
3
|
module Plugins
|
3
4
|
class RemoveEmptyAttribute < Base
|
@@ -10,6 +11,7 @@ module SvgOptimizer
|
|
10
11
|
end
|
11
12
|
|
12
13
|
private
|
14
|
+
|
13
15
|
def remove_if_empty(node, attr)
|
14
16
|
node.remove_attribute(attr) if node[attr].empty?
|
15
17
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module SvgOptimizer
|
2
3
|
module Plugins
|
3
4
|
class RemoveEmptyContainer < Base
|
@@ -11,6 +12,7 @@ module SvgOptimizer
|
|
11
12
|
end
|
12
13
|
|
13
14
|
private
|
15
|
+
|
14
16
|
def remove_node(node)
|
15
17
|
node.children.empty? && node.remove
|
16
18
|
remove_node(node.parent) if node.parent
|
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module SvgOptimizer
|
2
3
|
module Plugins
|
3
4
|
class RemoveUselessStrokeAndFill < Base
|
4
|
-
SELECTOR = %w
|
5
|
-
STROKE_ATTRS = %w
|
6
|
-
FILL_ATTRS = %w
|
5
|
+
SELECTOR = %w[circle ellipse line path polygon polyline rect].join(",")
|
6
|
+
STROKE_ATTRS = %w[stroke stroke-opacity stroke-width stroke-dashoffset]
|
7
|
+
FILL_ATTRS = %w[fill-opacity fill-rule]
|
7
8
|
|
8
9
|
def process
|
9
|
-
return if xml.css(
|
10
|
+
return if xml.css("style, script").any?
|
11
|
+
|
10
12
|
xml.css(SELECTOR).each do |node|
|
11
13
|
remove_useless_attributes(node)
|
12
14
|
end
|
@@ -15,48 +17,47 @@ module SvgOptimizer
|
|
15
17
|
private
|
16
18
|
|
17
19
|
def remove_useless_attributes(node)
|
18
|
-
return if inherited_attribute(node,
|
20
|
+
return if inherited_attribute(node, "id")
|
19
21
|
remove_stroke(node) if remove_stroke?(node)
|
20
22
|
remove_fill(node) if remove_fill?(node)
|
21
23
|
end
|
22
24
|
|
23
25
|
def remove_stroke(node)
|
24
26
|
STROKE_ATTRS.each { |attr| node.delete(attr) }
|
25
|
-
node[
|
27
|
+
node["stroke"] = "none" if decline_inherited_stroke?(node)
|
26
28
|
end
|
27
29
|
|
28
30
|
def remove_fill(node)
|
29
31
|
FILL_ATTRS.each { |attr| node.delete(attr) }
|
30
|
-
node[
|
32
|
+
node["fill"] = "none" if decline_inherited_fill?(node)
|
31
33
|
end
|
32
34
|
|
33
35
|
def decline_inherited_stroke?(node)
|
34
|
-
(inherited_attribute(node.parent,
|
36
|
+
(inherited_attribute(node.parent, "stroke") || "none") != "none"
|
35
37
|
end
|
36
38
|
|
37
39
|
def decline_inherited_fill?(node)
|
38
|
-
!inherited_attribute(node,
|
40
|
+
!inherited_attribute(node, "fill") || node.has_attribute?("fill")
|
39
41
|
end
|
40
42
|
|
41
43
|
def remove_stroke?(node)
|
42
|
-
return true if (inherited_attribute(node,
|
43
|
-
return true if inherited_attribute(node,
|
44
|
-
return inherited_attribute(node,
|
44
|
+
return true if (inherited_attribute(node, "stroke") || "none") == "none"
|
45
|
+
return true if inherited_attribute(node, "stroke-opacity") == "0"
|
46
|
+
return inherited_attribute(node, "stroke-width") == "0"
|
45
47
|
end
|
46
48
|
|
47
49
|
def remove_fill?(node)
|
48
|
-
fill = inherited_attribute(node,
|
49
|
-
return true if fill ==
|
50
|
-
return true if inherited_attribute(node,
|
50
|
+
fill = inherited_attribute(node, "fill")
|
51
|
+
return true if fill == "none"
|
52
|
+
return true if inherited_attribute(node, "fill-opacity") == "0"
|
51
53
|
return !fill
|
52
54
|
end
|
53
55
|
|
54
56
|
def inherited_attribute(node, attr)
|
55
|
-
return
|
57
|
+
return if node.nil? || node.document?
|
56
58
|
return node[attr] if node.has_attribute?(attr)
|
57
59
|
inherited_attribute(node.parent, attr)
|
58
60
|
end
|
59
61
|
end
|
60
62
|
end
|
61
63
|
end
|
62
|
-
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "codeclimate-test-reporter"
|
3
|
+
CodeClimate::TestReporter.start
|
4
|
+
|
1
5
|
require "bundler/setup"
|
2
6
|
require "svg_optimizer"
|
7
|
+
require "securerandom"
|
3
8
|
|
4
9
|
module RSpecHelpers
|
5
10
|
SVG = <<-XML
|
@@ -11,6 +16,10 @@ module RSpecHelpers
|
|
11
16
|
</svg>
|
12
17
|
XML
|
13
18
|
|
19
|
+
def build_svg(content)
|
20
|
+
SVG % content
|
21
|
+
end
|
22
|
+
|
14
23
|
def fixtures_path
|
15
24
|
File.expand_path("../fixtures", __FILE__)
|
16
25
|
end
|
@@ -24,16 +33,16 @@ module RSpecHelpers
|
|
24
33
|
|
25
34
|
def with_svg_plugin(content)
|
26
35
|
let(:file_path) { File.join(fixtures_path, content) }
|
27
|
-
let(:input_content) { File.file?(file_path) ? File.read(file_path) :
|
36
|
+
let(:input_content) { File.file?(file_path) ? File.read(file_path) : build_svg(content) }
|
28
37
|
setup_plugin_test
|
29
38
|
end
|
30
39
|
|
31
40
|
def test_with_fixture_set(name)
|
32
|
-
Dir.glob(File.join(fixtures_path, name,
|
41
|
+
Dir.glob(File.join(fixtures_path, name, "*.svg")).each do |fixture|
|
33
42
|
context "output from fixture: #{fixture}" do
|
34
43
|
let(:fixture_content) { File.read(fixture) }
|
35
|
-
let(:input_content) { fixture_content.split(
|
36
|
-
let(:expected_content) { fixture_content.split(
|
44
|
+
let(:input_content) { fixture_content.split("@@@")[0].strip }
|
45
|
+
let(:expected_content) { fixture_content.split("@@@")[1].strip }
|
37
46
|
let(:expected) { Nokogiri::XML(expected_content, &:noblanks).root.to_xml }
|
38
47
|
subject { xml.root.to_xml }
|
39
48
|
setup_plugin_test
|
@@ -46,8 +55,6 @@ module RSpecHelpers
|
|
46
55
|
end
|
47
56
|
|
48
57
|
RSpec.configure do |config|
|
49
|
-
config.run_all_when_everything_filtered = true
|
50
|
-
config.filter_run :focus
|
51
58
|
config.order = "random"
|
52
59
|
config.extend RSpecHelpers
|
53
60
|
config.include RSpecHelpers
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe SvgOptimizer do
|
4
|
+
context "file saving" do
|
5
|
+
let(:input) { "/tmp/#{SecureRandom.uuid}.svg" }
|
6
|
+
let(:output) { "/tmp/#{SecureRandom.uuid}.svg" }
|
7
|
+
let(:raw_svg) { build_svg("<!-- foo --><text>SVG</text>") }
|
8
|
+
let(:output_svg) { SvgOptimizer.optimize(raw_svg) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
File.open(input, "w") {|file| file << raw_svg }
|
12
|
+
end
|
13
|
+
|
14
|
+
it "overrides existing file with optimized version" do
|
15
|
+
SvgOptimizer.optimize_file(input)
|
16
|
+
expect(File.read(input)).to eq(output_svg)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "saves new file file with optimized version" do
|
20
|
+
SvgOptimizer.optimize_file(input, output)
|
21
|
+
expect(File.read(input)).to eq(raw_svg)
|
22
|
+
expect(File.read(output)).to eq(output_svg)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "plugin selection" do
|
27
|
+
it "applies all plugins" do
|
28
|
+
svg = build_svg <<-SVG
|
29
|
+
<!-- foo -->
|
30
|
+
<g></g>
|
31
|
+
<!-- bar -->
|
32
|
+
SVG
|
33
|
+
|
34
|
+
xml = Nokogiri::XML(SvgOptimizer.optimize(svg))
|
35
|
+
|
36
|
+
expect(xml.xpath("//comment()")).to be_empty
|
37
|
+
expect(xml.css("g")).to be_empty
|
38
|
+
end
|
39
|
+
|
40
|
+
it "applies just specified plugins" do
|
41
|
+
plugins = [SvgOptimizer::Plugins::RemoveComment]
|
42
|
+
svg = build_svg <<-SVG
|
43
|
+
<!-- foo -->
|
44
|
+
<g></g>
|
45
|
+
<!-- bar -->
|
46
|
+
SVG
|
47
|
+
|
48
|
+
xml = Nokogiri::XML(SvgOptimizer.optimize(svg, plugins))
|
49
|
+
|
50
|
+
expect(xml.xpath("//comment()")).to be_empty
|
51
|
+
expect(xml.css("g").length).to eql(1)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: svg_optimizer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nando Vieira
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: codeclimate-test-reporter
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
description: SVG optimization based on Node's SVGO
|
84
98
|
email:
|
85
99
|
- fnando.vieira@gmail.com
|
@@ -138,6 +152,7 @@ files:
|
|
138
152
|
- spec/svg_optimizer/plugins/remove_raster_image_spec.rb
|
139
153
|
- spec/svg_optimizer/plugins/remove_unused_namespace_spec.rb
|
140
154
|
- spec/svg_optimizer/plugins/remove_useless_stroke_and_fill_spec.rb
|
155
|
+
- spec/svg_optimizer/svg_optimizer_spec.rb
|
141
156
|
homepage: https://github.com/fnando/svg_optimizer
|
142
157
|
licenses:
|
143
158
|
- MIT
|
@@ -196,3 +211,4 @@ test_files:
|
|
196
211
|
- spec/svg_optimizer/plugins/remove_raster_image_spec.rb
|
197
212
|
- spec/svg_optimizer/plugins/remove_unused_namespace_spec.rb
|
198
213
|
- spec/svg_optimizer/plugins/remove_useless_stroke_and_fill_spec.rb
|
214
|
+
- spec/svg_optimizer/svg_optimizer_spec.rb
|