From fdb9b1b663619e0a3e0ae58e74096d08ad9fe91c Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Sat, 14 Jul 2012 14:09:40 -0700 Subject: [PATCH 1/2] Extend Filesystem::searchpath_find() to be able to search recursively. --- src/include/filesystem.h | 16 ++++++++++------ src/libOpenImageIO/filesystem_test.cpp | 25 +++++++++++++++++++++++++ src/libutil/filesystem.cpp | 19 ++++++++++++------- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/include/filesystem.h b/src/include/filesystem.h index 2bee9be069..8a4e16a905 100644 --- a/src/include/filesystem.h +++ b/src/include/filesystem.h @@ -78,8 +78,9 @@ inline std::string file_extension (const std::string &filepath) { return extension (filepath, false); } -/// Replace the file extension of a filename or filepath. Does not -/// alter filepath, just returns a new string +/// Replace the file extension of a filename or filepath. Does not alter +/// filepath, just returns a new string. Note that the new_extension +/// should contain a leading '.' dot. DLLPUBLIC std::string replace_extension (const std::string &filepath, const std::string &new_extension); @@ -96,12 +97,15 @@ DLLPUBLIC void searchpath_split (const std::string &searchpath, /// directories, returning the full path as a string. If the file is /// not found in any of the listed directories, return an empty string. /// If the filename is absolute, the directory list will not be used. -/// If testcwd is true, "." will be tested before the searchpath; if -/// testcwd is false, "." will only be tested if it's explicitly in -/// dirs. +/// If testcwd is true, "." will be tested before the searchpath; +/// otherwise, "." will only be tested if it's explicitly in dirs. If +/// recursive is true, the directories will be searched recursively, +/// finding a matching file in any subdirectory of the directories +/// listed in dirs; otherwise. DLLPUBLIC std::string searchpath_find (const std::string &filename, const std::vector &dirs, - bool testcwd = true); + bool testcwd = true, + bool recursive = false); /// Return true if the path is an "absolute" (not relative) path. /// If 'dot_is_absolute' is true, consider "./foo" absolute. diff --git a/src/libOpenImageIO/filesystem_test.cpp b/src/libOpenImageIO/filesystem_test.cpp index d34b2e7912..7302e5aa60 100644 --- a/src/libOpenImageIO/filesystem_test.cpp +++ b/src/libOpenImageIO/filesystem_test.cpp @@ -54,9 +54,34 @@ void test_filename_decomposition () +void test_filename_searchpath_find () +{ + // This will be run via testsuite/unit_filesystem, from the + // build/ARCH/libOpenImageIO directory. One level up will be + // build/ARCH. + std::vector dirs; + dirs.push_back (".."); + std::string s; + + // non-recursive search success + s = Filesystem::searchpath_find ("License.txt", dirs, false, false); + OIIO_CHECK_EQUAL (s, "../License.txt"); + + // non-recursive search failure (file is in a subdirectory) + s = Filesystem::searchpath_find ("version.h", dirs, false, false); + OIIO_CHECK_EQUAL (s, ""); + + // recursive search success (file is in a subdirectory) + s = Filesystem::searchpath_find ("version.h", dirs, false, true); + OIIO_CHECK_EQUAL (s, "../include/version.h"); +} + + + int main (int argc, char *argv[]) { test_filename_decomposition (); + test_filename_searchpath_find (); return unit_test_failures; } diff --git a/src/libutil/filesystem.cpp b/src/libutil/filesystem.cpp index 59b5ae9e56..12210aafa7 100644 --- a/src/libutil/filesystem.cpp +++ b/src/libutil/filesystem.cpp @@ -141,7 +141,7 @@ Filesystem::searchpath_split (const std::string &searchpath, std::string Filesystem::searchpath_find (const std::string &filename, const std::vector &dirs, - bool testcwd) + bool testcwd, bool recursive) { bool abs = Filesystem::path_is_absolute (filename); @@ -152,12 +152,6 @@ Filesystem::searchpath_find (const std::string &filename, return filename; } -#if 0 // NO, don't do this any more. - // If an absolute filename was not found, we're done. - if (abs) - return std::string(); -#endif - // Relative filename, not yet found -- try each directory in turn BOOST_FOREACH (const std::string &d, dirs) { // std::cerr << "\tPath = '" << d << "'\n"; @@ -168,6 +162,17 @@ Filesystem::searchpath_find (const std::string &filename, // std::cerr << "Found '" << f << "'\n"; return f.string(); } + + if (recursive && Filesystem::is_directory (f.string())) { + std::vector subdirs; + for (boost::filesystem::directory_iterator s(d); + s != boost::filesystem::directory_iterator(); ++s) + if (Filesystem::is_directory(s->path().string())) + subdirs.push_back (s->path().string()); + std::string found = searchpath_find (filename, subdirs, false, true); + if (found.size()) + return found; + } } return std::string(); } From b2a967a672f9346da17dc914f71f17c10616381c Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Tue, 3 Jul 2012 16:52:47 -0700 Subject: [PATCH 2/2] Basic text rendering into images with ImageBufAlgo::render_text and oiitool --text --- src/CMakeLists.txt | 1 + src/cmake/externalpackages.cmake | 22 +++++ src/doc/oiiotool.tex | 81 +++++++++++------ src/include/imagebufalgo.h | 14 +++ src/libOpenImageIO/CMakeLists.txt | 6 ++ src/libOpenImageIO/imagebufalgo.cpp | 129 ++++++++++++++++++++++++++++ src/oiiotool/oiiotool.cpp | 74 +++++++++++++++- 7 files changed, 298 insertions(+), 29 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e9d284141b..1ee319a2d6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -77,6 +77,7 @@ set (USE_FIELD3D ON CACHE BOOL "Use Field3D if found") set (USE_OPENJPEG ON CACHE BOOL "Use OpenJpeg if found") set (USE_OCIO ON CACHE BOOL "Use OpenColorIO for color management if found") set (USE_OPENCV ON CACHE BOOL "Use OpenCV if found") +set (USE_FREETYPE ON CACHE BOOL "Use Freetype if found") set (NOTHREADS OFF CACHE BOOL "Compile with no threads or locking") set (PYTHON_VERSION 2.6) set (USE_EXTERNAL_PUGIXML OFF CACHE BOOL diff --git a/src/cmake/externalpackages.cmake b/src/cmake/externalpackages.cmake index b8b8afeea1..145517e410 100644 --- a/src/cmake/externalpackages.cmake +++ b/src/cmake/externalpackages.cmake @@ -388,3 +388,25 @@ endif () # end OpenCV setup ########################################################################### + + +########################################################################### +# Freetype setup + +if (USE_FREETYPE) + find_package (Freetype) + if (FREETYPE_FOUND) + add_definitions ("-DUSE_FREETYPE") + message (STATUS "Freetype includes = ${FREETYPE_INCLUDE_DIRS} ") + message (STATUS "Freetype libs = ${FREETYPE_LIBRARIES} ") + else () + message (STATUS "Freetype library not found") + endif () +else () + message (STATUS "Not using Freetype") +endif () + +# end Freetype setup +########################################################################### + + diff --git a/src/doc/oiiotool.tex b/src/doc/oiiotool.tex index b9b1df2dcd..c2ebf8ab05 100644 --- a/src/doc/oiiotool.tex +++ b/src/doc/oiiotool.tex @@ -534,8 +534,40 @@ \section{\oiiotool commands that change the current image metadata} \apiend +\section{\oiiotool commands that adjust the image stack} -\section{\oiiotool commands that make new images} +\apiitem{--pop} +Pop the image stack, discarding the current image and thereby +making the next image on the stack into the new current image. +\apiend + +\apiitem{--dup} +Duplicate the current image and push the duplicate on the stack. +Note that this results in both the current and the next image +on the stack being identical copies. +\apiend + +\apiitem{--unmip} +If the current image is MIP-mapped, discard all but the top level +(i.e., replacing the current image with a new image consisting of only the +highest-resolution level). +\apiend + +\apiitem{--selectmip {\rm \emph{level}}} +If the current image is MIP-mapped, replace the current image with a new +image consisting of only the given \emph{level} of the MIPmap. +Level 0 is the highest resolution version, level 1 is the next-lower +resolution version, etc. +\apiend + +\apiitem{--subimage {\rm \emph{n}}} +If the current image has multiple subimages, replace the current image +with a new image consisting of only the given subimage. +\apiend + + + +\section{\oiiotool commands that make entirely new images} \apiitem{--create {\rm \emph{size channels}}} @@ -613,23 +645,8 @@ \section{\oiiotool commands that make new images} \end{tabular} \apiend -\apiitem{--unmip} -If the current image is MIP-mapped, discard all but the top level -(i.e., replacing the current image with a new image consisting of only the -highest-resolution level). -\apiend - -\apiitem{--selectmip {\rm \emph{level}}} -If the current image is MIP-mapped, replace the current image with a new -image consisting of only the given \emph{level} of the MIPmap. -Level 0 is the highest resolution version, level 1 is the next-lower -resolution version, etc. -\apiend -\apiitem{--subimage {\rm \emph{n}}} -If the current image has multiple subimages, replace the current image -with a new image consisting of only the given subimage. -\apiend +\section{\oiiotool commands that do image processing} \apiitem{--add} Replace the \emph{two} top images with a new image that is the sum of @@ -725,18 +742,32 @@ \section{\oiiotool commands that make new images} finite values within a $3 \times 3$ region surrounding the pixel. \apiend -\apiitem{--pop} -Pop the image stack, discarding the current image and thereby -making the next image on the stack into the new current image. -\apiend +\apiitem{--text {\rm \emph{words}}} +Draw (rasterize) text overtop of the current image. + +\begin{tabular}{p{10pt} p{1in} p{3.75in}} + & {\cf x=}\emph{xpos} & $x$ position (in pixel coordinates) of the text \\ + & {\cf y=}\emph{ypos} & $y$ position (in pixel coordinates) of the text \\ + & {\cf size=}\emph{size} & font size (height, in pixels) \\ + & {\cf font=}\emph{name} & font name, full path to the font file on + disk (use double quotes {\cf "name"} if the path name includes spaces) \\ + & {\cf color=}\emph{r,g,b,...} & specify the color of the text \\ +\end{tabular} + +The default positions the text starting at the center of the image, +drawn 16 pixels high in opaque white in all channels (1,1,1,...), and +using a default font (which may be system dependent). + +\noindent Examples: + +\begin{code} + oiiotool in.exr --text:x=10:y=400:size=40 "Hello world" -o out.exr +\end{code} -\apiitem{--dup} -Duplicate the current image and push the duplicate on the stack. -Note that this results in both the current and the next image -on the stack being identical copies. \apiend + \section{\oiiotool commands for color management} \apiitem{--iscolorspace {\rm \emph{colorspace}}} diff --git a/src/include/imagebufalgo.h b/src/include/imagebufalgo.h index 3551510290..2e66f1b5ca 100644 --- a/src/include/imagebufalgo.h +++ b/src/include/imagebufalgo.h @@ -353,6 +353,20 @@ bool DLLPUBLIC over (ImageBuf &R, const ImageBuf &A, const ImageBuf &B, ROI roi = ROI(), int threads = 0); +/// Render a text string into image R, essentially doing an "over" of +/// the character into the existing pixel data. The baseline of the +/// first character will start at position (x,y). The font is given by +/// fontname as a full pathname to the font file (defaulting to some +/// reasonable system font if not supplied at all), and with a nominal +/// height of fontheight (in pixels). The characters will be drawn in +/// opaque white (1.0,1.0,...) in all channels, unless textcolor is +/// supplied (and is expected to point to a float array of length at +/// least equal to R.spec().nchannels). +bool DLLPUBLIC render_text (ImageBuf &R, int x, int y, + const std::string &text, + int fontsize=16, const std::string &fontname="", + const float *textcolor = NULL); + /// Helper template for generalized multithreading for image processing diff --git a/src/libOpenImageIO/CMakeLists.txt b/src/libOpenImageIO/CMakeLists.txt index 30278a7ee3..d712e3c79e 100644 --- a/src/libOpenImageIO/CMakeLists.txt +++ b/src/libOpenImageIO/CMakeLists.txt @@ -214,6 +214,12 @@ if (OpenCV_FOUND) target_link_libraries (OpenImageIO opencv_core opencv_highgui) endif () +# Include Freetype if using it +if (FREETYPE_FOUND) + include_directories (${FREETYPE_INCLUDE_DIRS}) + target_link_libraries (OpenImageIO ${FREETYPE_LIBRARIES}) +endif () + if (WIN32) diff --git a/src/libOpenImageIO/imagebufalgo.cpp b/src/libOpenImageIO/imagebufalgo.cpp index 139f7f6a07..e317a24aae 100644 --- a/src/libOpenImageIO/imagebufalgo.cpp +++ b/src/libOpenImageIO/imagebufalgo.cpp @@ -54,6 +54,13 @@ #include "sysutil.h" #include "filter.h" #include "thread.h" +#include "filesystem.h" + +#ifdef USE_FREETYPE +#include +#include FT_FREETYPE_H +#endif + OIIO_NAMESPACE_ENTER @@ -1292,5 +1299,127 @@ ImageBufAlgo::over (ImageBuf &R, const ImageBuf &A, const ImageBuf &B, ROI roi, +#ifdef USE_FREETYPE +namespace { // anon +static mutex ft_mutex; +static FT_Library ft_library = NULL; +static bool ft_broken = false; +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) +const char *default_font_name = "cour"; +#elif defined (__APPLE__) +const char *default_font_name = "Courier New"; +#elif defined (_WIN32) +const char *default_font_name = "Courier"; +#else +const char *default_font_name = "cour"; +#endif +} // anon namespace +#endif + + +bool +ImageBufAlgo::render_text (ImageBuf &R, int x, int y, const std::string &text, + int fontsize, const std::string &font_, + const float *textcolor) +{ +#ifdef USE_FREETYPE + // If we know FT is broken, don't bother trying again + if (ft_broken) + return false; + + // Thread safety + lock_guard ft_lock (ft_mutex); + int error = 0; + + // If FT not yet initialized, do it now. + if (! ft_library) { + error = FT_Init_FreeType (&ft_library); + if (error) { + ft_broken = true; + return false; + } + } + + // A set of likely directories for fonts to live, across several systems. + std::vector search_dirs; + std::string home = getenv ("HOME"); + if (! home.empty()) { + search_dirs.push_back (home + "/fonts"); + search_dirs.push_back (home + "/Fonts"); + search_dirs.push_back (home + "/Library/Fonts"); + } + search_dirs.push_back ("/usr/share/fonts"); + search_dirs.push_back ("/Library/Fonts"); + search_dirs.push_back ("C:/Windows/Fonts"); + search_dirs.push_back ("/opt/local/share/fonts"); + + // Try to find the font. Experiment with several extensions + std::string font = font_; + if (font.empty()) + font = default_font_name; + if (! Filesystem::is_regular (font)) { + // Font specified is not a full path + std::string f; + static const char *extensions[] = { "", ".ttf", ".pfa", ".pfb", NULL }; + for (int i = 0; f.empty() && extensions[i]; ++i) + f = Filesystem::searchpath_find (font+extensions[i], + search_dirs, true, true); + if (! f.empty()) + font = f; + } + + FT_Face face; // handle to face object + error = FT_New_Face (ft_library, font.c_str(), 0 /* face index */, &face); + if (error) + return false; // couldn't open the face + + error = FT_Set_Pixel_Sizes (face, // handle to face object + 0, // pixel_width + fontsize); // pixel_heigh + if (error) { + FT_Done_Face (face); + return false; // couldn't set the character size + } + + FT_GlyphSlot slot = face->glyph; // a small shortcut + int nchannels = R.spec().nchannels; + float *pixelcolor = ALLOCA (float, nchannels); + if (! textcolor) { + float *localtextcolor = ALLOCA (float, nchannels); + for (int c = 0; c < nchannels; ++c) + localtextcolor[c] = 1.0f; + textcolor = localtextcolor; + } + + for (size_t n = 0, e = text.size(); n < e; ++n) { + // load glyph image into the slot (erase previous one) + error = FT_Load_Char (face, text[n], FT_LOAD_RENDER); + if (error) + continue; // ignore errors + // now, draw to our target surface + for (int j = 0; j < slot->bitmap.rows; ++j) { + int ry = y + j - slot->bitmap_top; + for (int i = 0; i < slot->bitmap.width; ++i) { + int rx = x + i + slot->bitmap_left; + float b = slot->bitmap.buffer[slot->bitmap.pitch*j+i] / 255.0f; + R.getpixel (rx, ry, pixelcolor); + for (int c = 0; c < nchannels; ++c) + pixelcolor[c] = b*textcolor[c] + (1.0f-b) * pixelcolor[c]; + R.setpixel (rx, ry, pixelcolor); + } + } + // increment pen position + x += slot->advance.x >> 6; + } + + FT_Done_Face (face); + return true; + +#else + return false; // Font rendering not supported +#endif +} + + } OIIO_NAMESPACE_EXIT diff --git a/src/oiiotool/oiiotool.cpp b/src/oiiotool/oiiotool.cpp index cf59dfae51..1d2b5a250f 100644 --- a/src/oiiotool/oiiotool.cpp +++ b/src/oiiotool/oiiotool.cpp @@ -1427,6 +1427,69 @@ action_over (int argc, const char *argv[]) +static int +action_text (int argc, const char *argv[]) +{ + if (ot.postpone_callback (1, action_text, argc, argv)) + return 0; + + // Read and copy the top-of-stack image + ImageRecRef A (ot.pop()); + ot.read (A); + ot.push (new ImageRec (*A, 0, 0, true, false)); + ImageBuf &Rib ((*ot.curimg)(0,0)); + const ImageSpec &Rspec = Rib.spec(); + ImageBufAlgo::zero (Rib); + + // Set up defaults for text placement, size, font, color + int x = Rspec.x + Rspec.width/2; + int y = Rspec.y + Rspec.height/2; + int fontsize = 16; + std::string font = ""; + float *textcolor = ALLOCA (float, Rspec.nchannels); + for (int c = 0; c < Rspec.nchannels; ++c) + textcolor[c] = 1.0f; + + // Parse optional arguments for overrides + std::string command = argv[0]; + size_t pos; + while ((pos = command.find_first_of(":")) != std::string::npos) { + command = command.substr (pos+1, std::string::npos); + if (Strutil::istarts_with(command,"x=")) { + x = atoi (command.c_str()+2); + } else if (Strutil::istarts_with(command,"y=")) { + y = atoi (command.c_str()+2); + } else if (Strutil::istarts_with(command,"size=")) { + fontsize = atoi (command.c_str()+5); + } else if (Strutil::istarts_with(command,"color=")) { + // Parse comma-separated color list + size_t numpos = 6; + for (int c = 0; c < Rspec.nchannels && numpos < command.size() && command[numpos] != ':'; ++c) { + textcolor[c] = atof (command.c_str()+numpos); + while (numpos < command.size() && command[numpos] != ':' && command[numpos] != ',') + ++numpos; + if (command[numpos]) + ++numpos; + } + } else if (Strutil::istarts_with(command,"font=")) { + font = ""; + int s = 5; + bool quote = (command[s] == '\"'); + if (quote) + ++s; + for ( ; command[s] && command[s] != ':' && command[s] != '\"'; ++s) + font += command[s]; + } + } + + ImageBufAlgo::render_text (Rib, x, y, argv[1] /* the text */, + fontsize, font, textcolor); + + return 0; +} + + + static void getargs (int argc, char *argv[]) { @@ -1493,10 +1556,6 @@ getargs (int argc, char *argv[]) "Create a patterned image (args: pattern, geom, channels)", "--capture %@", action_capture, NULL, "Capture an image (args: camera=%%d)", - "--unmip %@", action_unmip, NULL, "Discard all but the top level of a MIPmap", - "--selectmip %@ %d", action_selectmip, NULL, - "Select just one MIP level (0 = highest res)", - "--subimage %@ %d", action_select_subimage, NULL, "Select just one subimage", "--diff %@", action_diff, NULL, "Print report on the difference of two images (modified by --fail, --failpercent, --hardfail, --warn, --warnpercent --hardwarn)", "--add %@", action_add, NULL, "Add two images", "--sub %@", action_sub, NULL, "Subtract two images", @@ -1509,6 +1568,13 @@ getargs (int argc, char *argv[]) "--croptofull %@", action_croptofull, NULL, "Crop or pad to make pixel data region match the \"full\" region", "--resize %@ %s", action_resize, NULL, "Resize (640x480, 50%)", "--fixnan %@ %s", action_fixnan, NULL, "Fix NaN/Inf values in the image (options: none, black, box3)", + "--text %@ %s", action_text, NULL, + "Render text into the current image", + "", "Image stack manipulation:", + "--unmip %@", action_unmip, NULL, "Discard all but the top level of a MIPmap", + "--selectmip %@ %d", action_selectmip, NULL, + "Select just one MIP level (0 = highest res)", + "--subimage %@ %d", action_select_subimage, NULL, "Select just one subimage", "--pop %@", action_pop, NULL, "Throw away the current image", "--dup %@", action_dup, NULL,