svg2img 0.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.
data/Cargo.toml ADDED
@@ -0,0 +1,7 @@
1
+ # This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
2
+ # a Rust project. Your extensions dependencies should be added to the Cargo.toml
3
+ # in the ext/ directory.
4
+
5
+ [workspace]
6
+ members = ["./ext/svg2img"]
7
+ resolver = "2"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Orvar Segerström
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Svg2img
2
+
3
+ Convert SVG to image. Uses a bundled native binary, and requires no external dependencies.
4
+ Supported output formats for now:
5
+
6
+ - png
7
+ - jpg
8
+ - webp
9
+ - gif
10
+
11
+ ## Installation
12
+
13
+ Add the `svg2img` gem to your Gemfile and run `bundle install`:
14
+
15
+ ```ruby
16
+ gem "svg2img"
17
+ ```
18
+
19
+ Alternatively, you can install the gem manually:
20
+
21
+ ```sh
22
+ gem install svg2img
23
+ ```
24
+
25
+ ### Precompiled gems
26
+
27
+ We recommend installing the `svg2img` precompiled gems available for Linux and macOS. Installing a precompiled gem avoids the need to compile from source code, which is generally slower and less reliable.
28
+
29
+ When installing the `svg2img` gem for the first time using `bundle install`, Bundler will automatically download the precompiled gem for your current platform. However, you will need to inform Bundler of any additional platforms you plan to use.
30
+
31
+ To do this, lock your Bundle to the required platforms you will need from the list of supported platforms below:
32
+
33
+ ```sh
34
+ bundle lock --add-platform x86_64-linux # Standard Linux (e.g. Heroku, GitHub Actions, etc.)
35
+ bundle lock --add-platform x86_64-linux-musl # MUSL Linux deployments (i.e. Alpine Linux)
36
+ bundle lock --add-platform aarch64-linux # ARM64 Linux deployments (i.e. AWS Graviton2)
37
+ bundle lock --add-platform x86_64-darwin # Intel MacOS (i.e. pre-M1)
38
+ bundle lock --add-platform arm64-darwin # Apple Silicon MacOS (i.e. M1)
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ Example usage:
44
+
45
+ ```ruby
46
+ require "svg2img"
47
+
48
+ circle_svg = <<~SVG
49
+ <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
50
+ <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
51
+ </svg>
52
+ SVG
53
+ png_path = Svg2Img.process_svg(circle_svg, output_format: :png)
54
+ # png_path is a path to the generated PNG file
55
+ ```
56
+
57
+ ## Development
58
+
59
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
60
+
61
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
62
+
63
+ ## Contributing
64
+
65
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/svg2img.
66
+
67
+ ## License
68
+
69
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rb_sys/extensiontask"
5
+
6
+ task build: :compile
7
+
8
+ GEMSPEC = Gem::Specification.load("svg2img.gemspec")
9
+
10
+ RbSys::ExtensionTask.new("svg2img", GEMSPEC) do |ext|
11
+ ext.lib_dir = "lib/svg2img"
12
+ end
13
+
14
+ task default: :compile
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "svg2img"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ authors = ["Orvar Segerström <orvarsegerstrom@gmail.com>"]
6
+ license = "MIT"
7
+ publish = false
8
+
9
+ [lib]
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ anyhow = "1.0.86"
14
+ image = "0.25.2"
15
+ magnus = { version = "0.6.2" }
16
+ resvg = "0.43.0"
17
+ uuid = { version = "1.10.0", features = ["v4"] }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("svg2img/svg2img")
@@ -0,0 +1,195 @@
1
+ use anyhow::Context;
2
+ use image::{DynamicImage, GenericImageView, ImageBuffer};
3
+ use magnus::{function, prelude::*, Error, Ruby};
4
+ use std::panic::{self, AssertUnwindSafe};
5
+
6
+ #[magnus::init]
7
+ fn init(ruby: &Ruby) -> Result<(), Error> {
8
+ let module = ruby.define_module("Svg2Img")?;
9
+ module.define_singleton_method("process_svg", function!(process_svg_rb, 2))?;
10
+ Ok(())
11
+ }
12
+
13
+ fn process_svg_rb(svg: String, options: magnus::RHash) -> Result<String, magnus::Error> {
14
+ let mut format = image::ImageFormat::Png;
15
+
16
+ if let Some(format_option) = get_string_option(&options, "format")? {
17
+ format = match format_option.as_str() {
18
+ "png" => image::ImageFormat::Png,
19
+ "jpeg" | "jpg" => image::ImageFormat::Jpeg,
20
+ "gif" => image::ImageFormat::Gif,
21
+ "webp" => image::ImageFormat::WebP,
22
+ format => {
23
+ return Err(magnus::Error::new(
24
+ magnus::exception::arg_error(),
25
+ format!("svg2img: Invalid output format: {format}"),
26
+ ));
27
+ }
28
+ };
29
+ }
30
+ let options = Options {
31
+ max_width: get_option(&options, "max_width")?,
32
+ max_height: get_option(&options, "max_height")?,
33
+ format,
34
+ output_path: get_option(&options, "output_path")?,
35
+ };
36
+ process_svg(svg, options)
37
+ .map_err(|err| magnus::Error::new(magnus::exception::runtime_error(), format!("{err:?}")))
38
+ }
39
+
40
+ fn get_option<T>(options: &magnus::RHash, key: &str) -> Result<Option<T>, magnus::Error>
41
+ where
42
+ T: magnus::TryConvert,
43
+ {
44
+ let value = options
45
+ .get(magnus::Symbol::new(key))
46
+ .or_else(|| options.get(key));
47
+ let Some(value) = value else {
48
+ return Ok(None);
49
+ };
50
+ match T::try_convert(value) {
51
+ Ok(value) => Ok(Some(value)),
52
+ Err(err) => Err(magnus::Error::new(
53
+ magnus::exception::arg_error(),
54
+ format!("svg2img: Invalid option {key}: {err:?}"),
55
+ )),
56
+ }
57
+ }
58
+ fn get_string_option(options: &magnus::RHash, key: &str) -> Result<Option<String>, magnus::Error> {
59
+ // Try get_option with String, then try again with Symbol
60
+ let string_option = get_option::<String>(options, key);
61
+ if let Ok(Some(string_option)) = string_option {
62
+ return Ok(Some(string_option));
63
+ }
64
+
65
+ let symbol_option = get_option::<magnus::Symbol>(options, key)?;
66
+ if let Some(symbol) = symbol_option {
67
+ let symbol_string = unsafe {
68
+ symbol.to_s().map_err(|err| magnus::Error::new(
69
+ magnus::exception::arg_error(),
70
+ format!("svg2img: Invalid option {key} (could not convert from Symbol to string): {err:?}"),
71
+ ))?.into_owned()
72
+ };
73
+ return Ok(Some(symbol_string));
74
+ }
75
+
76
+ Ok(None)
77
+ }
78
+
79
+ struct Options {
80
+ max_width: Option<u32>,
81
+ max_height: Option<u32>,
82
+ format: image::ImageFormat,
83
+ output_path: Option<String>,
84
+ }
85
+
86
+ fn process_svg(svg: String, options: Options) -> Result<String, anyhow::Error> {
87
+ let image = image_from_svg(svg.as_bytes())?;
88
+ let (old_width, old_height) = image.dimensions();
89
+
90
+ let (mut width, mut height) = (old_width, old_height);
91
+ if let Some(max_width) = options.max_width {
92
+ if width > max_width {
93
+ height = height * max_width / width;
94
+ width = max_width;
95
+ }
96
+ }
97
+ if let Some(max_height) = options.max_height {
98
+ if height > max_height {
99
+ width = width * max_height / height;
100
+ height = max_height;
101
+ }
102
+ }
103
+
104
+ let mut image = image.resize_exact(width, height, image::imageops::FilterType::Lanczos3);
105
+
106
+ if options.format == image::ImageFormat::Jpeg {
107
+ // Convert from rgba8 to rgb8
108
+ image = DynamicImage::ImageRgb8(image.into_rgb8())
109
+ }
110
+
111
+ let mut buf = Vec::new();
112
+ let mut cursor = std::io::Cursor::new(&mut buf);
113
+ image
114
+ .write_to(&mut cursor, options.format)
115
+ .context("Failed to write image to buffer")?;
116
+
117
+ let output_path = options.output_path.unwrap_or_else(|| {
118
+ let random_filename = format!(
119
+ "svg2img-{}.{}",
120
+ uuid::Uuid::new_v4(),
121
+ options
122
+ .format
123
+ .extensions_str()
124
+ .first()
125
+ .unwrap_or(&"whatever")
126
+ );
127
+ std::env::temp_dir()
128
+ .join(random_filename)
129
+ .to_string_lossy()
130
+ .to_string()
131
+ });
132
+ std::fs::write(&output_path, buf).context("Failed to write image to file")?;
133
+
134
+ Ok(output_path)
135
+ }
136
+
137
+ fn image_from_svg(bytes: &[u8]) -> Result<DynamicImage, anyhow::Error> {
138
+ let tree = resvg::usvg::Tree::from_data(bytes, &resvg::usvg::Options::default())
139
+ .context("Failed to parse SVG")?;
140
+
141
+ const TARGET_SIZE: u32 = 512;
142
+ let mut pixmap = resvg::tiny_skia::Pixmap::new(TARGET_SIZE, TARGET_SIZE)
143
+ .context("Failed to create Pixmap for SVG rendering")?;
144
+ let ratio = tree.size().width() / tree.size().height();
145
+ let scaled_width = if ratio > 1.0 {
146
+ TARGET_SIZE
147
+ } else {
148
+ (TARGET_SIZE as f32 * ratio).round() as u32
149
+ };
150
+ let scaled_height = if ratio > 1.0 {
151
+ (TARGET_SIZE as f32 / ratio).round() as u32
152
+ } else {
153
+ TARGET_SIZE
154
+ };
155
+
156
+ // Scale svg and place it centered
157
+ let transform = resvg::tiny_skia::Transform {
158
+ sx: scaled_width as f32 / tree.size().width(),
159
+ sy: scaled_height as f32 / tree.size().height(),
160
+ tx: (TARGET_SIZE - scaled_width) as f32 / 2.0,
161
+ ty: (TARGET_SIZE - scaled_height) as f32 / 2.0,
162
+ ..Default::default()
163
+ };
164
+
165
+ let result = panic::catch_unwind(AssertUnwindSafe(|| {
166
+ resvg::render(&tree, transform, &mut pixmap.as_mut());
167
+ }));
168
+ if let Err(panic) = result {
169
+ let panic_message = panic
170
+ .downcast_ref::<String>()
171
+ .map(String::as_str)
172
+ .or_else(|| {
173
+ panic
174
+ .downcast_ref::<&'static str>()
175
+ .map(std::ops::Deref::deref)
176
+ })
177
+ .unwrap_or("Box<Any>");
178
+ return Err(anyhow::anyhow!("SVG rendering panicked: {}", panic_message));
179
+ }
180
+
181
+ rgba_to_image(pixmap.width(), pixmap.height(), pixmap.data())
182
+ }
183
+
184
+ fn rgba_to_image(width: u32, height: u32, data: &[u8]) -> Result<DynamicImage, anyhow::Error> {
185
+ let mut image_data = Vec::with_capacity((width * height * 4) as usize);
186
+ for pixel in data.chunks(4) {
187
+ image_data.push(pixel[0]); // R
188
+ image_data.push(pixel[1]); // G
189
+ image_data.push(pixel[2]); // B
190
+ image_data.push(pixel[3]); // A
191
+ }
192
+ let buffer = ImageBuffer::from_raw(width, height, image_data)
193
+ .context("Failed to convert to ImageBuffer")?;
194
+ Ok(DynamicImage::ImageRgba8(buffer))
195
+ }
data/flake.lock ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "nodes": {
3
+ "flake-compat": {
4
+ "flake": false,
5
+ "locked": {
6
+ "lastModified": 1696426674,
7
+ "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
8
+ "owner": "edolstra",
9
+ "repo": "flake-compat",
10
+ "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
11
+ "type": "github"
12
+ },
13
+ "original": {
14
+ "owner": "edolstra",
15
+ "repo": "flake-compat",
16
+ "type": "github"
17
+ }
18
+ },
19
+ "flake-utils": {
20
+ "inputs": {
21
+ "systems": "systems"
22
+ },
23
+ "locked": {
24
+ "lastModified": 1694529238,
25
+ "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
26
+ "owner": "numtide",
27
+ "repo": "flake-utils",
28
+ "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
29
+ "type": "github"
30
+ },
31
+ "original": {
32
+ "owner": "numtide",
33
+ "repo": "flake-utils",
34
+ "type": "github"
35
+ }
36
+ },
37
+ "nixpkgs": {
38
+ "locked": {
39
+ "lastModified": 1709961763,
40
+ "narHash": "sha256-6H95HGJHhEZtyYA3rIQpvamMKAGoa8Yh2rFV29QnuGw=",
41
+ "owner": "NixOS",
42
+ "repo": "nixpkgs",
43
+ "rev": "3030f185ba6a4bf4f18b87f345f104e6a6961f34",
44
+ "type": "github"
45
+ },
46
+ "original": {
47
+ "owner": "NixOS",
48
+ "ref": "nixos-unstable",
49
+ "repo": "nixpkgs",
50
+ "type": "github"
51
+ }
52
+ },
53
+ "nixpkgs-ruby": {
54
+ "inputs": {
55
+ "flake-compat": "flake-compat",
56
+ "flake-utils": "flake-utils",
57
+ "nixpkgs": [
58
+ "nixpkgs"
59
+ ]
60
+ },
61
+ "locked": {
62
+ "lastModified": 1722577194,
63
+ "narHash": "sha256-nWLATuXQYs/AHFxC9mi/uo6mVUz6nFYcWNd6flGCxVk=",
64
+ "owner": "bobvanderlinden",
65
+ "repo": "nixpkgs-ruby",
66
+ "rev": "b2ac79f24e50faac5d4ce3a878b7a9f0270fa2bd",
67
+ "type": "github"
68
+ },
69
+ "original": {
70
+ "owner": "bobvanderlinden",
71
+ "repo": "nixpkgs-ruby",
72
+ "type": "github"
73
+ }
74
+ },
75
+ "root": {
76
+ "inputs": {
77
+ "nixpkgs": "nixpkgs",
78
+ "nixpkgs-ruby": "nixpkgs-ruby"
79
+ }
80
+ },
81
+ "systems": {
82
+ "locked": {
83
+ "lastModified": 1681028828,
84
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
85
+ "owner": "nix-systems",
86
+ "repo": "default",
87
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
88
+ "type": "github"
89
+ },
90
+ "original": {
91
+ "owner": "nix-systems",
92
+ "repo": "default",
93
+ "type": "github"
94
+ }
95
+ }
96
+ },
97
+ "root": "root",
98
+ "version": 7
99
+ }
data/flake.nix ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ description = "Devshell with all the dependencies needed to develop and build the project";
3
+
4
+ inputs = {
5
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
+ nixpkgs-ruby.url = "github:bobvanderlinden/nixpkgs-ruby";
7
+ nixpkgs-ruby.inputs.nixpkgs.follows = "nixpkgs";
8
+ };
9
+
10
+
11
+ outputs = { self, nixpkgs, nixpkgs-ruby }:
12
+ let
13
+ # Boilerplate function for generating attributes for all systems
14
+ forAllSystems = function:
15
+ nixpkgs.lib.genAttrs [
16
+ "x86_64-linux"
17
+ "aarch64-linux"
18
+ "x86_64-darwin"
19
+ "aarch64-darwin"
20
+ ]
21
+ (system:
22
+ (function (import nixpkgs {
23
+ inherit system;
24
+ })) system);
25
+ in
26
+ {
27
+ packages = forAllSystems (pkgs: system:
28
+ let
29
+ ruby = nixpkgs-ruby.lib.packageFromRubyVersionFile {
30
+ file = ./.ruby-version;
31
+ inherit system;
32
+ };
33
+ tools = [ ruby ] ++ (with pkgs; [
34
+ nodejs_20
35
+ yarn
36
+ mprocs
37
+ ]);
38
+ gemDependencies = with pkgs; [
39
+ zstd
40
+ libxml2
41
+ libxslt
42
+ imagemagick
43
+ ];
44
+ root = builtins.getEnv "PWD";
45
+ in
46
+ {
47
+ default = pkgs.mkShell {
48
+ buildInputs = tools ++ gemDependencies;
49
+ shellHook = ''
50
+ # https://github.com/sass/sassc-ruby/issues/148#issuecomment-644450274
51
+ bundle config build.sassc --disable-lto
52
+ export BUNDLE_BUILD__SASSC="--disable-lto"
53
+
54
+ export GEM_HOME="${root}/.bundle"
55
+ export GEM_PATH="${root}/.bundle"
56
+ export PATH="${root}/.bundle/bin:$PATH"
57
+ export RUBY_YJIT_ENABLE=1
58
+ '';
59
+ };
60
+ });
61
+ };
62
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Svg2Img
4
+ VERSION = "0.1.0"
5
+ end
data/lib/svg2img.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "svg2img/version"
4
+ require_relative "svg2img/svg2img"
5
+
6
+ module Svg2Img
7
+ class Error < StandardError; end
8
+ # Your code goes here...
9
+ end
data/sig/svg2img.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Svg2Img
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: svg2img
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Orvar Segerström
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - orvarsegerstrom@gmail.com
16
+ executables: []
17
+ extensions:
18
+ - ext/svg2img/Cargo.toml
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".envrc"
22
+ - ".rspec"
23
+ - ".ruby-version"
24
+ - CHANGELOG.md
25
+ - Cargo.lock
26
+ - Cargo.toml
27
+ - LICENSE.txt
28
+ - README.md
29
+ - Rakefile
30
+ - ext/svg2img/Cargo.toml
31
+ - ext/svg2img/extconf.rb
32
+ - ext/svg2img/src/lib.rs
33
+ - flake.lock
34
+ - flake.nix
35
+ - lib/svg2img.rb
36
+ - lib/svg2img/version.rb
37
+ - sig/svg2img.rbs
38
+ homepage: https://github.com/0rvar/svg2img-rb
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ homepage_uri: https://github.com/0rvar/svg2img-rb
43
+ source_code_uri: https://github.com/0rvar/svg2img-rb
44
+ changelog_uri: https://github.com/0rvar/svg2img-rb/blob/master/CHANGELOG.md
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 3.0.0
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.3.11
59
+ requirements: []
60
+ rubygems_version: 3.5.9
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Convert svg to png, jpg, or webp
64
+ test_files: []