From 17cbcc2877496effb069457b26444e9c95a5dec8 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Wed, 4 May 2022 02:18:01 +0200 Subject: [PATCH] Added mono image class --- src/db/db/dbStreamLayers.h | 16 +-- src/laybasic/laybasic/layImage.cc | 161 ++++++++++++++++++++- src/laybasic/laybasic/layImage.h | 176 ++++++++++++++++++++++- src/laybasic/unit_tests/layImageTests.cc | 175 +++++++++++++++++++++- src/tl/tl/tlCopyOnWrite.h | 2 - testdata/lay/au_mono.png | Bin 0 -> 6520 bytes 6 files changed, 514 insertions(+), 16 deletions(-) create mode 100644 testdata/lay/au_mono.png diff --git a/src/db/db/dbStreamLayers.h b/src/db/db/dbStreamLayers.h index 6baf6ef36..3ee89a468 100644 --- a/src/db/db/dbStreamLayers.h +++ b/src/db/db/dbStreamLayers.h @@ -201,6 +201,8 @@ public: /** * @brief Returns the first logical layer for a given layer specification + * The first value of the pair indicates whether there is a valid mapping. + * The second value will give the layer to map to. */ template std::pair first_logical (const L &p) const @@ -215,6 +217,8 @@ public: /** * @brief Returns the first logical layer for a given layer specification + * The first value of the pair indicates whether there is a valid mapping. + * The second value will give the layer to map to. */ template std::pair first_logical (const L &p, db::Layout &layout) const @@ -230,24 +234,21 @@ public: /** * @brief Query a layer mapping * - * @return A pair telling if the layer is mapped (first=true) and - * the logical layer mapped (second) if this is the case. + * @return A set of layers which are designated targets. */ std::set logical (const LDPair &p) const; /** * @brief Query a layer mapping from a name * - * @return A pair telling if the layer is mapped (first=true) and - * the logical layer mapped (second) if this is the case. + * @return A set of layers which are designated targets. */ std::set logical (const std::string &name) const; /** * @brief Query a layer mapping from a name or LDPair * - * @return A pair telling if the layer is mapped (first=true) and - * the logical layer mapped (second) if this is the case. + * @return A set of layers which are designated targets. * * @param p The layer that is looked for */ @@ -256,8 +257,7 @@ public: /** * @brief Query or install a layer mapping from a name or LDPair * - * @return A pair telling if the layer is mapped (first=true) and - * the logical layer mapped (second) if this is the case. + * @return A set of layers which are designated targets. * * @param p The layer that is looked for * diff --git a/src/laybasic/laybasic/layImage.cc b/src/laybasic/laybasic/layImage.cc index 1b49d6782..49575c8cd 100644 --- a/src/laybasic/laybasic/layImage.cc +++ b/src/laybasic/laybasic/layImage.cc @@ -26,7 +26,11 @@ namespace lay { +// ----------------------------------------------------------------------------------------------------- +// Image implementation + Image::Image (unsigned int w, unsigned int h, lay::color_t *data) + : m_data () { m_width = w; m_height = h; @@ -35,16 +39,21 @@ Image::Image (unsigned int w, unsigned int h, lay::color_t *data) } Image::Image (unsigned int w, unsigned int h, const lay::color_t *data, unsigned int stride) + : m_data () { m_width = w; m_height = h; m_transparent = false; - lay::color_t *d = new color_t [w * h]; + tl_assert ((stride % sizeof (lay::color_t)) == 0); + stride /= sizeof (lay::color_t); + + lay::color_t *d = new lay::color_t [w * h]; + lay::color_t *new_data = d; if (data) { for (unsigned int i = 0; i < h; ++i) { - for (unsigned int i = 0; i < h; ++i) { + for (unsigned int j = 0; j < w; ++j) { *d++ = *data++; } if (stride > w) { @@ -53,7 +62,7 @@ Image::Image (unsigned int w, unsigned int h, const lay::color_t *data, unsigned } } - m_data.reset (new ImageData (d, w * h)); + m_data.reset (new ImageData (new_data, w * h)); } Image::Image () @@ -211,4 +220,150 @@ Image::diff (const Image &other) const return res; } +// ----------------------------------------------------------------------------------------------------- +// MonoImage implementation + +static unsigned int +stride_from_width (unsigned int w) +{ + // Qt needs 32bit-aligned data + return 4 * ((w + 31) / 32); +} + +MonoImage::MonoImage (unsigned int w, unsigned int h, uint8_t *data) +{ + m_width = w; + m_height = h; + m_stride = stride_from_width (w); + m_data.reset (new MonoImageData (data, m_stride * h)); +} + +MonoImage::MonoImage (unsigned int w, unsigned int h, const uint8_t *data, unsigned int stride) +{ + m_width = w; + m_height = h; + m_stride = stride_from_width (w); + + uint8_t *d = new uint8_t [m_stride * h]; + uint8_t *new_data = d; + + if (data) { + for (unsigned int i = 0; i < h; ++i) { + memcpy (d, data, m_stride); + d += m_stride; + data += m_stride; + if (stride > m_stride) { + data += stride - m_stride; + } + } + } + + m_data.reset (new MonoImageData (new_data, m_stride * h)); +} + +MonoImage::MonoImage () +{ + m_width = 0; + m_height = 0; + m_stride = 0; +} + +MonoImage::MonoImage (const MonoImage &other) +{ + operator= (other); +} + +MonoImage::MonoImage (MonoImage &&other) +{ + swap (other); +} + +MonoImage::~MonoImage () +{ + // .. nothing yet .. +} + +MonoImage & +MonoImage::operator= (const MonoImage &other) +{ + if (this != &other) { + m_width = other.m_width; + m_height = other.m_height; + m_stride = other.m_stride; + m_data = other.m_data; + } + return *this; +} + +MonoImage & +MonoImage::operator= (MonoImage &&other) +{ + if (this != &other) { + swap (other); + } + return *this; +} + +void +MonoImage::swap (MonoImage &other) +{ + if (this == &other) { + return; + } + + std::swap (m_width, other.m_width); + std::swap (m_height, other.m_height); + std::swap (m_stride, other.m_stride); + m_data.swap (other.m_data); +} + +void +MonoImage::fill (bool value) +{ + uint8_t c = value ? 0xff : 0; + uint8_t *d = data (); + for (unsigned int i = 0; i < m_height; ++i) { + for (unsigned int j = 0; j < m_stride; ++j) { + *d++ = c; + } + } +} + +uint8_t * +MonoImage::scan_line (unsigned int n) +{ + tl_assert (n < m_height); + return m_data->data () + n * m_stride; +} + +const uint8_t * +MonoImage::scan_line (unsigned int n) const +{ + tl_assert (n < m_height); + return m_data->data () + n * m_stride; +} + +uint8_t * +MonoImage::data () +{ + return m_data->data (); +} + +const uint8_t * +MonoImage::data () const +{ + return m_data->data (); +} + +#if defined(HAVE_QT) +QImage +MonoImage::to_image () const +{ + QImage img = QImage ((const uchar *) data (), m_width, m_height, QImage::Format_MonoLSB); + img.setColor (0, 0xff000000); + img.setColor (1, 0xffffffff); + return img; +} +#endif + } diff --git a/src/laybasic/laybasic/layImage.h b/src/laybasic/laybasic/layImage.h index 020da278a..da1476bcf 100644 --- a/src/laybasic/laybasic/layImage.h +++ b/src/laybasic/laybasic/layImage.h @@ -29,6 +29,7 @@ #include "tlCopyOnWrite.h" #include +#include #if defined(HAVE_QT) # include @@ -38,7 +39,7 @@ namespace lay { /** - * @brief An 32bit RGBA image class + * @brief An 32bit RGB/RGBA image class * * This class substitutes QImage in Qt-less applications. * It provides 32bit RGBA pixels with the format used by lay::Color. @@ -131,6 +132,14 @@ public: return m_height; } + /** + * @brief Gets the image stride (number of bytes per row) + */ + unsigned int stride () const + { + return sizeof (lay::color_t) * m_width; + } + /** * @brief Fills the image with the given color */ @@ -228,6 +237,171 @@ private: tl::copy_on_write_ptr m_data; }; +/** + * @brief An monochrome image class + * + * This class substitutes QImage for monochrome images in Qt-less applications. + */ + +class LAYBASIC_PUBLIC MonoImage +{ +public: + /** + * @brief Creates an image with the given height and width + * + * If data is given, the image is initialized with the given data and will take ownership over the + * data block. + * + * Lines are byte-aligned. + */ + MonoImage (unsigned int w, unsigned int h, uint8_t *data); + + /** + * @brief Creates an image with the given height and width + * + * If data is given, the image is initialized with the given data. A copy of the data is created. + * + * "stride" specifies the stride (distance in bytes between two rows of data). + * The size of the data block needs to be stride*h elements or bytes(w)*h if stride is not given. + */ + MonoImage (unsigned int w, unsigned int h, const uint8_t *data = 0, unsigned int stride = 0); + + /** + * @brief Default constructor + */ + MonoImage (); + + /** + * @brief Copy constructor + */ + MonoImage (const MonoImage &other); + + /** + * @brief Move constructor + */ + MonoImage (MonoImage &&other); + + /** + * @brief Destructor + */ + ~MonoImage (); + + /** + * @brief Assignment + */ + MonoImage &operator= (const MonoImage &other); + + /** + * @brief Move constructor + */ + MonoImage &operator= (MonoImage &&other); + + /** + * @brief Swaps this image with another one + */ + void swap (MonoImage &other); + + /** + * @brief Gets the images width + */ + unsigned int width () const + { + return m_width; + } + + /** + * @brief Gets the images width + */ + unsigned int height () const + { + return m_height; + } + + /** + * @brief Gets the image stride (number of bytes per row) + */ + unsigned int stride () const + { + return m_stride; + } + + /** + * @brief Fills the image with the given color + */ + void fill (bool value); + + /** + * @brief Gets the scanline for row n + */ + uint8_t *scan_line (unsigned int n); + + /** + * @brief Gets the scanline for row n (const version) + */ + const uint8_t *scan_line (unsigned int n) const; + + /** + * @brief Gets the data pointer + */ + uint8_t *data (); + + /** + * @brief Gets the data pointer (const version) + */ + const uint8_t *data () const; + +#if defined(HAVE_QT) + /** + * @brief Produces a QMonoImage object from the image + */ + QImage to_image () const; +#endif + +private: + class MonoImageData + { + public: + MonoImageData () + : mp_data (0), m_length (0) + { + // .. nothing yet .. + } + + MonoImageData (uint8_t *data, size_t length) + : mp_data (data), m_length (length) + { + // .. nothing yet .. + } + + MonoImageData (const MonoImageData &other) + { + m_length = other.length (); + mp_data = new uint8_t [other.length ()]; + memcpy (mp_data, other.data (), m_length * sizeof (uint8_t)); + } + + ~MonoImageData () + { + delete[] mp_data; + mp_data = 0; + } + + size_t length () const { return m_length; } + uint8_t *data () { return mp_data; } + const uint8_t *data () const { return mp_data; } + + private: + uint8_t *mp_data; + size_t m_length; + + MonoImageData &operator= (const MonoImageData &other); + }; + + unsigned int m_width, m_height; + unsigned int m_stride; + tl::copy_on_write_ptr m_data; +}; + } #endif diff --git a/src/laybasic/unit_tests/layImageTests.cc b/src/laybasic/unit_tests/layImageTests.cc index 304e62e5a..d680feaed 100644 --- a/src/laybasic/unit_tests/layImageTests.cc +++ b/src/laybasic/unit_tests/layImageTests.cc @@ -36,8 +36,8 @@ static bool compare_images (const QImage &qimg, const std::string &au) qimg2.load (tl::to_qstring (au)); if (qimg2.width () == (int) qimg.width () && qimg2.height () == (int) qimg.height ()) { - for (int i = 0; i < qimg.width (); ++i) { - for (int j = 0; j < qimg.height (); ++j) { + for (int j = 0; j < qimg.height (); ++j) { + for (int i = 0; i < qimg.width (); ++i) { if (((const lay::color_t *) qimg.scanLine (j))[i] != ((const lay::color_t *) qimg2.scanLine (j))[i]) { return false; } @@ -49,6 +49,58 @@ static bool compare_images (const QImage &qimg, const std::string &au) } } +static bool compare_images_mono (const QImage &qimg, const std::string &au) +{ + QImage qimg2; + qimg2.load (tl::to_qstring (au)); + + if (qimg2.width () == (int) qimg.width () && qimg2.height () == (int) qimg.height ()) { + // NOTE: slooooow ... + for (int j = 0; j < qimg.height (); ++j) { + for (int i = 0; i < qimg.width (); ++i) { + if ((qimg.scanLine (j)[i / 8] & (0x80 >> (i % 8))) != (qimg2.scanLine (j)[i / 8] & (0x80 >> (i % 8)))) { + return false; + } + } + } + return true; + } else { + return false; + } +} + +static bool compare_images (const lay::Image &img, const lay::Image &img2) +{ + if (img2.width () == img.width () && img2.height () == img.height ()) { + for (unsigned int j = 0; j < img.height (); ++j) { + for (unsigned int i = 0; i < img.width (); ++i) { + if (((const lay::color_t *) img.scan_line (j))[i] != ((const lay::color_t *) img2.scan_line (j))[i]) { + return false; + } + } + } + return true; + } else { + return false; + } +} + +static bool compare_images (const lay::MonoImage &img, const lay::MonoImage &img2) +{ + if (img2.width () == img.width () && img2.height () == img.height ()) { + for (unsigned int j = 0; j < img.height (); ++j) { + for (unsigned int i = 0; i < img.stride (); ++i) { + if (((const uint8_t *) img.scan_line (j))[i] != ((const uint8_t *) img2.scan_line (j))[i]) { + return false; + } + } + } + return true; + } else { + return false; + } +} + #endif TEST(1) @@ -56,6 +108,7 @@ TEST(1) lay::Image img (15, 25); EXPECT_EQ (img.width (), 15); EXPECT_EQ (img.height (), 25); + EXPECT_EQ (img.stride (), 15 * sizeof (lay::color_t)); EXPECT_EQ (img.transparent (), false); img.set_transparent (true); @@ -85,6 +138,7 @@ TEST(1) EXPECT_EQ (img.scan_line (5)[10], 0x332211); img2 = img; + EXPECT_EQ (compare_images (img, img2), true); EXPECT_EQ (img.scan_line (5)[10], 0x332211); EXPECT_EQ (img2.scan_line (5)[10], 0x332211); @@ -94,21 +148,25 @@ TEST(1) EXPECT_EQ (img2.width (), 10); EXPECT_EQ (img2.height (), 16); img2.fill (0x010203); + EXPECT_EQ (compare_images (img, img2), false); EXPECT_EQ (img.scan_line (5)[10], 0x332211); EXPECT_EQ (img2.scan_line (5)[8], 0x010203); img = std::move (img2); + EXPECT_EQ (compare_images (img, img2), false); EXPECT_EQ (img.width (), 10); EXPECT_EQ (img.height (), 16); EXPECT_EQ (img.scan_line (5)[8], 0x010203); lay::Image img3 (img); + EXPECT_EQ (compare_images (img, img3), true); EXPECT_EQ (img3.width (), 10); EXPECT_EQ (img3.height (), 16); EXPECT_EQ (img3.scan_line (5)[8], 0x010203); img.fill (0x102030); + EXPECT_EQ (compare_images (img, img3), false); EXPECT_EQ (img3.width (), 10); EXPECT_EQ (img3.height (), 16); EXPECT_EQ (img3.scan_line (5)[8], 0x010203); @@ -120,6 +178,14 @@ TEST(1) EXPECT_EQ (img4.width (), 10); EXPECT_EQ (img4.height (), 16); EXPECT_EQ (img4.scan_line (5)[8], 0x102030); + + // other constructors + EXPECT_EQ (compare_images (lay::Image (img4.width (), img4.height (), (const lay::color_t *) img4.data ()), img4), true); + EXPECT_EQ (compare_images (lay::Image (img4.width (), img4.height (), (const lay::color_t *) img4.data (), img4.stride ()), img4), true); + + lay::color_t *dnew = new lay::color_t [ img4.width () * img4.height () * sizeof (lay::color_t) ]; + memcpy (dnew, (const lay::color_t *) img4.data (), img4.width () * img4.height () * sizeof (lay::color_t)); + EXPECT_EQ (compare_images (lay::Image (img4.width (), img4.height (), dnew), img4), true); } #if defined(HAVE_QT) @@ -260,3 +326,108 @@ TEST(3) #endif } +// Monochrome version + +TEST(11) +{ + lay::MonoImage img (15, 25); + EXPECT_EQ (img.width (), 15); + EXPECT_EQ (img.height (), 25); + EXPECT_EQ (img.stride (), 4); + + img.fill (true); + EXPECT_EQ (img.scan_line (5)[1], 0xff); + + lay::MonoImage img2; + img2 = img; + EXPECT_EQ (img2.width (), 15); + EXPECT_EQ (img2.height (), 25); + + EXPECT_EQ (img.scan_line (5)[1], 0xff); + EXPECT_EQ (img2.scan_line (5)[1], 0xff); + + img2.fill (false); + EXPECT_EQ (img.scan_line (5)[1], 0xff); + EXPECT_EQ (img2.scan_line (5)[1], 0); + + img2.swap (img); + EXPECT_EQ (img2.scan_line (5)[1], 0xff); + EXPECT_EQ (img.scan_line (5)[1], 0); + + img2 = img; + EXPECT_EQ (compare_images (img, img2), true); + EXPECT_EQ (img.scan_line (5)[1], 0); + EXPECT_EQ (img2.scan_line (5)[1], 0); + + img2 = lay::MonoImage (10, 16); + EXPECT_EQ (img.width (), 15); + EXPECT_EQ (img.height (), 25); + EXPECT_EQ (img2.width (), 10); + EXPECT_EQ (img2.height (), 16); + img2.fill (true); + EXPECT_EQ (compare_images (img, img2), false); + + EXPECT_EQ (img.scan_line (5)[1], 0); + EXPECT_EQ (img2.scan_line (5)[0], 0xff); + + img = std::move (img2); + EXPECT_EQ (compare_images (img, img2), false); + EXPECT_EQ (img.width (), 10); + EXPECT_EQ (img.height (), 16); + EXPECT_EQ (img.scan_line (5)[0], 0xff); + + lay::MonoImage img3 (img); + EXPECT_EQ (compare_images (img, img3), true); + EXPECT_EQ (img3.width (), 10); + EXPECT_EQ (img3.height (), 16); + EXPECT_EQ (img3.scan_line (5)[1], 0xff); + + img.fill (false); + EXPECT_EQ (compare_images (img, img3), false); + EXPECT_EQ (img3.width (), 10); + EXPECT_EQ (img3.height (), 16); + EXPECT_EQ (img3.scan_line (5)[1], 0xff); + EXPECT_EQ (img.width (), 10); + EXPECT_EQ (img.height (), 16); + EXPECT_EQ (img.scan_line (5)[1], 0); + + lay::MonoImage img4 (std::move (img)); + EXPECT_EQ (img4.width (), 10); + EXPECT_EQ (img4.height (), 16); + EXPECT_EQ (img4.scan_line (5)[1], 0); + + // other constructors + EXPECT_EQ (compare_images (lay::MonoImage (img4.width (), img4.height (), (const uint8_t *) img4.data ()), img4), true); + EXPECT_EQ (compare_images (lay::MonoImage (img4.width (), img4.height (), (const uint8_t *) img4.data (), img4.stride ()), img4), true); + + uint8_t *dnew = new uint8_t [ img4.width () * img4.height () * sizeof (uint8_t) ]; + memcpy (dnew, (const uint8_t *) img4.data (), img4.stride () * img4.height ()); + EXPECT_EQ (compare_images (lay::MonoImage (img4.width (), img4.height (), dnew), img4), true); +} + +#if defined(HAVE_QT) + +TEST(12) +{ + lay::MonoImage img (227, 231); + + for (unsigned int i = 0; i < img.stride (); ++i) { + for (unsigned int j = 0; j < img.height (); ++j) { + img.scan_line (j) [i] = uint8_t (i * j); + } + } + + EXPECT_EQ (img.to_image ().format () == QImage::Format_MonoLSB, true); + + std::string tmp = tmp_file ("test.png"); + QImage qimg = img.to_image (); + qimg.save (tl::to_qstring (tmp)); + tl::info << "PNG file written to " << tmp; + + std::string au = tl::testsrc () + "/testdata/lay/au_mono.png"; + tl::info << "PNG file read from " << au; + + EXPECT_EQ (compare_images_mono (qimg.convertToFormat (QImage::Format_Mono), au), true); +} + +#endif diff --git a/src/tl/tl/tlCopyOnWrite.h b/src/tl/tl/tlCopyOnWrite.h index 54ef6dc47..ed1aabca7 100644 --- a/src/tl/tl/tlCopyOnWrite.h +++ b/src/tl/tl/tlCopyOnWrite.h @@ -153,7 +153,6 @@ public: } tl::MutexLocker locker (&ms_lock); - std::swap (mp_x, other.mp_x); std::swap (mp_holder, other.mp_holder); } @@ -250,7 +249,6 @@ public: } private: - X *mp_x; copy_on_write_holder *mp_holder; void release () diff --git a/testdata/lay/au_mono.png b/testdata/lay/au_mono.png new file mode 100644 index 0000000000000000000000000000000000000000..5b7781c39f0a491f01f6400302a9315063a71889 GIT binary patch literal 6520 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 eim$T