Skip to content

Commit 5dd689c

Browse files
committed
jpegio: add bitmaptools.blit like clipping to decode
1 parent 8d288f9 commit 5dd689c

File tree

6 files changed

+357
-19
lines changed

6 files changed

+357
-19
lines changed

shared-bindings/jpegio/JpegDecoder.c

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "py/builtin.h"
2929
#include "py/runtime.h"
3030

31+
#include "shared-bindings/bitmaptools/__init__.h"
3132
#include "shared-bindings/displayio/Bitmap.h"
3233
#include "shared-bindings/jpegio/JpegDecoder.h"
3334
#include "shared-module/jpegio/JpegDecoder.h"
@@ -106,6 +107,15 @@ MP_DEFINE_CONST_FUN_OBJ_2(jpegio_jpegdecoder_open_obj, jpegio_jpegdecoder_open);
106107
//| self,
107108
//| bitmap: displayio.Bitmap,
108109
//| scale: int = 0,
110+
//| x: int = 0,
111+
//| y: int = 0,
112+
//| *,
113+
//| x1: int,
114+
//| y1: int,
115+
//| x2: int,
116+
//| y2: int,
117+
//| skip_source_index: int,
118+
//| skip_dest_index: int,
109119
//| ) -> None:
110120
//| """Decode JPEG data
111121
//|
@@ -115,21 +125,45 @@ MP_DEFINE_CONST_FUN_OBJ_2(jpegio_jpegdecoder_open_obj, jpegio_jpegdecoder_open);
115125
//| The image is optionally downscaled by a factor of ``2**scale``.
116126
//| Scaling by a factor of 8 (scale=3) is particularly efficient in terms of decoding time.
117127
//|
128+
//| The remaining parameters are as for `bitmaptools.blit`.
129+
//| Because JPEG is a lossy data format, chroma keying based on the "source
130+
//| index" is not reliable, because the same original RGB value might end
131+
//| up being decompressed as a similar but not equal color value. Using a
132+
//| higher JPEG encoding quality can help, but ultimately it will not be
133+
//| perfect.
134+
//|
118135
//| After a call to ``decode``, you must ``open`` a new JPEG. It is not
119136
//| possible to repeatedly ``decode`` the same jpeg data, even if it is to
120137
//| select different scales or crop regions from it.
121138
//|
122139
//| :param Bitmap bitmap: Optional output buffer
123140
//| :param int scale: Scale factor from 0 to 3, inclusive.
141+
//| :param int x: Horizontal pixel location in bitmap where source_bitmap upper-left
142+
//| corner will be placed
143+
//| :param int y: Vertical pixel location in bitmap where source_bitmap upper-left
144+
//| corner will be placed
145+
//| :param int x1: Minimum x-value for rectangular bounding box to be copied from the source bitmap
146+
//| :param int y1: Minimum y-value for rectangular bounding box to be copied from the source bitmap
147+
//| :param int x2: Maximum x-value (exclusive) for rectangular bounding box to be copied from the source bitmap
148+
//| :param int y2: Maximum y-value (exclusive) for rectangular bounding box to be copied from the source bitmap
149+
//| :param int skip_source_index: bitmap palette index in the source that will not be copied,
150+
//| set to None to copy all pixels
151+
//| :param int skip_dest_index: bitmap palette index in the destination bitmap that will not get overwritten
152+
//| by the pixels from the source
124153
//| """
125154
//|
126155
STATIC mp_obj_t jpegio_jpegdecoder_decode(mp_uint_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
127156
jpegio_jpegdecoder_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]);
128157

