Skip to content

Commit 8d288f9

Browse files
committed
jpegio: split open & decode; add support for decoding from files; add test
1 parent 2e17368 commit 8d288f9

File tree

7 files changed

+433
-35
lines changed

7 files changed

+433
-35
lines changed

locale/circuitpython.pot

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ msgstr ""
212212
msgid "%q must be of type %q or %q, not %q"
213213
msgstr ""
214214

215+
#: shared-bindings/jpegio/JpegDecoder.c
216+
msgid "%q must be of type %q, %q, or %q, not %q"
217+
msgstr ""
218+
215219
#: py/argcheck.c shared-module/synthio/__init__.c
216220
msgid "%q must be of type %q, not %q"
217221
msgstr ""
@@ -249,6 +253,10 @@ msgstr ""
249253
msgid "%q() takes %d positional arguments but %d were given"
250254
msgstr ""
251255

256+
#: shared-module/jpegio/JpegDecoder.c
257+
msgid "%q() without %q()"
258+
msgstr ""
259+
252260
#: shared-bindings/usb_hid/Device.c
253261
msgid "%q, %q, and %q must all be the same length"
254262
msgstr ""

shared-bindings/jpegio/JpegDecoder.c

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
*/
2626

2727
#include "py/obj.h"
28+
#include "py/builtin.h"
2829
#include "py/runtime.h"
2930

3031
#include "shared-bindings/displayio/Bitmap.h"
@@ -35,8 +36,19 @@
3536
//| class JpegDecoder:
3637
//| """A JPEG decoder
3738
//|
38-
//| A JPEG decoder allocates a few thousand bytes of memory. To reduce memory fragmentation,
39+
//| A JpegDecoder allocates a few thousand bytes of memory. To reduce memory fragmentation,
3940
//| create a single JpegDecoder object and use it anytime a JPEG image needs to be decoded.
41+
//|
42+
//| Example::
43+
//|
44+
//| from jpegio import JpegDecoder
45+
//| from displayio import Bitmap
46+
//|
47+
//| decoder = JpegDecoder()
48+
//| width, height = decoder.open("/sd/example.jpg")
49+
//| bitmap = Bitmap(width, height)
50+
//| decoder.decode(bitmap)
51+
//| # .. do something with bitmap
4052
//| """
4153
//|
4254
//| def __init__(self) -> None: ...
@@ -54,51 +66,88 @@ STATIC mp_obj_t jpegio_jpegdecoder_make_new(const mp_obj_type_t *type, size_t n_
5466
return MP_OBJ_FROM_PTR(self);
5567
}
5668

69+
//| @overload
70+
//| def open(self, filename: str) -> Tuple[int, int]: ...
71+
//| @overload
72+
//| def open(self, buffer: ReadableBuffer) -> Tuple[int, int]: ...
73+
//| @overload
74+
//| def open(self, bytesio: io.BytesIO) -> Tuple[int, int]:
75+
//| """Use the specified object as the JPEG data source.
76+
//|
77+
//| The source may be a filename, a binary buffer in memory, or an opened binary stream.
78+
//|
79+
//| The single parameter is positional-only (write ``open(f)``, not
80+
//| ``open(filename=f)`` but due to technical limitations this is
81+
//| not shown in the function signature in the documentation.
82+
//|
83+
//| Returns the image size as the tuple ``(width, height)``."""
84+
STATIC mp_obj_t jpegio_jpegdecoder_open(mp_obj_t self_in, mp_obj_t arg) {
85+
jpegio_jpegdecoder_obj_t *self = MP_OBJ_TO_PTR(self_in);
86+
if (mp_obj_is_str(arg)) {
87+
arg = mp_call_function_2(
88+
MP_OBJ_FROM_PTR(&mp_builtin_open_obj),
89+
arg,
90+
MP_OBJ_NEW_QSTR(MP_QSTR_rb));
91+
}
92+
93+
mp_buffer_info_t bufinfo;
94+
const mp_stream_p_t *proto = mp_get_stream(arg);
95+
96+
if (proto && proto->read && !proto->is_text) {
97+
return common_hal_jpegio_jpegdecoder_set_source_file(self, arg);
98+
} else if (mp_get_buffer(arg, &bufinfo, MP_BUFFER_READ)) {
99+
return common_hal_jpegio_jpegdecoder_set_source_buffer(self, arg);
100+
}
101+
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q, %q, or %q, not %q"), MP_QSTR_data_source, MP_QSTR_str, MP_QSTR_BytesIO, MP_QSTR_ReadableBuffer);
102+
}
103+
MP_DEFINE_CONST_FUN_OBJ_2(jpegio_jpegdecoder_open_obj, jpegio_jpegdecoder_open);
104+
57105
//| def decode(
58-
//| self, data: ReadableBuffer, bitmap: displayio.Bitmap | None = None, scale=0
59-
//| ) -> tuple[int, int]:
106+
//| self,
107+
//| bitmap: displayio.Bitmap,
108+
//| scale: int = 0,
109+
//| ) -> None:
60110
//| """Decode JPEG data
61111
//|
62-
//| If ``bitmap`` is None, only the header is decoded.
63-
//| Otherwise, the bitmap must be large enough to contain the decoded image.
112+
//| The bitmap must be large enough to contain the decoded image.
64113
//| The pixel data is stored in the `displayio.Colorspace.RGB565_SWAPPED` colorspace.
65114
//|
66115
//| The image is optionally downscaled by a factor of ``2**scale``.
67116
//| Scaling by a factor of 8 (scale=3) is particularly efficient in terms of decoding time.
68117
//|
69-
//| :param ReadableBuffer data: Data in JPEG format
118+
//| After a call to ``decode``, you must ``open`` a new JPEG. It is not
119+
//| possible to repeatedly ``decode`` the same jpeg data, even if it is to
120+
//| select different scales or crop regions from it.
121+
//|
70122
//| :param Bitmap bitmap: Optional output buffer
71-
//| :param int scale: Scale factor from 0 to 3.
72-
//| :returns: The size of the (possibly scaled) image as ``width, height``
123+
//| :param int scale: Scale factor from 0 to 3, inclusive.
73124
//| """
74125
//|
75126
STATIC mp_obj_t jpegio_jpegdecoder_decode(mp_uint_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
76127
jpegio_jpegdecoder_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]);
77128

