changeset 2227:71e573e4dcc1

added GlyphAlphabet::IndentUtf8() and GlyphBitmapAlphabet::RenderColorText()
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 25 Apr 2025 14:20:44 +0200
parents 639b4960bb51
children 0191a7693b50
files OrthancStone/Sources/Fonts/GlyphAlphabet.cpp OrthancStone/Sources/Fonts/GlyphAlphabet.h OrthancStone/Sources/Fonts/GlyphBitmapAlphabet.cpp OrthancStone/Sources/Fonts/GlyphBitmapAlphabet.h OrthancStone/Sources/Fonts/GlyphTextureAlphabet.cpp OrthancStone/Sources/Toolbox/DynamicBitmap.cpp OrthancStone/Sources/Toolbox/DynamicBitmap.h OrthancStone/UnitTestsSources/CMakeLists.txt OrthancStone/UnitTestsSources/ImageToolboxTests.cpp
diffstat 9 files changed, 317 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancStone/Sources/Fonts/GlyphAlphabet.cpp	Fri Apr 25 12:52:46 2025 +0200
+++ b/OrthancStone/Sources/Fonts/GlyphAlphabet.cpp	Fri Apr 25 14:20:44 2025 +0200
@@ -23,6 +23,8 @@
 
 #include "GlyphAlphabet.h"
 
+#include "../Toolbox/DynamicBitmap.h"
+
 #include <OrthancException.h>
 #include <Toolbox.h>
 
@@ -142,6 +144,8 @@
   void GlyphAlphabet::Apply(ITextVisitor& visitor,
                             const std::string& utf8) const
   {
+    DynamicBitmap empty(Orthanc::PixelFormat_Grayscale8, 0, 0, true);
+
     size_t pos = 0;
     int x = 0;
     int y = 0;
@@ -167,21 +171,33 @@
         size_t length;
         Orthanc::Toolbox::Utf8ToUnicodeCharacter(unicode, length, utf8, pos);
 
-        Content::const_iterator glyph = content_.find(unicode);
-
-        if (glyph != content_.end())
+        if (unicode == '\r' ||
+            IsDeviceControlCharacter(unicode))
         {
-          assert(glyph->second != NULL);
-          const Orthanc::IDynamicObject* payload =
-            (glyph->second->HasPayload() ? &glyph->second->GetPayload() : NULL);
+          /**
+           * This is a device control character, which is used to change the color of the text.
+           * Make sure that such a character is invisible (i.e., zero width and height).
+           **/
+          visitor.Visit(unicode, x, y, 0, 0, &empty);
+        }
+        else
+        {
+          Content::const_iterator glyph = content_.find(unicode);
+
+          if (glyph != content_.end())
+          {
+            assert(glyph->second != NULL);
+            const Orthanc::IDynamicObject* payload =
+              (glyph->second->HasPayload() ? &glyph->second->GetPayload() : NULL);
             
-          visitor.Visit(unicode,
-                        x + glyph->second->GetOffsetLeft(),
-                        y + glyph->second->GetOffsetTop(),
-                        glyph->second->GetWidth(),
-                        glyph->second->GetHeight(),
-                        payload);
-          x += glyph->second->GetAdvanceX();
+            visitor.Visit(unicode,
+                          x + glyph->second->GetOffsetLeft(),
+                          y + glyph->second->GetOffsetTop(),
+                          glyph->second->GetWidth(),
+                          glyph->second->GetHeight(),
+                          payload);
+            x += glyph->second->GetAdvanceX();
+          }
         }
         
         assert(length != 0);
@@ -189,4 +205,135 @@
       }
     }
   }
