lilimg 0.2.1 → 0.2.2

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: a4bc3f91be83cff03e7680e73b94427087e6e44a88037f974b137aba75cc8a5b
4
- data.tar.gz: ed63c2c8ec68ef51bb53dcf40d31eb9f03ade06bbfeecc334bfebd50bee59560
3
+ metadata.gz: 7344a27d23f240ea551b8db61d3f8eba90e514e506c023420bd92beed2a814d9
4
+ data.tar.gz: 46f1fa4a072cd1a25fd309f9f414b3200c94fa5cc3d065fc7ab8a19ee575ec1a
5
5
  SHA512:
6
- metadata.gz: 8c60b48052865276f135a3ad039fbcc4d3f589be520a51851156650a9749fb985c899bceec75807fb5b81ca16a207be611b50a0cf6909dde21707b7343abd56d
7
- data.tar.gz: 239ad652b2639c7d1f7a61176674e7babf495299716ed925972090fa671a90cf0fcb66a672788978fae2f1d7aa3c33fbf0a1d712234ae523a78b7811a217c33f
6
+ metadata.gz: 965fa9322f61aeafb20ba16c25585603b1cd1c387a7b9aa244fbfe458d73491ad15198a9ef8252964ca9eb6e7ffe2fc13a3540b47bf38b946f3001cc69387ffc
7
+ data.tar.gz: 89c780e97b318a3e005ef6fbb86f1e0e5d6ddb12e4dafcbdca67cf02e4580ec2445380264e5456bcb5d854e9b185448bb6328b04927dd97ec9cfa8af590ca937
data/example/gray.jpg ADDED
Binary file
data/ext/lilimg/lilimg.c CHANGED
@@ -7,122 +7,120 @@
7
7
  #include "lodepng.h"
8
8
  #include "jo_gif.h"
9
9
 
