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 +4 -4
- data/example/gray.jpg +0 -0
- data/ext/lilimg/lilimg.c +65 -67
- data/lilimg.gemspec +1 -1
- data/spec/lilimg_spec.rb +75 -12
- metadata +4 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7344a27d23f240ea551b8db61d3f8eba90e514e506c023420bd92beed2a814d9
|
|
4
|
+
data.tar.gz: 46f1fa4a072cd1a25fd309f9f414b3200c94fa5cc3d065fc7ab8a19ee575ec1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 =
|
|
28
|
-
int size = RSTRING_LEN(buf);
|
|
29
|
-
unsigned char * jpeg = (unsigned char *)
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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 *
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 =
|
|
82
|
-
int size = RSTRING_LEN(buf);
|
|
83
|
-
unsigned char * png = (unsigned char *)
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
free(
|
|
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
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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: []
|