129-
enum { ARG_bitmap, ARG_scale };
158+
enum { ARG_bitmap, ARG_scale, ARG_x, ARG_y, ARGS_X1_Y1_X2_Y2, ARG_skip_source_index, ARG_skip_dest_index };
130159
static const mp_arg_t allowed_args[] = {
131160
{ MP_QSTR_bitmap, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = mp_const_none } },
132161
{ MP_QSTR_scale, MP_ARG_INT, {.u_int = 0 } },
162+
{ MP_QSTR_x, MP_ARG_INT, {.u_int = 0 } },
163+
{ MP_QSTR_y, MP_ARG_INT, {.u_int = 0 } },
164+
ALLOWED_ARGS_X1_Y1_X2_Y2(0, 0),
165+
{MP_QSTR_skip_source_index, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
166+
{MP_QSTR_skip_dest_index, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
133167
};
134168
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
135169
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
@@ -141,7 +175,33 @@ STATIC mp_obj_t jpegio_jpegdecoder_decode(mp_uint_t n_args, const mp_obj_t *pos_
141175
int scale = args[ARG_scale].u_int;
142176
mp_arg_validate_int_range(scale, 0, 3, MP_QSTR_scale);
143177

144-
common_hal_jpegio_jpegdecoder_decode_into(self, bitmap, scale);
178+
int x = mp_arg_validate_int_range(args[ARG_x].u_int, 0, bitmap->width, MP_QSTR_x);
179+
int y = mp_arg_validate_int_range(args[ARG_x].u_int, 0, bitmap->height, MP_QSTR_y);
180+
bitmaptools_rect_t lim = bitmaptools_validate_coord_range_pair(&args[ARG_x1], bitmap->width, bitmap->height);
181+
182+
uint32_t skip_source_index;
183+
bool skip_source_index_none; // flag whether skip_value was None
184+
185+
if (args[ARG_skip_source_index].u_obj == mp_const_none) {
186+
skip_source_index = 0;
187+
skip_source_index_none = true;
188+
} else {
189+
skip_source_index = mp_obj_get_int(args[ARG_skip_source_index].u_obj);
190+
skip_source_index_none = false;
191+
}
192+
193+
uint32_t skip_dest_index;
194+
bool skip_dest_index_none; // flag whether skip_self_value was None
195+
196+
if (args[ARG_skip_dest_index].u_obj == mp_const_none) {
197+
skip_dest_index = 0;
198+
skip_dest_index_none = true;
199+
} else {
200+
skip_dest_index = mp_obj_get_int(args[ARG_skip_dest_index].u_obj);
201+
skip_dest_index_none = false;
202+
}
203+
common_hal_jpegio_jpegdecoder_decode_into(self, bitmap, scale, x, y, &lim, skip_source_index, skip_source_index_none, skip_dest_index, skip_dest_index_none);
204+
145205
return mp_const_none;
146206
}
147207
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(jpegio_jpegdecoder_decode_obj, 1, jpegio_jpegdecoder_decode);

shared-bindings/jpegio/JpegDecoder.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "py/obj.h"
2828
#include "py/stream.h"
2929
#include "shared-module/displayio/Bitmap.h"
30+
#include "shared-bindings/bitmaptools/__init__.h"
3031

3132
extern const mp_obj_type_t jpegio_jpegdecoder_type;
3233

@@ -36,4 +37,9 @@ void common_hal_jpegio_jpegdecoder_construct(jpegio_jpegdecoder_obj_t *self);
3637
void common_hal_jpegio_jpegdecoder_close(jpegio_jpegdecoder_obj_t *self);
3738
mp_obj_t common_hal_jpegio_jpegdecoder_set_source_buffer(jpegio_jpegdecoder_obj_t *self, mp_obj_t jpeg_data);
3839
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);
40+
void common_hal_jpegio_jpegdecoder_decode_into(
41+
jpegio_jpegdecoder_obj_t *self,
42+
displayio_bitmap_t *bitmap, int scale, int16_t x, int16_t y,
43+
bitmaptools_rect_t *lim,
44+
uint32_t skip_source_index, bool skip_source_index_none,
45+
uint32_t skip_dest_index, bool skip_dest_index_none);

