Add image_as_jpeg()
authorCarl Hetherington <cth@carlh.net>
Sat, 18 Dec 2021 22:34:26 +0000 (23:34 +0100)
committerCarl Hetherington <cth@carlh.net>
Thu, 28 Apr 2022 22:27:17 +0000 (00:27 +0200)
src/lib/image_jpeg.cc [new file with mode: 0644]
src/lib/image_jpeg.h [new file with mode: 0644]
src/lib/wscript
src/tools/wscript
test/image_test.cc
test/test.cc
test/wscript
wscript

diff --git a/src/lib/image_jpeg.cc b/src/lib/image_jpeg.cc
new file mode 100644 (file)
index 0000000..28bbb75
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+    Copyright (C) 2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcpomatic_assert.h"
+#include "exceptions.h"
+#include "image.h"
+#include <jpeglib.h>
+
+#include "i18n.h"
+
+
+using std::shared_ptr;
+
+
+struct destination_mgr
+{
+       jpeg_destination_mgr pub;
+       std::vector<uint8_t>* data;
+};
+
+
+static void
+init_destination (j_compress_ptr)
+{
+
+}
+
+
+/* Empty the output buffer (i.e. make more space for data); we'll make
+ * the output buffer bigger instead.
+ */
+static boolean
+empty_output_buffer (j_compress_ptr state)
+{
+       auto dest = reinterpret_cast<destination_mgr*> (state->dest);
+
+       auto const old_size = dest->data->size();
+       dest->data->resize (old_size * 2);
+
+       dest->pub.next_output_byte = dest->data->data() + old_size;
+       dest->pub.free_in_buffer = old_size;
+
+       return TRUE;
+}
+
+
+static void
+term_destination (j_compress_ptr state)
+{
+       auto dest = reinterpret_cast<destination_mgr*> (state->dest);
+
+       dest->data->resize (dest->data->size() - dest->pub.free_in_buffer);
+}
+
+
+void
+error_exit (j_common_ptr)
+{
+       throw EncodeError (N_("JPEG encoding error"));
+}
+
+
+dcp::ArrayData
+image_as_jpeg (shared_ptr<const Image> image, int quality)
+{
+       if (image->pixel_format() != AV_PIX_FMT_RGB24) {
+               return image_as_jpeg(
+                       Image::ensure_alignment(image, Image::Alignment::PADDED)->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_RGB24, Image::Alignment::PADDED, false),
+                       quality
+                       );
+       }
+
+       struct jpeg_compress_struct compress;
+       struct jpeg_error_mgr error;
+
+       compress.err = jpeg_std_error (&error);
+       error.error_exit = error_exit;
+       jpeg_create_compress (&compress);
+
+       auto mgr = new destination_mgr;
+       compress.dest = reinterpret_cast<jpeg_destination_mgr*>(mgr);
+
+       mgr->pub.init_destination = init_destination;
+       mgr->pub.empty_output_buffer = empty_output_buffer;
+       mgr->pub.term_destination = term_destination;
+
+       std::vector<uint8_t> data(4096);
+       mgr->data = &data;
+       mgr->pub.next_output_byte = data.data();
+       mgr->pub.free_in_buffer = data.size();
+
+       compress.image_width = image->size().width;
+       compress.image_height = image->size().height;
+       compress.input_components = 3;
+       compress.in_color_space = JCS_RGB;
+
+       jpeg_set_defaults (&compress);
+       jpeg_set_quality (&compress, quality, TRUE);
+
+       jpeg_start_compress (&compress, TRUE);
+
+       JSAMPROW row[1];
+       auto const source_data = image->data()[0];
+       auto const source_stride = image->stride()[0];
+       for (auto y = 0U; y < compress.image_height; ++y) {
+               row[0] = source_data + y * source_stride;
+               jpeg_write_scanlines (&compress, row, 1);
+       }
+
+       jpeg_finish_compress (&compress);
+       delete mgr;
+       jpeg_destroy_compress (&compress);
+
+       return dcp::ArrayData (data.data(), data.size());
+}
+
+
diff --git a/src/lib/image_jpeg.h b/src/lib/image_jpeg.h
new file mode 100644 (file)
index 0000000..5c017fc
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+    Copyright (C) 2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+dcp::ArrayData image_as_jpeg (std::shared_ptr<const Image> image, int quality);
index 49b0e4bac664f30f54614a628c014e58d136ddda..d1eef8e33ad067d462fe73b1471d64532a8eb29d 100644 (file)
@@ -122,6 +122,7 @@ sources = """
           image_decoder.cc
           image_examiner.cc
           image_filename_sorter.cc
+          image_jpeg.cc
           image_png.cc
           image_proxy.cc
           j2k_image_proxy.cc
@@ -207,7 +208,7 @@ def build(bld):
                  AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE
                  BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2 BOOST_REGEX
                  SAMPLERATE POSTPROC TIFF SSH DCP CXML GLIB LZMA XML++
-                 CURL ZIP BZ2 FONTCONFIG PANGOMM CAIROMM XMLSEC SUB ICU NETTLE PNG LEQM_NRT
+                 CURL ZIP BZ2 FONTCONFIG PANGOMM CAIROMM XMLSEC SUB ICU NETTLE PNG JPEG LEQM_NRT
                  """
 
     if bld.env.TARGET_OSX:
index fe5e037a501e3cf285f1a54a64d0d458238d469e..bb3798aa70cdfba5bb03736eaa12c4c799df310f 100644 (file)
@@ -30,7 +30,7 @@ def configure(conf):
 def build(bld):
     uselib =  'BOOST_THREAD BOOST_DATETIME DCP XMLSEC CXML XMLPP AVFORMAT AVFILTER AVCODEC '
     uselib += 'AVUTIL SWSCALE SWRESAMPLE POSTPROC CURL BOOST_FILESYSTEM SSH ZIP CAIROMM FONTCONFIG PANGOMM SUB '
