From 8a1723d4dafe396376d5301baf1fbebdc5bd2455 Mon Sep 17 00:00:00 2001 From: Juan Miguel Carceller Date: Tue, 27 Oct 2026 14:15:16 +0100 Subject: [PATCH] [graf2d] Load native image helpers on demand Avoid loading the image backend when creating a native painter for ordinary batch drawing. Resolve a typed image utility plugin on first use instead. Assisted-by: Codex:GPT-6 --- .../TVirtualImageUtils/P010_TASImageUtils.C | 4 + graf2d/asimage/CMakeLists.txt | 1 + graf2d/asimage/inc/LinkDefUtils.h | 1 + graf2d/asimage/inc/TASImageUtils.h | 28 ++++++ graf2d/gpad/test/CMakeLists.txt | 6 +- graf2d/gpad/test/canvaspainter.cxx | 23 +++++ graf2d/gpad/test/padpainter_image.cxx | 91 +++++++++++++++++++ graf2d/graf/CMakeLists.txt | 2 + graf2d/graf/inc/LinkDef.h | 1 + graf2d/graf/inc/TVirtualImageUtils.h | 25 +++++ graf2d/graf/src/TVirtualImageUtils.cxx | 18 ++++ graf2d/padpainter/CMakeLists.txt | 6 -- graf2d/padpainter/src/TPadPainter.cxx | 53 +++++------ 13 files changed, 224 insertions(+), 35 deletions(-) create mode 100644 etc/plugins/TVirtualImageUtils/P010_TASImageUtils.C create mode 100644 graf2d/asimage/inc/TASImageUtils.h create mode 100644 graf2d/gpad/test/canvaspainter.cxx create mode 100644 graf2d/gpad/test/padpainter_image.cxx create mode 100644 graf2d/graf/inc/TVirtualImageUtils.h create mode 100644 graf2d/graf/src/TVirtualImageUtils.cxx diff --git a/etc/plugins/TVirtualImageUtils/P010_TASImageUtils.C b/etc/plugins/TVirtualImageUtils/P010_TASImageUtils.C new file mode 100644 index 0000000000000..f64a1b5b19169 --- /dev/null +++ b/etc/plugins/TVirtualImageUtils/P010_TASImageUtils.C @@ -0,0 +1,4 @@ +void P010_TASImageUtils() +{ + gPluginMgr->AddHandler("TVirtualImageUtils", "*", "TASImageUtils", "ASImageUtils", "TASImageUtils()"); +} diff --git a/graf2d/asimage/CMakeLists.txt b/graf2d/asimage/CMakeLists.txt index 6ef9aa0e6007e..88d8f66d20df9 100644 --- a/graf2d/asimage/CMakeLists.txt +++ b/graf2d/asimage/CMakeLists.txt @@ -37,6 +37,7 @@ endif() ROOT_STANDARD_LIBRARY_PACKAGE(ASImageUtils NO_INSTALL_HEADERS HEADERS + TASImageUtils.h TASUtils.h SOURCES TASUtils.cxx diff --git a/graf2d/asimage/inc/LinkDefUtils.h b/graf2d/asimage/inc/LinkDefUtils.h index 0abd290f2d1e3..4371a94453a28 100644 --- a/graf2d/asimage/inc/LinkDefUtils.h +++ b/graf2d/asimage/inc/LinkDefUtils.h @@ -14,6 +14,7 @@ #pragma link off all classes; #pragma link off all functions; +#pragma link C++ class TASImageUtils; #pragma link C++ class TASUtils; #endif diff --git a/graf2d/asimage/inc/TASImageUtils.h b/graf2d/asimage/inc/TASImageUtils.h new file mode 100644 index 0000000000000..300e11f82c676 --- /dev/null +++ b/graf2d/asimage/inc/TASImageUtils.h @@ -0,0 +1,28 @@ +// For the licensing terms see $ROOTSYS/LICENSE. +// For the list of contributors see $ROOTSYS/README/CREDITS. + +#ifndef ROOT_TASImageUtils +#define ROOT_TASImageUtils + +#include "TASUtils.h" +#include "TVirtualImageUtils.h" + +/// Image utility plugin backed by TASImage. +class TASImageUtils : public TVirtualImageUtils { +public: + TASImageUtils() = default; + + Bool_t PaintImage(TImage *image, Drawable_t window, Int_t x, Int_t y, Bool_t opaque) override + { + return TASUtils::PaintImage(image, window, x, y, opaque); + } + + Bool_t FillImageFromWindow(TImage *image, Window_t window, Int_t width, Int_t height) override + { + return TASUtils::FillImageFromWindow(image, window, width, height); + } + + ClassDefOverride(TASImageUtils, 0) // TASImage implementation of native image operations +}; + +#endif diff --git a/graf2d/gpad/test/CMakeLists.txt b/graf2d/gpad/test/CMakeLists.txt index 8cff9a1bc79a7..fbafd8d18f985 100644 --- a/graf2d/gpad/test/CMakeLists.txt +++ b/graf2d/gpad/test/CMakeLists.txt @@ -7,11 +7,15 @@ ROOT_ADD_GTEST(TRatioPlot ratioplot.cxx LIBRARIES Gpad) ROOT_ADD_GTEST(TPad pdftitle.cxx LIBRARIES Gpad) ROOT_ADD_GTEST(TPDF pdfurl.cxx LIBRARIES Gpad) +ROOT_ADD_GTEST(TCanvasPainter canvaspainter.cxx LIBRARIES Gpad Hist) +add_dependencies(TCanvasPainter ROOTPadPainter HistPainter) if(asimage) ROOT_ADD_GTEST(TASImagePDF tasimage_pdf.cxx LIBRARIES Gpad ASImage ZLIB::ZLIB) + ROOT_ADD_GTEST(TPadPainterImage padpainter_image.cxx LIBRARIES Gpad) + add_dependencies(TPadPainterImage ROOTPadPainter ASImageUtils) endif() if(webgui) ROOT_ADD_GTEST(SaveHTML savehtml.cxx LIBRARIES Gpad) -endif() \ No newline at end of file +endif() diff --git a/graf2d/gpad/test/canvaspainter.cxx b/graf2d/gpad/test/canvaspainter.cxx new file mode 100644 index 0000000000000..df3521113cba8 --- /dev/null +++ b/graf2d/gpad/test/canvaspainter.cxx @@ -0,0 +1,23 @@ +#include "gtest/gtest.h" + +#include "TCanvas.h" +#include "TH1D.h" +#include "TROOT.h" +#include "TSystem.h" + +#include + +TEST(TCanvasPainter, BatchHistogramDoesNotLoadImages) +{ + gROOT->SetBatch(kTRUE); + ASSERT_EQ(std::string(gSystem->GetLibraries()).find("libASImage"), std::string::npos); + TCanvas canvas("batch_painter", "batch painter"); + ASSERT_NE(canvas.GetCanvasPainter(), nullptr); + + TH1D histogram("histogram", "Batch histogram;X;Entries", 10, 0., 10.); + histogram.Fill(5.); + histogram.Draw(); + canvas.Update(); + EXPECT_FALSE(canvas.IsModified()); + EXPECT_EQ(std::string(gSystem->GetLibraries()).find("libASImage"), std::string::npos); +} diff --git a/graf2d/gpad/test/padpainter_image.cxx b/graf2d/gpad/test/padpainter_image.cxx new file mode 100644 index 0000000000000..c56f63d2fd89b --- /dev/null +++ b/graf2d/gpad/test/padpainter_image.cxx @@ -0,0 +1,91 @@ +#include "gtest/gtest.h" + +#include "TCanvas.h" +#include "TImage.h" +#include "TPluginManager.h" +#include "TROOT.h" +#include "TSystem.h" +#include "TVirtualPadPainter.h" +#include "TVirtualX.h" + +#include +#include +#include + +namespace { + +// Supply pixels and record image painting without requiring a window server. +class ImageBackend : public TVirtualX { +public: + Int_t fCopies = 0; + Int_t fX = 0; + Int_t fY = 0; + UInt_t fWidth = 0; + UInt_t fHeight = 0; + + Window_t GetWindowID(Int_t) override { return 1; } + Window_t GetCurrentWindow() const override { return 1; } + GContext_t CreateGC(Drawable_t, GCValues_t *) override { return 1; } + Pixmap_t CreatePixmapFromData(unsigned char *, UInt_t, UInt_t) override { return 1; } + + unsigned char *GetColorBits(Drawable_t, Int_t, Int_t, UInt_t width, UInt_t height) override + { + auto pixels = new unsigned char[4 * width * height]; + std::fill_n(pixels, 4 * width * height, 255); + return pixels; + } + + void CopyArea(Drawable_t, Drawable_t, GContext_t, Int_t, Int_t, UInt_t w, UInt_t h, Int_t x, Int_t y) override + { + ++fCopies; + fX = x; + fY = y; + fWidth = w; + fHeight = h; + } +}; + +class BackendGuard { + TVirtualX *fPrevious = gVirtualX; + +public: + explicit BackendGuard(TVirtualX &backend) { gVirtualX = &backend; } + ~BackendGuard() { gVirtualX = fPrevious; } +}; + +} // namespace + +TEST(TPadPainterImage, LoadHelperPluginOnDemand) +{ + gROOT->SetBatch(kTRUE); + ASSERT_EQ(std::string(gSystem->GetLibraries()).find("libASImage"), std::string::npos); + TCanvas canvas("image_painter", "image painter", 64, 64); + auto painter = canvas.GetPainter(); + ASSERT_NE(painter, nullptr); + painter->OnPad(&canvas); + + ASSERT_NE(gPluginMgr->FindHandler("TVirtualImageUtils"), nullptr); + EXPECT_EQ(std::string(gSystem->GetLibraries()).find("libASImage"), std::string::npos); + + std::unique_ptr image(TImage::Create()); + ASSERT_NE(image, nullptr); + EXPECT_EQ(std::string(gSystem->GetLibraries()).find("libASImageUtils"), std::string::npos); + + ImageBackend backend; + BackendGuard guard(backend); + ASSERT_TRUE(painter->FillImageFromPad(image.get(), &canvas)); + EXPECT_TRUE(image->IsValid()); + EXPECT_EQ(image->GetWidth(), canvas.GetPadWidth()); + EXPECT_EQ(image->GetHeight(), canvas.GetPadHeight()); + EXPECT_NE(std::string(gSystem->GetLibraries()).find("libASImageUtils"), std::string::npos); + + painter->DrawImage(image.get(), 7, 9); + EXPECT_EQ(backend.fCopies, 1); + EXPECT_EQ(backend.fX, 7); + EXPECT_EQ(backend.fY, 9); + EXPECT_EQ(backend.fWidth, image->GetWidth()); + EXPECT_EQ(backend.fHeight, image->GetHeight()); + + TImage unsupportedImage(1, 1); + EXPECT_FALSE(painter->FillImageFromPad(&unsupportedImage, &canvas)); +} diff --git a/graf2d/graf/CMakeLists.txt b/graf2d/graf/CMakeLists.txt index 6812c0e909553..3d5b00285e6dd 100644 --- a/graf2d/graf/CMakeLists.txt +++ b/graf2d/graf/CMakeLists.txt @@ -48,6 +48,7 @@ ROOT_STANDARD_LIBRARY_PACKAGE(Graf TTF.h TTFhandle.h TText.h + TVirtualImageUtils.h TWbox.h SOURCES src/TArc.cxx @@ -87,6 +88,7 @@ ROOT_STANDARD_LIBRARY_PACKAGE(Graf src/TTF.cxx src/TTFhandle.cxx src/TText.cxx + src/TVirtualImageUtils.cxx src/TWbox.cxx DICTIONARY_OPTIONS -writeEmptyRootPCM diff --git a/graf2d/graf/inc/LinkDef.h b/graf2d/graf/inc/LinkDef.h index ccb0862fc2b64..1932c69ca62d2 100644 --- a/graf2d/graf/inc/LinkDef.h +++ b/graf2d/graf/inc/LinkDef.h @@ -58,6 +58,7 @@ #pragma link C++ class TText-; #pragma link C++ class TTF; #pragma link C++ class TTFhandle; +#pragma link C++ class TVirtualImageUtils; #pragma link C++ class TWbox+; #pragma link C++ enum TImage::EImageFileTypes; diff --git a/graf2d/graf/inc/TVirtualImageUtils.h b/graf2d/graf/inc/TVirtualImageUtils.h new file mode 100644 index 0000000000000..4409bb1797798 --- /dev/null +++ b/graf2d/graf/inc/TVirtualImageUtils.h @@ -0,0 +1,25 @@ +// For the licensing terms see $ROOTSYS/LICENSE. +// For the list of contributors see $ROOTSYS/README/CREDITS. + +#ifndef ROOT_TVirtualImageUtils +#define ROOT_TVirtualImageUtils + +#include "Rtypes.h" +#include "GuiTypes.h" + +class TImage; + +/// Interface for image operations on native windows. +class TVirtualImageUtils { +public: + virtual ~TVirtualImageUtils() = default; + + virtual Bool_t PaintImage(TImage *image, Drawable_t window, Int_t x, Int_t y, Bool_t opaque) = 0; + virtual Bool_t FillImageFromWindow(TImage *image, Window_t window, Int_t width, Int_t height) = 0; + + static TVirtualImageUtils *Create(); + + ClassDef(TVirtualImageUtils, 0) // Image operations for native windows +}; + +#endif diff --git a/graf2d/graf/src/TVirtualImageUtils.cxx b/graf2d/graf/src/TVirtualImageUtils.cxx new file mode 100644 index 0000000000000..1e5dbef6f19de --- /dev/null +++ b/graf2d/graf/src/TVirtualImageUtils.cxx @@ -0,0 +1,18 @@ +// For the licensing terms see $ROOTSYS/LICENSE. +// For the list of contributors see $ROOTSYS/README/CREDITS. + +#include "TVirtualImageUtils.h" +#include "TPluginManager.h" + +//////////////////////////////////////////////////////////////////////////////// +/// Create an image utility plugin. The caller owns the returned object. +/// Returns nullptr if no implementation can be loaded. + +TVirtualImageUtils *TVirtualImageUtils::Create() +{ + auto handler = gPluginMgr->FindHandler("TVirtualImageUtils"); + if (!handler || handler->LoadPlugin() == -1) + return nullptr; + + return reinterpret_cast(handler->ExecPlugin(0)); +} diff --git a/graf2d/padpainter/CMakeLists.txt b/graf2d/padpainter/CMakeLists.txt index 2553f0411cda1..e59f122de338f 100644 --- a/graf2d/padpainter/CMakeLists.txt +++ b/graf2d/padpainter/CMakeLists.txt @@ -22,10 +22,4 @@ ROOT_STANDARD_LIBRARY_PACKAGE(ROOTPadPainter ) -if(asimage) - target_link_libraries(ROOTPadPainter PUBLIC ASImageUtils) - target_compile_definitions(ROOTPadPainter PRIVATE USE_ASIMAGEUTILS) -endif() - - ROOT_INSTALL_HEADERS() diff --git a/graf2d/padpainter/src/TPadPainter.cxx b/graf2d/padpainter/src/TPadPainter.cxx index 071a0866e7440..b6a412db6eace 100644 --- a/graf2d/padpainter/src/TPadPainter.cxx +++ b/graf2d/padpainter/src/TPadPainter.cxx @@ -24,13 +24,17 @@ #include "TROOT.h" #include "TMath.h" #include "TPad.h" - -#ifdef USE_ASIMAGEUTILS -#include "TASUtils.h" -#endif +#include "TVirtualImageUtils.h" namespace { +// Image helpers must not become a load-time dependency of ordinary pad painting. +TVirtualImageUtils *GetImageUtils() +{ + static const std::unique_ptr imageUtils(TVirtualImageUtils::Create()); + return imageUtils.get(); +} + //Typedef is fine, but let's pretend we look cool and modern: using size_type = std::vector::size_type; @@ -232,19 +236,13 @@ void TPadPainter::DrawImage(TImage *img, Int_t x, Int_t y, Int_t flags) { Window_t wid = gVirtualX->GetWindowID(fPad->GetPixmapID()); - // use old API to draw image on gVirtualX window - // img->PaintImage(wid, x, y, 0, 0, 0, 0, flags ? "" : "opaque"); -#ifdef USE_ASIMAGEUTILS - // new API invokes - TASUtils::PaintImage(img, wid, x, y, !flags); -#else - ::Error("TPadPainter::DrawImage", "TASImage not compiled, image painting is not possible"); - (void) wid; - (void) img; - (void) x; - (void) y; - (void) flags; -#endif + auto imageUtils = GetImageUtils(); + if (!imageUtils) { + ::Error("TPadPainter::DrawImage", "Cannot load image utility plugin"); + return; + } + + imageUtils->PaintImage(img, wid, x, y, !flags); } //////////////////////////////////////////////////////////////////////////////// @@ -589,29 +587,28 @@ TVirtualPadPainter::ESaveImageResult TPadPainter::SaveAsImage(TVirtualPad *pad, //////////////////////////////////////////////////////////////////////////////// /// Fill image object with the pixel data from the pad -/// Use TASUtils::FillImageFromWindow method which provides native X11 handling +/// Use the image utility plugin, which provides native X11 handling. Bool_t TPadPainter::FillImageFromPad(TImage *image, TVirtualPad *pad) const { if (!image || !pad) return kFALSE; -#ifdef USE_ASIMAGEUTILS - auto width = pad->GetPadWidth(); - auto height = pad->GetPadHeight(); + auto imageUtils = GetImageUtils(); + if (!imageUtils) { + ::Error("TPadPainter::FillImageFromPad", "Cannot load image utility plugin"); + return kFALSE; + } + + Int_t width = pad->GetPadWidth(); + Int_t height = pad->GetPadHeight(); Int_t wid = (pad == pad->GetCanvas()) ? pad->GetCanvasID() : pad->GetPixmapID(); gVirtualX->SelectWindow(wid); Window_t wd = gVirtualX->GetCurrentWindow(); - return TASUtils::FillImageFromWindow(image, wd, width, height); -#else - ::Error("TPadPainter::FillImageFromPad", "Cannot work without TASImage class"); - (void) image; - (void) pad; - return kFALSE; -#endif + return imageUtils->FillImageFromWindow(image, wd, width, height); } //Aux. private functions.