shared-module/jpegio/JpegDecoder.c

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,23 +143,90 @@ mp_obj_t common_hal_jpegio_jpegdecoder_set_source_buffer(jpegio_jpegdecoder_obj_
143143
return common_hal_jpegio_jpegdecoder_decode_common(self, buffer_input);
144144
}
145145

146+
#define DECODER_CONTINUE (1)
147+
#define DECODER_INTERRUPT (0)
146148
static int bitmap_output(JDEC *jd, void *data, JRECT *rect) {
147149
jpegio_jpegdecoder_obj_t *self = CONTAINER_OF(jd, jpegio_jpegdecoder_obj_t, decoder);
148-
common_hal_bitmaptools_arrayblit(self->dest, data, 2, rect->left, rect->top, rect->right + 1, rect->bottom + 1, false, 0);
150+
int src_width = rect->right - rect->left + 1, src_pixel_stride = src_width /* in units of pixels! */, src_height = rect->bottom - rect->top + 1;
151+
152+
displayio_bitmap_t src = {
153+
.width = src_width,
154+
.height = src_height,
155+
.data = data,
156+
.stride = src_pixel_stride / 2, /* in units of uint32_t */
157+
.bits_per_value = 16,
158+
.x_shift = 1,
159+
.x_mask = 1,
160+
.bitmask = 0xffff,
161+
};
162+
163+
int x = self->x;
164+
int y = self->y;
165+
int x1 = self->lim.x1 - rect->left;
166+
int x2 = self->lim.x2 - rect->left;
167+
int y1 = self->lim.y1 - rect->top;
168+
int y2 = self->lim.y2 - rect->top;
169+
170+
if (y2 < y1) {
171+
// The last row in the source image to copy FROM is above of this, so
172+
// no more pixels on any rows
173+
return DECODER_INTERRUPT;
174+
}
175+
176+
y2 = MIN(y2, src_height);
177+
if (x2 < x1) {
178+
// The last column in the source image to copy FROM is left of this, so
179+
// no more pixels on this row but could be on subsequent rows
180+
return DECODER_CONTINUE;
181+
}
182+
x2 = MIN(x2, src_width);
183+
184+
// The first column in the source image to copy FROM is left of this, so copy starting with
185+
// *local source* x1 equal to 0, and *target* x adjusted right by the same amount
186+
if (x1 < 0) {
187+
x += -x1;
188+
x1 = 0;
189+
}
190+
191+
// the same, but for Y coordinates
192+
if (y1 < 0) {
193+
y += -y1;
194+
y1 = 0;
195+
}
196+
197+
// blit takes care of x, y out of range
198+
assert(x1 >= 0);
199+
assert(y1 >= 0);
200+
// blit takes care of x1 >= x2 and y1 >= y2 cases
201+
assert(x2 <= src_width);
202+
assert(y2 <= src_height);
203+
204+
common_hal_bitmaptools_blit(self->dest, &src, x, y, x1, y1, x2, y2, self->skip_source_index, self->skip_source_index_none, self->skip_dest_index, self->skip_dest_index_none);
149205
return 1;
150206
}
151207

152-
void common_hal_jpegio_jpegdecoder_decode_into(jpegio_jpegdecoder_obj_t *self, displayio_bitmap_t *bitmap, int scale) {
208+
void common_hal_jpegio_jpegdecoder_decode_into(
209+
jpegio_jpegdecoder_obj_t *self,
210+
displayio_bitmap_t *bitmap, int scale, int16_t x, int16_t y,
211+
bitmaptools_rect_t *lim,
212+
uint32_t skip_source_index, bool skip_source_index_none,
213+
uint32_t skip_dest_index, bool skip_dest_index_none) {
153214
if (self->data_obj == MP_OBJ_NULL) {
154215
mp_raise_RuntimeError_varg(MP_ERROR_TEXT("%q() without %q()"), MP_QSTR_decode, MP_QSTR_open);
155216
}
156-
int dst_height = self->decoder.height >> scale;
157-
int dst_width = self->decoder.width >> 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"));
160-
}
217+
218+
self->x = x;
219+
self->y = y;
220+
self->lim = *lim;
221+
self->skip_source_index = skip_source_index;
222+
self->skip_source_index_none = skip_source_index_none;
223+
self->skip_dest_index = skip_dest_index;
224+
self->skip_dest_index_none = skip_dest_index_none;
225+
161226
self->dest = bitmap;
162227
JRESULT result = jd_decomp(&self->decoder, bitmap_output, scale);
163228
common_hal_jpegio_jpegdecoder_close(self);
164-
check_jresult(result);
229+
if (result != JDR_INTR) {
230+
check_jresult(result);
231+
}
165232
}