+
+
+  bool GlyphAlphabet::IsDeviceControlCharacter(uint32_t unicode)
+  {
+    return (unicode == 0x11 ||
+            unicode == 0x12 ||
+            unicode == 0x13 ||
+            unicode == 0x14);
+  }
+
+
+  static void Copy(std::string& target,
+                   const std::string& source,
+                   size_t start,
+                   size_t end,
+                   bool ignoreDeviceControl)
+  {
+    size_t i = start;
+    while (i < end)
+    {
+      uint32_t unicode;
+      size_t length;
+      Orthanc::Toolbox::Utf8ToUnicodeCharacter(unicode, length, source, i);
+      assert(length != 0);
+
+      if (unicode != '\r' &&
+          (!ignoreDeviceControl || !GlyphAlphabet::IsDeviceControlCharacter(unicode)))
+      {
+        for (size_t j = 0; j < length; j++)
+        {
+          target.push_back(source[i + j]);
+        }
+      }
+
+      i += length;
+    }
+
+    if (i != end)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  void GlyphAlphabet::IndentUtf8(std::string& target,
+                                 const std::string& source,
+                                 unsigned int maxLineWidth,
+                                 bool ignoreDeviceControl /* whether DC1, DC2, DC3, and DC4 codes are used to change color */)
+  {
+    if (maxLineWidth == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    target.clear();
+    target.reserve(source.size());
+
+    unsigned int currentLineWidth = 0;
+
+    size_t pos = 0;
+    while (pos < source.size())
+    {
+      if (source[pos] == ' ' ||
+          (ignoreDeviceControl && IsDeviceControlCharacter(source[pos])))
+      {
+        pos++;
+      }
+      else if (source[pos] == '\n')
+      {
+        target.push_back('\n');
+        currentLineWidth = 0;
+        pos++;
+      }
+      else
+      {
+        // We are at the beginning of a word
+        size_t wordEnd = pos;
+        unsigned int wordLength = 0;  // Will be smaller than "wordEnd - pos" because of UTF8
+        while (wordEnd < source.size())
+        {
+          uint32_t unicode;
+          size_t length;
+          Orthanc::Toolbox::Utf8ToUnicodeCharacter(unicode, length, source, wordEnd);
+          assert(length != 0);
+
+          if (unicode == '\r' ||
+              (ignoreDeviceControl && IsDeviceControlCharacter(unicode)))
+          {
+            // Ignore carriage returns (and possibly device control characters)
+            wordEnd += length;
+          }
+          else if (unicode == '\n' ||
+                   unicode == ' ')
+          {
+            break;  // We found the end of the word
+          }
+          else
+          {
+            wordEnd += length;
+            wordLength ++;
+          }
+        }
+
+        if (wordLength != 0)
+        {
+          if (currentLineWidth == 0)
+          {
+            Copy(target, source, pos, wordEnd, ignoreDeviceControl);
+            currentLineWidth = wordLength;
+          }
+          else
+          {
+            if (currentLineWidth + wordLength + 1 <= maxLineWidth)
+            {
+              target.push_back(' ');
+              Copy(target, source, pos, wordEnd, ignoreDeviceControl);
+              currentLineWidth += wordLength + 1;
+            }
+            else
+            {
+              target.push_back('\n');
+              Copy(target, source, pos, wordEnd, ignoreDeviceControl);
+              currentLineWidth = wordLength;
+            }
+          }
+        }
+
+        pos = wordEnd;
+      }
+    }
+  }
 }
--- a/OrthancStone/Sources/Fonts/GlyphAlphabet.h	Fri Apr 25 12:52:46 2025 +0200
+++ b/OrthancStone/Sources/Fonts/GlyphAlphabet.h	Fri Apr 25 14:20:44 2025 +0200
@@ -106,5 +106,12 @@
 
     void Apply(ITextVisitor& visitor,
                const std::string& utf8) const;
+
+    static bool IsDeviceControlCharacter(uint32_t unicode);
+
+    static void IndentUtf8(std::string& target,
+                           const std::string& source,
+                           unsigned int maxLineWidth,
+                           bool ignoreDeviceControl /* whether DC1, DC2, DC3, and DC4 codes are used to change color */);
   };
 }
