loomy 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e58a5dd9cc41368d235d075797fed3c422b7abb9db8c38372f382c53ebe0a4a5
4
+ data.tar.gz: 8ea209c82cf8442a48408c9a607bdf5f967fe70262b74660bb427a3d8b28e193
5
+ SHA512:
6
+ metadata.gz: c32b8633ff0d8160e342470f6214b6813cb01618b699be83c6b4acf10039a7e57785f70805973b2751f46776cfd0ec2ef67124f670a49ddd6e54b9721c31ce33
7
+ data.tar.gz: bd6720746b685ca99ed7eefa3d6fab6f6815f7de54d5a3b2d53fed406abfc76b24698c8cae736e56eb4e660b48257712f99897f1d0cfcee98e4047bdd74976dc
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ test/tmp/*
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in loomy.gemspec
4
+ gemspec
5
+
6
+ gem "benchmark-ips"
data/Gemfile.lock ADDED
@@ -0,0 +1,74 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ loomy (0.1.0)
5
+ ruby-vips (~> 2.3)
6
+ zeitwerk (~> 2.7)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ benchmark-ips (2.14.0)
12
+ ffi (1.17.3)
13
+ ffi (1.17.3-aarch64-linux-gnu)
14
+ ffi (1.17.3-aarch64-linux-musl)
15
+ ffi (1.17.3-arm-linux-gnu)
16
+ ffi (1.17.3-arm-linux-musl)
17
+ ffi (1.17.3-arm64-darwin)
18
+ ffi (1.17.3-x86-linux-gnu)
19
+ ffi (1.17.3-x86-linux-musl)
20
+ ffi (1.17.3-x86_64-darwin)
21
+ ffi (1.17.3-x86_64-linux-gnu)
22
+ ffi (1.17.3-x86_64-linux-musl)
23
+ logger (1.7.0)
24
+ minitest (6.0.1)
25
+ prism (~> 1.5)
26
+ prism (1.9.0)
27
+ rake (13.3.1)
28
+ ruby-vips (2.3.0)
29
+ ffi (~> 1.12)
30
+ logger
31
+ zeitwerk (2.7.4)
32
+
33
+ PLATFORMS
34
+ aarch64-linux-gnu
35
+ aarch64-linux-musl
36
+ arm-linux-gnu
37
+ arm-linux-musl
38
+ arm64-darwin
39
+ ruby
40
+ x86-linux-gnu
41
+ x86-linux-musl
42
+ x86_64-darwin
43
+ x86_64-linux-gnu
44
+ x86_64-linux-musl
45
+
46
+ DEPENDENCIES
47
+ benchmark-ips
48
+ loomy!
49
+ minitest (~> 6.0)
50
+ rake (~> 13.0)
51
+
52
+ CHECKSUMS
53
+ benchmark-ips (2.14.0) sha256=b72bc8a65d525d5906f8cd94270dccf73452ee3257a32b89fbd6684d3e8a9b1d
54
+ ffi (1.17.3) sha256=0e9f39f7bb3934f77ad6feab49662be77e87eedcdeb2a3f5c0234c2938563d4c
55
+ ffi (1.17.3-aarch64-linux-gnu) sha256=28ad573df26560f0aedd8a90c3371279a0b2bd0b4e834b16a2baa10bd7a97068
56
+ ffi (1.17.3-aarch64-linux-musl) sha256=020b33b76775b1abacc3b7d86b287cef3251f66d747092deec592c7f5df764b2
57
+ ffi (1.17.3-arm-linux-gnu) sha256=5bd4cea83b68b5ec0037f99c57d5ce2dd5aa438f35decc5ef68a7d085c785668
58
+ ffi (1.17.3-arm-linux-musl) sha256=0d7626bb96265f9af78afa33e267d71cfef9d9a8eb8f5525344f8da6c7d76053
59
+ ffi (1.17.3-arm64-darwin) sha256=0c690555d4cee17a7f07c04d59df39b2fba74ec440b19da1f685c6579bb0717f
60
+ ffi (1.17.3-x86-linux-gnu) sha256=868a88fcaf5186c3a46b7c7c2b2c34550e1e61a405670ab23f5b6c9971529089
61
+ ffi (1.17.3-x86-linux-musl) sha256=f0286aa6ef40605cf586e61406c446de34397b85dbb08cc99fdaddaef8343945
62
+ ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5
63
+ ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
64
+ ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
65
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
66
+ loomy (0.1.0)
67
+ minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
68
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
69
+ rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
70
+ ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
71
+ zeitwerk (2.7.4) sha256=2bef90f356bdafe9a6c2bd32bcd804f83a4f9b8bc27f3600fff051eb3edcec8b
72
+
73
+ BUNDLED WITH
74
+ 4.0.3
data/README.md ADDED
@@ -0,0 +1,160 @@
1
+ <p align="center">
2
+ <img src="assets/logo.png" width="240" alt="Loomy Logo">
3
+ </p>
4
+
5
+ <h1 align="center">Loomy 🧶</h1>
6
+
7
+ <p align="center">
8
+ <strong>The friendly pixel-weaver for Ruby.</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://rubygems.org/gems/loomy"><img src="https://img.shields.io/gem/v/loomy.svg" alt="Gem Version"></a>
13
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
14
+ <a href="https://github.com/JohnAnon9771/loomy/actions"><img src="https://img.shields.io/badge/tests-passing-success.svg" alt="Tests Status"></a>
15
+ </p>
16
+
17
+ ---
18
+
19
+ **Loomy** is a modern, high-performance image processing engine for Ruby. Think of it as a master weaver for your images: it takes raw layers (the threads) and weaves them into complex compositions using a smart, declarative DSL.
20
+
21
+ Built on top of `libvips`, Loomy doesn't just process images; it optimizes the entire "weaving" process (AST) to ensure your pipelines are as light and fast as silk.
22
+
23
+ ## 🚀 Key Features
24
+
25
+ - **High Performance**: Built on `libvips` with a custom Batch Composition engine that flattens operations into a single efficient pipeline.
26
+ - **Hierarchical Groups**: Nest layers and groups to create complex layouts with shared effects.
27
+ - **Intelligent DSL**: Declarative, block-based syntax. Define properties naturally without complex argument lists.
28
+ - **AST Optimizer**: A smart "pre-weaving" layer that prunes invisible layers and pre-calculates geometry before rendering.
29
+ - **Extensible Effects**: Dynamic registry to add your own image processing strategies.
30
+
31
+ ## 📦 Installation
32
+
33
+ Add this line to your application's Gemfile:
34
+
35
+ ```ruby
36
+ gem 'loomy'
37
+ ```
38
+
39
+ And then execute:
40
+
41
+ ```bash
42
+ bundle install
43
+ ```
44
+
45
+ ## 🛠 Usage
46
+
47
+ ### 1. Generating Images
48
+
49
+ Loomy offers two ways to get your results: `render` (write to file) and `generate` (get a `Vips::Image` object).
50
+
51
+ #### Render to file
52
+
53
+ ```ruby
54
+ Loomy.render("output.png", size: [1200, 630]) do
55
+ layer "background.jpg"
56
+ end
57
+ ```
58
+
59
+ #### Generate in-memory (Web Servers / Testing)
60
+
61
+ ```ruby
62
+ # Returns a Vips::Image object
63
+ image = Loomy.generate(size: [1200, 630]) do
64
+ layer "background.jpg"
65
+ end
66
+
67
+ # Get binary buffer for HTTP response or S3
68
+ buffer = image.write_to_buffer(".png")
69
+ ```
70
+
71
+ ### 2. Hierarchical Groups
72
+
73
+ Group layers to apply effects or positioning to a set of nodes collectively.
74
+
75
+ ```ruby
76
+ Loomy.render("banner.png", size: [800, 400]) do
77
+ group x: 50, y: 50 do
78
+ layer "icon.png", width: 50
79
+ layer "text.png", x: 60
80
+
81
+ # Apply blur to the entire group
82
+ blur radius: 2
83
+ end
84
+ end
85
+ ```
86
+
87
+ ### 3. The Smart DSL
88
+
89
+ Forget about complex argument lists. Describe your image layout naturally:
90
+
91
+ ```ruby
92
+ require 'loomy'
93
+
94
+ Loomy.render("output.png", size: [1200, 630]) do
95
+ # Background
96
+ layer "background.jpg" do
97
+ fit :cover
98
+ blur radius: 10
99
+ end
100
+
101
+ # Overlay
102
+ layer "avatar.png" do
103
+ x :center
104
+ y :center
105
+ width "20%"
106
+ trim true # Auto-crop transparent borders
107
+ end
108
+ end
109
+ ```
110
+
111
+ ### 4. Reusable Styles
112
+
113
+ Define common looks and apply them anywhere.
114
+
115
+ ```ruby
116
+ # Define a style
117
+ Loomy.define_style :hero_layer do
118
+ x 50
119
+ y 100
120
+ blend :overlay
121
+ end
122
+
123
+ Loomy.render("post.png", size: [1000, 1000]) do
124
+ layer "texture.png" do
125
+ use :hero_layer # Apply the style
126
+ width 500 # Override or extend
127
+ end
128
+ end
129
+ ```
130
+
131
+ ### 5. Custom Effects & Registry
132
+
133
+ Loomy is extensible. You can register your own `libvips` processors:
134
+
135
+ ```ruby
136
+ # Register a custom effect
137
+ Loomy.register_effect(MyCustomEffectClass, ->(img, effect_node) {
138
+ img.my_vips_operation(effect_node.param)
139
+ })
140
+ ```
141
+
142
+ ## ⚡ Performance
143
+
144
+ Loomy is designed for scale. Leveraging `libvips`' streaming architecture and our proprietary AST optimization, Loomy delivers exceptional throughput:
145
+
146
+ | Complexity | Throughput (M1) |
147
+ | :---------------------------- | :-------------- |
148
+ | Simple Composites | ~70 images/sec |
149
+ | Complex (5+ layers + effects) | ~60 images/sec |
150
+
151
+ ## 🗺 Roadmap
152
+
153
+ - [ ] Full support for relative geometry (`%`, `vh`, `vw`)
154
+ - [ ] Rounded corners and masking
155
+ - [ ] SVG support as layers
156
+ - [ ] Smart Saliency Masking (Background Removal)
157
+
158
+ ## 📄 License
159
+
160
+ 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,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
data/assets/logo.png ADDED
Binary file
data/bench.rb ADDED
@@ -0,0 +1,76 @@
1
+ require "benchmark/ips"
2
+ $LOAD_PATH.unshift File.expand_path("lib", __dir__)
3
+ require "loomy"
4
+
5
+ # Ensure assets are generated
6
+ unless File.exist?("test/assets/base.png")
7
+ Vips::Image.black(1024, 1024, bands: 3).linear([1], [128,128,128]).bandjoin(255).cast(:uchar).write_to_file("test/assets/base.png")
8
+ end
9
+
10
+ unless File.exist?("test/assets/overlay.png")
11
+ Vips::Image.black(500, 500, bands: 3).linear([1], [255,0,0]).bandjoin(255).cast(:uchar).write_to_file("test/assets/overlay.png")
12
+ end
13
+
14
+ # Large assets (4200x4800)
15
+ unless File.exist?("test/assets/base_large.png")
16
+ puts "Generating large base asset (4200x4800)..."
17
+ Vips::Image.black(4200, 4800, bands: 3).linear([1], [50,50,50]).bandjoin(255).cast(:uchar).write_to_file("test/assets/base_large.png")
18
+ end
19
+
20
+ unless File.exist?("test/assets/overlay_large.png")
21
+ puts "Generating large overlay asset (2000x2000)..."
22
+ Vips::Image.black(2000, 2000, bands: 3).linear([1], [0,255,0]).bandjoin(255).cast(:uchar).write_to_file("test/assets/overlay_large.png")
23
+ end
24
+
25
+ unless File.exist?("test/assets/trim_source_large.png")
26
+ puts "Generating large trim asset..."
27
+ # 2000x2000 image with 500x500 content in middle, rest transparent
28
+ content = Vips::Image.black(500, 500, bands: 3).linear([1], [0,0,255]).bandjoin(255)
29
+ padded = content.embed(750, 750, 2000, 2000, extend: :background, background: [0,0,0,0])
30
+ padded.cast(:uchar).write_to_file("test/assets/trim_source_large.png")
31
+ end
32
+
33
+ Benchmark.ips do |x|
34
+ x.config(time: 5, warmup: 2)
35
+
36
+ x.report("simple_composite") do
37
+ Loomy.render("bench_output_simple.png", size: [1024, 1024]) do
38
+ layer "test/assets/base.png"
39
+ layer "test/assets/overlay.png", x: 200, y: 200
40
+ end
41
+ end
42
+
43
+ x.report("complex_composite") do
44
+ Loomy.render("bench_output_complex.png", size: [1024, 1024]) do
45
+ layer "test/assets/base.png"
46
+ layer "test/assets/overlay.png", x: 50, y: 50, blend: :multiply
47
+ layer "test/assets/overlay.png", x: 400, y: 400, width: 200, height: 200, fit: :cover
48
+ layer "test/assets/overlay.png", x: 600, y: 100, blur: 5
49
+ layer "test/assets/overlay.png", x: 100, y: 600, grayscale: true
50
+ end
51
+ end
52
+
53
+ x.report("large_composite_4k") do
54
+ Loomy.render("bench_output_large.png", size: [4200, 4800]) do
55
+ layer "test/assets/base_large.png"
56
+ layer "test/assets/overlay_large.png", x: 500, y: 500
57
+ layer "test/assets/overlay.png", x: 100, y: 100 # Mixing sizes
58
+ end
59
+ end
60
+
61
+ x.report("trim_composite") do
62
+ Loomy.render("bench_output_trim.png", size: [4200, 4800]) do
63
+ layer "test/assets/base_large.png"
64
+ # This image is 2000x2000 but only has 500x500 content.
65
+ # With trim: true, it should act like 500x500 image.
66
+ layer "test/assets/trim_source_large.png", trim: true, x: 1000, y: 1000
67
+ end
68
+ end
69
+
70
+ x.compare!
71
+ end
72
+
73
+ # Cleanup outputs (keep inputs for re-runs)
74
+ ["bench_output_simple.png", "bench_output_complex.png", "bench_output_large.png", "bench_output_trim.png"].each do |f|
75
+ File.delete(f) if File.exist?(f)
76
+ end
@@ -0,0 +1,19 @@
1
+ require_relative 'node'
2
+
3
+ module Loomy
4
+ module AST
5
+ class Canvas < Node
6
+ def width
7
+ properties[:size][0]
8
+ end
9
+
10
+ def height
11
+ properties[:size][1]
12
+ end
13
+
14
+ def accept(visitor)
15
+ visitor.visit_canvas(self)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'node'
2
+
3
+ module Loomy
4
+ module AST
5
+ class Effect < Node
6
+ def map_source
7
+ properties[:map]
8
+ end
9
+
10
+ def accept(visitor)
11
+ visitor.visit_effect(self)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ require_relative '../effect'
2
+
3
+ module Loomy
4
+ module AST
5
+ module Effects
6
+ class Blur < Effect
7
+ def radius
8
+ properties[:radius] || 0
9
+ end
10
+
11
+ def accept(visitor)
12
+ visitor.visit_blur_effect(self)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ require_relative '../effect'
2
+
3
+ module Loomy
4
+ module AST
5
+ module Effects
6
+ class ColorAdjustment < Effect
7
+ def brightness
8
+ properties[:brightness] || 1.0
9
+ end
10
+
11
+ def contrast
12
+ properties[:contrast] || 1.0
13
+ end
14
+
15
+ def accept(visitor)
16
+ visitor.visit_color_adjustment_effect(self)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ require_relative '../effect'
2
+
3
+ module Loomy
4
+ module AST
5
+ module Effects
6
+ class Displacement < Effect
7
+ def scale
8
+ properties[:scale] || 20
9
+ end
10
+
11
+ def accept(visitor)
12
+ visitor.visit_displacement_effect(self)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ require_relative '../effect'
2
+
3
+ module Loomy
4
+ module AST
5
+ module Effects
6
+ class Grayscale < Effect
7
+ def accept(visitor)
8
+ visitor.visit_grayscale_effect(self)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ require_relative '../effect'
2
+
3
+ module Loomy
4
+ module AST
5
+ module Effects
6
+ class Lighting < Effect
7
+ def type
8
+ properties[:type] || :ambient
9
+ end
10
+
11
+ def strength
12
+ properties[:strength] || 1.0
13
+ end
14
+
15
+ def accept(visitor)
16
+ visitor.visit_lighting_effect(self)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'node'
2
+
3
+ module Loomy
4
+ module AST
5
+ class Group < Node
6
+ def x
7
+ properties[:x] || 0
8
+ end
9
+
10
+ def y
11
+ properties[:y] || 0
12
+ end
13
+
14
+ def width
15
+ properties[:width]
16
+ end
17
+
18
+ def height
19
+ properties[:height]
20
+ end
21
+
22
+ def blend_mode
23
+ properties[:blend] || :over
24
+ end
25
+
26
+ def effects
27
+ @effects ||= []
28
+ end
29
+
30
+ def add_effect(effect)
31
+ effects << effect
32
+ end
33
+
34
+ def accept(visitor)
35
+ visitor.visit_group(self)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'node'
2
+
3
+ module Loomy
4
+ module AST
5
+ class Layer < Node
6
+ def source
7
+ properties[:source]
8
+ end
9
+
10
+ def blend_mode
11
+ properties[:blend] || :over
12
+ end
13
+
14
+ def x
15
+ properties[:x] || 0
16
+ end
17
+
18
+ def y
19
+ properties[:y] || 0
20
+ end
21
+
22
+ def width
23
+ properties[:width]
24
+ end
25
+
26
+ def height
27
+ properties[:height]
28
+ end
29
+
30
+ def fit
31
+ properties[:fit]
32
+ end
33
+
34
+ def gravity
35
+ properties[:gravity] || :centre
36
+ end
37
+
38
+ def trim
39
+ properties[:trim]
40
+ end
41
+
42
+ def effects
43
+ @effects ||= []
44
+ end
45
+
46
+ def add_effect(effect)
47
+ effects << effect
48
+ end
49
+
50
+ def accept(visitor)
51
+ visitor.visit_layer(self)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,21 @@
1
+ module Loomy
2
+ module AST
3
+ class Node
4
+ attr_reader :children, :properties
5
+
6
+ def initialize(properties = {})
7
+ @properties = properties
8
+ @children = []
9
+ end
10
+
11
+ def add_child(node)
12
+ @children << node
13
+ node
14
+ end
15
+
16
+ def accept(visitor)
17
+ visitor.visit_node(self)
18
+ end
19
+ end
20
+ end
21
+ end