-    uselib += 'SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE RTAUDIO PNG LEQM_NRT '
+    uselib += 'SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE RTAUDIO PNG JPEG LEQM_NRT '
 
     if bld.env.ENABLE_DISK:
         if bld.env.TARGET_LINUX:
index 18d96d4fc4a4f388f158ce6e1cc3119fd950bc88..8d934bdd62fd429b77e454daf4fcc7917b7e1676 100644 (file)
@@ -30,6 +30,7 @@
 #include "lib/image.h"
 #include "lib/image_content.h"
 #include "lib/image_decoder.h"
+#include "lib/image_jpeg.h"
 #include "lib/image_png.h"
 #include "lib/ffmpeg_image_proxy.h"
 #include "test.h"
@@ -384,6 +385,19 @@ BOOST_AUTO_TEST_CASE (as_png_test)
 }
 
 
+BOOST_AUTO_TEST_CASE (as_jpeg_test)
+{
+       auto proxy = make_shared<FFmpegImageProxy>("test/data/3d_test/000001.png");
+       auto image_rgb = proxy->image(Image::Alignment::PADDED).image;
+       auto image_bgr = image_rgb->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_BGRA, Image::Alignment::PADDED, false);
+       image_as_jpeg(image_rgb, 60).write("build/test/as_jpeg_rgb.jpeg");
+       image_as_jpeg(image_bgr, 60).write("build/test/as_jpeg_bgr.jpeg");
+
+       check_image ("test/data/as_jpeg_rgb.jpeg", "build/test/as_jpeg_rgb.jpeg");
+       check_image ("test/data/as_jpeg_bgr.jpeg", "build/test/as_jpeg_bgr.jpeg");
+}
+
+
 /* Very dumb test to fade black to make sure it stays black */
 static void
 fade_test_format_black (AVPixelFormat f, string name)
index 6b2e584a200af4bec0f4c70b30ec2d9dfdef9328..7136b038d99bbb9c28bc1a8569cdb0f6b226e72f 100644 (file)
@@ -369,6 +369,8 @@ rms_error (boost::filesystem::path ref, boost::filesystem::path check)
        FFmpegImageProxy check_proxy (check);
        auto check_image = check_proxy.image(Image::Alignment::COMPACT).image;
 
+       BOOST_REQUIRE_EQUAL (ref_image->planes(), check_image->planes());
+
        BOOST_REQUIRE_EQUAL (ref_image->pixel_format(), check_image->pixel_format());
        auto const format = ref_image->pixel_format();
 
@@ -417,6 +419,19 @@ rms_error (boost::filesystem::path ref, boost::filesystem::path check)
                        }
                        break;
                }
+               case AV_PIX_FMT_YUVJ420P:
+               {
+                       for (int c = 0; c < ref_image->planes(); ++c) {
+                               for (int y = 0; y < height / ref_image->vertical_factor(c); ++y) {
+                                       auto p = ref_image->data()[c] + y * ref_image->stride()[c];
+                                       auto q = check_image->data()[c] + y * check_image->stride()[c];
+                                       for (int x = 0; x < ref_image->line_size()[c]; ++x) {
+                                               sum_square += pow((*p++ - *q++), 2);
+                                       }
+                               }
+                       }
+                       break;
+               }
                default:
                        BOOST_REQUIRE_MESSAGE (false, "unrecognised pixel format " << format);
        }
index 9b1c0173e49ce9b53386c1b01e5fb311ab5047db..1178264f0c48ba827955dacf8d566ba8da4181eb 100644 (file)
@@ -36,7 +36,7 @@ def build(bld):
     obj = bld(features='cxx cxxprogram')
     obj.name   = 'unit-tests'
     obj.uselib =  'BOOST_TEST BOOST_THREAD BOOST_FILESYSTEM BOOST_DATETIME SNDFILE SAMPLERATE DCP FONTCONFIG CAIROMM PANGOMM XMLPP '
-    obj.uselib += 'AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE SWRESAMPLE POSTPROC CXML SUB GLIB CURL SSH XMLSEC BOOST_REGEX ICU NETTLE PNG '
+    obj.uselib += 'AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE SWRESAMPLE POSTPROC CXML SUB GLIB CURL SSH XMLSEC BOOST_REGEX ICU NETTLE PNG JPEG '
     obj.uselib += 'LEQM_NRT ZIP '
     if bld.env.TARGET_WINDOWS_64 or bld.env.TARGET_WINDOWS_32:
         obj.uselib += 'WINSOCK2 DBGHELP SHLWAPI MSWSOCK BOOST_LOCALE '
diff --git a/wscript b/wscript
index 6b5f582d9b2714d9bd98885a65136c65da94a688..c0d337d801f446a5ee150f783e0d821bcf2e7f4e 100644 (file)
--- a/wscript
+++ b/wscript
@@ -348,6 +348,18 @@ def configure(conf):
     # libpng
     conf.check_cfg(package='libpng', args='--cflags --libs', uselib_store='PNG', mandatory=True)
 
+    # libjpeg
+    conf.check_cxx(fragment="""
+                            #include <cstddef>
+                            #include <cstdio>
+                            #include <jpeglib.h>
+                            int main() { struct jpeg_compress_struct compress; jpeg_create_compress (&compress); return 0; }
+                            """,
+                   msg='Checking for libjpeg',
+                   libpath='/usr/local/lib',
+                   lib=['jpeg'],
+                   uselib_store='JPEG')
+
     # lwext4
     if conf.options.enable_disk:
         conf.check_cxx(fragment="""