--- a/OrthancStone/Sources/Fonts/GlyphBitmapAlphabet.cpp	Fri Apr 25 12:52:46 2025 +0200
+++ b/OrthancStone/Sources/Fonts/GlyphBitmapAlphabet.cpp	Fri Apr 25 14:20:44 2025 +0200
@@ -25,6 +25,7 @@
 
 #include "TextBoundingBox.h"
 #include "../Toolbox/DynamicBitmap.h"
+#include "../Toolbox/ImageToolbox.h"
 
 #include <Images/Image.h>
 #include <Images/ImageProcessing.h>
@@ -38,6 +39,12 @@
     const GlyphBitmapAlphabet&  that_;
     int                         offsetX_;
     int                         offsetY_;
+    bool                        useColors_;
+    Color                       activeColor_;
+    Color                       color1_;
+    Color                       color2_;
+    Color                       color3_;
+    Color                       color4_;
       
   public:
     RenderTextVisitor(Orthanc::ImageAccessor&  target,
@@ -47,10 +54,24 @@
       target_(target),
       that_(that),
       offsetX_(offsetX),
-      offsetY_(offsetY)
+      offsetY_(offsetY),
+      useColors_(false)
     {
     }
 
+    void SetColors(const Color& color1,
+                   const Color& color2,
+                   const Color& color3,
+                   const Color& color4)
+    {
+      useColors_ = true;
+      activeColor_ = color1;
+      color1_ = color1;
+      color2_ = color2;
+      color3_ = color3;
+      color4_ = color4;
+    }
+
     virtual void Visit(uint32_t unicode,
                        int x,
                        int y,
@@ -70,7 +91,35 @@
              static_cast<unsigned int>(top) + height <= target_.GetHeight() &&
              width == glyph.GetBitmap().GetWidth() &&
              height == glyph.GetBitmap().GetHeight());
-        
+
+      if (useColors_)
+      {
+        if (unicode == 0x11)
+        {
+          activeColor_ = color1_;
+        }
+        else if (unicode == 0x12)
+        {
+          activeColor_ = color2_;
+        }
+        else if (unicode == 0x13)
+        {
+          activeColor_ = color3_;
+        }
+        else if (unicode == 0x14)
+        {
+          activeColor_ = color4_;
+        }
+        else
+        {
+          Orthanc::ImageAccessor region;
+          target_.GetRegion(region, left, top, width, height);
+
+          std::unique_ptr<Orthanc::ImageAccessor> colorized(ImageToolbox::Colorize(glyph.GetBitmap(), activeColor_));
+          Orthanc::ImageProcessing::Copy(region, *colorized);
+        }
+      }
+      else
       {
         Orthanc::ImageAccessor region;
         target_.GetRegion(region, left, top, width, height);
@@ -103,7 +152,7 @@
     std::unique_ptr<Orthanc::ImageAccessor> bitmap(
       new Orthanc::Image(Orthanc::PixelFormat_Grayscale8,
                          box.GetWidth(), box.GetHeight(),
-                         true /* force minimal pitch */));
+                         true /* force minimal pitch, to be used in OpenGL textures */));
 
     Orthanc::ImageProcessing::Set(*bitmap, 0);
 
@@ -120,4 +169,31 @@
     alphabet_.Register(font, utf8);
     return RenderText(utf8);
   }
+
+
+  Orthanc::ImageAccessor* GlyphBitmapAlphabet::RenderColorText(FontRenderer& font,
+                                                               const std::string& utf8,
+                                                               const Color color1,
+                                                               const Color color2,
+                                                               const Color color3,
+                                                               const Color color4)
+  {
+    alphabet_.Register(font, utf8);
+
+    TextBoundingBox box(alphabet_, utf8);
+
+    std::unique_ptr<Orthanc::ImageAccessor> bitmap(
+      new Orthanc::Image(Orthanc::PixelFormat_RGB24,
+                         box.GetWidth(), box.GetHeight(),
+                         true /* force minimal pitch, to be used in OpenGL textures */));
+
+    Orthanc::ImageProcessing::Set(*bitmap, 0);
+
+    RenderTextVisitor visitor(*bitmap, *this, -box.GetLeft(), -box.GetTop());
+    visitor.SetColors(color1, color2, color3, color4);
+
+    alphabet_.Apply(visitor, utf8);
+
+    return bitmap.release();
+  }
 }