shared-module/jpegio/JpegDecoder.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,9 @@ typedef struct jpegio_jpegdecoder_obj {
3737
mp_obj_t data_obj;
3838
mp_buffer_info_t bufinfo;
3939
displayio_bitmap_t *dest;
40-
int scale;
40+
uint16_t x, y;
41+
bitmaptools_rect_t lim;
42+
uint32_t skip_source_index, skip_dest_index;
43+
bool skip_source_index_none, skip_dest_index_none;
44+
uint8_t scale;
4145
} jpegio_jpegdecoder_obj_t;

tests/circuitpython/jpegio_decompress.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from displayio import Bitmap
44
import binascii
55
import jpegio
6+
import bitmaptools
67

78
content = binascii.a2b_base64(
89
b"""
@@ -54,21 +55,40 @@
5455
decoder = jpegio.JpegDecoder()
5556

5657

57-
def test(jpeg_input, scale):
58+
def dump_bitmap(b):
59+
w = b.width
60+
h = b.height
61+
62+
for y in range(min(30, h)):
63+
for x in range(min(72, w)):
64+
print(end="#" if (b[x, y] & 0x7E0) < 0x400 else " ")
65+
print("..." if w > 72 else "")
66+
print("...\n" if h > 24 else "")
67+
68+
69+
def test(jpeg_input, scale, fill=0xFFFF, **position_and_crop):
5870
w, h = decoder.open(jpeg_input)
5971
w >>= scale
6072
h >>= scale
6173
print(f"{w}x{h}")
6274

6375
b = Bitmap(w, h, 65535)
76+
b.fill(fill)
6477

65-
decoder.decode(b, scale=scale)
78+
decoder.decode(b, scale=scale, **position_and_crop)
6679

67-
for y in range(min(30, h)):
68-
for x in range(min(72, w)):
69-
print(end="#" if (b[x, y] & 0x7E0) < 0x400 else " ")
70-
print("..." if w > 72 else "")
71-
print("...\n" if h > 24 else "")
80+
dump_bitmap(b)
81+
82+
if position_and_crop:
83+
position_and_crop.setdefault("x", 0)
84+
position_and_crop.setdefault("y", 0)
85+
full = Bitmap(w, h, 65535)
86+
decoder.open(jpeg_input)
87+
decoder.decode(full, scale=scale)
88+
refb = Bitmap(w, h, 65535)
89+
refb.fill(fill)
90+
bitmaptools.blit(refb, full, **position_and_crop)
91+
print(f"{memoryview(refb) == memoryview(b)=}")
7292

7393

7494
print("bytes")
@@ -84,3 +104,12 @@ def test(jpeg_input, scale):
84104
test(io.BytesIO(content), scale=1)
85105
test(io.BytesIO(content), scale=2)
86106
test(io.BytesIO(content), scale=3)
107+
108+
print("crop & move")
109+
test(content, scale=3, x=8, y=8)
110+
test(content, scale=3, x1=8, y1=8)
111+
test(content, scale=3, x2=16, y2=16)
112+
test(content, scale=3, x=12, y=12, x1=8, y1=12, x2=16, y2=16)
113+
114+
print("color key")
115+
test(content, scale=0, skip_source_index=0x4529, fill=0)

0 commit comments

Comments
 (0)