mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-05-28 00:30:23 -04:00
Add JPEG 2000 support
JPEG 2000 support using OpenJPEG library. - Add read/write support to JP2/J2K format for Gray/RGB(A)/CMYK images @8/16-bits - Read test case images generated by Photoshop The plugin has the following limitations: - Resolution is not set (JP2_RES box is marked "For the future" in jp2.h) - Metadata are not set (as with resolution) Closes #13
This commit is contained in:
parent
e6a0f8758b
commit
c97ee00f5e
@ -78,6 +78,13 @@ if(KIMAGEFORMATS_JXL)
|
|||||||
endif()
|
endif()
|
||||||
add_feature_info(LibJXL LibJXL_FOUND "required for the QImage plugin for JPEG XL images")
|
add_feature_info(LibJXL LibJXL_FOUND "required for the QImage plugin for JPEG XL images")
|
||||||
|
|
||||||
|
option(KIMAGEFORMATS_JP2 "Enable plugin for JPEG 2000 format" ON)
|
||||||
|
if(KIMAGEFORMATS_JP2)
|
||||||
|
find_package(OpenJPEG CONFIG)
|
||||||
|
endif()
|
||||||
|
add_feature_info(OpenJPEG OpenJPEG_FOUND "required for the QImage plugin for JPEG 2000 images")
|
||||||
|
|
||||||
|
|
||||||
find_package(LibRaw 0.20.2)
|
find_package(LibRaw 0.20.2)
|
||||||
set_package_properties(LibRaw PROPERTIES
|
set_package_properties(LibRaw PROPERTIES
|
||||||
TYPE OPTIONAL
|
TYPE OPTIONAL
|
||||||
|
21
README.md
21
README.md
@ -32,6 +32,7 @@ The following image formats have read and write support:
|
|||||||
- DirectDraw Surface (dds)
|
- DirectDraw Surface (dds)
|
||||||
- Encapsulated PostScript (eps)
|
- Encapsulated PostScript (eps)
|
||||||
- High Efficiency Image File Format (heif)
|
- High Efficiency Image File Format (heif)
|
||||||
|
- JPEG 2000 (jp2, j2k, jpf)
|
||||||
- JPEG XL (jxl)
|
- JPEG XL (jxl)
|
||||||
- JPEG XR (jxr)
|
- JPEG XR (jxr)
|
||||||
- OpenEXR (exr)
|
- OpenEXR (exr)
|
||||||
@ -75,6 +76,12 @@ The DDS plugin is a fork from Qt 5.6 with bug fixes and improvements.
|
|||||||
|
|
||||||
The plugin was forked because Qt Project no longer supports its DDS plugin.
|
The plugin was forked because Qt Project no longer supports its DDS plugin.
|
||||||
|
|
||||||
|
### The JP2 plugin
|
||||||
|
|
||||||
|
The JP2 plugin is based on the popular and wide used OpenJPEG library.
|
||||||
|
|
||||||
|
The Qt project has a no longer supported JPEG 2000 plugin based on Jasper.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This framework is licensed under the
|
This framework is licensed under the
|
||||||
@ -134,6 +141,7 @@ plugin ('n/a' means no limit, i.e. the limit depends on the format encoding).
|
|||||||
- EPS: n/a
|
- EPS: n/a
|
||||||
- HDR: n/a (large image)
|
- HDR: n/a (large image)
|
||||||
- HEIF: n/a
|
- HEIF: n/a
|
||||||
|
- JP2: 300,000 x 300,000 pixels
|
||||||
- JXL: 262,144 x 262,144 pixels, in any case no larger than 256 megapixels
|
- JXL: 262,144 x 262,144 pixels, in any case no larger than 256 megapixels
|
||||||
- JXR: n/a, in any case no larger than 4 GB
|
- JXR: n/a, in any case no larger than 4 GB
|
||||||
- KRA: same size as Qt's PNG plugin
|
- KRA: same size as Qt's PNG plugin
|
||||||
@ -178,8 +186,8 @@ been used or the maximum size of the image that can be saved has been limited.
|
|||||||
PSD plugin loads CMYK, Lab and Multichannel images and converts them to RGB
|
PSD plugin loads CMYK, Lab and Multichannel images and converts them to RGB
|
||||||
without using the ICC profile.
|
without using the ICC profile.
|
||||||
|
|
||||||
JXR, PSD and SCT plugins natively support 4-channel CMYK images when compiled
|
JP2, JXR, PSD and SCT plugins natively support 4-channel CMYK images when
|
||||||
with Qt 6.8+.
|
compiled with Qt 6.8+.
|
||||||
|
|
||||||
### The DDS plugin
|
### The DDS plugin
|
||||||
|
|
||||||
@ -218,6 +226,15 @@ create a temporary PDF file which is then converted to EPS. Therefore, if
|
|||||||
The following defines can be defined in cmake to modify the behavior of the plugin:
|
The following defines can be defined in cmake to modify the behavior of the plugin:
|
||||||
- `HDR_HALF_QUALITY`: on read, a 16-bit float image is returned instead of a 32-bit float one.
|
- `HDR_HALF_QUALITY`: on read, a 16-bit float image is returned instead of a 32-bit float one.
|
||||||
|
|
||||||
|
### The JP2 plugin
|
||||||
|
|
||||||
|
**This plugin can be disabled by setting `KIMAGEFORMATS_JP2` to `OFF`
|
||||||
|
in your cmake options.**
|
||||||
|
|
||||||
|
JP2 plugin has the following limitations due to the lack of support by OpenJPEG:
|
||||||
|
- Metadata are not supported
|
||||||
|
- Image resolution is not supported
|
||||||
|
|
||||||
### The JXL plugin
|
### The JXL plugin
|
||||||
|
|
||||||
**The current version of the plugin limits the image size to 256 megapixels
|
**The current version of the plugin limits the image size to 256 megapixels
|
||||||
|
@ -119,6 +119,18 @@ if (LibHeif_FOUND)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (OpenJPEG_FOUND)
|
||||||
|
kimageformats_read_tests(
|
||||||
|
jp2
|
||||||
|
)
|
||||||
|
# CMYK writing is incorrect in versions before 2.5.3
|
||||||
|
if ("${OPENJPEG_MAJOR_VERSION}.${OPENJPEG_MINOR_VERSION}.${OPENJPEG_BUILD_VERSION}" VERSION_GREATER_EQUAL "2.5.3")
|
||||||
|
kimageformats_write_tests(
|
||||||
|
jp2-nodatacheck-lossless
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
||||||
kimageformats_read_tests(
|
kimageformats_read_tests(
|
||||||
jxl
|
jxl
|
||||||
|
BIN
autotests/read/jp2/testcard_cmyk.jp2
Normal file
BIN
autotests/read/jp2/testcard_cmyk.jp2
Normal file
Binary file not shown.
11
autotests/read/jp2/testcard_cmyk.jp2.json
Normal file
11
autotests/read/jp2/testcard_cmyk.jp2.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"minQtVersion" : "6.8.0",
|
||||||
|
"fileName" : "testcard_cmyk.tif"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maxQtVersion" : "6.7.99",
|
||||||
|
"unsupportedFormat" : true,
|
||||||
|
"comment" : "Qt versions lower than 6.8 do not support CMYK format so this test should be skipped."
|
||||||
|
}
|
||||||
|
]
|
BIN
autotests/read/jp2/testcard_cmyk.tif
Normal file
BIN
autotests/read/jp2/testcard_cmyk.tif
Normal file
Binary file not shown.
BIN
autotests/read/jp2/testcard_cmyk16.jp2
Normal file
BIN
autotests/read/jp2/testcard_cmyk16.jp2
Normal file
Binary file not shown.
11
autotests/read/jp2/testcard_cmyk16.jp2.json
Normal file
11
autotests/read/jp2/testcard_cmyk16.jp2.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"minQtVersion" : "6.8.0",
|
||||||
|
"fileName" : "testcard_cmyk16.tif"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maxQtVersion" : "6.7.99",
|
||||||
|
"unsupportedFormat" : true,
|
||||||
|
"comment" : "Qt versions lower than 6.8 do not support CMYK format so this test should be skipped."
|
||||||
|
}
|
||||||
|
]
|
BIN
autotests/read/jp2/testcard_cmyk16.tif
Normal file
BIN
autotests/read/jp2/testcard_cmyk16.tif
Normal file
Binary file not shown.
BIN
autotests/read/jp2/testcard_gray.jp2
Normal file
BIN
autotests/read/jp2/testcard_gray.jp2
Normal file
Binary file not shown.
BIN
autotests/read/jp2/testcard_gray.png
Normal file
BIN
autotests/read/jp2/testcard_gray.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
autotests/read/jp2/testcard_gray16.jp2
Normal file
BIN
autotests/read/jp2/testcard_gray16.jp2
Normal file
Binary file not shown.
BIN
autotests/read/jp2/testcard_gray16.png
Normal file
BIN
autotests/read/jp2/testcard_gray16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
autotests/read/jp2/testcard_rgb.jp2
Normal file
BIN
autotests/read/jp2/testcard_rgb.jp2
Normal file
Binary file not shown.
BIN
autotests/read/jp2/testcard_rgb.png
Normal file
BIN
autotests/read/jp2/testcard_rgb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
autotests/read/jp2/testcard_rgb16.jp2
Normal file
BIN
autotests/read/jp2/testcard_rgb16.jp2
Normal file
Binary file not shown.
BIN
autotests/read/jp2/testcard_rgb16.png
Normal file
BIN
autotests/read/jp2/testcard_rgb16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
autotests/read/jp2/testcard_rgba.j2k
Normal file
BIN
autotests/read/jp2/testcard_rgba.j2k
Normal file
Binary file not shown.
BIN
autotests/read/jp2/testcard_rgba.jp2
Normal file
BIN
autotests/read/jp2/testcard_rgba.jp2
Normal file
Binary file not shown.
BIN
autotests/read/jp2/testcard_rgba.png
Normal file
BIN
autotests/read/jp2/testcard_rgba.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
autotests/read/jp2/testcard_rgba16.jp2
Normal file
BIN
autotests/read/jp2/testcard_rgba16.jp2
Normal file
Binary file not shown.
BIN
autotests/read/jp2/testcard_rgba16.png
Normal file
BIN
autotests/read/jp2/testcard_rgba16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
autotests/write/format/jp2/Format_A2BGR30_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_A2BGR30_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_A2RGB30_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_A2RGB30_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_ARGB32.jp2
Normal file
BIN
autotests/write/format/jp2/Format_ARGB32.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_ARGB32_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_ARGB32_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_ARGB4444_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_ARGB4444_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_ARGB6666_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_ARGB6666_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_ARGB8555_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_ARGB8555_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_ARGB8565_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_ARGB8565_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_BGR30.jp2
Normal file
BIN
autotests/write/format/jp2/Format_BGR30.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_BGR888.jp2
Normal file
BIN
autotests/write/format/jp2/Format_BGR888.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_CMYK8888.jp2
Normal file
BIN
autotests/write/format/jp2/Format_CMYK8888.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_Grayscale16.jp2
Normal file
BIN
autotests/write/format/jp2/Format_Grayscale16.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_Grayscale8.jp2
Normal file
BIN
autotests/write/format/jp2/Format_Grayscale8.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_Indexed8.jp2
Normal file
BIN
autotests/write/format/jp2/Format_Indexed8.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_Mono.jp2
Normal file
BIN
autotests/write/format/jp2/Format_Mono.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_MonoLSB.jp2
Normal file
BIN
autotests/write/format/jp2/Format_MonoLSB.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGB16.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGB16.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGB30.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGB30.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGB32.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGB32.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGB444.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGB444.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGB555.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGB555.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGB666.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGB666.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGB888.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGB888.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBA16FPx4.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBA16FPx4.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBA16FPx4_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBA16FPx4_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBA32FPx4.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBA32FPx4.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBA32FPx4_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBA32FPx4_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBA64.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBA64.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBA64_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBA64_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBA8888.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBA8888.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBA8888_Premultiplied.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBA8888_Premultiplied.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBX16FPx4.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBX16FPx4.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBX32FPx4.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBX32FPx4.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBX64.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBX64.jp2
Normal file
Binary file not shown.
BIN
autotests/write/format/jp2/Format_RGBX8888.jp2
Normal file
BIN
autotests/write/format/jp2/Format_RGBX8888.jp2
Normal file
Binary file not shown.
@ -86,6 +86,12 @@ if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (OpenJPEG_FOUND)
|
||||||
|
kimageformats_add_plugin(kimg_jp2 SOURCES jp2.cpp scanlineconverter.cpp)
|
||||||
|
target_include_directories(kimg_jp2 PRIVATE ${OPENJPEG_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(kimg_jp2 PRIVATE ${OPENJPEG_LIBRARIES})
|
||||||
|
endif()
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
kimageformats_add_plugin(kimg_pcx SOURCES pcx.cpp)
|
kimageformats_add_plugin(kimg_pcx SOURCES pcx.cpp)
|
||||||
|
848
src/imageformats/jp2.cpp
Normal file
848
src/imageformats/jp2.cpp
Normal file
@ -0,0 +1,848 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the KDE project
|
||||||
|
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "jp2_p.h"
|
||||||
|
#include "scanlineconverter_p.h"
|
||||||
|
#include "util_p.h"
|
||||||
|
|
||||||
|
#include <QColorSpace>
|
||||||
|
#include <QIODevice>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QImageReader>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include <openjpeg.h>
|
||||||
|
|
||||||
|
/* *** JP2_MAX_IMAGE_WIDTH and JP2_MAX_IMAGE_HEIGHT ***
|
||||||
|
* The maximum size in pixel allowed by the plugin.
|
||||||
|
*/
|
||||||
|
#ifndef JP2_MAX_IMAGE_WIDTH
|
||||||
|
#define JP2_MAX_IMAGE_WIDTH 300000
|
||||||
|
#endif
|
||||||
|
#ifndef JP2_MAX_IMAGE_HEIGHT
|
||||||
|
#define JP2_MAX_IMAGE_HEIGHT JP2_MAX_IMAGE_WIDTH
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* *** JP2_ENABLE_HDR ***
|
||||||
|
* Enable float image formats. Disabled by default
|
||||||
|
* due to lack of test images.
|
||||||
|
*/
|
||||||
|
#ifndef JP2_ENABLE_HDR
|
||||||
|
// #define JP2_ENABLE_HDR
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define JP2_SUBTYPE QByteArrayLiteral("JP2")
|
||||||
|
#define J2K_SUBTYPE QByteArrayLiteral("J2K")
|
||||||
|
|
||||||
|
static void error_callback(const char *msg, void *client_data)
|
||||||
|
{
|
||||||
|
Q_UNUSED(client_data)
|
||||||
|
qCritical() << msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void warning_callback(const char *msg, void *client_data)
|
||||||
|
{
|
||||||
|
Q_UNUSED(client_data)
|
||||||
|
qWarning() << msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void info_callback(const char *msg, void *client_data)
|
||||||
|
{
|
||||||
|
Q_UNUSED(client_data)
|
||||||
|
qInfo() << msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
static OPJ_SIZE_T jp2_read(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data)
|
||||||
|
{
|
||||||
|
auto dev = (QIODevice*)p_user_data;
|
||||||
|
if (dev == nullptr) {
|
||||||
|
return OPJ_SIZE_T(-1);
|
||||||
|
}
|
||||||
|
return OPJ_SIZE_T(dev->read((char*)p_buffer, (qint64)p_nb_bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
static OPJ_SIZE_T jp2_write(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data)
|
||||||
|
{
|
||||||
|
auto dev = (QIODevice*)p_user_data;
|
||||||
|
if (dev == nullptr) {
|
||||||
|
return OPJ_SIZE_T(-1);
|
||||||
|
}
|
||||||
|
return OPJ_SIZE_T(dev->write((char*)p_buffer, (qint64)p_nb_bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
static OPJ_BOOL jp2_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data)
|
||||||
|
{
|
||||||
|
auto dev = (QIODevice*)p_user_data;
|
||||||
|
if (dev == nullptr) {
|
||||||
|
return OPJ_FALSE;
|
||||||
|
}
|
||||||
|
return dev->seek(p_nb_bytes) ? OPJ_TRUE : OPJ_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static OPJ_OFF_T jp2_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data)
|
||||||
|
{
|
||||||
|
auto dev = (QIODevice*)p_user_data;
|
||||||
|
if (dev == nullptr) {
|
||||||
|
return OPJ_OFF_T();
|
||||||
|
}
|
||||||
|
if (dev->seek(dev->pos() + p_nb_bytes)) {
|
||||||
|
return p_nb_bytes;
|
||||||
|
}
|
||||||
|
return OPJ_OFF_T();
|
||||||
|
}
|
||||||
|
|
||||||
|
class JP2HandlerPrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
JP2HandlerPrivate()
|
||||||
|
: m_jp2_stream(nullptr)
|
||||||
|
, m_jp2_image(nullptr)
|
||||||
|
, m_jp2_codec(nullptr)
|
||||||
|
, m_quality(-1)
|
||||||
|
, m_subtype(JP2_SUBTYPE)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
~JP2HandlerPrivate()
|
||||||
|
{
|
||||||
|
if (m_jp2_image) {
|
||||||
|
opj_image_destroy(m_jp2_image);
|
||||||
|
m_jp2_image = nullptr;
|
||||||
|
}
|
||||||
|
if (m_jp2_stream) {
|
||||||
|
opj_stream_destroy(m_jp2_stream);
|
||||||
|
m_jp2_stream = nullptr;
|
||||||
|
}
|
||||||
|
if (m_jp2_codec) {
|
||||||
|
opj_destroy_codec(m_jp2_codec);
|
||||||
|
m_jp2_codec = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief detectDecoderFormat
|
||||||
|
* \param device
|
||||||
|
* \return The codec JP2 found.
|
||||||
|
*/
|
||||||
|
OPJ_CODEC_FORMAT detectDecoderFormat(QIODevice *device) const
|
||||||
|
{
|
||||||
|
auto ba = device->peek(32);
|
||||||
|
if (ba.left(12) == QByteArray::fromHex("0000000c6a5020200d0a870a")) {
|
||||||
|
// if (ba.mid(20, 4) == QByteArray::fromHex("6a707820")) // 'jpx '
|
||||||
|
// return OPJ_CODEC_JPX; // JPEG 2000 Part 2 (not supported -> try reading as JP2)
|
||||||
|
return OPJ_CODEC_JP2;
|
||||||
|
}
|
||||||
|
if (ba.left(5) == QByteArray::fromHex("ff4fff5100")) {
|
||||||
|
return OPJ_CODEC_J2K;
|
||||||
|
}
|
||||||
|
return OPJ_CODEC_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool createStream(QIODevice *device, bool read)
|
||||||
|
{
|
||||||
|
if (device == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_jp2_stream == nullptr) {
|
||||||
|
m_jp2_stream = opj_stream_default_create(read ? OPJ_TRUE : OPJ_FALSE);
|
||||||
|
}
|
||||||
|
if (m_jp2_stream == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
opj_stream_set_user_data(m_jp2_stream, device, nullptr);
|
||||||
|
opj_stream_set_user_data_length(m_jp2_stream, read ? device->size() : 0);
|
||||||
|
opj_stream_set_read_function(m_jp2_stream, jp2_read);
|
||||||
|
opj_stream_set_write_function(m_jp2_stream, jp2_write);
|
||||||
|
opj_stream_set_skip_function(m_jp2_stream, jp2_skip);
|
||||||
|
opj_stream_set_seek_function(m_jp2_stream, jp2_seek);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isImageValid(const opj_image_t *i) const
|
||||||
|
{
|
||||||
|
return i && i->comps && i->numcomps > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enableThreads(opj_codec_t *codec) const
|
||||||
|
{
|
||||||
|
if (!opj_has_thread_support()) {
|
||||||
|
qInfo() << "OpenJPEG doesn't support multi-threading!";
|
||||||
|
} else if (!opj_codec_set_threads(codec, std::max(1, QThread::idealThreadCount() / 2))) {
|
||||||
|
qWarning() << "Unable to enable multi-threading!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool createDecoder(QIODevice *device)
|
||||||
|
{
|
||||||
|
if (m_jp2_codec) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
auto jp2Format = detectDecoderFormat(device);
|
||||||
|
if (jp2Format == OPJ_CODEC_UNKNOWN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_jp2_codec = opj_create_decompress(jp2Format);
|
||||||
|
if (m_jp2_codec == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
enableThreads(m_jp2_codec);
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
// opj_set_info_handler(m_jp2_codec, info_callback, nullptr);
|
||||||
|
// opj_set_warning_handler(m_jp2_codec, warning_callback, nullptr);
|
||||||
|
#endif
|
||||||
|
opj_set_error_handler(m_jp2_codec, error_callback, nullptr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool readHeader(QIODevice *device)
|
||||||
|
{
|
||||||
|
if (!createStream(device, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_jp2_image) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createDecoder(device)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
opj_set_default_decoder_parameters(&m_dparameters);
|
||||||
|
if (!opj_setup_decoder(m_jp2_codec, &m_dparameters)) {
|
||||||
|
qCritical() << "Failed to setup JP2 decoder!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opj_read_header(m_jp2_stream, m_jp2_codec, &m_jp2_image)) {
|
||||||
|
qCritical() << "Failed to read JP2 header!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isImageValid(m_jp2_image);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
bool jp2ToImage(QImage *img) const
|
||||||
|
{
|
||||||
|
Q_ASSERT(img->depth() == 8 * sizeof(T) || img->depth() == 32 * sizeof(T));
|
||||||
|
for (qint32 c = 0, cc = m_jp2_image->numcomps; c < cc; ++c) {
|
||||||
|
auto cs = cc == 1 ? 1 : 4;
|
||||||
|
auto &&jc = m_jp2_image->comps[c];
|
||||||
|
if (jc.data == nullptr)
|
||||||
|
return false;
|
||||||
|
if (qint32(jc.w) != img->width() || qint32(jc.h) != img->height())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// discriminate between int and float (avoid complicating things by creating classes with template specializations)
|
||||||
|
if (std::numeric_limits<T>::is_integer) {
|
||||||
|
auto divisor = 1;
|
||||||
|
if (jc.prec > sizeof(T) * 8) {
|
||||||
|
// convert to the wanted precision (e.g. 16-bit -> 8-bit: divisor = 65535 / 255 = 257)
|
||||||
|
divisor = std::max(1, int(((1ll << jc.prec) - 1) / ((1ll << (sizeof(T) * 8)) - 1)));
|
||||||
|
}
|
||||||
|
for (qint32 y = 0, h = img->height(); y < h; ++y) {
|
||||||
|
auto ptr = reinterpret_cast<T *>(img->scanLine(y));
|
||||||
|
for (qint32 x = 0, w = img->width(); x < w; ++x) {
|
||||||
|
qint32 v = jc.data[y * w + x] / divisor;
|
||||||
|
if (jc.sgnd) // never seen
|
||||||
|
v -= std::numeric_limits<typename std::make_signed<T>::type>::min();
|
||||||
|
*(ptr + x * cs + c) = std::clamp(v, qint32(std::numeric_limits<T>::lowest()), qint32(std::numeric_limits<T>::max()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // float
|
||||||
|
for (qint32 y = 0, h = img->height(); y < h; ++y) {
|
||||||
|
auto ptr = reinterpret_cast<T *>(img->scanLine(y));
|
||||||
|
for (qint32 x = 0, w = img->width(); x < w; ++x) {
|
||||||
|
*(ptr + x * cs + c) = jc.data[y * w + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
void alphaFix(QImage *img) const
|
||||||
|
{
|
||||||
|
if (m_jp2_image->numcomps == 3) {
|
||||||
|
Q_ASSERT(img->depth() == 32 * sizeof(T));
|
||||||
|
for (qint32 y = 0, h = img->height(); y < h; ++y) {
|
||||||
|
auto ptr = reinterpret_cast<T *>(img->scanLine(y));
|
||||||
|
for (qint32 x = 0, w = img->width(); x < w; ++x) {
|
||||||
|
*(ptr + x * 4 + 3) = std::numeric_limits<T>::is_iec559 ? 1 : std::numeric_limits<T>::max();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage readImage(QIODevice *device)
|
||||||
|
{
|
||||||
|
if (!readHeader(device)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto img = imageAlloc(size(), format());
|
||||||
|
if (img.isNull()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opj_decode(m_jp2_codec, m_jp2_stream, m_jp2_image)) {
|
||||||
|
qCritical() << "Failed to decoding JP2 image!";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto f = img.format();
|
||||||
|
if (f == QImage::Format_RGBA32FPx4 || f == QImage::Format_RGBX32FPx4) {
|
||||||
|
if (!jp2ToImage<quint32>(&img))
|
||||||
|
return {};
|
||||||
|
alphaFix<float>(&img);
|
||||||
|
} else if (f == QImage::Format_RGBA64 || f == QImage::Format_RGBX64 || f == QImage::Format_Grayscale16) {
|
||||||
|
if (!jp2ToImage<quint16>(&img))
|
||||||
|
return {};
|
||||||
|
alphaFix<quint16>(&img);
|
||||||
|
} else {
|
||||||
|
if (!jp2ToImage<quint8>(&img))
|
||||||
|
return {};
|
||||||
|
alphaFix<quint8>(&img);
|
||||||
|
}
|
||||||
|
|
||||||
|
img.setColorSpace(colorSpace());
|
||||||
|
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkSizeLimits(qint32 width, qint32 height, qint32 nchannels) const
|
||||||
|
{
|
||||||
|
if (width > JP2_MAX_IMAGE_WIDTH || height > JP2_MAX_IMAGE_HEIGHT || width < 1 || height < 1) {
|
||||||
|
qCritical() << "Maximum image size is limited to" << JP2_MAX_IMAGE_WIDTH << "x" << JP2_MAX_IMAGE_HEIGHT << "pixels";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenJPEG uses a shadow copy @32-bit/channel so we need to do a check
|
||||||
|
auto maxBytes = qint64(QImageReader::allocationLimit()) * 1024 * 1024;
|
||||||
|
auto neededBytes = qint64(width) * height * nchannels * 4;
|
||||||
|
if (maxBytes > 0 && neededBytes > maxBytes) {
|
||||||
|
qCritical() << "Allocation limit set to" << (maxBytes / 1024 / 1024) << "MiB but" << (neededBytes / 1024 / 1024) << "MiB are needed!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkSizeLimits(const QSize &size, qint32 nchannels) const
|
||||||
|
{
|
||||||
|
return checkSizeLimits(size.width(), size.height(), nchannels);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize size() const
|
||||||
|
{
|
||||||
|
QSize sz;
|
||||||
|
if (isImageValid(m_jp2_image)) {
|
||||||
|
auto &&c0 = m_jp2_image->comps[0];
|
||||||
|
auto tmp = QSize(c0.w, c0.h);
|
||||||
|
if (checkSizeLimits(tmp, m_jp2_image->numcomps))
|
||||||
|
sz = tmp;
|
||||||
|
}
|
||||||
|
return sz;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage::Format format() const
|
||||||
|
{
|
||||||
|
auto fmt = QImage::Format_Invalid;
|
||||||
|
if (isImageValid(m_jp2_image)) {
|
||||||
|
auto &&c0 = m_jp2_image->comps[0];
|
||||||
|
auto prec = c0.prec;
|
||||||
|
for (quint32 c = 1; c < m_jp2_image->numcomps; ++c) {
|
||||||
|
auto &&cc = m_jp2_image->comps[c];
|
||||||
|
if (cc.prec != prec)
|
||||||
|
prec = 0;
|
||||||
|
}
|
||||||
|
auto jp2cs = m_jp2_image->color_space;
|
||||||
|
if (jp2cs == OPJ_CLRSPC_UNKNOWN || jp2cs == OPJ_CLRSPC_UNSPECIFIED) {
|
||||||
|
if (m_jp2_image->numcomps == 1)
|
||||||
|
jp2cs = OPJ_CLRSPC_GRAY;
|
||||||
|
else
|
||||||
|
jp2cs = OPJ_CLRSPC_SRGB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// *** IMPORTANT: To keep the code simple, the returned formats must have 1 or 4 channels (8/16/32-bits)
|
||||||
|
if (jp2cs == OPJ_CLRSPC_SRGB) {
|
||||||
|
if (m_jp2_image->numcomps == 3 || m_jp2_image->numcomps == 4) {
|
||||||
|
auto hasAlpha = m_jp2_image->numcomps == 4;
|
||||||
|
if (prec == 8)
|
||||||
|
fmt = hasAlpha ? QImage::Format_RGBA8888 : QImage::Format_RGBX8888;
|
||||||
|
else if (prec == 16)
|
||||||
|
fmt = hasAlpha ? QImage::Format_RGBA64 : QImage::Format_RGBX64;
|
||||||
|
#ifdef JP2_ENABLE_HDR
|
||||||
|
else if (prec == 32) // not sure about this
|
||||||
|
fmt = hasAlpha ? QImage::Format_RGBA32FPx4 : QImage::Format_RGBX32FPx4;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} else if (jp2cs == OPJ_CLRSPC_GRAY) {
|
||||||
|
if (m_jp2_image->numcomps == 1) {
|
||||||
|
if (prec == 8)
|
||||||
|
fmt = QImage::Format_Grayscale8;
|
||||||
|
else if (prec == 16)
|
||||||
|
fmt = QImage::Format_Grayscale16;
|
||||||
|
}
|
||||||
|
} else if (jp2cs == OPJ_CLRSPC_CMYK) {
|
||||||
|
if (m_jp2_image->numcomps == 4) {
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||||
|
if (prec == 8 || prec == 16)
|
||||||
|
fmt = QImage::Format_CMYK8888;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QColorSpace colorSpace() const
|
||||||
|
{
|
||||||
|
QColorSpace cs;
|
||||||
|
if (m_jp2_image) {
|
||||||
|
if (m_jp2_image->icc_profile_buf && m_jp2_image->icc_profile_len > 0) {
|
||||||
|
cs = QColorSpace::fromIccProfile(QByteArray((char *)m_jp2_image->icc_profile_buf, m_jp2_image->icc_profile_len));
|
||||||
|
}
|
||||||
|
if (!cs.isValid()) {
|
||||||
|
if (m_jp2_image->color_space == OPJ_CLRSPC_SRGB)
|
||||||
|
cs = QColorSpace(QColorSpace::SRgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief isSupported
|
||||||
|
* \return True if the current JP2 image i ssupported by the plugin. Otherwise false.
|
||||||
|
*/
|
||||||
|
bool isSupported() const
|
||||||
|
{
|
||||||
|
return format() != QImage::Format_Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray subType() const
|
||||||
|
{
|
||||||
|
return m_subtype;
|
||||||
|
}
|
||||||
|
void setSubType(const QByteArray &type)
|
||||||
|
{
|
||||||
|
m_subtype = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint32 quality() const
|
||||||
|
{
|
||||||
|
return m_quality;
|
||||||
|
}
|
||||||
|
void setQuality(qint32 quality)
|
||||||
|
{
|
||||||
|
m_quality = std::clamp(quality, -1, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief encoderFormat
|
||||||
|
* \return The encoder format set by subType.
|
||||||
|
*/
|
||||||
|
OPJ_CODEC_FORMAT encoderFormat() const
|
||||||
|
{
|
||||||
|
return subType() == J2K_SUBTYPE ? OPJ_CODEC_J2K : OPJ_CODEC_JP2;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool imageToJp2(const QImage &image)
|
||||||
|
{
|
||||||
|
auto ncomp = image.hasAlphaChannel() ? 4 : 3;
|
||||||
|
auto prec = 8;
|
||||||
|
auto cs = OPJ_CLRSPC_SRGB;
|
||||||
|
auto convFormat = image.format();
|
||||||
|
auto isFloat = false;
|
||||||
|
|
||||||
|
switch (image.format()) {
|
||||||
|
case QImage::Format_Mono:
|
||||||
|
case QImage::Format_MonoLSB:
|
||||||
|
case QImage::Format_Alpha8:
|
||||||
|
case QImage::Format_Grayscale8:
|
||||||
|
ncomp = 1;
|
||||||
|
cs = OPJ_CLRSPC_GRAY;
|
||||||
|
convFormat = QImage::Format_Grayscale8;
|
||||||
|
break;
|
||||||
|
case QImage::Format_Indexed8:
|
||||||
|
if (image.isGrayscale()) {
|
||||||
|
ncomp = 1;
|
||||||
|
cs = OPJ_CLRSPC_GRAY;
|
||||||
|
convFormat = QImage::Format_Grayscale8;
|
||||||
|
} else {
|
||||||
|
ncomp = 4;
|
||||||
|
cs = OPJ_CLRSPC_SRGB;
|
||||||
|
convFormat = QImage::Format_RGBA8888;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QImage::Format_Grayscale16:
|
||||||
|
ncomp = 1;
|
||||||
|
prec = 16;
|
||||||
|
cs = OPJ_CLRSPC_GRAY;
|
||||||
|
convFormat = QImage::Format_Grayscale16;
|
||||||
|
break;
|
||||||
|
case QImage::Format_RGBX16FPx4:
|
||||||
|
case QImage::Format_RGBX32FPx4:
|
||||||
|
isFloat = true;
|
||||||
|
#ifdef JP2_ENABLE_HDR
|
||||||
|
prec = 32;
|
||||||
|
convFormat = QImage::Format_RGBX32FPx4;
|
||||||
|
cs = OPJ_CLRSPC_UNSPECIFIED;
|
||||||
|
break;
|
||||||
|
#else
|
||||||
|
Q_FALLTHROUGH();
|
||||||
|
#endif
|
||||||
|
case QImage::Format_RGBX64:
|
||||||
|
case QImage::Format_RGB30:
|
||||||
|
case QImage::Format_BGR30:
|
||||||
|
prec = 16;
|
||||||
|
convFormat = QImage::Format_RGBX64;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QImage::Format_RGBA16FPx4:
|
||||||
|
case QImage::Format_RGBA16FPx4_Premultiplied:
|
||||||
|
case QImage::Format_RGBA32FPx4:
|
||||||
|
case QImage::Format_RGBA32FPx4_Premultiplied:
|
||||||
|
isFloat = true;
|
||||||
|
#ifdef JP2_ENABLE_HDR
|
||||||
|
prec = 32;
|
||||||
|
convFormat = QImage::Format_RGBA32FPx4;
|
||||||
|
cs = OPJ_CLRSPC_UNSPECIFIED;
|
||||||
|
break;
|
||||||
|
#else
|
||||||
|
Q_FALLTHROUGH();
|
||||||
|
#endif
|
||||||
|
case QImage::Format_RGBA64:
|
||||||
|
case QImage::Format_RGBA64_Premultiplied:
|
||||||
|
case QImage::Format_A2RGB30_Premultiplied:
|
||||||
|
case QImage::Format_A2BGR30_Premultiplied:
|
||||||
|
prec = 16;
|
||||||
|
convFormat = QImage::Format_RGBA64;
|
||||||
|
break;
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||||
|
case QImage::Format_CMYK8888: // requires OpenJPEG 2.5.3+
|
||||||
|
if (strcmp(opj_version(), "2.5.3") >= 0) {
|
||||||
|
ncomp = 4;
|
||||||
|
cs = OPJ_CLRSPC_CMYK;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
Q_FALLTHROUGH();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
if (image.depth() > 32) {
|
||||||
|
qWarning() << "The image is saved losing precision!";
|
||||||
|
}
|
||||||
|
convFormat = ncomp == 4 ? QImage::Format_RGBA8888 : QImage::Format_RGBX8888;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkSizeLimits(image.size(), ncomp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
opj_set_default_encoder_parameters(&m_cparameters);
|
||||||
|
m_cparameters.cod_format = encoderFormat();
|
||||||
|
m_cparameters.tile_size_on = 1;
|
||||||
|
m_cparameters.cp_tdx = 1024;
|
||||||
|
m_cparameters.cp_tdy = 1024;
|
||||||
|
|
||||||
|
if (m_quality > -1 && m_quality < 100) {
|
||||||
|
m_cparameters.irreversible = 1;
|
||||||
|
m_cparameters.tcp_numlayers = 1;
|
||||||
|
m_cparameters.cp_disto_alloc = 1;
|
||||||
|
m_cparameters.tcp_rates[0] = 100.0 - (m_quality < 10 ? m_quality : 10 + (std::log10(m_quality) - 1) * 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<opj_image_cmptparm_t> cmptparm(new opj_image_cmptparm_t[ncomp]);
|
||||||
|
for (int i = 0; i < ncomp; ++i) {
|
||||||
|
auto &&p = cmptparm.get() + i;
|
||||||
|
memset(p, 0, sizeof(opj_image_cmptparm_t));
|
||||||
|
p->dx = m_cparameters.subsampling_dx;
|
||||||
|
p->dy = m_cparameters.subsampling_dy;
|
||||||
|
p->w = image.width();
|
||||||
|
p->h = image.height();
|
||||||
|
p->x0 = 0;
|
||||||
|
p->y0 = 0;
|
||||||
|
p->prec = prec;
|
||||||
|
p->sgnd = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_jp2_image = opj_image_create(ncomp, cmptparm.get(), cs);
|
||||||
|
if (m_jp2_image == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (int(m_jp2_image->numcomps) != ncomp) {
|
||||||
|
return false; // paranoia
|
||||||
|
}
|
||||||
|
m_jp2_image->x1 = image.width();
|
||||||
|
m_jp2_image->y1 = image.height();
|
||||||
|
|
||||||
|
ScanLineConverter scl(convFormat);
|
||||||
|
if (prec < 32 && isFloat) {
|
||||||
|
scl.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgbLinear));
|
||||||
|
}
|
||||||
|
if (cs == OPJ_CLRSPC_SRGB) {
|
||||||
|
scl.setTargetColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||||
|
} else {
|
||||||
|
scl.setTargetColorSpace(image.colorSpace());
|
||||||
|
}
|
||||||
|
for (qint32 c = 0; c < ncomp; ++c) {
|
||||||
|
auto &&comp = m_jp2_image->comps[c];
|
||||||
|
auto mul = ncomp == 1 ? 1 : 4;
|
||||||
|
for (qint32 y = 0, h = image.height(); y < h; ++y) {
|
||||||
|
if (prec == 8) {
|
||||||
|
auto ptr = reinterpret_cast<const quint8 *>(scl.convertedScanLine(image, y));
|
||||||
|
for (qint32 x = 0, w = image.width(); x < w; ++x)
|
||||||
|
comp.data[y * w + x] = ptr[x * mul + c];
|
||||||
|
} else if (prec == 16) {
|
||||||
|
auto ptr = reinterpret_cast<const quint16 *>(scl.convertedScanLine(image, y));
|
||||||
|
for (qint32 x = 0, w = image.width(); x < w; ++x)
|
||||||
|
comp.data[y * w + x] = ptr[x * mul + c];
|
||||||
|
} else if (prec == 32) {
|
||||||
|
auto ptr = reinterpret_cast<const quint32 *>(scl.convertedScanLine(image, y));
|
||||||
|
for (qint32 x = 0, w = image.width(); x < w; ++x)
|
||||||
|
comp.data[y * w + x] = ptr[x * mul + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With SRGB, Gray and CMYK, writing the colorspace gives an assert
|
||||||
|
if (cs == OPJ_CLRSPC_UNKNOWN || cs == OPJ_CLRSPC_UNSPECIFIED) {
|
||||||
|
auto colorSpace = scl.targetColorSpace().iccProfile();
|
||||||
|
if (!colorSpace.isEmpty()) {
|
||||||
|
m_jp2_image->icc_profile_buf = reinterpret_cast<OPJ_BYTE *>(malloc(colorSpace.size()));
|
||||||
|
if (m_jp2_image->icc_profile_buf) {
|
||||||
|
memcpy(m_jp2_image->icc_profile_buf, colorSpace.data(), colorSpace.size());
|
||||||
|
m_jp2_image->icc_profile_len = colorSpace.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeImage(QIODevice *device, const QImage &image)
|
||||||
|
{
|
||||||
|
if (!imageToJp2(image)) {
|
||||||
|
qCritical() << "Error while creating JP2 image!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<opj_codec_t, std::function<void(opj_codec_t *)>> codec(opj_create_compress(encoderFormat()), opj_destroy_codec);
|
||||||
|
if (codec == nullptr) {
|
||||||
|
qCritical() << "Error while creating encoder!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
enableThreads(codec.get());
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
// opj_set_info_handler(m_jp2_codec, info_callback, nullptr);
|
||||||
|
// opj_set_warning_handler(m_jp2_codec, warning_callback, nullptr);
|
||||||
|
#endif
|
||||||
|
opj_set_error_handler(m_jp2_codec, error_callback, nullptr);
|
||||||
|
|
||||||
|
if (!opj_setup_encoder(codec.get(), &m_cparameters, m_jp2_image)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createStream(device, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opj_start_compress(codec.get(), m_jp2_image, m_jp2_stream)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!opj_encode(codec.get(), m_jp2_stream)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!opj_end_compress(codec.get(), m_jp2_stream)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// common
|
||||||
|
opj_stream_t *m_jp2_stream;
|
||||||
|
|
||||||
|
opj_image_t *m_jp2_image;
|
||||||
|
|
||||||
|
// read
|
||||||
|
opj_codec_t *m_jp2_codec;
|
||||||
|
|
||||||
|
opj_dparameters_t m_dparameters;
|
||||||
|
|
||||||
|
// write
|
||||||
|
opj_cparameters_t m_cparameters;
|
||||||
|
|
||||||
|
qint32 m_quality;
|
||||||
|
|
||||||
|
QByteArray m_subtype;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
JP2Handler::JP2Handler()
|
||||||
|
: QImageIOHandler()
|
||||||
|
, d(new JP2HandlerPrivate)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JP2Handler::canRead() const
|
||||||
|
{
|
||||||
|
if (canRead(device())) {
|
||||||
|
setFormat("jp2");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JP2Handler::canRead(QIODevice *device)
|
||||||
|
{
|
||||||
|
if (!device) {
|
||||||
|
qWarning("JP2Handler::canRead() called with no device");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device->isSequential()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JP2HandlerPrivate handler;
|
||||||
|
return handler.detectDecoderFormat(device) != OPJ_CODEC_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JP2Handler::read(QImage *image)
|
||||||
|
{
|
||||||
|
auto dev = device();
|
||||||
|
if (dev == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto img = d->readImage(dev);
|
||||||
|
if (img.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*image = img;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JP2Handler::write(const QImage &image)
|
||||||
|
{
|
||||||
|
if (image.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto dev = device();
|
||||||
|
if (dev == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return d->writeImage(dev, image);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JP2Handler::supportsOption(ImageOption option) const
|
||||||
|
{
|
||||||
|
if (option == QImageIOHandler::Size) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::ImageFormat) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::SubType) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::SupportedSubTypes) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::Quality) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JP2Handler::setOption(ImageOption option, const QVariant &value)
|
||||||
|
{
|
||||||
|
if (option == QImageIOHandler::SubType) {
|
||||||
|
auto st = value.toByteArray();
|
||||||
|
if (this->option(QImageIOHandler::SupportedSubTypes).toList().contains(st))
|
||||||
|
d->setSubType(st);
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::Quality) {
|
||||||
|
auto ok = false;
|
||||||
|
auto q = value.toInt(&ok);
|
||||||
|
if (ok) {
|
||||||
|
d->setQuality(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant JP2Handler::option(ImageOption option) const
|
||||||
|
{
|
||||||
|
QVariant v;
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::Size) {
|
||||||
|
if (d->readHeader(device())) {
|
||||||
|
v = d->size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::ImageFormat) {
|
||||||
|
if (d->readHeader(device())) {
|
||||||
|
v = d->format();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::SubType) {
|
||||||
|
v = d->subType();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::SupportedSubTypes) {
|
||||||
|
v = QVariant::fromValue(QList<QByteArray>() << JP2_SUBTYPE << J2K_SUBTYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::Quality) {
|
||||||
|
v = d->quality();
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImageIOPlugin::Capabilities JP2Plugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||||
|
{
|
||||||
|
if (format == "jp2" || format == "j2k") {
|
||||||
|
return Capabilities(CanRead | CanWrite);
|
||||||
|
}
|
||||||
|
// NOTE: JPF is the default extension of Photoshop for JP2 files.
|
||||||
|
if (format == "jpf") {
|
||||||
|
return Capabilities(CanRead);
|
||||||
|
}
|
||||||
|
if (!format.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!device->isOpen()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Capabilities cap;
|
||||||
|
if (device->isReadable() && JP2Handler::canRead(device)) {
|
||||||
|
cap |= CanRead;
|
||||||
|
}
|
||||||
|
if (device->isWritable()) {
|
||||||
|
cap |= CanWrite;
|
||||||
|
}
|
||||||
|
return cap;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImageIOHandler *JP2Plugin::create(QIODevice *device, const QByteArray &format) const
|
||||||
|
{
|
||||||
|
QImageIOHandler *handler = new JP2Handler;
|
||||||
|
handler->setDevice(device);
|
||||||
|
handler->setFormat(format);
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_jp2_p.cpp"
|
4
src/imageformats/jp2.json
Normal file
4
src/imageformats/jp2.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"Keys": [ "jp2", "j2k", "jpf" ],
|
||||||
|
"MimeTypes": [ "image/jp2" ]
|
||||||
|
}
|
44
src/imageformats/jp2_p.h
Normal file
44
src/imageformats/jp2_p.h
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the KDE project
|
||||||
|
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KIMG_JP2_P_H
|
||||||
|
#define KIMG_JP2_P_H
|
||||||
|
|
||||||
|
#include <QImageIOPlugin>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
|
||||||
|
class JP2HandlerPrivate;
|
||||||
|
class JP2Handler : public QImageIOHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
JP2Handler();
|
||||||
|
|
||||||
|
bool canRead() const override;
|
||||||
|
bool read(QImage *image) override;
|
||||||
|
bool write(const QImage &image) override;
|
||||||
|
|
||||||
|
bool supportsOption(QImageIOHandler::ImageOption option) const override;
|
||||||
|
void setOption(ImageOption option, const QVariant &value) override;
|
||||||
|
QVariant option(QImageIOHandler::ImageOption option) const override;
|
||||||
|
|
||||||
|
static bool canRead(QIODevice *device);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QScopedPointer<JP2HandlerPrivate> d;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JP2Plugin : public QImageIOPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "jp2.json")
|
||||||
|
|
||||||
|
public:
|
||||||
|
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||||
|
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KIMG_JP2_P_H
|
Loading…
Reference in New Issue
Block a user