--- a/OrthancStone/Sources/Fonts/GlyphBitmapAlphabet.h	Fri Apr 25 12:52:46 2025 +0200
+++ b/OrthancStone/Sources/Fonts/GlyphBitmapAlphabet.h	Fri Apr 25 14:20:44 2025 +0200
@@ -25,6 +25,8 @@
 
 #include "GlyphAlphabet.h"
 
+#include "../Scene2D/Color.h"
+
 #include <Images/ImageAccessor.h>
 
 namespace OrthancStone
@@ -58,5 +60,12 @@
 
     Orthanc::ImageAccessor* RenderText(FontRenderer& font,
                                        const std::string& utf8);
+
+    Orthanc::ImageAccessor* RenderColorText(FontRenderer& font,
+                                            const std::string& utf8,
+                                            const Color color1 = Color(255, 255, 255),
+                                            const Color color2 = Color(0, 0, 0),
+                                            const Color color3 = Color(0, 0, 0),
+                                            const Color color4 = Color(0, 0, 0));
   };
 }
--- a/OrthancStone/Sources/Fonts/GlyphTextureAlphabet.cpp	Fri Apr 25 12:52:46 2025 +0200
+++ b/OrthancStone/Sources/Fonts/GlyphTextureAlphabet.cpp	Fri Apr 25 14:20:44 2025 +0200
@@ -298,7 +298,7 @@
     std::unique_ptr<Orthanc::ImageAccessor> bitmap(
       new Orthanc::Image(Orthanc::PixelFormat_RGBA32,
                          box.GetWidth(), box.GetHeight(),
-                         true /* force minimal pitch */));
+                         true /* force minimal pitch, to be used in OpenGL textures */));
 
     Orthanc::ImageProcessing::Set(*bitmap, 0, 0, 0, 0);
 
--- a/OrthancStone/Sources/Toolbox/DynamicBitmap.cpp	Fri Apr 25 12:52:46 2025 +0200
+++ b/OrthancStone/Sources/Toolbox/DynamicBitmap.cpp	Fri Apr 25 14:20:44 2025 +0200
@@ -26,8 +26,19 @@
 #include <Images/Image.h>
 #include <OrthancException.h>
 
