Skip to content

Commit 5f387aa

Browse files
committed
write 10bit HDR10PQ AVIF image
1 parent 153c277 commit 5f387aa

3 files changed

Lines changed: 99 additions & 10 deletions

File tree

README.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
# pillow-avif-plugin
2-
3-
This is a plugin that adds support for AVIF files until official support has been added (see [this pull request](https://github.com/python-pillow/Pillow/pull/5201)).
4-
5-
To register this plugin with pillow you will need to add `import pillow_avif` somewhere in your application.
1+
# pillow-avif-plugin quick hack to support HDR10 ouput
62

73
## how to build and install on Windows
84

@@ -12,12 +8,30 @@ To register this plugin with pillow you will need to add `import pillow_avif` so
128
- install TortoiseGit
139
- install MSYS2 and add C:\MSYS\usr\bin to PATH environment variable
1410
- download https://www.nasm.us/pub/nasm/releasebuilds/2.15.05/win64/nasm-2.15.05-win64.zip , unzip onto C:\apps\NASM and add D:\apps\NASM to PATH environment variable.
15-
- on Administrator miniforge prompt, pip install pillow meson
11+
- on Administrator miniforge prompt, pip install meson
12+
- build and install quick hack pillow to support R16G16B16 https://github.com/manoreken2/Pillow/tree/main/winbuild
1613
- on Administrator miniforge prompt, cd to this directory, cd winbuild and python build_prepare.py
1714
- cd winbuild/build and build_dep_all.cmd and build_pillow_avif_plugin.cmd
1815
- cd to this directory and python -m pip install -e .
1916
- pip list should show pillow-avif-plugin
2017

18+
## how to write HDR10PQ AVIF file
19+
20+
```
21+
from PIL import Image
22+
from pillow_avif import AvifImagePlugin
23+
from pillow_avif import _avif
24+
import cv2
25+
26+
imCV = cv2.imread("a.png", cv2.IMREAD_UNCHANGED)
27+
imCV = cv2.cvtColor(imCV, cv2.COLOR_BGR2RGB)
28+
29+
print("imCV {0} {1}".format(imCV.dtype, imCV.shape))
30+
31+
im = Image.fromarray(imCV, mode="R16G16B16")
32+
im.save("out.avif", quality=100, range="full", subsampling="4:4:4", depth=10, color_primaries=9, transfer_characteristics=16, matrix_coefficients=0)
33+
```
34+
2135
## encoding options
2236

2337
## "qmin"

src/pillow_avif/AvifImagePlugin.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
DECODE_CODEC_CHOICE = "auto"
1818
CHROMA_UPSAMPLING = "auto"
1919

20-
_VALID_AVIF_MODES = {"RGB", "RGBA"}
20+
_VALID_AVIF_MODES = {"RGB", "RGBA", "R16G16B16"}
2121

2222

2323
if sys.version_info[0] == 2:
@@ -147,6 +147,7 @@ def _save(im, fp, filename, save_all=False):
147147
speed = info.get("speed", 6)
148148
codec = info.get("codec", "auto")
149149
range_ = info.get("range", "full")
150+
depth = info.get("depth", 8)
150151

151152
color_primaries = info.get("color_primaries", 1)
152153
transfer_characteristics = info.get("transfer_characteristics", 1)
@@ -195,7 +196,7 @@ def _save(im, fp, filename, save_all=False):
195196
speed,
196197
codec,
197198
range_,
198-
qcolor,
199+
depth,
199200
color_primaries,
200201
transfer_characteristics,
201202
matrix_coefficients,
@@ -225,7 +226,13 @@ def _save(im, fp, filename, save_all=False):
225226
# Make sure image mode is supported
226227
frame = ims
227228
rawmode = ims.mode
229+
230+
print("AvifImagePlugin.py _save rawmode={0}".format(rawmode))
231+
228232
if ims.mode not in _VALID_AVIF_MODES:
233+
234+
print("AvifImagePlugin.py _save ims.mode is not valid avif mode. converting. {0}".format(ims.mode))
235+
229236
alpha = (
230237
"A" in ims.mode
231238
or "a" in ims.mode
@@ -241,6 +248,9 @@ def _save(im, fp, filename, save_all=False):
241248
frame_dur = duration
242249

243250
# Append the frame to the animation encoder
251+
252+
print("AvifImagePlugin.py _save enc.add start")
253+
244254
enc.add(
245255
frame.tobytes("raw", rawmode),
246256
frame_dur,
@@ -250,6 +260,8 @@ def _save(im, fp, filename, save_all=False):
250260
is_single_frame,
251261
)
252262

263+
print("AvifImagePlugin.py _save enc.add end")
264+
253265
# Update frame index
254266
frame_idx += 1
255267

@@ -266,6 +278,8 @@ def _save(im, fp, filename, save_all=False):
266278

267279
fp.write(data)
268280

281+
print("AvifImagePlugin.py _save end.")
282+
269283

270284
Image.register_open(AvifImageFile.format, AvifImageFile, _accept)
271285
if SUPPORTED:

src/pillow_avif/_avif.c

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
202202
AvifEncoderObject *self = NULL;
203203
avifEncoder *encoder = NULL;
204204

205+
printf("%s:%d AvifEncoderNew\n", __FILE__, __LINE__);
206+
205207
char *subsampling = "4:2:0";
206208
int qmin = AVIF_QUANTIZER_BEST_QUALITY; // =0
207209
int qmax = 10; // "High Quality", but not lossless
@@ -226,7 +228,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
226228

227229
if (!PyArg_ParseTuple(
228230
args,
229-
"IIsiiissiiOOSSSO",
231+
"IIsiiissiiiiiiOOSSSO",
230232
&width,
231233
&height,
232234
&subsampling,
@@ -346,6 +348,24 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
346348
_add_codec_specific_options(encoder, advanced);
347349
#endif
348350

351+
printf(
352+
" maxT=%d qmin=%d qmax=%d codec=%d speed=%d yuvRange=%d subsampling=%d "
353+
"color_primaries=%d transferCharacteristics=%d matrix_coefficients=%d "
354+
"width=%d height=%d depth=%d\n",
355+
max_threads,
356+
enc_options.qmin,
357+
enc_options.qmax,
358+
(int)enc_options.codec,
359+
enc_options.speed,
360+
(int)enc_options.range,
361+
(int)enc_options.subsampling,
362+
color_primaries,
363+
transfer_characteristics,
364+
matrix_coefficients,
365+
width,
366+
height,
367+
depth);
368+
349369
self->encoder = encoder;
350370

351371
avifImage *image = avifImageCreateEmpty();
@@ -431,6 +451,8 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
431451
avifImage *image = self->image;
432452
avifImage *frame = NULL;
433453

454+
printf("%s:%d _encoder_add\n", __FILE__, __LINE__);
455+
434456
if (!PyArg_ParseTuple(
435457
args,
436458
"z#IIIsO",
@@ -483,16 +505,21 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
483505
rgb.depth = self->image->depth;
484506

485507
if (strcmp(mode, "RGBA") == 0) {
508+
printf("%s:%d mode=%s\n", __FILE__, __LINE__, mode);
486509
rgb.format = AVIF_RGB_FORMAT_RGBA;
487510
channels = 4;
488511
} else {
512+
printf("%s:%d mode=%s\n", __FILE__, __LINE__, mode);
489513
rgb.format = AVIF_RGB_FORMAT_RGB;
490514
channels = 3;
491515
}
492516

493517
avifRGBImageAllocatePixels(&rgb);
494518

519+
printf(" rgb.rowBytes=%u rgb.height=%u size=%d\n", rgb.rowBytes, rgb.height, (int)size);
520+
495521
if (rgb.rowBytes * rgb.height != size) {
522+
printf("%s:%d\n", __FILE__, __LINE__);
496523
PyErr_Format(
497524
PyExc_RuntimeError,
498525
"rgb data is incorrect size: %u * %u (%u) != %zd",
@@ -505,13 +532,39 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
505532
}
506533

507534
// rgb.pixels is safe for writes
508-
memcpy(rgb.pixels, rgb_bytes, size);
535+
{
536+
int w = 0;
537+
const uint16_t *hR = (const uint16_t *)rgb_bytes;
538+
uint16_t *hW = (uint16_t *)rgb.pixels;
539+
switch (frame->depth) {
540+
case 8:
541+
// original code path.
542+
memcpy(rgb.pixels, rgb_bytes, size);
543+
break;
544+
case 10:
545+
// R16G16B16 16bit => 10bit
546+
for (int i = 0; i < rgb.width * rgb.height * 3; ++i) {
547+
hW[i] = hR[i] >> 6;
548+
}
549+
break;
550+
case 12:
551+
// R16G16B16 16bit => 12bit
552+
for (int i = 0; i < rgb.width * rgb.height * 3; ++i) {
553+
hW[i] = hR[i] >> 4;
554+
}
555+
break;
556+
default:
557+
assert(0);
558+
break;
559+
}
560+
}
509561

510562
Py_BEGIN_ALLOW_THREADS
511563
result = avifImageRGBToYUV(frame, &rgb);
512564
Py_END_ALLOW_THREADS
513565

514566
if (result != AVIF_RESULT_OK) {
567+
printf("%s:%d\n", __FILE__, __LINE__);
515568
PyErr_Format(
516569
exc_type_for_avif_result(result),
517570
"Conversion to YUV failed: %s",
@@ -522,6 +575,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
522575

523576
uint32_t addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE;
524577
if (PyObject_IsTrue(is_single_frame)) {
578+
printf("%s:%d\n", __FILE__, __LINE__);
525579
addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE;
526580
}
527581

@@ -530,6 +584,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
530584
Py_END_ALLOW_THREADS
531585

532586
if (result != AVIF_RESULT_OK) {
587+
printf("%s:%d\n", __FILE__, __LINE__);
533588
PyErr_Format(
534589
exc_type_for_avif_result(result),
535590
"Failed to encode image: %s",
@@ -539,15 +594,19 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
539594
}
540595

541596
end:
597+
printf("%s:%d\n", __FILE__, __LINE__);
542598
avifRGBImageFreePixels(&rgb);
543599
if (!is_first_frame) {
600+
printf("%s:%d\n", __FILE__, __LINE__);
544601
avifImageDestroy(frame);
545602
}
546603

547604
if (ret == Py_None) {
605+
printf("%s:%d _encoder_add success. frame_index=%d\n", __FILE__, __LINE__, self->frame_index);
548606
self->frame_index++;
549607
Py_RETURN_NONE;
550608
} else {
609+
printf("%s:%d error\n", __FILE__, __LINE__);
551610
return ret;
552611
}
553612
}
@@ -556,6 +615,8 @@ PyObject *
556615
_encoder_finish(AvifEncoderObject *self) {
557616
avifEncoder *encoder = self->encoder;
558617

618+
printf("%s:%d _encoder_finish\n", __FILE__, __LINE__);
619+
559620
avifRWData raw = AVIF_DATA_EMPTY;
560621
avifResult result;
561622
PyObject *ret = NULL;

0 commit comments

Comments
 (0)