78-
enum { ARG_data, ARG_bitmap, ARG_scale };
129+
enum { ARG_bitmap, ARG_scale };
79130
static const mp_arg_t allowed_args[] = {
80-
{ MP_QSTR_data, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = mp_const_none } },
81-
{ MP_QSTR_bitmap, MP_ARG_OBJ, {.u_obj = mp_const_none } },
131+
{ MP_QSTR_bitmap, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = mp_const_none } },
82132
{ MP_QSTR_scale, MP_ARG_INT, {.u_int = 0 } },
83133
};
84134
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
85135
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
86136

87-
mp_buffer_info_t bufinfo;
88-
mp_get_buffer_raise(args[ARG_data].u_obj, &bufinfo, MP_BUFFER_READ);
89-
90137
mp_obj_t bitmap_in = args[ARG_bitmap].u_obj;
91-
mp_arg_validate_type_or_none(bitmap_in, &displayio_bitmap_type, MP_QSTR_bitmap);
92-
displayio_bitmap_t *bitmap = (args[ARG_bitmap].u_obj != mp_const_none) ? MP_OBJ_TO_PTR(args[ARG_bitmap].u_obj) : NULL;
138+
mp_arg_validate_type(bitmap_in, &displayio_bitmap_type, MP_QSTR_bitmap);
139+
displayio_bitmap_t *bitmap = MP_OBJ_TO_PTR(args[ARG_bitmap].u_obj);
93140

94141
int scale = args[ARG_scale].u_int;
95142
mp_arg_validate_int_range(scale, 0, 3, MP_QSTR_scale);
96143

97-
return common_hal_jpegio_jpegdecoder_decode(self, bitmap, &bufinfo, scale);
144+
common_hal_jpegio_jpegdecoder_decode_into(self, bitmap, scale);
145+
return mp_const_none;
98146
}
99-
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(jpegio_jpegdecoder_decode_obj, 2, jpegio_jpegdecoder_decode);
147+
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(jpegio_jpegdecoder_decode_obj, 1, jpegio_jpegdecoder_decode);
100148

101149
STATIC const mp_rom_map_elem_t jpegio_jpegdecoder_locals_dict_table[] = {
150+
{ MP_ROM_QSTR(MP_QSTR_open), MP_ROM_PTR(&jpegio_jpegdecoder_open_obj) },
102151
{ MP_ROM_QSTR(MP_QSTR_decode), MP_ROM_PTR(&jpegio_jpegdecoder_decode_obj) },
103152
};
104153
STATIC MP_DEFINE_CONST_DICT(jpegio_jpegdecoder_locals_dict, jpegio_jpegdecoder_locals_dict_table);

shared-bindings/jpegio/JpegDecoder.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@
2525
*/
2626

2727
#include "py/obj.h"
28+
#include "py/stream.h"
2829
#include "shared-module/displayio/Bitmap.h"
2930

3031
extern const mp_obj_type_t jpegio_jpegdecoder_type;
3132

3233
typedef struct jpegio_jpegdecoder_obj jpegio_jpegdecoder_obj_t;
3334