+
 namespace OrthancStone
 {
+  DynamicBitmap::DynamicBitmap(Orthanc::ImageAccessor* bitmap) :
+    bitmap_(bitmap)
+  {
+    if (bitmap == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+
   DynamicBitmap::DynamicBitmap(const Orthanc::ImageAccessor& bitmap) :
     bitmap_(Orthanc::Image::Clone(bitmap))
   {
@@ -36,4 +47,13 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
     }
   }
+
+
+  DynamicBitmap::DynamicBitmap(Orthanc::PixelFormat format,
+                               unsigned int width,
+                               unsigned int height,
+                               bool forceMinimalPitch)
+  {
+    bitmap_.reset(new Orthanc::Image(format, width, height, forceMinimalPitch));
+  }
 }
--- a/OrthancStone/Sources/Toolbox/DynamicBitmap.h	Fri Apr 25 12:52:46 2025 +0200
+++ b/OrthancStone/Sources/Toolbox/DynamicBitmap.h	Fri Apr 25 14:20:44 2025 +0200
@@ -37,7 +37,14 @@
     std::unique_ptr<Orthanc::ImageAccessor>  bitmap_;
 
   public:
-    explicit DynamicBitmap(const Orthanc::ImageAccessor& bitmap);
+    explicit DynamicBitmap(Orthanc::ImageAccessor* bitmap /* Takes ownership */);
+
+    explicit DynamicBitmap(const Orthanc::ImageAccessor& bitmap /* Clone the image */);
+
+    DynamicBitmap(Orthanc::PixelFormat format,
+                  unsigned int width,
+                  unsigned int height,
+                  bool forceMinimalPitch);
 
     const Orthanc::ImageAccessor& GetBitmap() const
     {
--- a/OrthancStone/UnitTestsSources/CMakeLists.txt	Fri Apr 25 12:52:46 2025 +0200
+++ b/OrthancStone/UnitTestsSources/CMakeLists.txt	Fri Apr 25 14:20:44 2025 +0200
@@ -19,7 +19,7 @@
 # <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8.10)
+cmake_minimum_required(VERSION 2.8.10...4.0)
 cmake_policy(SET CMP0058 NEW)
 
 project(UnitTests)
--- a/OrthancStone/UnitTestsSources/ImageToolboxTests.cpp	Fri Apr 25 12:52:46 2025 +0200
+++ b/OrthancStone/UnitTestsSources/ImageToolboxTests.cpp	Fri Apr 25 14:20:44 2025 +0200
@@ -21,6 +21,7 @@
  **/
 
 
+#include "../Sources/Fonts/GlyphAlphabet.h"
 #include "../Sources/Toolbox/ImageToolbox.h"
 
 // #include <boost/chrono.hpp>
@@ -261,3 +262,34 @@
   SimpleHisto_T_BinSize10_2<Orthanc::PixelFormat_Grayscale32>();
 }
 
+
+TEST(GlyphAlphabet, Indent)
+{
+  std::string s;
+  for (unsigned int i = 1; i < 11; i++)
+  {
+    OrthancStone::GlyphAlphabet::IndentUtf8(s, "Hello World", i, true); ASSERT_EQ("Hello\nWorld", s);
+  }
+
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "Hello World", 11, true);               ASSERT_EQ("Hello World", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "Hello World", 12, true);               ASSERT_EQ("Hello World", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "   \r   ", 2, true);                   ASSERT_EQ("", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "    \n    ", 2, true);                 ASSERT_EQ("\n", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "A\rB\r\r\rC\rD", 2, true);             ASSERT_EQ("ABCD", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "   AB\rC\rD    ", 2, true);            ASSERT_EQ("ABCD", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, " \r Hello \r \r World \r ", 10, true); ASSERT_EQ("Hello\nWorld", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, " \r Hello \r \r World \r ", 11, true); ASSERT_EQ("Hello World", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "HelloWorld", 1, true);                 ASSERT_EQ("HelloWorld", s);
+
+  // Tests with device control characters (\021 in octal equals 0x11)
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "   A\020\021B\022C\r\023D\024E\025    ", 2, true);
+  ASSERT_EQ("A\020BCDE\025", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "   A\020\021B\022C\r\023D\024E\025    ", 2, false);
+  ASSERT_EQ("A\020\021B\022C\023D\024E\025", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "X A\021B\022C\r\023D\024E Y", 9, true);   ASSERT_EQ("X ABCDE Y", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "X A\021B\022C\r\023D\024E Y", 9, false);  ASSERT_EQ("X\nA\021B\022C\023D\024E\nY", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "X A\021B\022C\r\023D\024E Y", 10, false); ASSERT_EQ("X\nA\021B\022C\023D\024E\nY", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "X A\021B\022C\r\023D\024E Y", 11, false); ASSERT_EQ("X A\021B\022C\023D\024E\nY", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "X A\021B\022C\r\023D\024E Y", 12, false); ASSERT_EQ("X A\021B\022C\023D\024E\nY", s);
+  OrthancStone::GlyphAlphabet::IndentUtf8(s, "X A\021B\022C\r\023D\024E Y", 13, false); ASSERT_EQ("X A\021B\022C\023D\024E Y", s);
+}