ruby-libgd 0.2.2 → 0.2.3

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: 86d2d815f83b82844f6e052eedcd00481e53bf8d270dc8db48a5c4bc18bcecbb
4
- data.tar.gz: fd3bea0c6cbeb6d6bc5d0f31716b0c32d57b62f3cad20f7955cbe695360bb87c
3
+ metadata.gz: 5f72abe4a18b66efb49a1e30a5ef170e69a31f4a3eb0ccf308e9c9f85603c229
4
+ data.tar.gz: c17c93e69f330862bc24bd655fbc7abb8017be59d0bd57723d5aeb7c9301e70f
5
5
  SHA512:
6
- metadata.gz: 1389a01c9e0de55a8a2aad3cda2781016c292fe6ddc44aebcb768f89e73c103f6a28466a253e92466f84990ea22e28cd76432464aa19f8a4894ad5d3d59a15bd
7
- data.tar.gz: 68453f430c861714bf37ce2c2c7016cafb22543f19d5c22c964d303c6c4bf163ee4d50ac3370ee37551c1e206aec6dff03897036052c2cde94cb2b7a9cd7aad5
6
+ metadata.gz: 7667691652b3fbb70b7945071ac3f0719d8162adb231b80c3b1e7eb17514db6714e1562791f6bdd20676bc77b3d96fc85b1dec0b5bfb7c07678efa86ec82fc48
7
+ data.tar.gz: f02dc5751f96bd1bd25ac4b693f4e18b968cc2fcaceaa687efd269572cca60a8ef506de4b3a79870a98fb91a8c164fd8bea78eb7df6bca1a1b1ab0b255647531
data/README.md CHANGED
@@ -0,0 +1,301 @@
1
+ # Ruby Libgd
2
+
3
+ <p align="center">
4
+ <a href="https://rubystacknews.com">
5
+ <img src="https://img.shields.io/badge/RubyStackNews-CC342D?style=for-the-badge&logo=ruby&logoColor=white" />
6
+ </a>
7
+ <a href="https://x.com/ruby_stack_news">
8
+ <img src="https://img.shields.io/badge/Twitter%20%40RubyStackNews-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white" />
9
+ </a>
10
+ <a href="https://www.linkedin.com/in/germ%C3%A1n-silva-56a12622/">
11
+ <img src="https://img.shields.io/badge/Germán%20Silva-0A66C2?style=for-the-badge&logo=linkedin&logoColor=white" />
12
+ </a>
13
+ </p>
14
+
15
+ <p align="right">
16
+ <img src="docs/images/logo.png" width="160" />
17
+ </p>
18
+
19
+ ## Library Status
20
+
21
+ ![CI](https://github.com/ggerman/libgd-gis/actions/workflows/ci.yml/badge.svg)
22
+
23
+ | Implemented | Implemented | Coming Next |
24
+ | :--: | :--: | :--: |
25
+ | <img src="./examples/charts/images/fruit_chart_with_text.png" width="250"> | <img src="./examples/charts/images/pie_chart.png" width="250"> | <img src="./examples/maps/parana_paracao_route.png" width="250"> |
26
+ | <img src="./examples/image_processing/images/processed.jpg" width="250"> | <img src="./examples/charts/images/stem_plot.png" width="250"> | <img src="./examples/maps/parana_paracao_polygon.png" width="250"> |
27
+ | <img src="./examples/basics/images/vintage.jpg" width="250"> | <img src="./examples/basics/images/antialias_demo.png" width="250"> | |
28
+
29
+ **Note:** The images shown in Table 1, along with the code used to generate them, are available in the `examples/` folder.
30
+
31
+ # Native GD bindings for modern Ruby
32
+
33
+ ## What is ruby-libgd?
34
+
35
+ ruby-libgd is a modern native Ruby binding to the GD Graphics Library, providing Ruby with a fast, embeddable, server-side graphics engine.
36
+
37
+ It enables Ruby to generate images, charts, dashboards, GIS tiles, and scientific graphics without spawning external processes.
38
+
39
+ ## 🐧Installation (Debian / Ubuntu /Dockerfile)
40
+
41
+ `gem install ruby-libgd`
42
+
43
+ System dependencies:
44
+
45
+ `apt install -y libgd-dev pkg-config`
46
+
47
+ ---
48
+
49
+ ## 🍎 macOS (Intel & Apple Silicon)
50
+
51
+ ruby-libgd is a native extension and depends on libgd, which must be installed via Homebrew.
52
+
53
+ Install libgd
54
+
55
+ ```bash
56
+ brew install gd
57
+ ```
58
+ Verify Homebrew’s install path:
59
+
60
+ ```bash
61
+ brew --prefix gd
62
+ ```
63
+
64
+ Typical values:
65
+ Apple Silicon (**M1 / M2 / M3**): /opt/homebrew
66
+
67
+ Intel Macs: /usr/local
68
+
69
+ ### Install ruby-libgd
70
+
71
+ Apple Silicon (**M1 / M2 / M3**)
72
+
73
+ Homebrew installs libraries into **/opt/homebrew**, which Ruby’s native extension builder does not detect automatically.
74
+
75
+ ## Install using:
76
+
77
+ ```bash
78
+ gem install ruby-libgd -- --with-gd-dir=/opt/homebrew
79
+ ```
80
+
81
+ ## Intel macOS
82
+
83
+ ```bash
84
+ gem install ruby-libgd
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 📚 Documentation
90
+
91
+ - 🇬🇧 **English**: https://ggerman.github.io/ruby-libgd/en/
92
+ - 🇯🇵 **日本語**: https://ggerman.github.io/ruby-libgd/jp/
93
+
94
+ ---
95
+
96
+ ## Examples:
97
+
98
+ All runnable examples live in:
99
+
100
+ examples/
101
+ basics/
102
+ image_processing/
103
+ charts/
104
+ images/
105
+
106
+ Each Ruby script in examples/ generates an image in examples/images/.
107
+
108
+
109
+ ## Links
110
+
111
+ GitHub: https://github.com/ggerman/ruby-libgd
112
+ RubyGems: https://rubygems.org/gems/ruby-libgd
113
+ Article: https://rubystacknews.com/2026/01/05/ruby-can-create-images-again/
114
+
115
+ ---
116
+
117
+ ## Build & Runtime Environment
118
+
119
+ **ruby-libgd** is developed and tested against **libgd 2.3.3** using a reproducible Docker environment.
120
+
121
+ This ensures that the native extension is compiled against a known, stable GD ABI and behaves consistently across systems.
122
+
123
+ Reference **Dockerfile**
124
+ ```dockerfile
125
+ FROM ruby:3.3
126
+
127
+ RUN apt update && apt -y upgrade
128
+ RUN apt install -y \
129
+ libgd-dev \
130
+ libgd3 \
131
+ libgd-tools \
132
+ pkg-config \
133
+ ruby-dev \
134
+ build-essential \
135
+ valgrind
136
+
137
+ RUN printf "prefix=/usr\n\
138
+ exec_prefix=\${prefix}\n\
139
+ libdir=\${exec_prefix}/lib/x86_64-linux-gnu\n\
140
+ includedir=\${prefix}/include\n\
141
+ \n\
142
+ Name: gd\n\
143
+ Description: GD Graphics Library\n\
144
+ Version: 2.3\n\
145
+ Libs: -L\${libdir} -lgd\n\
146
+ Cflags: -I\${includedir}\n" \
147
+ > /usr/lib/x86_64-linux-gnu/pkgconfig/gd.pc
148
+
149
+ ENV PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig
150
+
151
+ WORKDIR /app
152
+
153
+ COPY Gemfile Gemfile.lock ./
154
+ RUN bundle install
155
+
156
+ # Enforce dependency lockfile reproducibility
157
+ RUN bundle config --global frozen 1
158
+
159
+ COPY . .
160
+
161
+ ```
162
+
163
+ ## Why this matters
164
+
165
+ **ruby-libgd** is a native **C** extension. The exact **libgd** version and build flags directly affect:
166
+
167
+ - Alpha blending
168
+
169
+ - Color accuracy
170
+
171
+ - Filter behavior
172
+
173
+ - Memory safety
174
+
175
+
176
+ Using a pinned, containerized build environment guarantees that:
177
+
178
+ - The extension is compiled against libgd 2.3.x
179
+
180
+ - pkg-config resolves the correct headers and linker flags
181
+
182
+ - CI, contributors, and users see identical behavior
183
+
184
+
185
+ This is especially important for GIS, map tiles, and image pipelines where pixel-level consistency matters.
186
+
187
+ ## Example:
188
+
189
+ ![RubyStackNews](examples/images/rubystacknews-banner.png)
190
+
191
+ ```Ruby
192
+ require "gd"
193
+
194
+ WIDTH = 800
195
+ HEIGHT = 400
196
+
197
+ img = GD::Image.new(WIDTH, HEIGHT)
198
+
199
+ # ----------------------------
200
+ # Pastel palette
201
+ # ----------------------------
202
+ bg_top = [244, 240, 255] # lavender
203
+ bg_bottom = [230, 248, 255] # light sky
204
+
205
+ accent1 = [255, 140, 180] # pastel pink
206
+ accent2 = [120, 200, 255] # pastel blue
207
+ text_main = [60, 70, 120] # deep indigo
208
+ shadow = [180, 190, 220]
209
+
210
+ # ----------------------------
211
+ # Soft gradient background
212
+ # ----------------------------
213
+ (0...HEIGHT).each do |y|
214
+ t = y.to_f / (HEIGHT - 1)
215
+
216
+ r = (bg_top[0] + (bg_bottom[0] - bg_top[0]) * t).to_i
217
+ g = (bg_top[1] + (bg_bottom[1] - bg_top[1]) * t).to_i
218
+ b = (bg_top[2] + (bg_bottom[2] - bg_top[2]) * t).to_i
219
+
220
+ img.line(0, y, WIDTH - 1, y, [r, g, b])
221
+ end
222
+
223
+ # ----------------------------
224
+ # Floating pastel blobs
225
+ # ----------------------------
226
+ img.filled_circle(120, 100, 80, accent2)
227
+ img.filled_circle(680, 280, 100, accent1)
228
+ img.filled_circle(700, 80, 50, [200, 230, 255])
229
+
230
+ # ----------------------------
231
+ # Text
232
+ # ----------------------------
233
+ font = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
234
+ title_size = 52
235
+ subtitle_size = 20
236
+
237
+ title = "RubyStackNews"
238
+ subtitle = "The pulse of the Ruby ecosystem"
239
+
240
+ # Manual centering
241
+ title_x = 80
242
+ title_y = 190
243
+
244
+ # Soft shadow
245
+ img.text(title, {
246
+ x: title_x + 2,
247
+ y: title_y + 2,
248
+ size: title_size,
249
+ color: shadow,
250
+ font: font
251
+ })
252
+
253
+ # Main title
254
+ img.text(title, {
255
+ x: title_x,
256
+ y: title_y,
257
+ size: title_size,
258
+ color: text_main,
259
+ font: font
260
+ })
261
+
262
+ # Subtitle
263
+ img.text(subtitle, {
264
+ x: title_x,
265
+ y: title_y + 45,
266
+ size: subtitle_size,
267
+ color: [100,110,160],
268
+ font: font
269
+ })
270
+
271
+ # ----------------------------
272
+ # Call-to-action pill
273
+ # ----------------------------
274
+ pill_x = 80
275
+ pill_y = 260
276
+ pill_w = 260
277
+ pill_h = 44
278
+
279
+ img.filled_rectangle(pill_x, pill_y, pill_x + pill_w, pill_y + pill_h, [255,255,255])
280
+ img.rectangle(pill_x, pill_y, pill_x + pill_w, pill_y + pill_h, accent2, thickness: 2)
281
+
282
+ img.text("rubystacknews.com", {
283
+ x: pill_x + 22,
284
+ y: pill_y + 30,
285
+ size: 20,
286
+ color: text_main,
287
+ font: font
288
+ })
289
+
290
+ # ----------------------------
291
+ # Save
292
+ # ----------------------------
293
+ img.save("images/rubystacknews-banner.png")
294
+ puts "Saved images/rubystacknews-banner.png"
295
+
296
+ ```
297
+
298
+ ## License
299
+
300
+ MIT License.
301
+ Copyright (c) 2025 Germán Alberto Giménez Silva.
data/ext/gd/Makefile CHANGED
@@ -140,9 +140,9 @@ extout_prefix =
140
140
  target_prefix = /gd
141
141
  LOCAL_LIBS =
142
142
  LIBS = $(LIBRUBYARG_SHARED) -lm -lgd -lm -lpthread -lc
143
- ORIG_SRCS = arc.c blit.c char.c circle.c clip.c color.c draw_line.c ellipse.c encode.c fill.c filter.c gd.c image.c pixel.c polygon.c rectangle.c text.c transform.c version.c
143
+ ORIG_SRCS = arc.c blit.c char.c circle.c clip.c color.c draw_line.c ellipse.c encode.c fill.c filter.c gd.c gif.c image.c pixel.c polygon.c rectangle.c text.c transform.c version.c
144
144
  SRCS = $(ORIG_SRCS)
145
- OBJS = arc.o blit.o char.o circle.o clip.o color.o draw_line.o ellipse.o encode.o fill.o filter.o gd.o image.o pixel.o polygon.o rectangle.o text.o transform.o version.o
145
+ OBJS = arc.o blit.o char.o circle.o clip.o color.o draw_line.o ellipse.o encode.o fill.o filter.o gd.o gif.o image.o pixel.o polygon.o rectangle.o text.o transform.o version.o
146
146
  HDRS = $(srcdir)/clip.h $(srcdir)/ruby_gd.h
147
147
  LOCAL_HDRS =
148
148
  TARGET = gd
data/ext/gd/gd.c CHANGED
@@ -83,6 +83,8 @@ void Init_gd(void) {
83
83
  gd_define_transform(cGDImage);
84
84
  gd_define_encode(cGDImage);
85
85
 
86
+ gd_define_gif(mGD);
87
+
86
88
  gd_define_color(mGD);
87
89
  gd_define_version(mGD);
88
90
  }
data/ext/gd/gd.o CHANGED
Binary file
data/ext/gd/gd.so CHANGED
Binary file
data/ext/gd/gif.c ADDED
@@ -0,0 +1,151 @@
1
+ #include "ruby_gd.h"
2
+
3
+ typedef struct {
4
+ FILE *out;
5
+ gdImagePtr prev;
6
+ VALUE prev_rb;
7
+ int started;
8
+ int loop;
9
+ } gd_gif_wrapper;
10
+
11
+ static void gd_gif_free(void *p) {
12
+ gd_gif_wrapper *gif = (gd_gif_wrapper *)p;
13
+
14
+ if (gif->out) {
15
+ if (gif->started) {
16
+ gdImageGifAnimEnd(gif->out);
17
+ }
18
+ fclose(gif->out);
19
+ }
20
+
21
+ xfree(gif);
22
+ }
23
+
24
+ static size_t gd_gif_memsize(const void *p) {
25
+ return sizeof(gd_gif_wrapper);
26
+ }
27
+
28
+ static const rb_data_type_t gd_gif_type = {
29
+ "GD::Gif",
30
+ {
31
+ 0,
32
+ gd_gif_free,
33
+ gd_gif_memsize,
34
+ },
35
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
36
+ };
37
+
38
+ static VALUE gd_gif_alloc(VALUE klass) {
39
+ gd_gif_wrapper *gif;
40
+ VALUE obj = TypedData_Make_Struct(klass, gd_gif_wrapper, &gd_gif_type, gif);
41
+
42
+ gif->out = NULL;
43
+ gif->prev = NULL;
44
+ gif->prev_rb = Qnil;
45
+ gif->started = 0;
46
+ gif->loop = 0;
47
+
48
+ return obj;
49
+ }
50
+
51
+ /*
52
+ * GD::Gif.new("file.gif", loop: true)
53
+ */
54
+ static VALUE gd_gif_initialize(int argc, VALUE *argv, VALUE self) {
55
+ VALUE path, opts;
56
+ rb_scan_args(argc, argv, "11", &path, &opts);
57
+
58
+ gd_gif_wrapper *gif;
59
+ TypedData_Get_Struct(self, gd_gif_wrapper, &gd_gif_type, gif);
60
+
61
+ const char *filename = StringValueCStr(path);
62
+ gif->out = fopen(filename, "wb");
63
+ if (!gif->out) rb_sys_fail(filename);
64
+
65
+ gif->started = 0;
66
+ gif->prev = NULL;
67
+ gif->prev_rb = Qnil;
68
+
69
+ gif->loop = 0;
70
+ if (opts != Qnil) {
71
+ VALUE loopv = rb_hash_aref(opts, ID2SYM(rb_intern("loop")));
72
+ if (loopv == Qfalse) {
73
+ gif->loop = -1;
74
+ }
75
+ }
76
+
77
+ return self;
78
+ }
79
+
80
+ /*
81
+ * gif.add_frame(img, delay: 50)
82
+ */
83
+ static VALUE gd_gif_add_frame(int argc, VALUE *argv, VALUE self) {
84
+ VALUE rb_img, opts;
85
+ rb_scan_args(argc, argv, "11", &rb_img, &opts);
86
+
87
+ gd_image_wrapper *wrap;
88
+ TypedData_Get_Struct(rb_img, gd_image_wrapper, &gd_image_type, wrap);
89
+
90
+ gd_gif_wrapper *gif;
91
+ TypedData_Get_Struct(self, gd_gif_wrapper, &gd_gif_type, gif);
92
+
93
+ int delay = 50;
94
+ if (opts != Qnil) {
95
+ VALUE d = rb_hash_aref(opts, ID2SYM(rb_intern("delay")));
96
+ if (!NIL_P(d)) delay = NUM2INT(d);
97
+ }
98
+
99
+ if (!gif->started) {
100
+ gdImageGifAnimBegin(wrap->img, gif->out, 1, gif->loop);
101
+ gif->started = 1;
102
+ }
103
+
104
+ gdImageGifAnimAdd(
105
+ wrap->img,
106
+ gif->out,
107
+ 1,
108
+ 0, 0,
109
+ delay,
110
+ 1,
111
+ gif->prev
112
+ );
113
+
114
+ gif->prev = wrap->img;
115
+ gif->prev_rb = rb_img; /* prevent GC */
116
+
117
+ return Qtrue;
118
+ }
119
+
120
+ /*
121
+ * gif.close
122
+ */
123
+ static VALUE gd_gif_close(VALUE self) {
124
+ gd_gif_wrapper *gif;
125
+ TypedData_Get_Struct(self, gd_gif_wrapper, &gd_gif_type, gif);
126
+
127
+ if (gif->out) {
128
+ if (gif->started) {
129
+ gdImageGifAnimEnd(gif->out);
130
+ }
131
+ fclose(gif->out);
132
+ gif->out = NULL;
133
+ }
134
+
135
+ gif->prev = NULL;
136
+ gif->prev_rb = Qnil;
137
+
138
+ return Qtrue;
139
+ }
140
+
141
+ /*
142
+ * Init hook
143
+ */
144
+ void gd_define_gif(VALUE mGD) {
145
+ VALUE cGif = rb_define_class_under(mGD, "Gif", rb_cObject);
146
+
147
+ rb_define_alloc_func(cGif, gd_gif_alloc);
148
+ rb_define_method(cGif, "initialize", gd_gif_initialize, -1);
149
+ rb_define_method(cGif, "add_frame", gd_gif_add_frame, -1);
150
+ rb_define_method(cGif, "close", gd_gif_close, 0);
151
+ }
data/ext/gd/gif.o ADDED
Binary file
data/ext/gd/image.c CHANGED
@@ -36,13 +36,6 @@ static VALUE gd_image_initialize(int argc, VALUE *argv, VALUE self) {
36
36
  return self;
37
37
  }
38
38
 
39
- static VALUE gd_image_initialize_empty(VALUE self) {
40
- gd_image_wrapper *wrap;
41
- TypedData_Get_Struct(self, gd_image_wrapper, &gd_image_type, wrap);
42
- wrap->img = NULL;
43
- return self;
44
- }
45
-
46
39
  static VALUE gd_image_initialize_true_color(VALUE self, VALUE width, VALUE height) {
47
40
  gd_image_wrapper *wrap;
48
41
  TypedData_Get_Struct(self, gd_image_wrapper, &gd_image_type, wrap);
@@ -123,21 +116,6 @@ static VALUE gd_image_alloc(VALUE klass) {
123
116
  return obj;
124
117
  }
125
118
 
126
- static VALUE gd_image_new(VALUE klass, VALUE w, VALUE h) {
127
- VALUE obj = rb_class_new_instance(0, NULL, klass);
128
- gd_image_wrapper *wrap;
129
- TypedData_Get_Struct(obj, gd_image_wrapper, &gd_image_type, wrap);
130
-
131
- int width = NUM2INT(w);
132
- int height = NUM2INT(h);
133
- if (width <= 0 || height <= 0)
134
- rb_raise(rb_eArgError, "width and height must be positive");
135
-
136
- wrap->img = gdImageCreateTrueColor(width, height);
137
- if (!wrap->img) rb_raise(rb_eRuntimeError, "gdImageCreateTrueColor failed");
138
- return obj;
139
- }
140
-
141
119
  static VALUE gd_image_open(VALUE klass, VALUE path) {
142
120
  const char *filename = StringValueCStr(path);
143
121
 
data/ext/gd/image.o CHANGED
Binary file
data/ext/gd/ruby_gd.h CHANGED
@@ -32,6 +32,8 @@ void gd_define_transform(VALUE cGDImage);
32
32
  void gd_define_filter(VALUE cGDImage);
33
33
  void gd_define_encode(VALUE cGDImage);
34
34
 
35
+ void gd_define_gif(VALUE mGD);
36
+
35
37
  /* Color */
36
38
  void gd_define_color(VALUE mGD);
37
39
  void gd_define_version(VALUE mGD);
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-libgd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Germán Alberto Giménez Silva
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-14 00:00:00.000000000 Z
11
+ date: 2026-01-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: High-performance native Ruby bindings to libgd for image generation,
14
14
  drawing, filters, alpha blending, and transformations.
@@ -48,6 +48,8 @@ files:
48
48
  - ext/gd/gd.c
49
49
  - ext/gd/gd.o
50
50
  - ext/gd/gd.so
51
+ - ext/gd/gif.c
52
+ - ext/gd/gif.o
51
53
  - ext/gd/image.c
52
54
  - ext/gd/image.o
53
55
  - ext/gd/mkmf.log