3435
void common_hal_jpegio_jpegdecoder_construct(jpegio_jpegdecoder_obj_t *self);
35-
mp_obj_t common_hal_jpegio_jpegdecoder_decode(jpegio_jpegdecoder_obj_t *self, displayio_bitmap_t *bitmap, mp_buffer_info_t *jpeg_data, int scale);
36+
void common_hal_jpegio_jpegdecoder_close(jpegio_jpegdecoder_obj_t *self);
37+
mp_obj_t common_hal_jpegio_jpegdecoder_set_source_buffer(jpegio_jpegdecoder_obj_t *self, mp_obj_t jpeg_data);
38+
mp_obj_t common_hal_jpegio_jpegdecoder_set_source_file(jpegio_jpegdecoder_obj_t *self, mp_obj_t file_obj);
39+
void common_hal_jpegio_jpegdecoder_decode_into(jpegio_jpegdecoder_obj_t *self, displayio_bitmap_t *bitmap, int scale);

shared-module/jpegio/JpegDecoder.c

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
#include "shared-bindings/bitmaptools/__init__.h"
3131
#include "shared-module/jpegio/JpegDecoder.h"
3232

33+
typedef size_t (*input_func)(JDEC *jd, uint8_t *dest, size_t len);
34+
3335
// Given a pointer `ptr` to the field `field_name` inside a structure of type `type`,
3436
// retrieve a pointer to the containing object.
3537
// This is used to retrieve the jpegio_jpegdecoder_obj_t object given the JDEC.
@@ -71,12 +73,61 @@ static void check_jresult(JRESULT j) {
7173
}
7274

7375
void common_hal_jpegio_jpegdecoder_construct(jpegio_jpegdecoder_obj_t *self) {
74-
// Nothing(!)
76+
self->data_obj = MP_OBJ_NULL;
77+
}
78+
79+
void common_hal_jpegio_jpegdecoder_close(jpegio_jpegdecoder_obj_t *self) {
80+
self->data_obj = MP_OBJ_NULL;
81+
memset(&self->bufinfo, 0, sizeof(self->bufinfo));
82+
}
83+
84+
static mp_obj_t common_hal_jpegio_jpegdecoder_decode_common(jpegio_jpegdecoder_obj_t *self, input_func fun) {
85+
JRESULT result = jd_prepare(&self->decoder, fun, &self->workspace, sizeof(self->workspace), NULL);
86+
if (result != JDR_OK) {
87+
common_hal_jpegio_jpegdecoder_close(self);
88+
}
89+
check_jresult(result);
90+
mp_obj_t elems[] = {
91+
MP_OBJ_NEW_SMALL_INT(self->decoder.height),
92+
MP_OBJ_NEW_SMALL_INT(self->decoder.width)
93+
};
94+
return mp_obj_new_tuple(MP_ARRAY_SIZE(elems), elems);
95+
}
96+
97+
static size_t file_input(JDEC *jd, uint8_t *dest, size_t len) {
98+
jpegio_jpegdecoder_obj_t *self = CONTAINER_OF(jd, jpegio_jpegdecoder_obj_t, decoder);
99+
100+
if (!dest) {
101+
// caller passes NULL to skip data; we need to read over the data.
102+
// Don't assume a seekable stream, because we want to decode jpegs
103+
// right from a native socket object
104+
uint8_t buf[512];
105+
size_t total = 0;
106+
size_t read;
107+
do {
108+
size_t to_discard = MIN(len - total, sizeof(buf));
109+
read = file_input(jd, buf, to_discard);
110+
total += read;
111+
} while (read != 0 && total != len);
112+
return len;
113+
}
114+
115+
int errcode = 0;
116+
size_t result = mp_stream_rw(self->data_obj, dest, len, &errcode, MP_STREAM_RW_READ);
117+
if (errcode != 0) { // raise our own error in case of I/O failure, it's better than the decoder's error
118+
mp_raise_OSError(errcode);
119+
}
120+
return result;
121+
}
122+
123+
mp_obj_t common_hal_jpegio_jpegdecoder_set_source_file(jpegio_jpegdecoder_obj_t *self, mp_obj_t file_obj) {
124+
self->data_obj = file_obj;
125+
return common_hal_jpegio_jpegdecoder_decode_common(self, file_input);
75126
}
76127

