Skip to content

Commit 81fde9c

Browse files
authored
feat(sgi): Implement RLE encoding support for output (AcademySoftwareFoundation#4990)
Implement RLE compression support for the SGI output plugin. Reading RLE encoded images was already supported, but writing was never done up until this point. The existing sgi test seems sufficient to catch issues and it covers input/output of both 1 byte-per-pixel and 2 byte-per-pixel files. The documentation for the image plugins are sometimes not very clear about which attributes are relevant for input vs. output. There's usually 3 sections: Attributes, Attributes for Input, and Attributes for Output. Before this PR, SGI mentioned the "compression" attribute in the "general" Attributes section (rather than say just the Input section), which caused a bit of grief as the only way to discover that RLE was not implemented for Output was to glance at the file size of the resulting file... I had assumed that compression was supported for output too but discovered that it was not. Now that this PR implements the attribute for output I've left the documentation as-is in the "general" Attributes section since it applies to both read/writing now. But I'm open for suggestions here. Signed-off-by: Jesse Yurkovich <[email protected]>
1 parent 0d5e78d commit 81fde9c

File tree

1 file changed

+228
-21
lines changed

1 file changed

+228
-21
lines changed

src/sgi.imageio/sgioutput.cpp

Lines changed: 228 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,19 @@ class SgiOutput final : public ImageOutput {
2626
std::string m_filename;
2727
std::vector<unsigned char> m_scratch;
2828
unsigned int m_dither;
29-
std::vector<unsigned char> m_tilebuffer;
29+
bool m_want_rle;
30+
std::vector<unsigned char> m_uncompressed_image;
3031

3132
void init() { ioproxy_clear(); }
3233

3334
bool create_and_write_header();
3435

36+
bool write_scanline_raw(int y, const unsigned char* data);
37+
bool write_scanline_rle(int y, const unsigned char* data, int64_t& offset,
38+
std::vector<int>& start_table,
39+
std::vector<int>& length_table);
40+
bool write_buffered_pixels();
41+
3542
/// Helper - write, with error detection
3643
template<class T>
3744
bool fwrite(const T* buf, size_t itemsize = sizeof(T), size_t nitems = 1)
@@ -85,10 +92,12 @@ SgiOutput::open(const std::string& name, const ImageSpec& spec, OpenMode mode)
8592
? m_spec.get_int_attribute("oiio:dither", 0)
8693
: 0;
8794

95+
m_want_rle = m_spec.get_string_attribute("compression") == "rle";
96+
8897
// If user asked for tiles -- which this format doesn't support, emulate
89-
// it by buffering the whole image.
90-
if (m_spec.tile_width && m_spec.tile_height)
91-
m_tilebuffer.resize(m_spec.image_bytes());
98+
// it by buffering the whole image. RLE is treated similarly.
99+
if (m_want_rle || (m_spec.tile_width && m_spec.tile_height))
100+
m_uncompressed_image.resize(m_spec.image_bytes());
92101

93102
return create_and_write_header();
94103
}
@@ -102,32 +111,57 @@ SgiOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
102111
y = m_spec.height - y - 1;
103112
data = to_native_scanline(format, data, xstride, m_scratch, m_dither, y, z);
104113

114+
// If we are writing RLE data, just copy into the uncompressed buffer
115+
if (m_want_rle) {
116+
const auto scaneline_size = m_spec.scanline_bytes();
117+
memcpy(&m_uncompressed_image[y * scaneline_size], data, scaneline_size);
118+
119+
return true;
120+
}
121+
122+
return write_scanline_raw(y, (const unsigned char*)data);
123+
}
124+
125+
126+
127+
bool
128+
SgiOutput::write_tile(int x, int y, int z, TypeDesc format, const void* data,
129+
stride_t xstride, stride_t ystride, stride_t zstride)
130+
{
131+
// Emulate tiles by buffering the whole image
132+
return copy_tile_to_image_buffer(x, y, z, format, data, xstride, ystride,
133+
zstride, &m_uncompressed_image[0]);
134+
}
135+
136+
137+
138+
bool
139+
SgiOutput::write_scanline_raw(int y, const unsigned char* data)
140+
{
105141
// In SGI format all channels are saved to file separately: first, all
106142
// channel 1 scanlines are saved, then all channel2 scanlines are saved
107143
// and so on.
108-
//
109-
// Note that since SGI images are pretty archaic and most probably
110-
// people won't be too picky about full flexibility writing them, we
111-
// content ourselves with only writing uncompressed data, and don't
112-
// attempt to write with RLE encoding.
113144

114145
size_t bpc = m_spec.format.size(); // bytes per channel
115146
std::unique_ptr<unsigned char[]> channeldata(
116147
new unsigned char[m_spec.width * bpc]);
117148

118149
for (int64_t c = 0; c < m_spec.nchannels; ++c) {
119-
unsigned char* cdata = (unsigned char*)data + c * bpc;
150+
const unsigned char* cdata = data + c * bpc;
120151
for (int64_t x = 0; x < m_spec.width; ++x) {
121152
channeldata[x * bpc] = cdata[0];
122153
if (bpc == 2)
123154
channeldata[x * bpc + 1] = cdata[1];
124155
cdata += m_spec.nchannels * bpc; // advance to next pixel
125156
}
157+
126158
if (bpc == 2 && littleendian())
127159
swap_endian((unsigned short*)&channeldata[0], m_spec.width);
160+
128161
ptrdiff_t scanline_offset = sgi_pvt::SGI_HEADER_LEN
129162
+ ptrdiff_t(c * m_spec.height + y)
130163
* m_spec.width * bpc;
164+
131165
ioseek(scanline_offset);
132166
if (!iowrite(&channeldata[0], 1, m_spec.width * bpc)) {
133167
return false;
@@ -139,13 +173,179 @@ SgiOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
139173

140174

141175

176+
static bool
177+
data_equals(const unsigned char* data, int bpc, imagesize_t off1,
178+
imagesize_t off2)
179+
{
180+
if (bpc == 1) {
181+
return data[off1] == data[off2];
182+
} else {
183+
return data[off1] == data[off2] && data[off1 + 1] == data[off2 + 1];
184+
}
185+
}
186+
187+
188+
189+
static void
190+
data_set(unsigned char* data, int bpc, imagesize_t off,
191+
const unsigned char* val)
192+
{
193+
if (bpc == 1) {
194+
data[off] = val[0];
195+
} else {
196+
data[off] = val[1];
197+
data[off + 1] = val[0];
198+
}
199+
}
200+
201+
202+
203+
static void
204+
data_set(unsigned char* data, int bpc, imagesize_t off, const short val)
205+
{
206+
if (bpc == 1) {
207+
data[off] = static_cast<unsigned char>(val);
208+
} else {
209+
data[off] = static_cast<unsigned char>(val >> 8);
210+
data[off + 1] = static_cast<unsigned char>(val & 0xFF);
211+
}
212+
}
213+
214+
215+
142216
bool
143-
SgiOutput::write_tile(int x, int y, int z, TypeDesc format, const void* data,
144-
stride_t xstride, stride_t ystride, stride_t zstride)
217+
SgiOutput::write_scanline_rle(int y, const unsigned char* data, int64_t& offset,
218+
std::vector<int>& offset_table,
219+
std::vector<int>& length_table)
145220
{
146-
// Emulate tiles by buffering the whole image
147-
return copy_tile_to_image_buffer(x, y, z, format, data, xstride, ystride,
148-
zstride, &m_tilebuffer[0]);
221+
const size_t bpc = m_spec.format.size(); // bytes per channel
222+
const size_t xstride = m_spec.nchannels * bpc;
223+
const imagesize_t scanline_bytes = m_spec.scanline_bytes();
224+
225+
// Account for the worst case length when every pixel is different
226+
m_scratch.resize(bpc * (m_spec.width + (m_spec.width / 127 + 2)));
227+
228+
for (int64_t c = 0; c < m_spec.nchannels; ++c) {
229+
const unsigned char* cdata = data + c * bpc;
230+
231+
imagesize_t out = 0;
232+
imagesize_t pos = 0;
233+
while (pos < scanline_bytes) {
234+
imagesize_t start = pos;
235+
// Find the first run meeting a minimum length of 3
236+
imagesize_t ahead_1 = pos + xstride;
237+
imagesize_t ahead_2 = pos + xstride * 2;
238+
while (ahead_2 < scanline_bytes
239+
&& (!data_equals(cdata, bpc, ahead_1, ahead_2)
240+
|| !data_equals(cdata, bpc, pos, ahead_1))) {
241+
pos += xstride;
242+
ahead_1 += xstride;
243+
ahead_2 += xstride;
244+
}
245+
if (ahead_2 >= scanline_bytes) {
246+
// No more runs, just dump the rest as literals
247+
pos = scanline_bytes;
248+
}
249+
int count = int((pos - start) / xstride);
250+
while (count) {
251+
int todo = (count > 127) ? 127 : count;
252+
count -= todo;
253+
data_set(m_scratch.data(), bpc, out, 0x80 | todo);
254+
out += bpc;
255+
while (todo) {
256+
data_set(m_scratch.data(), bpc, out, cdata + start);
257+
out += bpc;
258+
start += xstride;
259+
todo -= 1;
260+
}
261+
}
262+
start = pos;
263+
if (start >= scanline_bytes)
264+
break;
265+
pos += xstride;
266+
while (pos < scanline_bytes
267+
&& data_equals(cdata, bpc, start, pos)) {
268+
pos += xstride;
269+
}
270+
count = int((pos - start) / xstride);
271+
while (count) {
272+
int curr_run = (count > 127) ? 127 : count;
273+
count -= curr_run;
274+
data_set(m_scratch.data(), bpc, out, curr_run);
275+
out += bpc;
276+
data_set(m_scratch.data(), bpc, out, cdata + start);
277+
out += bpc;
278+
}
279+
}
280+
data_set(m_scratch.data(), bpc, out, short(0));
281+
out += bpc;
282+
283+
// Fill in details about the scanline
284+
const int table_index = c * m_spec.height + y;
285+
offset_table[table_index] = static_cast<int>(offset);
286+
length_table[table_index] = static_cast<int>(out);
287+
288+
// Write the compressed data
289+
if (!iowrite(&m_scratch[0], 1, out))
290+
return false;
291+
offset += out;
292+
}
293+
294+
return true;
295+
}
296+
297+
298+
299+
bool
300+
SgiOutput::write_buffered_pixels()
301+
{
302+
OIIO_ASSERT(m_uncompressed_image.size());
303+
304+
const auto scanline_bytes = m_spec.scanline_bytes();
305+
if (m_want_rle) {
306+
// Prepare RLE tables
307+
const int64_t table_size = m_spec.height * m_spec.nchannels;
308+
const int64_t table_size_bytes = table_size * sizeof(int);
309+
std::vector<int> offset_table;
310+
std::vector<int> length_table;
311+
offset_table.resize(table_size);
312+
length_table.resize(table_size);
313+
314+
// Skip over the tables and start at the data area
315+
int64_t offset = sgi_pvt::SGI_HEADER_LEN + 2 * table_size_bytes;
316+
ioseek(offset);
317+
318+
// Write RLE compressed data
319+
for (int y = 0; y < m_spec.height; ++y) {
320+
const unsigned char* scanline_data
321+
= &m_uncompressed_image[y * scanline_bytes];
322+
if (!write_scanline_rle(y, scanline_data, offset, offset_table,
323+
length_table))
324+
return false;
325+
}
326+
327+
// Write the tables now that they're filled in with offsets/lengths
328+
ioseek(sgi_pvt::SGI_HEADER_LEN);
329+
if (littleendian()) {
330+
swap_endian(&offset_table[0], table_size);
331+
swap_endian(&length_table[0], table_size);
332+
}
333+
if (!iowrite(&offset_table[0], 1, table_size_bytes))
334+
return false;
335+
if (!iowrite(&length_table[0], 1, table_size_bytes))
336+
return false;
337+
338+
} else {
339+
// Write raw data
340+
for (int y = 0; y < m_spec.height; ++y) {
341+
unsigned char* scanline_data
342+
= &m_uncompressed_image[y * scanline_bytes];
343+
if (!write_scanline_raw(y, scanline_data))
344+
return false;
345+
}
346+
}
347+
348+
return true;
149349
}
150350

151351

@@ -160,15 +360,22 @@ SgiOutput::close()
160360

161361
bool ok = true;
162362
if (m_spec.tile_width) {
163-
// Handle tile emulation -- output the buffered pixels
164-
OIIO_ASSERT(m_tilebuffer.size());
363+
// We've been emulating tiles; now dump as scanlines.
364+
OIIO_ASSERT(m_uncompressed_image.size());
165365
ok &= write_scanlines(m_spec.y, m_spec.y + m_spec.height, 0,
166-
m_spec.format, &m_tilebuffer[0]);
167-
m_tilebuffer.clear();
168-
m_tilebuffer.shrink_to_fit();
366+
m_spec.format, &m_uncompressed_image[0]);
169367
}
170368

369+
// If we want RLE encoding or we were tiled, output all the processed scanlines now.
370+
if (ok && (m_want_rle || m_spec.tile_width)) {
371+
ok &= write_buffered_pixels();
372+
}
373+
374+
m_uncompressed_image.clear();
375+
m_uncompressed_image.shrink_to_fit();
376+
171377
init();
378+
172379
return ok;
173380
}
174381

@@ -179,7 +386,7 @@ SgiOutput::create_and_write_header()
179386
{
180387
sgi_pvt::SgiHeader sgi_header;
181388
sgi_header.magic = sgi_pvt::SGI_MAGIC;
182-
sgi_header.storage = sgi_pvt::VERBATIM;
389+
sgi_header.storage = m_want_rle ? sgi_pvt::RLE : sgi_pvt::VERBATIM;
183390
sgi_header.bpc = m_spec.format.size();
184391

185392
if (m_spec.height == 1 && m_spec.nchannels == 1)

0 commit comments

Comments
 (0)