Skip to content

Commit 5efb218

Browse files
bgilbertradarhere
andcommitted
Allow configuring JPEG restart marker interval on save
libjpeg allows specifying the marker interval either in MCU blocks or in MCU rows. Support both, via separate parameters, rather than requiring callers to do the math. Co-authored-by: Andrew Murray <[email protected]>
1 parent d05ff50 commit 5efb218

File tree

6 files changed

+47
-1
lines changed

6 files changed

+47
-1
lines changed

Tests/test_file_jpeg.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,26 @@ def test_save_low_quality_baseline_qtables(self):
643643
assert max(im2.quantization[0]) <= 255
644644
assert max(im2.quantization[1]) <= 255
645645

646+
@pytest.mark.parametrize(
647+
"blocks, rows, markers",
648+
((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)),
649+
)
650+
def test_restart_markers(self, blocks, rows, markers):
651+
im = Image.new("RGB", (32, 32)) # 16 MCUs
652+
out = BytesIO()
653+
im.save(
654+
out,
655+
format="JPEG",
656+
restart_marker_blocks=blocks,
657+
restart_marker_rows=rows,
658+
# force 8x8 pixel MCUs
659+
subsampling=0,
660+
)
661+
markers_found = sum(
662+
len(out.getvalue().split(bytes([0xFF, 0xD0 + i]))) - 1 for i in range(8)
663+
)
664+
assert markers_found == markers
665+
646666
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
647667
def test_load_djpeg(self):
648668
with Image.open(TEST_FILE) as img:

docs/handbook/image-file-formats.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
494494

495495
If absent, the setting will be determined by libjpeg or libjpeg-turbo.
496496

497+
**restart_marker_blocks**
498+
If present, emit a restart marker whenever the specified number of MCU
499+
blocks has been produced.
500+
501+
.. versionadded:: 10.2.0
502+
503+
**restart_marker_rows**
504+
If present, emit a restart marker whenever the specified number of MCU
505+
rows has been produced.
506+
507+
.. versionadded:: 10.2.0
508+
497509
**qtables**
498510
If present, sets the qtables for the encoder. This is listed as an
499511
advanced option for wizards in the JPEG documentation. Use with

src/PIL/JpegImagePlugin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,8 @@ def validate_qtables(qtables):
787787
dpi[0],
788788
dpi[1],
789789
subsampling,
790+
info.get("restart_marker_blocks", 0),
791+
info.get("restart_marker_rows", 0),
790792
qtables,
791793
comment,
792794
extra,

src/encode.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
10451045
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
10461046
Py_ssize_t xdpi = 0, ydpi = 0;
10471047
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
1048+
Py_ssize_t restart_marker_blocks = 0;
1049+
Py_ssize_t restart_marker_rows = 0;
10481050
PyObject *qtables = NULL;
10491051
unsigned int *qarrays = NULL;
10501052
int qtablesLen = 0;
@@ -1057,7 +1059,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
10571059

10581060
if (!PyArg_ParseTuple(
10591061
args,
1060-
"ss|nnnnnnnnOz#y#y#",
1062+
"ss|nnnnnnnnnnOz#y#y#",
10611063
&mode,
10621064
&rawmode,
10631065
&quality,
@@ -1068,6 +1070,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
10681070
&xdpi,
10691071
&ydpi,
10701072
&subsampling,
1073+
&restart_marker_blocks,
1074+
&restart_marker_rows,
10711075
&qtables,
10721076
&comment,
10731077
&comment_size,
@@ -1156,6 +1160,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
11561160
((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype;
11571161
((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi;
11581162
((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi;
1163+
((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_blocks = restart_marker_blocks;
1164+
((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = restart_marker_rows;
11591165
((JPEGENCODERSTATE *)encoder->state.context)->comment = comment;
11601166
((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size;
11611167
((JPEGENCODERSTATE *)encoder->state.context)->extra = extra;

src/libImaging/Jpeg.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ typedef struct {
8383
/* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */
8484
int subsampling;
8585

86+
/* Restart marker interval, in MCU blocks or MCU rows, or 0 for none */
87+
unsigned int restart_marker_blocks;
88+
unsigned int restart_marker_rows;
89+
8690
/* Converter input mode (input to the shuffler) */
8791
char rawmode[8 + 1];
8892

src/libImaging/JpegEncode.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
210210
}
211211
context->cinfo.smoothing_factor = context->smooth;
212212
context->cinfo.optimize_coding = (boolean)context->optimize;
213+
context->cinfo.restart_interval = context->restart_marker_blocks;
214+
context->cinfo.restart_in_rows = context->restart_marker_rows;
213215
if (context->xdpi > 0 && context->ydpi > 0) {
214216
context->cinfo.write_JFIF_header = TRUE;
215217
context->cinfo.density_unit = 1; /* dots per inch */

0 commit comments

Comments
 (0)