10
- unsigned char * rgb_to_rgba(unsigned char * buf, int w, int h) {
11
- int newsize = 4 * w * h;
12
- unsigned char * dest = (unsigned char *)malloc(newsize);
13
- int s = 0, d = 0;
14
- while (d < newsize) {
15
- dest[d++] = buf[s++];
16
- dest[d++] = buf[s++];
17
- dest[d++] = buf[s++];
10
+ // channels: 3 for RGB, 1 for grayscale
11
+ unsigned char * to_rgba(unsigned char * buf, int w, int h, int channels) {
12
+ size_t pixels = (size_t)w * h;
13
+ unsigned char * dest = (unsigned char *)malloc(pixels * 4);
14
+ if (!dest) return NULL;
15
+
16
+ size_t s = 0, d = 0;
17
+ for (size_t i = 0; i < pixels; i++) {
18
+ if (channels == 1) {
19
+ dest[d++] = buf[s];
20
+ dest[d++] = buf[s];
21
+ dest[d++] = buf[s++];
22
+ } else {
23
+ dest[d++] = buf[s++];
24
+ dest[d++] = buf[s++];
25
+ dest[d++] = buf[s++];
26
+ }
18
27
  dest[d++] = 255; // fully opaque
19
28
  }
20
29
 
21
30
  return dest;
22
31
  }
23
32
 
33
+ // Encodes rgba pixels as a GIF. Returns a malloc'ed buffer (caller frees) or NULL.
34
+ // jo_gif_end() closes the memstream, so out/bufsize are valid afterwards.
35
+ static char * rgba_to_gif(unsigned char * rgba, int w, int h, int max_colors, size_t * bufsize) {
36
+ char * out = NULL;
37
+ FILE * fp = open_memstream(&out, bufsize);
38
+ if (!fp) return NULL;
39
+
40
+ jo_gif_t gif = jo_gif_start(fp, w, h, 0, max_colors);
41
+ jo_gif_frame(&gif, rgba, 0, 0);
42
+ jo_gif_end(&gif);
43
+ return out;
44
+ }
45
+
46
+ // NOTE: buf points into the Ruby string, so it must never be freed here.
24
47
  static VALUE jpeg_to_gif(VALUE self, VALUE buf, VALUE max_colors) {
25
48
  rb_check_type(buf, T_STRING);
26
49
 
27
- int maxColors = INT2NUM(max_colors);
28
- int size = RSTRING_LEN(buf);
29
- unsigned char * jpeg = (unsigned char *)StringValuePtr(buf);
50
+ int maxColors = NUM2INT(max_colors);
51
+ int size = (int)RSTRING_LEN(buf);
52
+ unsigned char * jpeg = (unsigned char *)RSTRING_PTR(buf);
30
53
 
31
- if (jpeg[0] != 0xFF || jpeg[1] != 0xD8) {
32
- rb_raise(rb_eRuntimeError, "Not a JPEG image (%x %x should be FF D8)!", jpeg[0], jpeg[1]);
33
- return Qnil;
54
+ if (size < 2 || jpeg[0] != 0xFF || jpeg[1] != 0xD8) {
55
+ rb_raise(rb_eRuntimeError, "Not a JPEG image (should start with FF D8)!");
34
56
  }
35
57
 
36
58
  njInit();
37
59
 
38
60
  if (njDecode(jpeg, size)) {
39
- free(jpeg);
61
+ njDone();
40
62
  rb_raise(rb_eRuntimeError, "Error decoding input file (size %d)", size);
41
- return Qnil;
42
63
  }
43
64
 
44
65
  unsigned char * pixels = njGetImage();
45
- if (!pixels) {
46
- free(jpeg);
66
+ int w = njGetWidth(), h = njGetHeight();
67
+ if (!pixels || w <= 0 || h <= 0 || w > 32767 || h > 32767) {
68
+ njDone();
47
69
  rb_raise(rb_eRuntimeError, "Couldn't decode image.");
48
- return Qnil;
49
70
  }
50
71
 
51
- char * out;
52
- size_t bufsize;
53
- FILE * fp = open_memstream(&out, &bufsize);
54
- // FILE * fp = fopen("output.gif", "wb");
55
-
56
- if (!fp) {
57
- free(jpeg);
58
- rb_raise(rb_eRuntimeError, "Cannot initialize memstream");
59
- return Qnil;
60
- }
72
+ unsigned char * rgba = to_rgba(pixels, w, h, njIsColor() ? 3 : 1);
73
+ njDone(); // frees pixels
74
+ if (!rgba) rb_raise(rb_eNoMemError, "Cannot allocate pixel buffer");
61
75
 
62
- unsigned char * rgba = rgb_to_rgba(pixels, njGetWidth(), njGetHeight());
63
- jo_gif_t gif = jo_gif_start(fp, njGetWidth(), njGetHeight(), 0, maxColors);
64
- jo_gif_frame(&gif, rgba, 0, 0);
65
- jo_gif_end(&gif);
76
+ size_t bufsize = 0;
77
+ char * out = rgba_to_gif(rgba, w, h, maxColors, &bufsize);
78
+ free(rgba);
79
+ if (!out) rb_raise(rb_eRuntimeError, "Cannot initialize memstream");
66
80
 
67
81
  VALUE rstr = rb_str_new(out, bufsize);
68
-
69
- // free(jpeg); <-- this will cause all sorts of errors (FIXME)
70
- free(rgba);
71
82
  free(out);
72
- njDone();
73
83
 
74
84
  return rstr;
75
85
  }
76
86
 
87
+ // NOTE: buf points into the Ruby string, so it must never be freed here.
77
88
  static VALUE png_to_gif(VALUE self, VALUE buf, VALUE max_colors) {
78
-
79
89
  rb_check_type(buf, T_STRING);
80
90
 
81
- int maxColors = INT2NUM(max_colors);
82
- int size = RSTRING_LEN(buf);
83
- unsigned char * png = (unsigned char *)StringValuePtr(buf);
91
+ int maxColors = NUM2INT(max_colors);
92
+ int size = (int)RSTRING_LEN(buf);
93
+ unsigned char * png = (unsigned char *)RSTRING_PTR(buf);
84
94
 
85
95
  unsigned char png_header[] = {137, 80, 78, 71, 13, 10, 26, 10};
86
- if (memcmp(png, png_header, 8) != 0) {
96
+ if (size < 8 || memcmp(png, png_header, 8) != 0) {
87
97
  rb_raise(rb_eRuntimeError, "Not a PNG image!");
88
- return Qnil;
89
- }
90
-
91
- char * out;
92
- size_t bufsize;
93
- FILE * fp = open_memstream(&out, &bufsize);
94
-
95
- if (!fp) {
96
- free(png);
97
- rb_raise(rb_eRuntimeError, "Cannot initialize memstream");
98
- return Qnil;
99
98
  }
100
99
 
101
100
  unsigned err, w, h;
102
101
  LodePNGState state;
103
102
  lodepng_state_init(&state);
104
- unsigned char * rgba;
103
+ unsigned char * rgba = NULL;
105
104
 
106
105
  err = lodepng_decode(&rgba, &w, &h, &state, png, size);
106
+ lodepng_state_cleanup(&state);
107
107
  if (err) {
108
- lodepng_state_cleanup(&state);
109
- free(png);
110
- free(out); // memstream
108
+ free(rgba);
111
109
  rb_raise(rb_eRuntimeError, "Unable to decode PNG, error code %u\n", err);
112
- return Qnil;
113
110
  }
114
111
 
115
- // convert rgba data to memstream (fp)
116
- jo_gif_t gif = jo_gif_start(fp, w, h, 0, maxColors);
117
- jo_gif_frame(&gif, rgba, 0, 0);
118
- jo_gif_end(&gif);
112
+ if (w == 0 || h == 0 || w > 32767 || h > 32767) {
113
+ free(rgba);
114
+ rb_raise(rb_eRuntimeError, "Invalid PNG dimensions (%ux%u)", w, h);
115
+ }
119
116
 
120
- VALUE rstr = rb_str_new(out, bufsize);
117
+ size_t bufsize = 0;
118
+ char * out = rgba_to_gif(rgba, w, h, maxColors, &bufsize);
119
+ free(rgba);
120
+ if (!out) rb_raise(rb_eRuntimeError, "Cannot initialize memstream");
121
121
 
122
- free(rgba); // rgba raw data
123
- free(png); // png buffer, converted from buf VALUE
124
- free(out); // memstream
125
- lodepng_state_cleanup(&state);
122
+ VALUE rstr = rb_str_new(out, bufsize);
123
+ free(out);
126
124
 
127
125
  return rstr;
128
126
  }
data/lilimg.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'lilimg'
5
- s.version = '0.2.1'
5
+ s.version = '0.2.2'
6
6
  s.platform = Gem::Platform::RUBY
7
7
  s.authors = ['Tomas Pollak']
8
8
  s.email = ['tomas@forkhq.com']
data/spec/lilimg_spec.rb CHANGED
@@ -1,22 +1,85 @@
1
1
  $VERBOSE = '1'
2
+ require 'zlib'
2
3
  require_relative('../ext/lilimg/lilimg.so')
3
4
 
4
5
  describe 'Lilimg' do
6
+ def chunk(type, data)
7
+ [data.bytesize].pack('N') + type + data + [Zlib.crc32(type + data)].pack('N')
8
+ end
9
+
10
+ def build_png(w = 8, h = 8)
11
+ raw = (0...h).map { "\x00".b + ("\xFF\x00\x00\xFF".b * w) }.join
12
+ "\x89PNG\r\n\x1A\n".b +
13
+ chunk('IHDR', [w, h, 8, 6, 0, 0, 0].pack('NNCCCCC')) +
14
+ chunk('IDAT', Zlib::Deflate.deflate(raw)) +
15
+ chunk('IEND', '')
16
+ end
17
+
18
+ let(:jpeg) { IO.read('./example/label.jpg', mode: 'rb') }
19
+
20
+ describe '.jpeg_to_gif' do
21
+ it 'works in threads' do
22
+ threads = 10.times.map do
23
+ Thread.new do
24
+ expect(jpeg[0]).to eql("\xFF".b)
25
+ expect(jpeg[1]).to eql("\xD8".b)
26
+ gif = Lilimg.jpeg_to_gif(jpeg, 4)
27
+ expect(gif.to_s[0..3]).to eql("GIF8")
28
+ end
29
+ end
30
+ threads.each(&:join)
31
+ end
5
32
 
6
- it 'works in threads' do
7
- threads = 10.times.map do
8
- jpeg = IO.read('./example/label.jpg')
9
- Thread.new do
10
- # check for JPEG header
11
- expect(jpeg[0]).to eql("\xFF")
12
- expect(jpeg[1]).to eql("\xD8")
13
- # convert to GIF
14
- gif = Lilimg.jpeg_to_gif(jpeg, 4)
15
- expect(gif.to_s[0..3]).to eql("GIF8")
33
+ it 'does not corrupt the heap when called repeatedly' do
34
+ 20.times do
35
+ expect(Lilimg.jpeg_to_gif(jpeg.dup, 4)[0..3]).to eql("GIF8")
36
+ GC.start
16
37
  end
17
38
  end
18
- # puts "Waiting for processes to finish"
19
- threads.each(&:join)
39
+
40
+ it 'converts grayscale JPEGs' do
41
+ gray = IO.read('./example/gray.jpg', mode: 'rb')
42
+ 5.times { expect(Lilimg.jpeg_to_gif(gray.dup, 4)[0..3]).to eql("GIF8") }
43
+ GC.start
44
+ end
45
+
46
+ it 'raises on non-JPEG data' do
47
+ expect { Lilimg.jpeg_to_gif('hello world, not a jpeg at all', 4) }.to raise_error(RuntimeError, /Not a JPEG/)
48
+ end
49
+
50
+ it 'raises on truncated data and keeps working' do
51
+ expect { Lilimg.jpeg_to_gif(jpeg[0, 200], 4) }.to raise_error(RuntimeError)
52
+ GC.start
53
+ expect(Lilimg.jpeg_to_gif(jpeg, 4)[0..3]).to eql("GIF8")
54
+ end
55
+
56
+ it 'raises on a non-string' do
57
+ expect { Lilimg.jpeg_to_gif(nil, 4) }.to raise_error(TypeError)
58
+ end
20
59
  end
21
60
 
61
+ describe '.png_to_gif' do
62
+ it 'converts a PNG to a GIF' do
63
+ expect(Lilimg.png_to_gif(build_png, 4)[0..3]).to eql("GIF8")
64
+ end
65
+
66
+ it 'does not corrupt the heap when called repeatedly' do
67
+ png = build_png(64, 64)
68
+ 20.times do
69
+ expect(Lilimg.png_to_gif(png.dup, 4)[0..3]).to eql("GIF8")
70
+ GC.start
71
+ end
72
+ end
73
+
74
+ it 'raises on non-PNG data' do
75
+ expect { Lilimg.png_to_gif('hello world, not a png at all', 4) }.to raise_error(RuntimeError, /Not a PNG/)
76
+ end
77
+
78
+ it 'raises on a corrupt PNG and keeps working' do
79
+ png = build_png
80
+ expect { Lilimg.png_to_gif(png[0, 40], 4) }.to raise_error(RuntimeError, /Unable to decode/)
81
+ GC.start
82
+ expect(Lilimg.png_to_gif(png, 4)[0..3]).to eql("GIF8")
83
+ end
84
+ end
22
85
  end
metadata CHANGED
@@ -1,16 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lilimg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tomas Pollak
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2019-02-28 00:00:00.000000000 Z
10
+ date: 2026-10-14 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description:
14
12
  email:
15
13
  - tomas@forkhq.com
16
14
  executables: []
@@ -21,6 +19,7 @@ files:
21
19
  - ".gitignore"
22
20
  - Rakefile
23
21
  - example/build.sh
22
+ - example/gray.jpg
24
23
  - example/label.jpg
25
24
  - example/test.c
26
25
  - example/test.rb
@@ -37,7 +36,6 @@ files:
37
36
  homepage: https://github.com/tomas/lilimg
38
37
  licenses: []
39
38
  metadata: {}
40
- post_install_message:
41
39
  rdoc_options: []
42
40
  require_paths:
43
41
  - lib
@@ -52,9 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
50
  - !ruby/object:Gem::Version
53
51
  version: 1.3.6
54
52
  requirements: []
55
- rubyforge_project:
56
- rubygems_version: 2.7.3
57
- signing_key:
53
+ rubygems_version: 3.6.2
58
54
  specification_version: 4
59
55
  summary: Native image processing in Ruby without deps.
60
56
  test_files: []