77128
static size_t buffer_input(JDEC *jd, uint8_t *dest, size_t len) {
78129
jpegio_jpegdecoder_obj_t *self = CONTAINER_OF(jd, jpegio_jpegdecoder_obj_t, decoder);
79-
mp_buffer_info_t *src = &self->src;
130+
mp_buffer_info_t *src = &self->bufinfo;
80131
size_t to_copy = MIN(len, src->len);
81132
if (dest) { // passes NULL to skip data
82133
memcpy(dest, src->buf, to_copy);
@@ -86,24 +137,29 @@ static size_t buffer_input(JDEC *jd, uint8_t *dest, size_t len) {
86137
return to_copy;
87138
}
88139

140+
mp_obj_t common_hal_jpegio_jpegdecoder_set_source_buffer(jpegio_jpegdecoder_obj_t *self, mp_obj_t buffer_obj) {
141+
self->data_obj = buffer_obj;
142+
mp_get_buffer_raise(buffer_obj, &self->bufinfo, MP_BUFFER_READ);
143+
return common_hal_jpegio_jpegdecoder_decode_common(self, buffer_input);
144+
}
145+
89146
static int bitmap_output(JDEC *jd, void *data, JRECT *rect) {
90147
jpegio_jpegdecoder_obj_t *self = CONTAINER_OF(jd, jpegio_jpegdecoder_obj_t, decoder);
91148
common_hal_bitmaptools_arrayblit(self->dest, data, 2, rect->left, rect->top, rect->right + 1, rect->bottom + 1, false, 0);
92149
return 1;
93150
}
94151

95-
mp_obj_t common_hal_jpegio_jpegdecoder_decode(jpegio_jpegdecoder_obj_t *self, displayio_bitmap_t *bitmap, mp_buffer_info_t *jpeg_data, int scale) {
96-
self->src = *jpeg_data;
97-
check_jresult(jd_prepare(&self->decoder, buffer_input, &self->workspace, sizeof(self->workspace), NULL));
152+
void common_hal_jpegio_jpegdecoder_decode_into(jpegio_jpegdecoder_obj_t *self, displayio_bitmap_t *bitmap, int scale) {
153+
if (self->data_obj == MP_OBJ_NULL) {
154+
mp_raise_RuntimeError_varg(MP_ERROR_TEXT("%q() without %q()"), MP_QSTR_decode, MP_QSTR_open);
155+
}
98156
int dst_height = self->decoder.height >> scale;
99157
int dst_width = self->decoder.width >> scale;
100-
if (bitmap) {
101-
if (dst_width > bitmap->width || dst_height > bitmap->height) {
102-
mp_raise_ValueError(MP_ERROR_TEXT("Destination bitmap too small to contain image"));
103-
}
104-
self->dest = bitmap;
105-
check_jresult(jd_decomp(&self->decoder, bitmap_output, scale));
158+
if (dst_width > bitmap->width || dst_height > bitmap->height) {
159+
mp_raise_ValueError(MP_ERROR_TEXT("Destination bitmap too small to contain image"));
106160
}
107-
mp_obj_t elems[] = { MP_OBJ_NEW_SMALL_INT(dst_width), MP_OBJ_NEW_SMALL_INT(dst_height) };
108-
return mp_obj_new_tuple(MP_ARRAY_SIZE(elems), elems);
161+
self->dest = bitmap;
162+
JRESULT result = jd_decomp(&self->decoder, bitmap_output, scale);
163+
common_hal_jpegio_jpegdecoder_close(self);
164+
check_jresult(result);
109165
}

shared-module/jpegio/JpegDecoder.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ typedef struct jpegio_jpegdecoder_obj {
3434
mp_obj_base_t base;
3535
JDEC decoder;
3636
byte workspace[TJPGD_WORKSPACE_SIZE];
37-
mp_buffer_info_t src;
37+
mp_obj_t data_obj;
38+
mp_buffer_info_t bufinfo;
3839
displayio_bitmap_t *dest;
40+
int scale;
3941
} jpegio_jpegdecoder_obj_t;

tests/circuitpython/jpegio_decompress.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import io
2+
13
from displayio import Bitmap
24
import binascii
35
import jpegio
@@ -53,12 +55,14 @@
5355

5456

5557
def test(jpeg_input, scale):
56-
w, h = decoder.decode(jpeg_input, scale=scale)
58+
w, h = decoder.open(jpeg_input)
59+
w >>= scale
60+
h >>= scale
5761
print(f"{w}x{h}")
5862

5963
b = Bitmap(w, h, 65535)
6064

61-
decoder.decode(jpeg_input, b, scale=scale)
65+
decoder.decode(b, scale=scale)
6266

6367
for y in range(min(30, h)):
6468
for x in range(min(72, w)):
@@ -67,7 +71,16 @@ def test(jpeg_input, scale):
6771
print("...\n" if h > 24 else "")
6872

6973

74+
print("bytes")
7075
test(content, scale=0)
7176
test(content, scale=1)
7277
test(content, scale=2)
7378
test(content, scale=3)
79+
80+
bytesio = io.BytesIO(content)
81+
82+
print("BytesIO")
83+
test(io.BytesIO(content), scale=0)
84+
test(io.BytesIO(content), scale=1)
85+
test(io.BytesIO(content), scale=2)
86+
test(io.BytesIO(content), scale=3)

0 commit comments

Comments
 (0)