From 9f4ccaaedc525401c5abb3cad5dc5e817b51d287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20K=C3=B6fferlein?= Date: Sat, 2 May 2020 13:45:20 +0200 Subject: [PATCH] Issue #535 (Image handling enhancements) (#551) * Menu item to hide or show all images * Images: asymmetric color nodes. * Added RBA::image::from_s for reading image back from string. Added tests. * Added image file reader test (RBA) * Added lyimg format for image file persistence. * Small fix of unit tests. * Added GSI binding for new image features and tests. * Save and load for .lyimg formats in image properties dialog. --- src/img/img/ImagePropertiesPage.ui | 1036 ++++++++++++---------- src/img/img/gsiDeclImg.cc | 121 ++- src/img/img/img.pro | 6 +- src/img/img/imgObject.cc | 180 ++-- src/img/img/imgObject.h | 20 +- src/img/img/imgPlugin.cc | 9 + src/img/img/imgPlugin.h | 8 +- src/img/img/imgPropertiesPage.cc | 78 +- src/img/img/imgPropertiesPage.h | 1 + src/img/img/imgService.cc | 131 ++- src/img/img/imgService.h | 20 + src/img/img/imgStream.cc | 470 ++++++++++ src/img/img/imgStream.h | 60 ++ src/img/img/imgWidgets.cc | 130 ++- src/img/img/imgWidgets.h | 48 +- src/img/unit_tests/imgFile.cc | 292 ++++++ src/img/unit_tests/imgObject.cc | 36 +- src/img/unit_tests/unit_tests.pro | 1 + src/lay/lay/images/locked_16.png | Bin 0 -> 318 bytes src/lay/lay/images/unlocked_16.png | Bin 0 -> 368 bytes src/lay/lay/layResources.qrc | 2 + src/laybasic/laybasic/layAbstractMenu.cc | 3 + src/tl/tl/tlString.cc | 45 + src/tl/tl/tlString.h | 20 + testdata/img/gs.png | Bin 0 -> 58705 bytes testdata/ruby/imgObject.rb | 75 +- 26 files changed, 2142 insertions(+), 650 deletions(-) create mode 100644 src/img/img/imgStream.cc create mode 100644 src/img/img/imgStream.h create mode 100644 src/img/unit_tests/imgFile.cc create mode 100644 src/lay/lay/images/locked_16.png create mode 100644 src/lay/lay/images/unlocked_16.png create mode 100644 testdata/img/gs.png diff --git a/src/img/img/ImagePropertiesPage.ui b/src/img/img/ImagePropertiesPage.ui index 1c6ee4d0a..d0453605b 100644 --- a/src/img/img/ImagePropertiesPage.ui +++ b/src/img/img/ImagePropertiesPage.ui @@ -1,7 +1,8 @@ - + + ImagePropertiesPage - - + + 0 0 @@ -9,42 +10,65 @@ 642 - + Form - - - 9 - - + + 6 + + 9 + + + 9 + + + 9 + + + 9 + - - + + QFrame::NoFrame - + QFrame::Raised - - + + 0 - + + 0 + + + 0 + + + 0 + + 6 - - - - - 5 - 0 + + + + Browse + + + + + + + 0 0 - + Sans Serif 12 @@ -55,275 +79,187 @@ false - + Image Properties - - - - - 5 - 5 - 1 - 0 - - - - QFrame::Box - - - QFrame::Plain - - - - 0 - - - 0 - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - - 5 - 5 - 1 - 0 - - - - <Filename> - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - - - - Browse - - - - - - - <File info> - - - - - - - QFrame::Plain - - - File - - - - - - + + + QFrame::NoFrame - + QFrame::Plain - - + + 0 - + + 0 + + + 0 + + + 0 + + 6 - - - + + + h = - - + + - - - + + + x = - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + - - - + + + Perspective - - + + - - + + - - - + + + degree - - + + - - + + - - - + + + Shear angle - - - + + + Pixel Size - - - + + + degree - - - + + + micron - - - + + + Rotation angle - - - + + + micron - - - + + + w = - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - + + + micron - - - + + + Center - - - + + + x = - - - + + + Mirrored (at X-axis) - - - + + + micron - - + + - - - + + + degree - + - + Qt::Vertical - + 20 5 @@ -331,29 +267,29 @@ - - - + + + y = - - - + + + degree - + - + Qt::Vertical - + QSizePolicy::Fixed - + 507 16 @@ -361,18 +297,18 @@ - - + + - + - + Qt::Vertical - + QSizePolicy::Fixed - + 507 5 @@ -380,30 +316,30 @@ - - - + + + y = - - - + + + Landmarks - - - + + + Define - - - + + + (use them to align the image with the layout) @@ -411,65 +347,185 @@ + + + + QFrame::Plain + + + File + + + + + + + + 1 + 0 + + + + QFrame::Box + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 1 + 0 + + + + <Filename> + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + Save As ... + + + + + + + <File info> + + + - - + + Data To Display - - - 9 - - + + 6 + + 9 + + + 9 + + + 9 + + + 9 + - - + + QFrame::NoFrame - + QFrame::Raised - - - 0 - - + + 6 + + 0 + + + 0 + + + 0 + + + 0 + - - + + Pixel value range - + - - + + ... to - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + - + Qt::Horizontal - + 40 20 @@ -481,64 +537,97 @@ - - - 2 + + + 0 - - + + Color Mapping - - - 9 - - + + 6 + + 9 + + + 9 + + + 9 + + + 9 + - - - - 13 - 7 + + + 0 0 - + Qt::ActionsContextMenu - - <html><head><meta name="qrichtext" content="1" /><style type="text/css"> + + <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal; text-decoration:none;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Double-click to create a new node.</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Press "Delete" to delete the selected node.</p></body></html> +</style></head><body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal; text-decoration:none;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Double-click to create a new node.</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Press "Delete" to delete the selected node.</p></body></html> - - + + QFrame::NoFrame - + QFrame::Raised - - - 0 - - + + 6 + + 0 + + + 0 + + + 0 + + + 0 + + + + + Value + + + + + + + + 0 + 0 + + + + - + Qt::Horizontal - + 21 20 @@ -547,35 +636,19 @@ p, li { white-space: pre-wrap; } - - - Value - - - - - - - - 5 - 0 - 0 - 0 - - - - - - - + + Color - - - + + + QFrame::NoFrame + + + QFrame::Raised @@ -584,53 +657,62 @@ p, li { white-space: pre-wrap; } - - + + Brightness / Contrast - - + + 9 - + + 9 + + + 9 + + + 9 + + 6 - - - - 100 - - + + + -100 - + + 100 + + 1 - - - + + + 100% - - - + + + 3.0 - + - + Qt::Horizontal - + QSizePolicy::Fixed - + 40 20 @@ -638,119 +720,119 @@ p, li { white-space: pre-wrap; } - - - + + + 100 - + Qt::Horizontal - - - + + + 100% - - - + + + 0.3 - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - + + + -100 - + 100 - + Qt::Horizontal - - - + + + -100 - + 100 - + Qt::Horizontal - - - + + + Contrast - - - - 100 - - + + + -100 + + 100 + - - - + + + -100% - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - + + + Gamma - - - + + + -100% - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - + + + Brightness - + - + Qt::Horizontal - + QSizePolicy::Fixed - + 40 20 @@ -758,65 +840,74 @@ p, li { white-space: pre-wrap; } - - - - 3.000000000000000 - - + + + 0.000000000000000 - + + 3.000000000000000 + + 0.010000000000000 - - + + RGB Channels - - + + 9 - + + 9 + + + 9 + + + 9 + + 6 - - - + + + 2.000000000000000 - + 0.010000000000000 - - - + + + 0 - - - + + + x 2.0 - + - + Qt::Horizontal - + QSizePolicy::Fixed - + 40 20 @@ -824,114 +915,114 @@ p, li { white-space: pre-wrap; } - - - + + + 100 - + Qt::Horizontal - - - + + + Green - - - + + + 0 - - - + + + 0 - - - + + + 100 - + Qt::Horizontal - - - + + + 100 - + Qt::Horizontal - - - + + + Blue - - - + + + Red - - - + + + x 2.0 - - - + + + 2.000000000000000 - + 0.010000000000000 - - - + + + 2.000000000000000 - + 0.010000000000000 - - - + + + x 2.0 - + - + Qt::Horizontal - + QSizePolicy::Fixed - + 40 20 @@ -944,36 +1035,45 @@ p, li { white-space: pre-wrap; } - - + + QFrame::NoFrame - + QFrame::Raised - - - 0 - - + + 6 + + 0 + + + 0 + + + 0 + + + 0 + - - + + Preview (Auto apply) - + true - + Qt::Horizontal - + 40 20 @@ -982,8 +1082,8 @@ p, li { white-space: pre-wrap; } - - + + Reset @@ -996,10 +1096,10 @@ p, li { white-space: pre-wrap; } - + Qt::Vertical - + 524 16 @@ -1010,16 +1110,17 @@ p, li { white-space: pre-wrap; } - - lay::SimpleColorButton - QPushButton -
layWidgets.h
-
img::ColorBar QWidget
imgWidgets.h
+ + img::TwoColorWidget + QFrame +
imgWidgets.h
+ 1 +
width_le @@ -1048,7 +1149,6 @@ p, li { white-space: pre-wrap; } brightness_sb value_le browse_pb - color_pb gamma_slider brightness_slider diff --git a/src/img/img/gsiDeclImg.cc b/src/img/img/gsiDeclImg.cc index e72ee4a0c..1fbcc7ac5 100644 --- a/src/img/img/gsiDeclImg.cc +++ b/src/img/img/gsiDeclImg.cc @@ -26,6 +26,7 @@ #include "gsiSignals.h" #include "imgObject.h" #include "imgService.h" +#include "imgStream.h" #include "dbTilingProcessor.h" #include "layLayoutView.h" @@ -44,7 +45,12 @@ static void clear_colormap (img::DataMapping *dm) static void add_colormap (img::DataMapping *dm, double value, lay::color_t color) { - dm->false_color_nodes.push_back (std::make_pair (value, QColor (color))); + dm->false_color_nodes.push_back (std::make_pair (value, std::make_pair (QColor (color), QColor (color)))); +} + +static void add_colormap2 (img::DataMapping *dm, double value, lay::color_t lcolor, lay::color_t rcolor) +{ + dm->false_color_nodes.push_back (std::make_pair (value, std::make_pair (QColor (lcolor), QColor (rcolor)))); } static size_t num_colormap_entries (const img::DataMapping *dm) @@ -55,7 +61,25 @@ static size_t num_colormap_entries (const img::DataMapping *dm) static lay::color_t colormap_color (const img::DataMapping *dm, size_t i) { if (i < dm->false_color_nodes.size ()) { - return dm->false_color_nodes [i].second.rgb (); + return dm->false_color_nodes [i].second.first.rgb (); + } else { + return 0; + } +} + +static lay::color_t colormap_lcolor (const img::DataMapping *dm, size_t i) +{ + if (i < dm->false_color_nodes.size ()) { + return dm->false_color_nodes [i].second.first.rgb (); + } else { + return 0; + } +} + +static lay::color_t colormap_rcolor (const img::DataMapping *dm, size_t i) +{ + if (i < dm->false_color_nodes.size ()) { + return dm->false_color_nodes [i].second.second.rgb (); } else { return 0; } @@ -147,20 +171,53 @@ gsi::Class decl_ImageDataMapping ("lay", "ImageDataMapping", "blue component (0 to 255), the second byte the green component and the third byte the " "red component, i.e. 0xff0000 is red and 0x0000ff is blue. " ) + + gsi::method_ext ("add_colormap_entry", &gsi::add_colormap2, gsi::arg ("value"), gsi::arg ("lcolor"), gsi::arg ("rcolor"), + "@brief Add a colormap entry for this data mapping object.\n" + "@param value The value at which the given color should be applied.\n" + "@param lcolor The color to apply left of the value (a 32 bit RGB value).\n" + "@param rcolor The color to apply right of the value (a 32 bit RGB value).\n" + "\n" + "This settings establishes a color mapping for a given value in the monochrome channel. " + "The colors must be given as a 32 bit integer, where the lowest order byte describes the " + "blue component (0 to 255), the second byte the green component and the third byte the " + "red component, i.e. 0xff0000 is red and 0x0000ff is blue.\n" + "\n" + "In contrast to the version with one color, this version allows specifying a color left and right " + "of the value - i.e. a discontinuous step.\n" + "\n" + "This variant has been introduced in version 0.27.\n" + ) + gsi::method_ext ("num_colormap_entries", &gsi::num_colormap_entries, "@brief Returns the current number of color map entries.\n" "@return The number of entries.\n" ) + - gsi::method_ext ("colormap_color", &gsi::colormap_color, gsi::arg ("n"), - "@brief Returns the color for a given color map entry.\n" - "@param n The index of the entry (0..\\num_colormap_entries-1)\n" - "@return The color (see \\add_colormap_entry for a description).\n" - ) + gsi::method_ext ("colormap_value", &gsi::colormap_value, gsi::arg ("n"), "@brief Returns the vlue for a given color map entry.\n" "@param n The index of the entry (0..\\num_colormap_entries-1)\n" "@return The value (see \\add_colormap_entry for a description).\n" ) + + gsi::method_ext ("colormap_color", &gsi::colormap_color, gsi::arg ("n"), + "@brief Returns the color for a given color map entry.\n" + "@param n The index of the entry (0..\\num_colormap_entries-1)\n" + "@return The color (see \\add_colormap_entry for a description).\n" + "\n" + "NOTE: this version is deprecated and provided for backward compatibility. For discontinuous nodes " + "this method delivers the left-sided color." + ) + + gsi::method_ext ("colormap_lcolor", &gsi::colormap_lcolor, gsi::arg ("n"), + "@brief Returns the left-side color for a given color map entry.\n" + "@param n The index of the entry (0..\\num_colormap_entries-1)\n" + "@return The color (see \\add_colormap_entry for a description).\n" + "\n" + "This method has been introduced in version 0.27." + ) + + gsi::method_ext ("colormap_rcolor", &gsi::colormap_rcolor, gsi::arg ("n"), + "@brief Returns the right-side color for a given color map entry.\n" + "@param n The index of the entry (0..\\num_colormap_entries-1)\n" + "@return The color (see \\add_colormap_entry for a description).\n" + "\n" + "This method has been introduced in version 0.27." + ) + gsi::method_ext ("brightness=", &gsi::set_brightness, gsi::arg ("brightness"), "@brief Set the brightness\n" "See \\brightness for a description of this property.\n" @@ -350,6 +407,31 @@ private: tl::DeferredMethod dm_update_view; }; +static ImageRef *img_from_s (const std::string &s) +{ + std::auto_ptr img (new ImageRef ()); + img->from_string (s.c_str ()); + return img.release (); +} + +static ImageRef *load_image (const std::string &path) +{ + tl::InputFile file (path); + tl::InputStream stream (file); + + std::auto_ptr read; + read.reset (img::ImageStreamer::read (stream)); + // need to create a copy for now ... + return new ImageRef (*read); +} + +static void save_image (const ImageRef *image, const std::string &path) +{ + tl::OutputFile file (path); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, *image); +} + static ImageRef *new_image () { return new ImageRef (); @@ -474,6 +556,20 @@ static std::vector get_mask_data (ImageRef *obj) gsi::Class decl_BasicImage ("lay", "BasicImage", gsi::Methods (), "@hide"); gsi::Class decl_Image (decl_BasicImage, "lay", "Image", + gsi::constructor ("from_s", &gsi::img_from_s, gsi::arg ("s"), + "@brief Creates an image from the string returned by \\to_s.\n" + "This method has been introduced in version 0.27." + ) + + gsi::constructor ("read", &load_image, gsi::arg ("path"), + "@brief Loads the image from the given path.\n" + "\n" + "This method expects the image file as a KLayout image format file (.lyimg). " + "This is a XML-based format containing the image data plus placement and transformation " + "information for the image placement. In addition, image manipulation parameters for " + "false color display and color channel enhancement are embedded.\n" + "\n" + "This method has been introduced in version 0.27." + ) + gsi::constructor ("new", &gsi::new_image, "@brief Create a new image with the default attributes" "\n" @@ -614,6 +710,10 @@ gsi::Class decl_Image (decl_BasicImage, "lay", "Image", "@param t The magnifying transformation to apply\n" "@return The transformed object\n" ) + + gsi::method ("clear", &ImageRef::clear, + "@brief Clears the image data (sets to 0 or black).\n" + "This method has been introduced in version 0.27." + ) + gsi::method ("width", &ImageRef::width, "@brief Gets the width of the image in pixels\n" "@return The width in pixels\n" @@ -922,7 +1022,12 @@ gsi::Class decl_Image (decl_BasicImage, "lay", "Image", ) + gsi::method ("to_s", &ImageRef::to_string, "@brief Converts the image to a string\n" + "The string returned can be used to create an image object using \\from_s.\n" "@return The string\n" + ) + + gsi::method_ext ("write", &save_image, gsi::arg ("path"), + "@brief Saves the image to KLayout's image format (.lyimg)\n" + "This method has been introduced in version 0.27." ), "@brief An image to be stored as a layout annotation\n" "\n" @@ -1276,7 +1381,7 @@ public: { if (mp_image) { db::Matrix3d m = db::Matrix3d::disp ((p0 - db::DPoint ()) + db::DVector (nx * dx * 0.5, ny * dy * 0.5)) * db::Matrix3d::mag (dx, dy); - *mp_image = img::Object (nx, ny, m, false); + *mp_image = img::Object (nx, ny, m, false, false); } } diff --git a/src/img/img/img.pro b/src/img/img/img.pro index 2f667acbb..c5337e156 100644 --- a/src/img/img/img.pro +++ b/src/img/img/img.pro @@ -15,7 +15,8 @@ HEADERS = \ imgService.h \ imgWidgets.h \ imgForceLink.h \ - imgCommon.h + imgCommon.h \ + imgStream.h FORMS = \ AddNewImageDialog.ui \ @@ -31,7 +32,8 @@ SOURCES = \ imgPropertiesPage.cc \ imgService.cc \ imgWidgets.cc \ - imgForceLink.cc + imgForceLink.cc \ + imgStream.cc INCLUDEPATH += $$TL_INC $$DB_INC $$GSI_INC $$LAYBASIC_INC $$DB_INC DEPENDPATH += $$TL_INC $$DB_INC $$GSI_INC $$LAYBASIC_INC $$DB_INC diff --git a/src/img/img/imgObject.cc b/src/img/img/imgObject.cc index 1e0e45cd8..c0da813d9 100644 --- a/src/img/img/imgObject.cc +++ b/src/img/img/imgObject.cc @@ -23,6 +23,7 @@ #include "imgObject.h" #include "imgWidgets.h" // for interpolate_color() +#include "imgStream.h" #include "tlLog.h" #include "tlTimer.h" #include "layPlugin.h" @@ -50,8 +51,8 @@ namespace img DataMapping::DataMapping () : brightness (0.0), contrast (0.0), gamma (1.0), red_gain (1.0), green_gain (1.0), blue_gain (1.0) { - false_color_nodes.push_back (std::make_pair (0.0, QColor (0, 0, 0))); - false_color_nodes.push_back (std::make_pair (1.0, QColor (255, 255, 255))); + false_color_nodes.push_back (std::make_pair (0.0, std::make_pair (QColor (0, 0, 0), QColor (0, 0, 0)))); + false_color_nodes.push_back (std::make_pair (1.0, std::make_pair (QColor (255, 255, 255), QColor (255, 255, 255)))); } bool @@ -91,7 +92,10 @@ DataMapping::operator== (const DataMapping &d) const if (fabs (false_color_nodes[i].first - d.false_color_nodes[i].first) > epsilon) { return false; } - if (false_color_nodes[i].second != d.false_color_nodes[i].second) { + if (false_color_nodes[i].second.first != d.false_color_nodes[i].second.first) { + return false; + } + if (false_color_nodes[i].second.second != d.false_color_nodes[i].second.second) { return false; } } @@ -136,8 +140,11 @@ DataMapping::operator< (const DataMapping &d) const if (fabs (false_color_nodes[i].first - d.false_color_nodes[i].first) > epsilon) { return false_color_nodes[i].first < d.false_color_nodes[i].first; } - if (false_color_nodes[i].second != d.false_color_nodes[i].second) { - return false_color_nodes[i].second.rgb () < d.false_color_nodes[i].second.rgb (); + if (false_color_nodes[i].second.first != d.false_color_nodes[i].second.first) { + return false_color_nodes[i].second.first.rgb () < d.false_color_nodes[i].second.first.rgb (); + } + if (false_color_nodes[i].second.second != d.false_color_nodes[i].second.second) { + return false_color_nodes[i].second.second.rgb () < d.false_color_nodes[i].second.second.rgb (); } } @@ -186,10 +193,10 @@ DataMapping::create_data_mapping (bool monochrome, double xmin, double xmax, uns for (unsigned int i = 1; i < false_color_nodes.size (); ++i) { int h1, s1, v1; - false_color_nodes [i - 1].second.getHsv (&h1, &s1, &v1); + false_color_nodes [i - 1].second.second.getHsv (&h1, &s1, &v1); int h2, s2, v2; - false_color_nodes [i].second.getHsv (&h2, &s2, &v2); + false_color_nodes [i].second.first.getHsv (&h2, &s2, &v2); // The number of steps is chosen such that the full HSV band divides into approximately 200 steps double nsteps = 0.5 * sqrt (double (h1 - h2) * double (h1 - h2) + double (s1 - s2) * double (s1 - s2) + double (v1 - v2) * double (v1 - v2)); @@ -220,11 +227,11 @@ DataMapping::create_data_mapping (bool monochrome, double xmin, double xmax, uns double ylast = 0.0; if (channel == 0) { - ylast = false_color_nodes.back ().second.red (); + ylast = false_color_nodes.back ().second.second.red (); } else if (channel == 1) { - ylast = false_color_nodes.back ().second.green (); + ylast = false_color_nodes.back ().second.second.green (); } else if (channel == 2) { - ylast = false_color_nodes.back ().second.blue (); + ylast = false_color_nodes.back ().second.second.blue (); } gray_to_color->push_back (false_color_nodes.back ().first, ylast / 255.0); @@ -704,31 +711,16 @@ Object::Object () mp_pixel_data = 0; } -Object::Object (size_t w, size_t h, const db::DCplxTrans &trans, bool color) +Object::Object (size_t w, size_t h, const db::DCplxTrans &trans, bool color, bool byte_data) : m_trans (trans), m_id (make_id ()), m_min_value (0.0), m_max_value (1.0), m_min_value_set (false), m_max_value_set (false), m_visible (true), m_z_position (0) { m_updates_enabled = false; mp_pixel_data = 0; - mp_data = new DataHeader (w, h, color, false); + mp_data = new DataHeader (w, h, color, byte_data); mp_data->add_ref (); - - // The default data type is float - tl_assert (! is_byte_data ()); - - if (is_color ()) { - for (unsigned int c = 0; c < 3; ++c) { - float *d = mp_data->float_data (c); - for (size_t i = data_length (); i > 0; --i) { - *d++ = 0.0; - } - } - } else { - float *d = mp_data->float_data (); - for (size_t i = data_length (); i > 0; --i) { - *d++ = 0.0; - } - } + clear (); + m_updates_enabled = true; } Object::Object (size_t w, size_t h, const db::DCplxTrans &trans, unsigned char *d) @@ -802,32 +794,15 @@ Object::Object (const std::string &filename, const db::DCplxTrans &trans) m_updates_enabled = true; } -Object::Object (size_t w, size_t h, const db::Matrix3d &trans, bool color) +Object::Object (size_t w, size_t h, const db::Matrix3d &trans, bool color, bool byte_data) : m_trans (trans), m_id (make_id ()), m_min_value (0.0), m_max_value (1.0), m_min_value_set (false), m_max_value_set (false), m_visible (true), m_z_position (0) { m_updates_enabled = false; mp_pixel_data = 0; - mp_data = new DataHeader (w, h, color, false); + mp_data = new DataHeader (w, h, color, byte_data); mp_data->add_ref (); - - // The default data type is float - tl_assert (! is_byte_data ()); - - if (is_color ()) { - for (unsigned int c = 0; c < 3; ++c) { - float *d = mp_data->float_data (c); - for (size_t i = data_length (); i > 0; --i) { - *d++ = 0.0; - } - } - } else { - float *d = mp_data->float_data (); - for (size_t i = data_length (); i > 0; --i) { - *d++ = 0.0; - } - } - + clear (); m_updates_enabled = true; } @@ -1078,6 +1053,48 @@ Object::clone () const return new img::Object (*this); } +void +Object::clear () +{ + if (is_byte_data ()) { + + if (is_color ()) { + + for (unsigned int c = 0; c < 3; ++c) { + unsigned char *d = mp_data->byte_data (c); + for (size_t i = data_length (); i > 0; --i) { + *d++ = 0.0; + } + } + + } else { + + unsigned char *d = mp_data->byte_data (); + for (size_t i = data_length (); i > 0; --i) { + *d++ = 0.0; + } + + } + + } else if (is_color ()) { + + for (unsigned int c = 0; c < 3; ++c) { + float *d = mp_data->float_data (c); + for (size_t i = data_length (); i > 0; --i) { + *d++ = 0.0; + } + } + + } else { + + float *d = mp_data->float_data (); + for (size_t i = data_length (); i > 0; --i) { + *d++ = 0.0; + } + + } +} + db::DPolygon Object::image_box_poly (const db::DBox vp, const db::DCplxTrans &vpt) const { @@ -1266,18 +1283,33 @@ Object::from_string (const char *str, const char *base_dir) double x = 0.0; lay::ColorConverter cc; - QColor c; + QColor cl, cr; std::string s; m_data_mapping.false_color_nodes.clear (); while (! ex.at_end () && ! ex.test ("]")) { + ex.read (x); + ex.test (","); + + s.clear (); ex.read_word_or_quoted (s); - cc.from_string (s, c); - m_data_mapping.false_color_nodes.push_back (std::make_pair (x, c)); + cc.from_string (s, cl); + + if (ex.test (",")) { + s.clear (); + ex.read_word_or_quoted (s); + cc.from_string (s, cr); + } else { + cr = cl; + } + + m_data_mapping.false_color_nodes.push_back (std::make_pair (x, std::make_pair (cl, cr))); + ex.test (";"); + } } else if (ex.test ("width=")) { @@ -1456,6 +1488,24 @@ Object::read_file () tl::info << "Reading image file " << m_filename; } + try { + + tl::InputFile file (m_filename); + tl::InputStream stream (file); + std::auto_ptr read; + read.reset (img::ImageStreamer::read (stream)); + read->m_filename = m_filename; + + // for now we need to copy here ... + *this = *read; + + // exit on success + return; + + } catch (...) { + // continue with other formats ... + } + QImage qimage (tl::to_qstring (m_filename)); if (! qimage.isNull ()) { @@ -1604,7 +1654,12 @@ Object::to_string () const for (unsigned int i = 0; i < data_mapping ().false_color_nodes.size (); ++i) { os << data_mapping ().false_color_nodes[i].first; os << ","; - os << tl::to_word_or_quoted_string (cc.to_string (data_mapping ().false_color_nodes[i].second)); + const std::pair &clr = data_mapping ().false_color_nodes[i].second; + os << tl::to_word_or_quoted_string (cc.to_string (clr.first)); + if (clr.first != clr.second) { + os << ","; + os << tl::to_word_or_quoted_string (cc.to_string (clr.second)); + } os << ";"; } @@ -1681,6 +1736,25 @@ Object::to_string () const return os.str (); } +void +Object::swap (Object &other) +{ + m_filename.swap (other.m_filename); + std::swap (m_trans, other.m_trans); + std::swap (mp_data, other.mp_data); + std::swap (m_id, other.m_id); + std::swap (m_min_value, other.m_min_value); + std::swap (m_max_value, other.m_max_value); + std::swap (m_min_value_set, other.m_min_value_set); + std::swap (m_max_value_set, other.m_max_value_set); + std::swap (m_data_mapping, other.m_data_mapping); + std::swap (m_visible, other.m_visible); + std::swap (mp_pixel_data, other.mp_pixel_data); + m_landmarks.swap (other.m_landmarks); + std::swap (m_z_position, other.m_z_position); + std::swap (m_updates_enabled, other.m_updates_enabled); +} + size_t Object::width () const { diff --git a/src/img/img/imgObject.h b/src/img/img/imgObject.h index 82c9702ff..b68e5a040 100644 --- a/src/img/img/imgObject.h +++ b/src/img/img/imgObject.h @@ -52,6 +52,8 @@ class DataHeader; struct IMG_PUBLIC DataMapping { public: + typedef std::vector< std::pair > > false_color_nodes_type; + /** * @brief The constructor */ @@ -73,7 +75,7 @@ public: * Each node is a pair or x-value (normalized to a range of 0..1) and a corresponding color. * The list should have an element with x value of 0.0 and one with an x value of 1.0. */ - std::vector< std::pair > false_color_nodes; + false_color_nodes_type false_color_nodes; /** * @brief The brightness value @@ -177,8 +179,9 @@ public: * @param h The height of the image * @param trans The transformation from pixel space to micron space * @param color True to create a color image. + * @param byte_data True to make the image store the data in bytes */ - Object (size_t w, size_t h, const db::DCplxTrans &trans, bool color); + Object (size_t w, size_t h, const db::DCplxTrans &trans, bool color, bool byte_data); /** * @brief Constructor for a monochrome image with the given pixel values @@ -301,8 +304,9 @@ public: * @param h The height of the image * @param matrix The 3d transformation matrix from pixel space to micron space * @param color True to create a color image. + * @param byte_data True to create n image using bytes rather than floats */ - Object (size_t w, size_t h, const db::Matrix3d &matrix, bool color); + Object (size_t w, size_t h, const db::Matrix3d &matrix, bool color, bool byte_data); /** * @brief Constructor for a monochrome image with the given pixel values @@ -734,6 +738,11 @@ public: */ void set_data (size_t width, size_t height, const std::vector &red, const std::vector &green, const std::vector &blue); + /** + * @brief Clears the pixel data (sets the values to 0) + */ + void clear (); + /** * @brief Set the transformation matrix * @@ -937,6 +946,11 @@ public: */ virtual std::string to_string () const; + /** + * @brief Swap with another image object + */ + void swap (img::Object &other); + /** * @brief Return the memory used in bytes */ diff --git a/src/img/img/imgPlugin.cc b/src/img/img/imgPlugin.cc index fa88fe549..9df38ba76 100644 --- a/src/img/img/imgPlugin.cc +++ b/src/img/img/imgPlugin.cc @@ -29,6 +29,8 @@ namespace img { +const std::string cfg_images_visible ("images-visible"); + void PluginDeclaration::get_menu_entries (std::vector &menu_entries) const { @@ -39,6 +41,7 @@ PluginDeclaration::get_menu_entries (std::vector &menu_entries) menu_entries.push_back (lay::menu_item ("img::bring_to_front", "bring_to_front:edit", "edit_menu.image_menu.end", tl::to_string (QObject::tr ("Image Stack: Selected Images to Front")))); menu_entries.push_back (lay::menu_item ("img::bring_to_back", "bring_to_back:edit", "edit_menu.image_menu.end", tl::to_string (QObject::tr ("Image Stack: Selected Images to Back")))); menu_entries.push_back (lay::menu_item ("img::clear_all_images", "clear_all_images:edit", "edit_menu.image_menu.end", tl::to_string (QObject::tr ("Clear All Images")))); + menu_entries.push_back (lay::config_menu_item ("show_images", "view_menu.layout_group+", tl::to_string (QObject::tr ("Show Images")), cfg_images_visible, "?")); } lay::Plugin * @@ -54,6 +57,12 @@ PluginDeclaration::implements_editable (std::string &title) const return true; } +void +PluginDeclaration::get_options (std::vector < std::pair > &options) const +{ + options.push_back (std::pair (cfg_images_visible, "true")); +} + static tl::RegisteredClass config_decl (new img::PluginDeclaration (), 4000, "img::Plugin"); } diff --git a/src/img/img/imgPlugin.h b/src/img/img/imgPlugin.h index 5c1dcb632..bd392c7c8 100644 --- a/src/img/img/imgPlugin.h +++ b/src/img/img/imgPlugin.h @@ -29,18 +29,16 @@ namespace img { +extern const std::string cfg_images_visible; + class PluginDeclaration : public lay::PluginDeclaration { public: - virtual void get_options (std::vector < std::pair > & /*options*/) const - { - // .. nothing yet .. - } - virtual void get_menu_entries (std::vector &menu_entries) const; virtual lay::Plugin *create_plugin (db::Manager *manager, lay::Dispatcher *, lay::LayoutView *view) const; virtual bool implements_editable (std::string &title) const; + virtual void get_options (std::vector < std::pair > &options) const; }; } diff --git a/src/img/img/imgPropertiesPage.cc b/src/img/img/imgPropertiesPage.cc index b18293bb2..f53c0a100 100644 --- a/src/img/img/imgPropertiesPage.cc +++ b/src/img/img/imgPropertiesPage.cc @@ -23,9 +23,11 @@ #include "imgPropertiesPage.h" #include "imgLandmarksDialog.h" +#include "imgStream.h" #include "layLayoutView.h" #include "layFileDialog.h" #include "tlExceptions.h" +#include "tlFileUtils.h" namespace img { @@ -100,11 +102,12 @@ PropertiesPage::init () connect (action, SIGNAL (triggered ()), this, SLOT (reverse_color_order ())); false_color_control->addAction (action); - color_pb->set_color (QColor ()); + colors->set_color (std::make_pair (QColor (), QColor ())); + colors->setEnabled (false); connect (browse_pb, SIGNAL (clicked ()), this, SLOT (browse ())); - connect (color_pb, SIGNAL (color_changed (QColor)), false_color_control, SLOT (set_current_color (QColor))); - connect (false_color_control, SIGNAL (selection_changed (QColor)), color_pb, SLOT (set_color (QColor))); + connect (colors, SIGNAL (color_changed (std::pair)), false_color_control, SLOT (set_current_color (std::pair))); + connect (false_color_control, SIGNAL (selection_changed (std::pair)), colors, SLOT (set_color (std::pair))); connect (brightness_slider, SIGNAL (valueChanged (int)), this, SLOT (brightness_slider_changed (int))); connect (brightness_sb, SIGNAL (valueChanged (int)), this, SLOT (brightness_spinbox_changed (int))); @@ -126,6 +129,7 @@ PropertiesPage::init () connect (value_le, SIGNAL (returnPressed ()), this, SLOT (value_return_pressed ())); connect (reset_pb, SIGNAL (clicked ()), this, SLOT (reset_pressed ())); + connect (save_pb, SIGNAL (clicked ()), this, SLOT (save_pressed ())); connect (preview_cbx, SIGNAL (clicked ()), this, SLOT (preview_checked ())); connect (define_landmarks_pb, SIGNAL (clicked ()), this, SLOT (define_landmarks_pressed ())); } @@ -206,7 +210,8 @@ BEGIN_PROTECTED value_le->setText (QString ()); value_le->setEnabled (false); - color_pb->setEnabled (false_color_control->has_selection ()); + colors->setEnabled (false_color_control->has_selection ()); + colors->set_single_mode (false); double xmin, xmax; tl::from_string (tl::to_string (from_le->text ()), xmin); @@ -223,6 +228,10 @@ BEGIN_PROTECTED value_le->setText (tl::to_qstring (tl::sprintf ("%.4g", xx))); value_le->setEnabled (true); + } else if (false_color_control->has_selection ()) { + + colors->set_single_mode (true); + } recompute_histogram (); @@ -240,7 +249,8 @@ PropertiesPage::color_mapping_changed () value_le->setText (QString ()); value_le->setEnabled (false); - color_pb->setEnabled (false_color_control->has_selection ()); + colors->setEnabled (false_color_control->has_selection ()); + colors->set_single_mode (false); try { @@ -259,6 +269,10 @@ PropertiesPage::color_mapping_changed () value_le->setText (tl::to_qstring (tl::sprintf ("%.4g", xx))); value_le->setEnabled (true); + } else if (false_color_control->has_selection ()) { + + colors->set_single_mode (true); + } } catch (...) { } @@ -668,27 +682,27 @@ PropertiesPage::blue_spinbox_changed (double value) void PropertiesPage::black_to_white () { - std::vector > nodes; - nodes.push_back (std::make_pair (0.0, QColor (0, 0, 0))); - nodes.push_back (std::make_pair (1.0, QColor (255, 255, 255))); + std::vector > > nodes; + nodes.push_back (std::make_pair (0.0, std::make_pair (QColor (0, 0, 0), QColor (0, 0, 0)))); + nodes.push_back (std::make_pair (1.0, std::make_pair (QColor (255, 255, 255), QColor (255, 255, 255)))); false_color_control->set_nodes (nodes); } void PropertiesPage::white_to_black () { - std::vector > nodes; - nodes.push_back (std::make_pair (0.0, QColor (255, 255, 255))); - nodes.push_back (std::make_pair (1.0, QColor (0, 0, 0))); + std::vector > > nodes; + nodes.push_back (std::make_pair (0.0, std::make_pair (QColor (255, 255, 255), QColor (255, 255, 255)))); + nodes.push_back (std::make_pair (1.0, std::make_pair (QColor (0, 0, 0), QColor (0, 0, 0)))); false_color_control->set_nodes (nodes); } void PropertiesPage::red_to_blue () { - std::vector > nodes; - nodes.push_back (std::make_pair (0.0, QColor (255, 0, 0))); - nodes.push_back (std::make_pair (1.0, QColor (0, 0, 255))); + std::vector > > nodes; + nodes.push_back (std::make_pair (0.0, std::make_pair (QColor (255, 0, 0), QColor (255, 0, 0)))); + nodes.push_back (std::make_pair (1.0, std::make_pair (QColor (0, 0, 255), QColor (0, 0, 255)))); false_color_control->set_nodes (nodes); } @@ -696,18 +710,19 @@ PropertiesPage::red_to_blue () void PropertiesPage::blue_to_red () { - std::vector > nodes; - nodes.push_back (std::make_pair (0.0, QColor (0, 0, 255))); - nodes.push_back (std::make_pair (1.0, QColor (255, 0, 0))); + std::vector > > nodes; + nodes.push_back (std::make_pair (0.0, std::make_pair (QColor (0, 0, 255), QColor (0, 0, 255)))); + nodes.push_back (std::make_pair (1.0, std::make_pair (QColor (255, 0, 0), QColor (255, 0, 0)))); false_color_control->set_nodes (nodes); } void PropertiesPage::reverse_color_order () { - std::vector > nodes (false_color_control->nodes ()); + std::vector > > nodes (false_color_control->nodes ()); for (size_t i = 0; i < nodes.size () / 2; ++i) { - std::swap (nodes [i].second, nodes [nodes.size () - 1 - i].second); + std::swap (nodes [i].second.second, nodes [nodes.size () - 1 - i].second.first); + std::swap (nodes [i].second.first, nodes [nodes.size () - 1 - i].second.second); } false_color_control->set_nodes (nodes); } @@ -826,6 +841,31 @@ BEGIN_PROTECTED END_PROTECTED } +void +PropertiesPage::save_pressed () +{ +BEGIN_PROTECTED + + apply (); + + lay::FileDialog file_dialog (this, tl::to_string (QObject::tr ("Save As KLayout Image File")), tl::to_string (QObject::tr ("KLayout image files (*.lyimg);;All files (*)"))); + + std::string filename = mp_direct_image->filename (); + if (! filename.empty () && tl::extension (filename) != "lyimg") { + filename = tl::basename (filename) + ".lyimg"; + } + + if (file_dialog.get_save (filename)) { + + tl::OutputFile file (filename); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, *mp_direct_image); + + } + +END_PROTECTED +} + void PropertiesPage::reset_pressed () { diff --git a/src/img/img/imgPropertiesPage.h b/src/img/img/imgPropertiesPage.h index 6bbdc722d..1de6a3f88 100644 --- a/src/img/img/imgPropertiesPage.h +++ b/src/img/img/imgPropertiesPage.h @@ -86,6 +86,7 @@ private slots: void min_max_return_pressed (); void preview_checked (); void reset_pressed (); + void save_pressed (); void define_landmarks_pressed (); private: diff --git a/src/img/img/imgService.cc b/src/img/img/imgService.cc index 299982165..9df942dbe 100644 --- a/src/img/img/imgService.cc +++ b/src/img/img/imgService.cc @@ -34,8 +34,10 @@ #include "laybasicConfig.h" #include "layLayoutCanvas.h" #include "layProperties.h" +#include "layTipDialog.h" #include "tlExceptions.h" #include "imgService.h" +#include "imgPlugin.h" #include "ui_AddNewImageDialog.h" #include @@ -247,37 +249,6 @@ struct SortImagePtrByZOrder } }; -static const db::DUserObject *find_image (lay::LayoutView *view, const db::DPoint &p, const db::DBox &search_box, double l, double &dmin, const std::map *exclude = 0) -{ - std::vector images; - - // get valid images and sort by reverse z order (top one first) - lay::AnnotationShapes::touching_iterator r = view->annotation_shapes ().begin_touching (search_box); - while (! r.at_end ()) { - const img::Object *image = dynamic_cast ((*r).ptr ()); - if (image && image->is_visible () && (! exclude || exclude->find (view->annotation_shapes ().iterator_from_pointer (&*r)) == exclude->end ())) { - images.push_back (&*r); - } - ++r; - } - - std::stable_sort (images.begin (), images.end (), SortImagePtrByZOrder ()); - - // look for the "closest" image to the search box - dmin = std::numeric_limits ::max (); - const db::DUserObject *found = 0; - - for (std::vector ::const_iterator robj = images.begin (); robj != images.end (); ++robj) { - double d = std::numeric_limits ::max (); - if (is_selected (*dynamic_cast ((*robj)->ptr ()), p, view->box (), l, d)) { - found = *robj; - dmin = d; - } - } - - return found; -} - // ------------------------------------------------------------- View::View (img::Service *service, obj_iterator image_ref, img::View::Mode mode) @@ -440,7 +411,8 @@ Service::Service (db::Manager *manager, lay::LayoutView *view) mp_transient_view (0), m_move_mode (Service::move_none), m_moved_landmark (0), - m_keep_selection_for_landmark (false) + m_keep_selection_for_landmark (false), + m_images_visible (true) { // place images behind the grid z_order (-1); @@ -465,10 +437,29 @@ Service::annotations_changed () images_changed_event (); } -bool -Service::configure (const std::string & /*name*/, const std::string & /*value*/) +void +Service::show_images (bool f) { - return false; + if (m_images_visible != f) { + m_images_visible = f; + view ()->redraw (); + } +} + +bool +Service::configure (const std::string &name, const std::string &value) +{ + if (name == cfg_images_visible) { + + bool v = true; + tl::from_string (value, v); + show_images (v); + + return true; + + } else { + return false; + } } void @@ -649,7 +640,7 @@ Service::begin_move (lay::Editable::MoveMode mode, const db::DPoint &p, lay::ang m_p1 = p; double dmin = std::numeric_limits ::max (); - const db::DUserObject *robj = find_image (mp_view, p, search_dbox, l, dmin); + const db::DUserObject *robj = find_image (p, search_dbox, l, dmin); if (robj) { const img::Object *iobj = dynamic_cast (robj->ptr ()); @@ -919,6 +910,42 @@ Service::end_move (const db::DPoint &, lay::angle_constraint_type) m_move_mode = move_none; } +const db::DUserObject * +Service::find_image (const db::DPoint &p, const db::DBox &search_box, double l, double &dmin, const std::map *exclude) +{ + if (! m_images_visible) { + return 0; + } + + std::vector images; + + // get valid images and sort by reverse z order (top one first) + lay::AnnotationShapes::touching_iterator r = mp_view->annotation_shapes ().begin_touching (search_box); + while (! r.at_end ()) { + const img::Object *image = dynamic_cast ((*r).ptr ()); + if (image && image->is_visible () && (! exclude || exclude->find (mp_view->annotation_shapes ().iterator_from_pointer (&*r)) == exclude->end ())) { + images.push_back (&*r); + } + ++r; + } + + std::stable_sort (images.begin (), images.end (), SortImagePtrByZOrder ()); + + // look for the "closest" image to the search box + dmin = std::numeric_limits ::max (); + const db::DUserObject *found = 0; + + for (std::vector ::const_iterator robj = images.begin (); robj != images.end (); ++robj) { + double d = std::numeric_limits ::max (); + if (is_selected (*dynamic_cast ((*robj)->ptr ()), p, mp_view->box (), l, d)) { + found = *robj; + dmin = d; + } + } + + return found; +} + void Service::selection_to_view (img::View::Mode mode) { @@ -1134,7 +1161,7 @@ Service::click_proximity (const db::DPoint &pos, lay::Editable::SelectionMode mo // point selection: look for the "closest" images double dmin = std::numeric_limits ::max (); - const db::DUserObject *robj = find_image (mp_view, pos, search_dbox, l, dmin, exclude); + const db::DUserObject *robj = find_image (pos, search_dbox, l, dmin, exclude); // return the proximity value if (robj) { @@ -1157,7 +1184,7 @@ Service::transient_select (const db::DPoint &pos) // point selection: look for the "closest" image double dmin = std::numeric_limits ::max (); - const db::DUserObject *robj = find_image (mp_view, pos, search_dbox, l, dmin, &m_previous_selection); + const db::DUserObject *robj = find_image (pos, search_dbox, l, dmin, &m_previous_selection); // create the transient marker for the object found if (robj) { @@ -1200,6 +1227,10 @@ Service::clear_transient_selection () bool Service::select (const db::DBox &box, lay::Editable::SelectionMode mode) { + if (! m_images_visible) { + return false; + } + bool needs_update = false; bool any_selected = false; @@ -1274,7 +1305,7 @@ Service::select (const db::DBox &box, lay::Editable::SelectionMode mode) // point selection: look for the "closest" image double dmin = std::numeric_limits ::max (); - const db::DUserObject *robj = find_image (mp_view, box.p1 (), search_dbox, l, dmin, exclude); + const db::DUserObject *robj = find_image (box.p1 (), search_dbox, l, dmin, exclude); // select the one that was found if (robj) { @@ -1381,6 +1412,10 @@ Service::change_image_by_id (size_t id, const img::Object &to) void Service::render_bg (const lay::Viewport &vp, lay::ViewObjectCanvas &canvas) { + if (! m_images_visible) { + return; + } + std::vector images; lay::AnnotationShapes::touching_iterator user_object = mp_view->annotation_shapes ().begin_touching (vp.box ()); @@ -1409,11 +1444,29 @@ void Service::menu_activated (const std::string &symbol) { if (symbol == "img::clear_all_images") { + manager ()->transaction (tl::to_string (QObject::tr ("Clear all images"))); clear_images (); manager ()->commit (); + } else if (symbol == "img::add_image") { + + if (! images_visible ()) { + lay::TipDialog td (QApplication::activeWindow (), + tl::to_string (QObject::tr ("Images are not visible. If you add an image you will not see it.\n\n" + "Choose 'View/Show Images' to make images visible.")), + "add-image-while-not-visible", + lay::TipDialog::okcancel_buttons); + lay::TipDialog::button_type button = lay::TipDialog::null_button; + td.exec_dialog (button); + if (button == lay::TipDialog::cancel_button) { + // Don't bother the user with more dialogs. + return; + } + } + add_image (); + } else if (symbol == "img::bring_to_back") { bring_to_back (); } else if (symbol == "img::bring_to_front") { diff --git a/src/img/img/imgService.h b/src/img/img/imgService.h index b4bf52ae6..b0073e551 100644 --- a/src/img/img/imgService.h +++ b/src/img/img/imgService.h @@ -421,6 +421,19 @@ public: { return mp_view; } + + /** + * @brief Shows or hides the images + */ + void show_images (bool f); + + /** + * @brief Returns a value indicating whether images are shown or hidden + */ + bool images_visible () const + { + return m_images_visible; + } /** * @brief Implement the menu response function @@ -477,6 +490,8 @@ private: size_t m_moved_landmark; // Flag indicating that we want to keep the selection after the landmark was moved bool m_keep_selection_for_landmark; + // Flag indicating whether images are visible + bool m_images_visible; void show_message (); @@ -506,6 +521,11 @@ private: */ void copy_selected (); + /** + * @brief Finds an image object from the given point + */ + const db::DUserObject *find_image (const db::DPoint &p, const db::DBox &search_box, double l, double &dmin, const std::map *exclude = 0); + /** * @brief Update m_selected_image_views to reflect the selection */ diff --git a/src/img/img/imgStream.cc b/src/img/img/imgStream.cc new file mode 100644 index 000000000..5b605881c --- /dev/null +++ b/src/img/img/imgStream.cc @@ -0,0 +1,470 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2020 Matthias Koefferlein + + This program 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. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "imgStream.h" +#include "tlXMLParser.h" +#include "tlXMLWriter.h" +#include "tlTimer.h" +#include "layConverters.h" + +#include + +namespace img +{ + +class ImageProxy +{ +public: + ImageProxy (const img::Object *img = 0) + : mp_img (img), + m_width (1), m_height (1), + m_min_value (0.0), m_max_value (1.0), + m_color (false) + { + init (); + } + + bool is_color () const + { + return mp_img->is_color (); + } + + void set_color (bool f) + { + m_color = f; + } + + size_t width () const + { + return mp_img->width (); + } + + void set_width (size_t w) + { + m_width = w; + } + + size_t height () const + { + return mp_img->height (); + } + + void set_height (size_t h) + { + m_height = h; + } + + std::list::const_iterator begin_byte_data () const + { + return m_byte_data.begin (); + } + + std::list::const_iterator end_byte_data () const + { + return m_byte_data.end (); + } + + void push_byte_data (const std::string &s) + { + m_byte_data.push_back (s); + } + + std::list::const_iterator begin_data () const + { + return m_data.begin (); + } + + std::list::const_iterator end_data () const + { + return m_data.end (); + } + + void push_data (const std::string &s) + { + m_data.push_back (s); + } + + const db::Matrix3d &matrix () const + { + return mp_img->matrix (); + } + + void set_matrix (const db::Matrix3d &m) + { + m_matrix = m; + } + + double min_value () const + { + return mp_img->min_value (); + } + + void set_min_value (double h) + { + m_min_value = h; + } + + double max_value () const + { + return mp_img->max_value (); + } + + void set_max_value (double h) + { + m_max_value = h; + } + + const img::DataMapping &data_mapping () const + { + return mp_img->data_mapping (); + } + + void set_data_mapping (const img::DataMapping &dm) + { + m_data_mapping = dm; + } + + const img::Object::landmarks_type &landmarks () const + { + return mp_img->landmarks (); + } + + void set_landmarks (const img::Object::landmarks_type &lm) + { + m_landmarks = lm; + } + + img::Object *get_image () const; + +private: + const img::Object *mp_img; + + // reader mode + size_t m_width, m_height; + img::Object::landmarks_type m_landmarks; + img::DataMapping m_data_mapping; + double m_min_value, m_max_value; + db::Matrix3d m_matrix; + std::list m_byte_data, m_data; + bool m_color; + + void init (); +}; + +template +static void +string_to_pixels (img::Object *img, const std::string &s, size_t row, size_t w, bool color) +{ + tl::Extractor ex (s.c_str ()); + + size_t column = 0; + while (! ex.at_end () && column < w) { + + T1 r = 0; + T1 g = 0; + T1 b = 0; + T2 m = 0; + + unsigned int i = 0; + bool has_mask = false; + + while (! ex.at_end () && ! ex.test (";")) { + + if (i == 0) { + ex.read (r); + } else if (color && i == 1) { + ex.read (g); + } else if (color && i == 2) { + ex.read (b); + } else { + ex.read (m); + has_mask = true; + } + ++i; + + ex.test (","); + + } + + if (color) { + img->set_pixel (column, row, double (r), double (g), double (b)); + } else { + img->set_pixel (column, row, double (r)); + } + + if (has_mask) { + img->set_mask (column, row, m); + } + + ++column; + + } + +} + +img::Object * +ImageProxy::get_image () const +{ + std::auto_ptr img (new Object (std::max (size_t (1), m_width), std::max (size_t (1), m_height), m_matrix, m_color, ! m_byte_data.empty ())); + img->set_min_value (m_min_value); + img->set_max_value (m_max_value); + img->set_data_mapping (m_data_mapping); + img->set_landmarks (m_landmarks); + + if (! m_byte_data.empty ()) { + + std::list::const_iterator s = m_byte_data.begin (); + for (size_t i = 0; i < m_height; ++i) { + string_to_pixels (img.get (), *s++, i, m_width, m_color); + } + + } else { + + std::list::const_iterator s = m_data.begin (); + for (size_t i = 0; i < m_height; ++i) { + string_to_pixels (img.get (), *s++, i, m_width, m_color); + } + + } + + return img.release (); +} + +static void add_entry (std::string &heap, const float *&b, bool &first) +{ + if (b) { + if (! first) { + heap += ","; + } + heap += tl::to_string (*b++); + first = false; + } +} + +static void add_entry (std::string &heap, const unsigned char *&b, bool &first) +{ + if (b) { + if (! first) { + heap += ","; + } + heap += tl::to_string ((unsigned int) *b++); + first = false; + } +} + +template +static const std::string &data_to_string (std::string &heap, size_t l, const T1 *r, const T2 *g, const T3 *b, const T4 *m) +{ + heap.clear (); + + while (l-- > 0) { + bool first = true; + add_entry (heap, r, first); + add_entry (heap, g, first); + add_entry (heap, b, first); + add_entry (heap, m, first); + if (l > 0) { + heap += ";"; + } + } + + return heap; +} + +void +ImageProxy::init () +{ + if (!mp_img) { + return; + } + + size_t w = mp_img->width (); + size_t h = mp_img->height (); + + static std::string s; + + if (mp_img->is_color ()) { + + if (mp_img->is_byte_data ()) { + + const unsigned char *r = mp_img->byte_data (0); + const unsigned char *g = mp_img->byte_data (1); + const unsigned char *b = mp_img->byte_data (2); + const unsigned char *m = mp_img->mask (); + + for (size_t i = 0; i < h; ++i) { + m_byte_data.push_back (data_to_string (s, w, r + i * w, g + i * w, b + i * w, m ? (m + i * w) : 0)); + } + + } else { + + const float *r = mp_img->float_data (0); + const float *g = mp_img->float_data (1); + const float *b = mp_img->float_data (2); + const unsigned char *m = mp_img->mask (); + + for (size_t i = 0; i < h; ++i) { + m_data.push_back (data_to_string (s, w, r + i * w, g + i * w, b + i * w, m ? (m + i * w) : 0)); + } + + } + + } else { + + if (mp_img->is_byte_data ()) { + + const unsigned char *g = mp_img->byte_data (); + const unsigned char *m = mp_img->mask (); + + for (size_t i = 0; i < h; ++i) { + m_byte_data.push_back (data_to_string (s, w, g + i * w, (const unsigned char *) 0, (const unsigned char *) 0, m ? (m + i * w) : 0)); + } + + } else { + + const float *g = mp_img->float_data (); + const unsigned char *m = mp_img->mask (); + + for (size_t i = 0; i < h; ++i) { + m_data.push_back (data_to_string (s, w, g + i * w, (const float *) 0, (const float *) 0, m ? (m + i * w) : 0)); + } + + } + + } +} + +// -------------------------------------------------------------------------------------------------------------------------- + +namespace { + + struct PointConverter + { + std::string to_string (const db::DPoint &p) const + { + return p.to_string (); + } + + void from_string (const std::string &s, db::DPoint &p) const + { + tl::Extractor ex (s.c_str ()); + ex.read (p); + } + }; + + struct ColorMapConverter + { + std::string to_string (const std::pair > &cm) const + { + std::string s; + s = tl::to_string (cm.first); + s += ":"; + + lay::ColorConverter cc; + s += tl::to_word_or_quoted_string (cc.to_string (cm.second.first)); + if (cm.second.first != cm.second.second) { + s += ","; + s += tl::to_word_or_quoted_string (cc.to_string (cm.second.second)); + } + + return s; + } + + void from_string (const std::string &s, std::pair > &cm) const + { + tl::Extractor ex (s.c_str ()); + + ex.read (cm.first); + ex.test (":"); + + lay::ColorConverter cc; + + std::string w; + ex.read_word_or_quoted (w); + + cc.from_string (w, cm.second.first); + + if (ex.test (",")) { + + w.clear (); + ex.read_word_or_quoted (w); + + cc.from_string (w, cm.second.second); + + } else { + cm.second.second = cm.second.first; + } + } + }; + +} + +tl::XMLStruct s_img_structure ("image-data", + tl::make_member (&ImageProxy::is_color, &ImageProxy::set_color, "color") + + tl::make_member (&ImageProxy::width, &ImageProxy::set_width, "width") + + tl::make_member (&ImageProxy::height, &ImageProxy::set_height, "height") + + tl::make_member (&ImageProxy::matrix, &ImageProxy::set_matrix, "matrix") + + tl::make_member (&ImageProxy::min_value, &ImageProxy::set_min_value, "min-value") + + tl::make_member (&ImageProxy::max_value, &ImageProxy::set_max_value, "max-value") + + tl::make_element (&ImageProxy::data_mapping, &ImageProxy::set_data_mapping, "data-mapping", + tl::make_element (&img::DataMapping::false_color_nodes, "color-map", + tl::make_member (&img::DataMapping::false_color_nodes_type::begin, &img::DataMapping::false_color_nodes_type::end, &img::DataMapping::false_color_nodes_type::push_back, "color-map-entry", ColorMapConverter ()) + ) + + tl::make_member (&img::DataMapping::brightness, "brightness") + + tl::make_member (&img::DataMapping::contrast, "contrast") + + tl::make_member (&img::DataMapping::gamma, "gamma") + + tl::make_member (&img::DataMapping::red_gain, "red-gain") + + tl::make_member (&img::DataMapping::green_gain, "green-gain") + + tl::make_member (&img::DataMapping::blue_gain, "blue-gain") + ) + + tl::make_element (&ImageProxy::landmarks, &ImageProxy::set_landmarks, "landmarks", + tl::make_member (&img::Object::landmarks_type::begin, &img::Object::landmarks_type::end, &img::Object::landmarks_type::push_back, "landmark", PointConverter ()) + ) + + tl::make_member (&ImageProxy::begin_byte_data, &ImageProxy::end_byte_data, &ImageProxy::push_byte_data, "byte-data") + + tl::make_member (&ImageProxy::begin_data, &ImageProxy::end_data, &ImageProxy::push_data, "data") +); + +// -------------------------------------------------------------------------------------------------------------------------- + +img::Object * +ImageStreamer::read (tl::InputStream &stream) +{ + ImageProxy proxy; + + tl::SelfTimer timer (tl::verbosity () >= 21, tl::to_string (tr ("Reading image file: ")) + stream.source ()); + tl::XMLStreamSource in (stream, tl::to_string (tr ("Image file"))); + s_img_structure.parse (in, proxy); + + return proxy.get_image (); +} + +void +ImageStreamer::write (tl::OutputStream &stream, const img::Object &img) +{ + ImageProxy proxy (&img); + + tl::SelfTimer timer (tl::verbosity () >= 21, tl::to_string (tr ("Writing image file: ")) + stream.path ()); + s_img_structure.write (stream, proxy); +} + +} // namespace img + diff --git a/src/img/img/imgStream.h b/src/img/img/imgStream.h new file mode 100644 index 000000000..e6c5dfca2 --- /dev/null +++ b/src/img/img/imgStream.h @@ -0,0 +1,60 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2020 Matthias Koefferlein + + This program 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. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_imgStream +#define HDR_imgStream + +#include "imgCommon.h" + +#include "imgObject.h" +#include "tlStream.h" + +namespace img { + +/** + * @brief An object streaming image data from or to files + */ +struct IMG_PUBLIC ImageStreamer +{ +public: + /** + * @brief The constructor + */ + ImageStreamer () { } + + /** + * @brief Reads an image Object from a stream + * + * This method returns a new'd object. It's the responsibility of the caller to delete the object. + */ + static Object *read(tl::InputStream &stream); + + /** + * @brief Writes an image object to a stream + */ + static void write (tl::OutputStream &stream, const img::Object &img); +}; + +} + +#endif + diff --git a/src/img/img/imgWidgets.cc b/src/img/img/imgWidgets.cc index 297db30a9..b50dd5496 100644 --- a/src/img/img/imgWidgets.cc +++ b/src/img/img/imgWidgets.cc @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -45,36 +46,36 @@ const double epsilon = 1e-6; struct compare_first_of_node { - bool operator() (const std::pair &a, const std::pair &b) const + bool operator() (const std::pair > &a, const std::pair > &b) const { return a.first < b.first; } }; QColor -interpolated_color (const std::vector > &nodes, double x) +interpolated_color (const std::vector > > &nodes, double x) { if (nodes.size () < 1) { return QColor (); } else if (nodes.size () < 2) { - return nodes[0].second; + return x < nodes[0].first ? nodes[0].second.first : nodes[0].second.second; } else { - std::vector >::const_iterator p = std::lower_bound (nodes.begin (), nodes.end (), std::make_pair (x, QColor ()), compare_first_of_node ()); + std::vector > >::const_iterator p = std::lower_bound (nodes.begin (), nodes.end (), std::make_pair (x, std::make_pair (QColor (), QColor ())), compare_first_of_node ()); if (p == nodes.end ()) { - return nodes.back ().second; + return nodes.back ().second.second; } else if (p == nodes.begin ()) { - return nodes.front ().second; + return nodes.front ().second.first; } else { double x1 = p[-1].first; double x2 = p->first; int h1 = 0, s1 = 0, v1 = 0; - p[-1].second.getHsv (&h1, &s1, &v1); + p[-1].second.second.getHsv (&h1, &s1, &v1); int h2 = 0, s2 = 0, v2 = 0; - p->second.getHsv (&h2, &s2, &v2); + p->second.first.getHsv (&h2, &s2, &v2); int h = int (0.5 + h1 + double(x - x1) * double (h2 - h1) / double(x2 - x1)); int s = int (0.5 + s1 + double(x - x1) * double (s2 - s1) / double(x2 - x1)); @@ -89,11 +90,91 @@ interpolated_color (const std::vector > &nodes, doubl } } +// -------------------------------------------------------------------------------------------------------------------- + +TwoColorWidget::TwoColorWidget (QWidget *parent) + : QFrame (parent) +{ + setLayout (new QHBoxLayout (this)); + + mp_left = new lay::SimpleColorButton (this); + layout ()->addWidget (mp_left); + mp_right = new lay::SimpleColorButton (this); + layout ()->addWidget (mp_right); + mp_lock = new QToolButton (this); + layout ()->addWidget (mp_lock); + mp_lock->setCheckable (true); + mp_lock->setAutoRaise (true); + mp_lock->setIconSize (QSize (16, 16)); + + QIcon icon; + icon.addFile (":/locked_16.png", QSize (), QIcon::Normal, QIcon::On); + icon.addFile (":/unlocked_16.png", QSize (), QIcon::Normal, QIcon::Off); + mp_lock->setIcon (icon); + + connect (mp_left, SIGNAL (color_changed (QColor)), this, SLOT (lcolor_changed (QColor))); + connect (mp_right, SIGNAL (color_changed (QColor)), this, SLOT (rcolor_changed (QColor))); + connect (mp_lock, SIGNAL (clicked (bool)), this, SLOT (lock_changed (bool))); +} + +void +TwoColorWidget::set_color (std::pair c) +{ + mp_left->set_color (c.first); + mp_right->set_color (c.second); + mp_lock->setChecked (c.first == c.second); + mp_right->setVisible (! mp_lock->isChecked ()); +} + +void +TwoColorWidget::set_single_mode (bool f) +{ + mp_lock->setEnabled (! f); +} + +void +TwoColorWidget::lcolor_changed (QColor) +{ + if (mp_lock->isChecked ()) { + mp_right->set_color (mp_left->get_color ()); + } + emit color_changed (std::make_pair (mp_left->get_color (), mp_right->get_color ())); +} + +void +TwoColorWidget::rcolor_changed (QColor) +{ + if (mp_lock->isChecked ()) { + mp_left->set_color (mp_right->get_color ()); + } + emit color_changed (std::make_pair (mp_left->get_color (), mp_right->get_color ())); +} + +void +TwoColorWidget::lock_changed (bool checked) +{ + if (checked) { + + QColor cl = mp_left->get_color (); + QColor cr = mp_right->get_color (); + + QColor ca ((cl.red () + cr.red ()) / 2, (cl.green () + cr.green ()) / 2, (cl.blue () + cr.blue ()) / 2); + set_color (std::make_pair (ca, ca)); + + emit color_changed (std::make_pair (mp_left->get_color (), mp_right->get_color ())); + + } + + mp_right->setVisible (! mp_lock->isChecked ()); +} + +// -------------------------------------------------------------------------------------------------------------------- + ColorBar::ColorBar (QWidget *parent) : QWidget (parent), m_dragging (false), m_selected (-1) { - m_nodes.push_back (std::make_pair (0.0, QColor (0, 0, 0))); - m_nodes.push_back (std::make_pair (1.0, QColor (255, 255, 255))); + m_nodes.push_back (std::make_pair (0.0, std::make_pair (QColor (0, 0, 0), QColor (0, 0, 0)))); + m_nodes.push_back (std::make_pair (1.0, std::make_pair (QColor (255, 255, 255), QColor (255, 255, 255)))); } ColorBar::~ColorBar () @@ -122,7 +203,7 @@ ColorBar::mouseMoveEvent (QMouseEvent *event) } void -ColorBar::set_current_color (QColor c) +ColorBar::set_current_color (std::pair c) { if (has_selection ()) { m_nodes [m_selected].second = c; @@ -174,27 +255,27 @@ ColorBar::keyPressEvent (QKeyEvent *event) m_nodes.erase (m_nodes.begin () + m_selected); m_selected = -1; emit selection_changed (); - emit selection_changed (QColor ()); + emit selection_changed (std::make_pair (QColor (), QColor ())); update (); } } void -ColorBar::set_nodes (const std::vector > &nodes) +ColorBar::set_nodes (const std::vector > > &nodes) { m_nodes = nodes; std::sort (m_nodes.begin (), m_nodes.end (), compare_first_of_node ()); if (m_nodes.size () == 0 || fabs (m_nodes[0].first) > epsilon) { - m_nodes.insert (m_nodes.begin (), std::make_pair (0.0, QColor (0, 0, 0))); + m_nodes.insert (m_nodes.begin (), std::make_pair (0.0, std::make_pair (QColor (0, 0, 0), QColor (0, 0, 0)))); } else { m_nodes[0].first = 0.0; } - std::vector >::iterator w = m_nodes.begin (); - std::vector >::const_iterator nn = m_nodes.begin (); - for (std::vector >::const_iterator n = m_nodes.begin () + 1; n != m_nodes.end (); ++n) { + std::vector > >::iterator w = m_nodes.begin (); + std::vector > >::const_iterator nn = m_nodes.begin (); + for (std::vector > >::const_iterator n = m_nodes.begin () + 1; n != m_nodes.end (); ++n) { if (fabs (nn->first - n->first) > min_value_interval) { *w++ = *nn; nn = n; @@ -207,7 +288,7 @@ ColorBar::set_nodes (const std::vector > &nodes) if (m_nodes.back ().first > 1.0 - min_value_interval) { m_nodes.back ().first = 1.0; } else { - m_nodes.push_back (std::make_pair (1.0, QColor (255, 255, 255))); + m_nodes.push_back (std::make_pair (1.0, std::make_pair (QColor (255, 255, 255), QColor (255, 255, 255)))); } m_selected = -1; @@ -232,8 +313,8 @@ ColorBar::mousePressEvent (QMouseEvent *event) double xx = double (event->x () - xl) / double (xr - xl); double dmin = 100.0; - std::vector >::const_iterator pmin = m_nodes.end (); - for (std::vector >::const_iterator p = m_nodes.begin (); p != m_nodes.end (); ++p) { + std::vector > >::const_iterator pmin = m_nodes.end (); + for (std::vector > >::const_iterator p = m_nodes.begin (); p != m_nodes.end (); ++p) { double d = fabs (p->first - xx); if (d < 0.05 && d < dmin) { dmin = d; @@ -242,7 +323,7 @@ ColorBar::mousePressEvent (QMouseEvent *event) } if (pmin != m_nodes.end ()) { - m_selected = int (std::distance (std::vector >::const_iterator (m_nodes.begin ()), pmin)); + m_selected = int (std::distance (std::vector > >::const_iterator (m_nodes.begin ()), pmin)); emit selection_changed (); emit selection_changed (m_nodes [m_selected].second); m_dragging = true; @@ -250,7 +331,7 @@ ColorBar::mousePressEvent (QMouseEvent *event) } else { m_selected = -1; emit selection_changed (); - emit selection_changed (QColor ()); + emit selection_changed (std::make_pair (QColor (), QColor ())); update (); } @@ -278,10 +359,11 @@ ColorBar::mouseDoubleClickEvent (QMouseEvent *event) double xx = double (event->x () - xl) / double (xr - xl); - std::vector >::iterator p = std::lower_bound (m_nodes.begin (), m_nodes.end (), std::make_pair (xx, QColor ()), compare_first_of_node ()); + std::vector > >::iterator p = std::lower_bound (m_nodes.begin (), m_nodes.end (), std::make_pair (xx, std::make_pair (QColor (), QColor ())), compare_first_of_node ()); if (p != m_nodes.begin () && p != m_nodes.end ()) { m_selected = int (std::distance (m_nodes.begin (), p)); - m_nodes.insert (p, std::make_pair (xx, interpolated_color (m_nodes, xx))); + QColor ci = interpolated_color (m_nodes, xx); + m_nodes.insert (p, std::make_pair (xx, std::make_pair (ci, ci))); emit selection_changed (); emit selection_changed (m_nodes [m_selected].second); emit color_mapping_changed (); diff --git a/src/img/img/imgWidgets.h b/src/img/img/imgWidgets.h index cbcd6ba6f..1870fcb06 100644 --- a/src/img/img/imgWidgets.h +++ b/src/img/img/imgWidgets.h @@ -20,13 +20,15 @@ */ - - #ifndef HDR_imgWidgets #define HDR_imgWidgets +#include "layWidgets.h" + #include #include +#include +#include #include class QMouseEvent; @@ -41,7 +43,37 @@ namespace img * * TODO: move this somewhere else. */ -QColor interpolated_color (const std::vector > &nodes, double x); +QColor interpolated_color (const std::vector > > &nodes, double x); + +/** + * @brief A two-color widget + * + * This widget has two color buttons and a "lock" checkbox which makes both colors identical + */ +class TwoColorWidget + : public QFrame +{ +Q_OBJECT + +public: + TwoColorWidget (QWidget *parent); + +signals: + void color_changed (std::pair c); + +public slots: + void set_color (std::pair c); + void set_single_mode (bool f); + +private slots: + void lcolor_changed (QColor c); + void rcolor_changed (QColor c); + void lock_changed (bool checked); + +private: + lay::SimpleColorButton *mp_left, *mp_right; + QToolButton *mp_lock; +}; /** * @brief A color bar widget @@ -82,9 +114,9 @@ public: return m_selected >= 0; } - void set_nodes (const std::vector > &nodes); + void set_nodes (const std::vector > > &nodes); - const std::vector > &nodes () const + const std::vector > > &nodes () const { return m_nodes; } @@ -92,18 +124,18 @@ public: void set_histogram (const std::vector &histogram); public slots: - void set_current_color (QColor c); + void set_current_color (std::pair c); void set_current_position (double x); signals: void color_mapping_changed (); void selection_changed (); - void selection_changed (QColor c); + void selection_changed (std::pair c); private: bool m_dragging; int m_selected; - std::vector > m_nodes; + std::vector > > m_nodes; std::vector m_histogram; }; diff --git a/src/img/unit_tests/imgFile.cc b/src/img/unit_tests/imgFile.cc new file mode 100644 index 000000000..40ff82126 --- /dev/null +++ b/src/img/unit_tests/imgFile.cc @@ -0,0 +1,292 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2020 Matthias Koefferlein + + This program 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. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "imgStream.h" +#include "tlUnitTest.h" + +#include + +TEST(1_FloatMono) +{ + img::Object image (12, 8, db::DCplxTrans (1.5, 90.0, true, db::DVector (17, -42)), false, false); + + image.set_min_value (-0.25); + image.set_max_value (0.75); + + std::vector lm; + lm.push_back (db::DPoint (1, 2)); + lm.push_back (db::DPoint (-101, 102)); + image.set_landmarks (lm); + + img::DataMapping dm; + dm.blue_gain = 0.5; + dm.green_gain = 0.75; + dm.red_gain = 0.25; + dm.contrast = -0.5; + dm.gamma = 1.5; + dm.brightness = 1.25; + dm.false_color_nodes.clear (); + dm.false_color_nodes.push_back (std::make_pair (0.0, std::make_pair (QColor (0, 0, 0), QColor (0, 0, 0)))); + dm.false_color_nodes.push_back (std::make_pair (0.5, std::make_pair (QColor (255, 0, 0), QColor (0, 255, 0)))); + dm.false_color_nodes.push_back (std::make_pair (1.0, std::make_pair (QColor (255, 255, 255), QColor (255, 255, 255)))); + image.set_data_mapping (dm); + + image.set_pixel (0, 0, 0.25); + image.set_pixel (2, 5, 0.25); + image.set_pixel (7, 1, 0.125); + + std::string path = tmp_file ("tmp.lyimg"); + { + tl::OutputFile file (path); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, image); + } + + std::auto_ptr read; + + { + tl::InputFile file (path); + tl::InputStream stream (file); + read.reset (img::ImageStreamer::read (stream)); + } + + EXPECT_EQ (image.to_string (), read->to_string ()); +} + +TEST(2_FloatMonoWithMask) +{ + img::Object image (12, 8, db::DCplxTrans (1.5, 90.0, true, db::DVector (17, -42)), false, false); + + image.set_min_value (-0.25); + image.set_max_value (0.75); + + image.set_pixel (0, 0, 0.25); + image.set_pixel (2, 5, 0.25); + image.set_pixel (7, 1, 0.125); + + image.set_mask (1, 0, 1); + image.set_mask (1, 2, 1); + image.set_mask (1, 3, 0); + + std::string path = tmp_file ("tmp.lyimg"); + { + tl::OutputFile file (path); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, image); + } + + std::auto_ptr read; + + { + tl::InputFile file (path); + tl::InputStream stream (file); + read.reset (img::ImageStreamer::read (stream)); + } + + EXPECT_EQ (image.to_string (), read->to_string ()); +} + +TEST(3_ByteMono) +{ + img::Object image (12, 8, db::Matrix3d (db::DCplxTrans (1.5, 90.0, true, db::DVector (17, -42))), false, true); + + image.set_min_value (10); + image.set_max_value (240); + + image.set_pixel (0, 0, 50); + image.set_pixel (2, 5, 70); + image.set_pixel (7, 1, 120); + + std::string path = tmp_file ("tmp.lyimg"); + { + tl::OutputFile file (path); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, image); + } + + std::auto_ptr read; + + { + tl::InputFile file (path); + tl::InputStream stream (file); + read.reset (img::ImageStreamer::read (stream)); + } + + EXPECT_EQ (image.to_string (), read->to_string ()); +} + +TEST(4_ByteMonoWithMask) +{ + img::Object image (12, 8, db::DCplxTrans (1.5, 90.0, true, db::DVector (17, -42)), false, true); + + image.set_min_value (10); + image.set_max_value (240); + + image.set_pixel (0, 0, 50); + image.set_pixel (2, 5, 70); + image.set_pixel (7, 1, 120); + + image.set_mask (1, 0, 1); + image.set_mask (1, 2, 1); + image.set_mask (1, 3, 0); + + std::string path = tmp_file ("tmp.lyimg"); + { + tl::OutputFile file (path); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, image); + } + + std::auto_ptr read; + + { + tl::InputFile file (path); + tl::InputStream stream (file); + read.reset (img::ImageStreamer::read (stream)); + } + + EXPECT_EQ (image.to_string (), read->to_string ()); +} + +TEST(5_FloatColor) +{ + img::Object image (12, 8, db::DCplxTrans (1.5, 90.0, true, db::DVector (17, -42)), true, false); + + image.set_min_value (-0.25); + image.set_max_value (0.75); + + image.set_pixel (0, 0, 0.25, -0.25, -0.125); + image.set_pixel (2, 5, 0.25, 0.125, 0.625); + image.set_pixel (7, 1, 0.125, 0.25, 0.75); + + std::string path = tmp_file ("tmp.lyimg"); + { + tl::OutputFile file (path); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, image); + } + + std::auto_ptr read; + + { + tl::InputFile file (path); + tl::InputStream stream (file); + read.reset (img::ImageStreamer::read (stream)); + } + + EXPECT_EQ (image.to_string (), read->to_string ()); +} + +TEST(6_FloatColorWithMask) +{ + img::Object image (12, 8, db::DCplxTrans (1.5, 90.0, true, db::DVector (17, -42)), true, false); + + image.set_min_value (-0.25); + image.set_max_value (0.75); + + image.set_pixel (0, 0, 0.25, -0.25, -0.125); + image.set_pixel (2, 5, 0.25, 0.125, 0.625); + image.set_pixel (7, 1, 0.125, 0.25, 0.75); + + image.set_mask (1, 0, 1); + image.set_mask (1, 2, 1); + image.set_mask (1, 3, 0); + + std::string path = tmp_file ("tmp.lyimg"); + { + tl::OutputFile file (path); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, image); + } + + std::auto_ptr read; + + { + tl::InputFile file (path); + tl::InputStream stream (file); + read.reset (img::ImageStreamer::read (stream)); + } + + EXPECT_EQ (image.to_string (), read->to_string ()); +} + +TEST(7_ByteColor) +{ + img::Object image (12, 8, db::DCplxTrans (1.5, 90.0, true, db::DVector (17, -42)), true, true); + + image.set_min_value (10); + image.set_max_value (240); + + image.set_pixel (0, 0, 10, 20, 30); + image.set_pixel (2, 5, 11, 21, 31); + image.set_pixel (7, 1, 12, 22, 32); + + std::string path = tmp_file ("tmp.lyimg"); + { + tl::OutputFile file (path); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, image); + } + + std::auto_ptr read; + + { + tl::InputFile file (path); + tl::InputStream stream (file); + read.reset (img::ImageStreamer::read (stream)); + } + + EXPECT_EQ (image.to_string (), read->to_string ()); +} + +TEST(8_ByteColorWithMask) +{ + img::Object image (12, 8, db::DCplxTrans (1.5, 90.0, true, db::DVector (17, -42)), true, true); + + image.set_min_value (10); + image.set_max_value (240); + + image.set_pixel (0, 0, 10, 20, 30); + image.set_pixel (2, 5, 11, 21, 31); + image.set_pixel (7, 1, 12, 22, 32); + + image.set_mask (1, 0, 1); + image.set_mask (1, 2, 1); + image.set_mask (1, 3, 0); + + std::string path = tmp_file ("tmp.lyimg"); + { + tl::OutputFile file (path); + tl::OutputStream stream (file); + img::ImageStreamer::write (stream, image); + } + + std::auto_ptr read; + + { + tl::InputFile file (path); + tl::InputStream stream (file); + read.reset (img::ImageStreamer::read (stream)); + } + + EXPECT_EQ (image.to_string (), read->to_string ()); +} diff --git a/src/img/unit_tests/imgObject.cc b/src/img/unit_tests/imgObject.cc index bc8200d64..86f6f1b47 100644 --- a/src/img/unit_tests/imgObject.cc +++ b/src/img/unit_tests/imgObject.cc @@ -25,9 +25,16 @@ #include "imgObject.h" #include "tlUnitTest.h" +static img::Object from_s (const std::string &s) +{ + img::Object img; + img.from_string (s.c_str ()); + return img; +} + TEST(1) { - img::Object image (12, 8, db::DCplxTrans (), false); + img::Object image (12, 8, db::DCplxTrans (), false, false); EXPECT_EQ (image.is_color (), false); EXPECT_EQ (image.is_byte_data (), false); @@ -95,25 +102,42 @@ TEST(1) dm.green_gain = 0.75; dm.blue_gain = 2.5; QColor c (128, 255, 64); - dm.false_color_nodes.insert (dm.false_color_nodes.begin () + 1, std::make_pair (0.5, c)); + QColor c2 (64, 32, 192); + dm.false_color_nodes.insert (dm.false_color_nodes.begin () + 1, std::make_pair (0.5, std::make_pair (c, c))); image.set_data_mapping (dm); EXPECT_EQ (copy1.equals (&image), false); + EXPECT_EQ (from_s (image.to_string ()).equals (&image), true); + copy1 = image; + EXPECT_EQ (copy1.equals (&image), true); + dm.false_color_nodes.insert (dm.false_color_nodes.begin () + 1, std::make_pair (0.75, std::make_pair (c, c2))); + image.set_data_mapping (dm); + EXPECT_EQ (copy1.equals (&image), false); + EXPECT_EQ (from_s (image.to_string ()).equals (&image), true); copy1 = image; EXPECT_EQ (copy1.equals (&image), true); EXPECT_EQ (copy1.data_mapping ().brightness, 0.5); EXPECT_EQ (copy1.data_mapping ().red_gain, 1.25); - EXPECT_EQ (copy1.data_mapping ().false_color_nodes.size (), size_t (3)); + EXPECT_EQ (copy1.data_mapping ().false_color_nodes.size (), size_t (4)); img::Object copy2; copy2.from_string (image.to_string ().c_str ()); + EXPECT_EQ (copy2.equals (&image), true); EXPECT_EQ (copy2.data_mapping ().brightness, 0.5); EXPECT_EQ (tl::to_string (copy2.data_mapping ().red_gain), "1.25"); - EXPECT_EQ (copy2.data_mapping ().false_color_nodes.size (), size_t (3)); + EXPECT_EQ (copy2.data_mapping ().false_color_nodes.size (), size_t (4)); EXPECT_EQ (copy2.equals (&image), true); + img::Object copy3, empty; + copy3.swap (copy2); + EXPECT_EQ (copy3.equals (&image), true); + EXPECT_EQ (copy2.equals (&empty), true); + copy3.swap (copy2); + EXPECT_EQ (copy2.equals (&image), true); + EXPECT_EQ (copy3.equals (&empty), true); + EXPECT_EQ (image.to_string (), copy2.to_string ()); EXPECT_EQ (image.mask (1, 2), true); @@ -134,7 +158,7 @@ TEST(1) TEST(2) { for (unsigned int channel = 0; channel < 3; ++channel) { - img::Object image (12, 8, db::DCplxTrans (), true); + img::Object image (12, 8, db::DCplxTrans (), true, false); EXPECT_EQ (image.is_color (), true); @@ -203,7 +227,7 @@ TEST(2) dm.green_gain = 0.75; dm.blue_gain = 2.5; QColor c (128, 255, 64); - dm.false_color_nodes.insert (dm.false_color_nodes.begin () + 1, std::make_pair (0.5, c)); + dm.false_color_nodes.insert (dm.false_color_nodes.begin () + 1, std::make_pair (0.5, std::make_pair (c, c))); image.set_data_mapping (dm); EXPECT_EQ (copy1.equals (&image), false); diff --git a/src/img/unit_tests/unit_tests.pro b/src/img/unit_tests/unit_tests.pro index fec02cec1..63793df87 100644 --- a/src/img/unit_tests/unit_tests.pro +++ b/src/img/unit_tests/unit_tests.pro @@ -8,6 +8,7 @@ include($$PWD/../../lib_ut.pri) SOURCES = \ imgObject.cc \ + imgFile.cc INCLUDEPATH += $$IMG_INC $$DB_INC $$TL_INC $$LAYBASIC_INC $$GSI_INC DEPENDPATH += $$IMG_INC $$DB_INC $$TL_INC $$LAYBASIC_INC $$GSI_INC diff --git a/src/lay/lay/images/locked_16.png b/src/lay/lay/images/locked_16.png new file mode 100644 index 0000000000000000000000000000000000000000..7d29dc559d678cdb8ffa7f2d0a4e584a11527986 GIT binary patch literal 318 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2>u zwLzGX$zsxXprB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt;PfT^vI! zdd~(K@-;aK%-I>U?g96)X{{~p0$g$_Ni}UEe;l~E?H{mnLMwtnQ(HyAP~)l%JV;^H$Vu_5iyT-Pfuvm)klch@UB|r6nou~ zSyoCaIict0oduavE`cH@9APIu%eV$Me6iDMsNZqu`VQtrO-4MezaKjTy}{t=>gTe~ HDWM4fqNjC5 literal 0 HcmV?d00001 diff --git a/src/lay/lay/images/unlocked_16.png b/src/lay/lay/images/unlocked_16.png new file mode 100644 index 0000000000000000000000000000000000000000..6b5b0d538525e35c0457db2555abc77a7c856500 GIT binary patch literal 368 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2>u zwLzGX$zsxXprB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt*poT^vI! zdXG*t%sb>D;QF6wlc;9i1n=;K=*>I49N+5-PJh6%alw;CC+YmEV`AVqd_i?lu*~tz@9wJpd3(^>HE_w+ zYt6_4m#U<images/folder_12.png images/file_12.png images/empty_12.png + images/unlocked_16.png + images/locked_16.png syntax/ruby.xml diff --git a/src/laybasic/laybasic/layAbstractMenu.cc b/src/laybasic/laybasic/layAbstractMenu.cc index ba9a44f01..df59ab6ce 100644 --- a/src/laybasic/laybasic/layAbstractMenu.cc +++ b/src/laybasic/laybasic/layAbstractMenu.cc @@ -1468,6 +1468,9 @@ AbstractMenu::find_item (tl::Extractor &extr) if (n.empty ()) { // skip (avoids infinite loops on wrong paths) + while (! extr.at_end () && *extr != ';') { + ++extr; + } } else if (n == "begin") { diff --git a/src/tl/tl/tlString.cc b/src/tl/tl/tlString.cc index c57ccca0e..458b50808 100644 --- a/src/tl/tl/tlString.cc +++ b/src/tl/tl/tlString.cc @@ -940,6 +940,15 @@ Extractor::read (unsigned int &value) return *this; } +Extractor & +Extractor::read (unsigned char &value) +{ + if (! try_read (value)) { + error (tl::to_string (tr ("Expected an unsigned byte value"))); + } + return *this; +} + Extractor & Extractor::read (unsigned long &value) { @@ -967,6 +976,15 @@ Extractor::read (double &value) return *this; } +Extractor & +Extractor::read (float &value) +{ + if (! try_read (value)) { + error (tl::to_string (tr ("Expected a real number"))); + } + return *this; +} + Extractor & Extractor::read (int &value) { @@ -1099,6 +1117,14 @@ namespace return tl::to_string (tr ("Range overflow on unsigned integer")); } }; + + template <> struct overflow_msg_func + { + std::string operator() () const + { + return tl::to_string (tr ("Range overflow on unsigned byte")); + } + }; } template bool @@ -1167,6 +1193,12 @@ Extractor::try_read_unsigned_int (T &value) return true; } +bool +Extractor::try_read (unsigned char &value) +{ + return try_read_unsigned_int (value); +} + bool Extractor::try_read (unsigned int &value) { @@ -1202,6 +1234,19 @@ Extractor::try_read (long long &value) { return try_read_signed_int (value); } + +bool +Extractor::try_read (float &value) +{ + double d = value; + if (try_read (d)) { + value = d; + return true; + } else { + return false; + } +} + bool Extractor::try_read (double &value) { diff --git a/src/tl/tl/tlString.h b/src/tl/tl/tlString.h index 521c1b353..0bdbf2792 100644 --- a/src/tl/tl/tlString.h +++ b/src/tl/tl/tlString.h @@ -468,6 +468,11 @@ public: */ Extractor &read (unsigned int &value); + /** + * @brief Read an unsigned char int (see read of an unsigned int) + */ + Extractor &read (unsigned char &value); + /** * @brief Read an unsigned long (see read of an unsigned int) */ @@ -483,6 +488,11 @@ public: */ Extractor &read (double &value); + /** + * @brief Read a float (see read of an unsigned int) + */ + Extractor &read (float &value); + /** * @brief Read a signed int (see read of an unsigned int) */ @@ -576,6 +586,11 @@ public: */ bool try_read (int &value); + /** + * @brief Try to read an unsigned char int (see try to read an unsigned int) + */ + bool try_read (unsigned char &value); + /** * @brief Try to read an unsigned long (see try to read an unsigned int) */ @@ -601,6 +616,11 @@ public: */ bool try_read (double &value); + /** + * @brief Try to read a float (see try to read an unsigned int) + */ + bool try_read (float &value); + /** * @brief Try to read a boolean value (see try to read an unsigned int) * diff --git a/testdata/img/gs.png b/testdata/img/gs.png new file mode 100644 index 0000000000000000000000000000000000000000..be41b0436496d690bf4bb724715d8df5dd8dc6e8 GIT binary patch literal 58705 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�}s9}nKBGo2^ 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 literal 0 HcmV?d00001 diff --git a/testdata/ruby/imgObject.rb b/testdata/ruby/imgObject.rb index 1f35a1688..7c1af0ae8 100644 --- a/testdata/ruby/imgObject.rb +++ b/testdata/ruby/imgObject.rb @@ -53,14 +53,31 @@ class IMG_TestClass < TestBase assert_equal(dm.num_colormap_entries, 0) dm.add_colormap_entry(0, 0x123456); dm.add_colormap_entry(0.5, 0xff0000); + dm.add_colormap_entry(0.75, 0x123456, 0x654321); dm.add_colormap_entry(1.0, 0x00ff00); - assert_equal(dm.num_colormap_entries, 3) + assert_equal(dm.num_colormap_entries, 4) assert_equal(dm.colormap_color(0) & 0xffffff, 0x123456) assert_equal(dm.colormap_value(0), 0.0) assert_equal(dm.colormap_color(1) & 0xffffff, 0xff0000) + assert_equal(dm.colormap_lcolor(1) & 0xffffff, 0xff0000) + assert_equal(dm.colormap_rcolor(1) & 0xffffff, 0xff0000) assert_equal(dm.colormap_value(1), 0.5) - assert_equal(dm.colormap_color(2) & 0xffffff, 0x00ff00) - assert_equal(dm.colormap_value(2), 1.0) + assert_equal(dm.colormap_color(2) & 0xffffff, 0x123456) + assert_equal(dm.colormap_lcolor(2) & 0xffffff, 0x123456) + assert_equal(dm.colormap_rcolor(2) & 0xffffff, 0x654321) + assert_equal(dm.colormap_value(2), 0.75) + assert_equal(dm.colormap_color(3) & 0xffffff, 0x00ff00) + assert_equal(dm.colormap_value(3), 1.0) + + image = RBA::Image.new + image.data_mapping = dm + + dm2 = image.data_mapping + assert_equal(dm2.num_colormap_entries, 4) + + dm.clear_colormap + assert_equal(dm2.num_colormap_entries, 4) + assert_equal(dm.num_colormap_entries, 0) end @@ -74,12 +91,21 @@ class IMG_TestClass < TestBase assert_equal(image.to_s, "empty:") assert_equal(image.is_empty?, true) + image2 = RBA::Image::from_s(image.to_s) + assert_equal(image2.to_s, image.to_s) + image = RBA::Image.new(2, 3, []) assert_equal(image.to_s, "mono:matrix=(1,0,0) (0,1,0) (0,0,1);min_value=0;max_value=1;is_visible=true;z_position=0;brightness=0;contrast=0;gamma=1;red_gain=1;green_gain=1;blue_gain=1;color_mapping=[0,'#000000';1,'#ffffff';];width=2;height=3;data=[0;0;0;0;0;0;]") + image2 = RBA::Image::from_s(image.to_s) + assert_equal(image2.to_s, image.to_s) + image = RBA::Image.new(2, 3, [], [], []) assert_equal(image.to_s, "color:matrix=(1,0,0) (0,1,0) (0,0,1);min_value=0;max_value=1;is_visible=true;z_position=0;brightness=0;contrast=0;gamma=1;red_gain=1;green_gain=1;blue_gain=1;color_mapping=[0,'#000000';1,'#ffffff';];width=2;height=3;data=[0,0,0;0,0,0;0,0,0;0,0,0;0,0,0;0,0,0;]") + image2 = RBA::Image::from_s(image.to_s) + assert_equal(image2.to_s, image.to_s) + data = [0.0, 0.5, 1.5, 2.5, 10, 20] image = RBA::Image.new(2, 3, data) assert_equal(image.to_s, "mono:matrix=(1,0,0) (0,1,0) (0,0,1);min_value=0;max_value=1;is_visible=true;z_position=0;brightness=0;contrast=0;gamma=1;red_gain=1;green_gain=1;blue_gain=1;color_mapping=[0,'#000000';1,'#ffffff';];width=2;height=3;data=[0;0.5;1.5;2.5;10;20;]") @@ -164,18 +190,18 @@ class IMG_TestClass < TestBase assert_equal(image.filename, "") - if false - image = RBA::Image.new("/home/matthias/a.png") - assert_equal(image.trans.to_s, "r0 *1 0,0") - assert_equal(image.width, 728) - assert_equal(image.height, 762) - assert_equal(image.filename, "/home/matthias/a.png") - - image = RBA::Image.new("/home/matthias/a.png", t) - assert_equal(image.trans.to_s, "r90 *2.5 1,5") - assert_equal(image.width, 728) - assert_equal(image.height, 762) - end + fn = ENV["TESTSRC"] + "/testdata/img/gs.png" + + image = RBA::Image.new(fn) + assert_equal(image.trans.to_s, "r0 *1 -513.5,-349") + assert_equal(image.width, 1027) + assert_equal(image.height, 698) + assert_equal(image.filename, fn) + + image = RBA::Image.new(fn, t) + assert_equal(image.trans.to_s, "r90 *1 873.5,-1278.75") + assert_equal(image.width, 1027) + assert_equal(image.height, 698) image.min_value = -12.5 assert_equal(image.min_value, -12.5) @@ -370,6 +396,25 @@ class IMG_TestClass < TestBase end + def test_4 + + tmp = File::join($ut_testtmp, "tmp.lyimg") + + t = RBA::DCplxTrans.new(2.5, 90, false, RBA::DPoint.new(1, 5)) + + image = RBA::Image.new(2, 3, t, [1,2,3,4,5,6]) + assert_equal(image.to_s, "mono:matrix=(0,-2.5,-2.75) (2.5,0,7.5) (0,0,1);min_value=0;max_value=1;is_visible=true;z_position=0;brightness=0;contrast=0;gamma=1;red_gain=1;green_gain=1;blue_gain=1;color_mapping=[0,'#000000';1,'#ffffff';];width=2;height=3;data=[1;2;3;4;5;6;]") + + image.write(tmp) + + image.clear + assert_equal(image.to_s, "mono:matrix=(0,-2.5,-2.75) (2.5,0,7.5) (0,0,1);min_value=0;max_value=1;is_visible=true;z_position=0;brightness=0;contrast=0;gamma=1;red_gain=1;green_gain=1;blue_gain=1;color_mapping=[0,'#000000';1,'#ffffff';];width=2;height=3;data=[0;0;0;0;0;0;]") + + image2 = RBA::Image::read(tmp) + assert_equal(image2.to_s, "mono:matrix=(0,-2.5,-2.75) (2.5,0,7.5) (0,0,1);min_value=0;max_value=1;is_visible=true;z_position=0;brightness=0;contrast=0;gamma=1;red_gain=1;green_gain=1;blue_gain=1;color_mapping=[0,'#000000';1,'#ffffff';];width=2;height=3;data=[1;2;3;4;5;6;]") + + end + end load("test_epilogue.rb")