mirror of
https://github.com/opencv/opencv.git
synced 2026-11-09 18:04:29 -06:00
imgproc: shrink the text rendering footprint (HarfBuzz -Os, optional built-in fonts)
libopencv_imgproc grew noticeably with the HarfBuzz-based text engine. Measured on
macOS/arm64 (clang, Release, stripped) the text-related part of the library was
~830 KB: HarfBuzz 486 KB, the two Rubik fonts 296 KB, the engine ~25 KB, plus ~2 MB
of WenQuanYi when WITH_UNIFONT=ON (the previous default). With GCC and an unstripped
.so on Linux the HarfBuzz part alone is 700+ KB.
This patch brings it down to ~550 KB by default (HarfBuzz 377 KB + Rubik 146 KB +
engine 30 KB) without dropping functionality:
* 3rdparty/harfbuzz: build with -Os -fno-exceptions -fno-rtti -fno-threadsafe-statics
(like HarfBuzz's own build; shaping speed is irrelevant thanks to the glyph cache)
and HB_NO_VAR_COMPOSITES (VARC table, a 2024 format with no real fonts yet).
Variable-font support (wght axis etc.) is untouched. -22% of HarfBuzz code.
* Only the "sans" font (Rubik) is compiled into imgproc by default. Rubik Italic and
WenQuanYi Micro Hei became optional: WITH_ITALICFONT (new) and WITH_UNIFONT are
both OFF now. The build summary lists the built-in fonts.
* New API cv::FontFace::setBuiltinFont(name, path) fills or replaces any of the
three built-in slots ("sans", "italic", "uni") with an external font file. The
registered font takes part in the automatic fallback chain exactly like a
compiled-in one, so users can get CJK text or a true italic without rebuilding
OpenCV. The registry is process-wide; each thread's engine reloads its slots
lazily (generation counter), and the glyph cache is dropped on reload since
hb_font pointers are part of its key.
* "italic" resolves to "sans" when no italic font is built in or registered, so
FONT_ITALIC / FONT_HERSHEY_SCRIPT_* in the legacy putText keep working (upright).
* Fix: a character not covered by any font used to be drawn as '?' taken from the
last built-in slot, which can be empty now; the '?' is looked up like any other
character (user font first).
* Tests: opencv_test_imgproc registers Rubik-Italic.ttf.gz and
WenQuanYiMicroHei.ttf.gz from opencv_extra (testdata/highgui/drawing) as the
"italic"/"uni" slots, so Drawing.ttf_text reproduces the reference picture 1:1
with the default build and the legacy hershey-font tests are unchanged.
Drawing.builtin_font_slots covers the new API. Tests skip if the fonts are absent.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
9
3rdparty/harfbuzz/CMakeLists.txt
vendored
9
3rdparty/harfbuzz/CMakeLists.txt
vendored
@@ -30,8 +30,17 @@ add_library(${HARFBUZZ_LIBRARY} STATIC ${OPENCV_3RDPARTY_EXCLUDE_FROM_ALL} ${lib
|
||||
target_compile_definitions(${HARFBUZZ_LIBRARY} PRIVATE
|
||||
HB_TINY
|
||||
HB_HAS_RASTER # request the software rasterizer: keeps draw/paint/color + CFF under HB_TINY
|
||||
HB_NO_VAR_COMPOSITES # drop the VARC table (variable composite glyphs, 2024 format; ~20 KB of code)
|
||||
"HB_CONFIG_OVERRIDE_H=\"hb-opencv-config.hh\"")
|
||||
|
||||
# Optimize HarfBuzz for size: it is ~0.5 MB of libopencv_imgproc at -O3 and ~18% less at -Os,
|
||||
# while text rendering speed is dominated by the glyph cache, not by shaping. Target options
|
||||
# come after CMAKE_CXX_FLAGS_<CONFIG> on the command line, so -Os overrides -O3.
|
||||
# HarfBuzz's own build also disables exceptions/RTTI/thread-safe statics (it uses none).
|
||||
if(CV_GCC OR CV_CLANG)
|
||||
target_compile_options(${HARFBUZZ_LIBRARY} PRIVATE -Os -fno-exceptions -fno-rtti -fno-threadsafe-statics)
|
||||
endif()
|
||||
|
||||
set_target_properties(${HARFBUZZ_LIBRARY} PROPERTIES
|
||||
CXX_STANDARD 17
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
|
||||
@@ -516,7 +516,12 @@ OCV_OPTION(ENABLE_CUDA_FIRST_CLASS_LANGUAGE "Enable CUDA as a first class langua
|
||||
OCV_OPTION(ENABLE_PYLINT "Add target with Pylint checks" (BUILD_DOCS OR BUILD_EXAMPLES) IF (NOT CMAKE_CROSSCOMPILING AND NOT APPLE_FRAMEWORK) )
|
||||
OCV_OPTION(ENABLE_FLAKE8 "Add target with Python flake8 checker" (BUILD_DOCS OR BUILD_EXAMPLES) IF (NOT CMAKE_CROSSCOMPILING AND NOT APPLE_FRAMEWORK) )
|
||||
|
||||
OCV_OPTION(WITH_UNIFONT "Build 'uni' font (WQY MicroHei) into OpenCV" (NOT BUILD_opencv_js)
|
||||
# Optional built-in fonts. Only the "sans" font (Rubik) is always compiled into imgproc;
|
||||
# the others cost ~150 KB (italic) and ~2 MB (uni) of library size and can instead be
|
||||
# supplied at run time as external files via cv::FontFace::setBuiltinFont().
|
||||
OCV_OPTION(WITH_ITALICFONT "Build 'italic' font (Rubik Italic) into OpenCV" OFF
|
||||
VERIFY HAVE_ITALICFONT)
|
||||
OCV_OPTION(WITH_UNIFONT "Build 'uni' font (WQY MicroHei) into OpenCV" OFF
|
||||
VERIFY HAVE_UNIFONT)
|
||||
|
||||
if(ENABLE_IMPL_COLLECTION)
|
||||
@@ -1452,8 +1457,15 @@ if(WITH_VTK OR HAVE_VTK)
|
||||
status(" VTK support:" HAVE_VTK THEN "YES (ver ${VTK_VERSION})" ELSE NO)
|
||||
endif()
|
||||
|
||||
if(WITH_UNIFONT OR HAVE_UNIFONT)
|
||||
status(" Built-in Unicode font:" HAVE_UNIFONT THEN "YES" ELSE "NO")
|
||||
if(HAVE_HARFBUZZ)
|
||||
set(_builtin_fonts "sans")
|
||||
if(HAVE_ITALICFONT)
|
||||
set(_builtin_fonts "${_builtin_fonts} italic")
|
||||
endif()
|
||||
if(HAVE_UNIFONT)
|
||||
set(_builtin_fonts "${_builtin_fonts} uni")
|
||||
endif()
|
||||
status(" Built-in fonts:" "${_builtin_fonts}")
|
||||
endif()
|
||||
|
||||
# ========================== MEDIA IO ==========================
|
||||
|
||||
@@ -150,4 +150,7 @@
|
||||
/* The font "uni" (WQY MicroHei) is available */
|
||||
#cmakedefine HAVE_UNIFONT
|
||||
|
||||
/* Rubik Italic font is compiled into imgproc as the built-in "italic" font */
|
||||
#cmakedefine HAVE_ITALICFONT
|
||||
|
||||
#endif // OPENCV_CVCONFIG_H_INCLUDED
|
||||
|
||||
@@ -59,7 +59,12 @@ if(HAVE_HARFBUZZ)
|
||||
endif()
|
||||
|
||||
ocv_blob2hdr("${CMAKE_CURRENT_SOURCE_DIR}/fonts/Rubik.ttf.gz" "${CMAKE_CURRENT_BINARY_DIR}/builtin_font_sans.h" OcvBuiltinFontSans)
|
||||
ocv_blob2hdr("${CMAKE_CURRENT_SOURCE_DIR}/fonts/Rubik-Italic.ttf.gz" "${CMAKE_CURRENT_BINARY_DIR}/builtin_font_italic.h" OcvBuiltinFontItalic)
|
||||
unset(HAVE_ITALICFONT)
|
||||
unset(HAVE_ITALICFONT CACHE)
|
||||
if(WITH_ITALICFONT)
|
||||
ocv_blob2hdr("${CMAKE_CURRENT_SOURCE_DIR}/fonts/Rubik-Italic.ttf.gz" "${CMAKE_CURRENT_BINARY_DIR}/builtin_font_italic.h" OcvBuiltinFontItalic)
|
||||
set(HAVE_ITALICFONT ON CACHE INTERNAL "")
|
||||
endif()
|
||||
if (HAVE_UNIFONT)
|
||||
ocv_blob2hdr("${CMAKE_CURRENT_BINARY_DIR}/${UNIFONT_NAME}" "${CMAKE_CURRENT_BINARY_DIR}/builtin_font_uni.h" OcvBuiltinFontUni)
|
||||
endif()
|
||||
|
||||
@@ -4312,8 +4312,17 @@ public:
|
||||
/** @brief loads default font */
|
||||
CV_WRAP FontFace();
|
||||
/** @brief loads font at the specified path or with specified name.
|
||||
@param fontPathOrName either path to the custom font or the name of embedded font: "sans", "italic" or "uni".
|
||||
Empty fontPathOrName means the default embedded font.
|
||||
@param fontPathOrName either path to a custom font file (.ttf/.otf, optionally gzip-compressed)
|
||||
or the name of one of the built-in font slots:
|
||||
- "sans": the default font (Rubik, always compiled in);
|
||||
- "italic": Rubik Italic, compiled in only if OpenCV was built with WITH_ITALICFONT=ON;
|
||||
otherwise it can be provided with setBuiltinFont(), and if it is not available,
|
||||
"italic" silently resolves to "sans";
|
||||
- "uni": WenQuanYi Micro Hei (CJK), compiled in only with WITH_UNIFONT=ON; otherwise
|
||||
provide it with setBuiltinFont(), or loading fails.
|
||||
The built-in slots also serve as the fallback chain: characters missing from the
|
||||
user's font are taken from "sans", then "italic", then "uni".
|
||||
Empty fontPathOrName means the default embedded font ("sans").
|
||||
*/
|
||||
CV_WRAP FontFace(const String& fontPathOrName);
|
||||
|
||||
@@ -4332,6 +4341,23 @@ public:
|
||||
CV_WRAP bool setInstance(const std::vector<int>& params);
|
||||
CV_WRAP bool getInstance(CV_OUT std::vector<int>& params) const;
|
||||
|
||||
/** @brief Provides or replaces one of the built-in font slots with an external font file.
|
||||
@param fontName the slot: "sans", "italic" or "uni".
|
||||
@param fontPath path to a .ttf/.otf font (optionally gzip-compressed, e.g. `.ttf.gz`).
|
||||
An empty path restores the compiled-in font of that slot (if any), otherwise the slot
|
||||
becomes empty.
|
||||
@return false if the slot name is unknown or the font file cannot be loaded (the
|
||||
registry is left unchanged).
|
||||
|
||||
This is the way to get CJK text or a true italic when OpenCV is built without
|
||||
WITH_UNIFONT / WITH_ITALICFONT (the defaults), e.g.
|
||||
`cv::FontFace::setBuiltinFont("uni", "/usr/share/fonts/NotoSansCJK-Regular.ttc")`.
|
||||
The registered font participates in the automatic fallback chain like the compiled-in
|
||||
one would. The change is global (all threads) and applies to FontFace objects created
|
||||
afterwards; existing FontFace objects keep the font they were created with.
|
||||
*/
|
||||
CV_WRAP static bool setBuiltinFont(const String& fontName, const String& fontPath);
|
||||
|
||||
struct Impl;
|
||||
|
||||
Impl* operator -> ();
|
||||
|
||||
@@ -12,8 +12,11 @@
|
||||
// Script/category come from HarfBuzz's Unicode functions; direction is a
|
||||
// lightweight RTL heuristic (full UAX#9 BiDi is a TODO). For each code point
|
||||
// the engine picks the first font that covers it: the user font first, then
|
||||
// the built-in fallbacks (sans/italic/unifont); uncovered -> '?'. Adjacent
|
||||
// compatible runs are then merged.
|
||||
// the built-in font slots (sans/italic/uni); uncovered -> '?'. Adjacent
|
||||
// compatible runs are then merged. Only "sans" (Rubik) is always compiled in;
|
||||
// "italic" and "uni" are optional (WITH_ITALICFONT / WITH_UNIFONT) and any slot
|
||||
// can be (re)filled at run time with an external file via
|
||||
// FontFace::setBuiltinFont() (see the registry below).
|
||||
// 3. Shape each run with hb_shape(), yielding glyph ids and positions
|
||||
// (advances include GPOS kerning); RTL runs come out already reordered.
|
||||
// 4. Lay out and render the glyphs. Each glyph is rasterized once by HarfBuzz's
|
||||
@@ -49,10 +52,14 @@
|
||||
#include <hb-raster.h>
|
||||
|
||||
#include "builtin_font_sans.h"
|
||||
#ifdef HAVE_ITALICFONT
|
||||
#include "builtin_font_italic.h"
|
||||
#endif
|
||||
#ifdef HAVE_UNIFONT
|
||||
#include "builtin_font_uni.h"
|
||||
#endif
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -68,23 +75,51 @@ typedef struct BuiltinFontData
|
||||
bool italic;
|
||||
} BuiltinFontData;
|
||||
|
||||
// The built-in font slots, in fallback order. A slot is backed by a compiled-in
|
||||
// gzipped blob (gzdata/size, when the corresponding WITH_* option is ON), or by an
|
||||
// external file registered with FontFace::setBuiltinFont(), or is empty.
|
||||
enum
|
||||
{
|
||||
BUILTIN_FONTS_NUM = 2
|
||||
BUILTIN_FONT_SANS = 0,
|
||||
BUILTIN_FONT_ITALIC = 1,
|
||||
BUILTIN_FONT_UNI = 2,
|
||||
BUILTIN_FONTS_NUM = 3
|
||||
};
|
||||
|
||||
static const BuiltinFontData builtinFontData[BUILTIN_FONTS_NUM] =
|
||||
{
|
||||
{OcvBuiltinFontSans, sizeof(OcvBuiltinFontSans), "sans", 1.0, false},
|
||||
#ifdef HAVE_ITALICFONT
|
||||
{OcvBuiltinFontItalic, sizeof(OcvBuiltinFontItalic), "italic", 1.0, true},
|
||||
#else
|
||||
{0, 0, "italic", 1.0, true},
|
||||
#endif
|
||||
#ifdef HAVE_UNIFONT
|
||||
+1
|
||||
{OcvBuiltinFontUni, sizeof(OcvBuiltinFontUni), "uni", 1.0, true},
|
||||
#else
|
||||
{0, 0, "uni", 1.0, true},
|
||||
#endif
|
||||
};
|
||||
|
||||
static BuiltinFontData builtinFontData[BUILTIN_FONTS_NUM+1] =
|
||||
static int builtinFontIndex(const String& name)
|
||||
{
|
||||
{OcvBuiltinFontSans, sizeof(OcvBuiltinFontSans), "sans", 1.0, false},
|
||||
{OcvBuiltinFontItalic, sizeof(OcvBuiltinFontItalic), "italic", 1.0, true},
|
||||
#ifdef HAVE_UNIFONT
|
||||
{OcvBuiltinFontUni, sizeof(OcvBuiltinFontUni), "uni", 1.0, true},
|
||||
#endif
|
||||
{0, 0, 0, 0.0, false}
|
||||
};
|
||||
for(int i = 0; i < BUILTIN_FONTS_NUM; i++)
|
||||
if(name == builtinFontData[i].name)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Registry of external files provided via FontFace::setBuiltinFont(). It is
|
||||
// process-wide, while the loaded fonts live in each thread's FontRenderEngine:
|
||||
// the engine compares its generation with builtinFontGeneration on every use and
|
||||
// reloads its slots when they differ.
|
||||
static std::mutex& builtinFontMutex()
|
||||
{
|
||||
static std::mutex m;
|
||||
return m;
|
||||
}
|
||||
static std::string builtinFontPaths[BUILTIN_FONTS_NUM];
|
||||
static std::atomic<int> builtinFontGeneration(0);
|
||||
|
||||
static bool inflate(const void* src, size_t srclen, std::vector<uchar>& dst)
|
||||
{
|
||||
@@ -195,6 +230,19 @@ struct FontFace::Impl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load an external file into a built-in slot: it is then addressed by the
|
||||
// slot name ("italic", "uni", ...) and takes part in the fallback chain.
|
||||
bool setStdExternal(const BuiltinFontData& fontdata, const String& path)
|
||||
{
|
||||
if(!set(path))
|
||||
return false;
|
||||
currname = fontdata.name;
|
||||
scalefactor = fontdata.sf;
|
||||
italic = fontdata.italic;
|
||||
initParams();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set(const String& fontname)
|
||||
{
|
||||
CV_Assert(!fontname.empty());
|
||||
@@ -397,6 +445,7 @@ public:
|
||||
hb_uni_funcs = hb_unicode_funcs_get_default();
|
||||
max_cache_size = (size_t)MAX_CACHE_SIZE;
|
||||
raster_draw = 0;
|
||||
builtin_generation = -1; // force the first syncBuiltinFonts() to load the slots
|
||||
}
|
||||
|
||||
~FontRenderEngine()
|
||||
@@ -435,10 +484,41 @@ public:
|
||||
FontFace& getStdFontFace(int i)
|
||||
{
|
||||
CV_Assert(i >= 0 && i < BUILTIN_FONTS_NUM);
|
||||
builtin_ffaces[i]->setStd(builtinFontData[i]);
|
||||
syncBuiltinFonts();
|
||||
return builtin_ffaces[i];
|
||||
}
|
||||
|
||||
// (Re)load the built-in font slots of this thread's engine if
|
||||
// FontFace::setBuiltinFont() changed the registry since the last call.
|
||||
void syncBuiltinFonts()
|
||||
{
|
||||
int gen = builtinFontGeneration.load(std::memory_order_acquire);
|
||||
if(gen == builtin_generation)
|
||||
return;
|
||||
std::string paths[BUILTIN_FONTS_NUM];
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(builtinFontMutex());
|
||||
for(int i = 0; i < BUILTIN_FONTS_NUM; i++)
|
||||
paths[i] = builtinFontPaths[i];
|
||||
gen = builtinFontGeneration.load(std::memory_order_acquire);
|
||||
}
|
||||
for(int i = 0; i < BUILTIN_FONTS_NUM; i++)
|
||||
{
|
||||
const BuiltinFontData& fontdata = builtinFontData[i];
|
||||
// a fresh Impl: FontFace objects handed out earlier keep the old one alive
|
||||
FontFace fface;
|
||||
bool ok = !paths[i].empty() && fface->setStdExternal(fontdata, paths[i]);
|
||||
if(!ok && fontdata.size > 1)
|
||||
ok = fface->setStd(fontdata);
|
||||
builtin_ffaces[i] = ok ? fface : FontFace();
|
||||
}
|
||||
builtin_generation = gen;
|
||||
// hb_font pointers are part of the glyph cache key; drop the cache so that a
|
||||
// recycled address of a destroyed font cannot alias its glyphs.
|
||||
glyph_cache.clear();
|
||||
all_cached.clear();
|
||||
}
|
||||
|
||||
Point putText_( Mat& img, Size imgsize, const String& str_, Point org,
|
||||
const uchar* color, FontFace& fontface, int size,
|
||||
int weight, PutTextFlags flags, Range wrapRange, bool render,
|
||||
@@ -446,7 +526,7 @@ public:
|
||||
|
||||
protected:
|
||||
FontFace builtin_ffaces[BUILTIN_FONTS_NUM];
|
||||
bool builtin_ffaces_initialized;
|
||||
int builtin_generation;
|
||||
|
||||
hb_unicode_funcs_t* hb_uni_funcs;
|
||||
|
||||
@@ -481,24 +561,21 @@ bool FontFace::set(const String& fontname_)
|
||||
fontname = "sans";
|
||||
if(impl->hb_font != 0 && impl->currname == fontname)
|
||||
return true;
|
||||
int i = 0;
|
||||
for( ; i < BUILTIN_FONTS_NUM; i++ )
|
||||
{
|
||||
if( builtinFontData[i].name == fontname && builtinFontData[i].size > 1 )
|
||||
break;
|
||||
}
|
||||
if( i >= BUILTIN_FONTS_NUM )
|
||||
i = -1;
|
||||
int i = builtinFontIndex(fontname);
|
||||
|
||||
FontRenderEngine& engine = fontRenderEngine;
|
||||
|
||||
bool ok = false;
|
||||
if( i >= 0 )
|
||||
{
|
||||
FontFace& builtin_fface = engine.getStdFontFace(i);
|
||||
if(builtin_fface.impl->hb_font)
|
||||
FontFace* builtin_fface = &engine.getStdFontFace(i);
|
||||
// no italic font compiled in or registered: fall back to the upright one,
|
||||
// so that e.g. FONT_ITALIC in the legacy putText() keeps working
|
||||
if(!(*builtin_fface)->hb_font && i == BUILTIN_FONT_ITALIC)
|
||||
builtin_fface = &engine.getStdFontFace(BUILTIN_FONT_SANS);
|
||||
if((*builtin_fface)->hb_font)
|
||||
{
|
||||
impl = builtin_fface.impl;
|
||||
impl = builtin_fface->impl;
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
@@ -511,6 +588,25 @@ bool FontFace::set(const String& fontname_)
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool FontFace::setBuiltinFont(const String& fontName, const String& fontPath)
|
||||
{
|
||||
int i = builtinFontIndex(fontName);
|
||||
if(i < 0)
|
||||
return false;
|
||||
if(!fontPath.empty())
|
||||
{
|
||||
// validate the file now, so that the caller learns about a bad path immediately
|
||||
// and a broken registration cannot leave a slot empty
|
||||
Impl probe;
|
||||
if(!probe.set(fontPath))
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(builtinFontMutex());
|
||||
builtinFontPaths[i] = fontPath;
|
||||
builtinFontGeneration.fetch_add(1, std::memory_order_acq_rel);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FontFace::getBuiltinFontData(const String& fontname_,
|
||||
const uchar*& data, size_t& size)
|
||||
{
|
||||
@@ -519,20 +615,16 @@ bool FontFace::getBuiltinFontData(const String& fontname_,
|
||||
fontname = "sans";
|
||||
data = 0;
|
||||
size = 0;
|
||||
for(int i = 0; i < BUILTIN_FONTS_NUM; i++)
|
||||
int i = builtinFontIndex(fontname);
|
||||
if(i < 0)
|
||||
return false;
|
||||
FontFace& builtin_fface = fontRenderEngine.getStdFontFace(i);
|
||||
if( builtin_fface.impl->hb_font )
|
||||
{
|
||||
if(builtinFontData[i].name == fontname && builtinFontData[i].size > 1)
|
||||
{
|
||||
FontFace& builtin_fface = fontRenderEngine.getStdFontFace(i);
|
||||
if( builtin_fface.impl->hb_font )
|
||||
{
|
||||
std::vector<uchar>& fbuf = builtin_fface.impl->fontbuf;
|
||||
size = fbuf.size();
|
||||
data = size > 0 ? &fbuf[0] : 0;
|
||||
return size > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::vector<uchar>& fbuf = builtin_fface.impl->fontbuf;
|
||||
size = fbuf.size();
|
||||
data = size > 0 ? &fbuf[0] : 0;
|
||||
return size > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -963,15 +1055,13 @@ Point FontRenderEngine::putText_(
|
||||
|
||||
fontface->setParams(size, weight);
|
||||
|
||||
syncBuiltinFonts();
|
||||
for(j = 0; j < BUILTIN_FONTS_NUM; j++)
|
||||
{
|
||||
FontFace& fface = builtin_ffaces[j];
|
||||
if(!builtin_ffaces_initialized)
|
||||
fface.set(builtinFontData[j].name);
|
||||
if (fface->hb_font)
|
||||
fface->setParams(size, weight);
|
||||
}
|
||||
builtin_ffaces_initialized = true;
|
||||
|
||||
if(alignment == PUT_TEXT_ALIGN_RIGHT)
|
||||
std::swap(x0, x1);
|
||||
@@ -1115,19 +1205,24 @@ Point FontRenderEngine::putText_(
|
||||
glob_dir = dir;
|
||||
}
|
||||
|
||||
// pick the first font that covers the character: the user font, then the
|
||||
// built-in slots (empty slots have hb_font == 0 and are skipped by hbGlyphIndex)
|
||||
hb_font_t* cand_hb = hb_font0;
|
||||
for(j = -1; j < BUILTIN_FONTS_NUM; j++)
|
||||
for(int attempt = 0; attempt < 2; attempt++)
|
||||
{
|
||||
cand_hb = j < 0 ? hb_font0 : builtin_ffaces[j]->hb_font;
|
||||
glyph_index = hbGlyphIndex(cand_hb, c);
|
||||
if(glyph_index != 0)
|
||||
break;
|
||||
if(j+1 == BUILTIN_FONTS_NUM)
|
||||
for(j = -1; j < BUILTIN_FONTS_NUM; j++)
|
||||
{
|
||||
chars[i] = c = '?'; // replace the character with 'unknown' ~ <?> (TBD: replace ? with 0xFFFD)
|
||||
break;
|
||||
cand_hb = j < 0 ? hb_font0 : builtin_ffaces[j]->hb_font;
|
||||
glyph_index = hbGlyphIndex(cand_hb, c);
|
||||
if(glyph_index != 0)
|
||||
break;
|
||||
}
|
||||
if(j < BUILTIN_FONTS_NUM || attempt > 0)
|
||||
break;
|
||||
chars[i] = c = '?'; // no font covers the character: draw 'unknown' ~ <?> instead (TBD: 0xFFFD)
|
||||
}
|
||||
if(j >= BUILTIN_FONTS_NUM)
|
||||
j = -1; // not even '?' is available; stay with the user font
|
||||
int fontidx = j;
|
||||
int seglen = i - segstart;
|
||||
// if the current segment (if any) ends with 1 or more neutral/punctuation characters,
|
||||
@@ -1500,6 +1595,7 @@ FontFace::~FontFace() {}
|
||||
bool FontFace::setInstance(const std::vector<int>&) { return false; }
|
||||
bool FontFace::getInstance(std::vector<int>&) const { return false; }
|
||||
bool FontFace::getBuiltinFontData(const String&, const uchar*&, size_t&) { return false; }
|
||||
bool FontFace::setBuiltinFont(const String&, const String&) { return false; }
|
||||
|
||||
#define OPENCV_NO_TEXT_RENDERING_MSG \
|
||||
"Text rendering is not supported. Compile OpenCV with WITH_HARFBUZZ=ON."
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
//M*/
|
||||
|
||||
#include "test_precomp.hpp"
|
||||
#include "opencv2/core/utils/filesystem.hpp"
|
||||
|
||||
namespace opencv_test { namespace {
|
||||
|
||||
@@ -755,13 +756,26 @@ typedef struct TextProp
|
||||
bool italic;
|
||||
} TextProp;
|
||||
|
||||
#ifdef HAVE_UNIFONT // there are other tests for text drawing, so the functionality is tested anyway,
|
||||
// but this test needs concrete unicode font to compare the printed text
|
||||
// (including CJK characters) with the reference picture from the database
|
||||
// External fonts for the "italic" and "uni" built-in slots, shipped in opencv_extra
|
||||
// and registered for the whole test run by test_main.cpp (see registerExternalTestFonts).
|
||||
static string externalTestFont(const char* slot)
|
||||
{
|
||||
const char* fname = strcmp(slot, "italic") == 0 ? "Rubik-Italic.ttf.gz" : "WenQuanYiMicroHei.ttf.gz";
|
||||
string path = TS::ptr()->get_data_path() + "../highgui/drawing/" + fname;
|
||||
if (!utils::fs::exists(path))
|
||||
throw SkipTestException(string("font is missing in opencv_extra: ") + path);
|
||||
return path;
|
||||
}
|
||||
|
||||
// The reference picture was rendered with Rubik Italic as the "italic" font and
|
||||
// WenQuanYi Micro Hei as the "uni" (CJK) font. Neither is compiled into OpenCV by
|
||||
// default (WITH_ITALICFONT / WITH_UNIFONT are OFF); the external copies from
|
||||
// opencv_extra reproduce the picture 1:1.
|
||||
TEST(Drawing, ttf_text)
|
||||
{
|
||||
string ts_data_path = TS::ptr()->get_data_path();
|
||||
string custom_font_path = ts_data_path + "../highgui/drawing/";
|
||||
externalTestFont("italic");
|
||||
externalTestFont("uni");
|
||||
|
||||
FontFace sans("sans");
|
||||
FontFace italic("italic");
|
||||
@@ -858,7 +872,66 @@ TEST(Drawing, ttf_text)
|
||||
EXPECT_LT(cv::norm(refimg, img, NORM_L1), 6500);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Re-registers the external test fonts when a test that unregistered them finishes.
|
||||
struct ExternalTestFontsGuard
|
||||
{
|
||||
~ExternalTestFontsGuard()
|
||||
{
|
||||
FontFace::setBuiltinFont("italic", externalTestFont("italic"));
|
||||
FontFace::setBuiltinFont("uni", externalTestFont("uni"));
|
||||
}
|
||||
};
|
||||
|
||||
TEST(Drawing, builtin_font_slots)
|
||||
{
|
||||
string italic_font = externalTestFont("italic"), uni_font = externalTestFont("uni");
|
||||
ExternalTestFontsGuard guard;
|
||||
|
||||
EXPECT_FALSE(FontFace::setBuiltinFont("no-such-slot", ""));
|
||||
EXPECT_FALSE(FontFace::setBuiltinFont("uni", "no-such-file.ttf"));
|
||||
|
||||
// back to the compiled-in state: only the fonts selected at build time
|
||||
ASSERT_TRUE(FontFace::setBuiltinFont("italic", ""));
|
||||
ASSERT_TRUE(FontFace::setBuiltinFont("uni", ""));
|
||||
{
|
||||
// "italic" always resolves to some font: the compiled-in Rubik Italic if any,
|
||||
// otherwise the upright "sans" (so FONT_ITALIC in the legacy API keeps working)
|
||||
FontFace italic("italic");
|
||||
#ifdef HAVE_ITALICFONT
|
||||
EXPECT_EQ(italic.getName(), "italic");
|
||||
#else
|
||||
EXPECT_EQ(italic.getName(), "sans");
|
||||
#endif
|
||||
FontFace uni;
|
||||
#ifdef HAVE_UNIFONT
|
||||
EXPECT_TRUE(uni.set("uni"));
|
||||
#else
|
||||
EXPECT_FALSE(uni.set("uni"));
|
||||
#endif
|
||||
}
|
||||
Mat img(60, 300, CV_8UC3, Scalar::all(255));
|
||||
FontFace sans("sans");
|
||||
Rect r_before = getTextSize(img.size(), "打印文字", Point(10, 40), sans, 30, 400, PUT_TEXT_ALIGN_LEFT);
|
||||
|
||||
// register the external CJK font: it now serves both as "uni" and as a fallback for "sans"
|
||||
ASSERT_TRUE(FontFace::setBuiltinFont("uni", uni_font));
|
||||
FontFace uni("uni");
|
||||
EXPECT_EQ(uni.getName(), "uni");
|
||||
Rect r_after = getTextSize(img.size(), "打印文字", Point(10, 40), sans, 30, 400, PUT_TEXT_ALIGN_LEFT);
|
||||
#ifndef HAVE_UNIFONT
|
||||
// without the CJK font the characters were not covered and measured as '?'
|
||||
EXPECT_NE(r_before.width, r_after.width);
|
||||
#else
|
||||
EXPECT_EQ(r_before.width, r_after.width);
|
||||
#endif
|
||||
putText(img, "打印文字", Point(10, 40), Scalar(0, 0, 0), sans, 30, 400, PUT_TEXT_ALIGN_LEFT);
|
||||
EXPECT_LT(mean(img)[0], 255.0); // something has been drawn
|
||||
|
||||
ASSERT_TRUE(FontFace::setBuiltinFont("italic", italic_font));
|
||||
FontFace italic("italic");
|
||||
EXPECT_EQ(italic.getName(), "italic");
|
||||
}
|
||||
|
||||
// Experiment (not a regression test): render Arabic + Devanagari with FiraGO,
|
||||
// which covers both scripts, and dump a PNG for visual inspection.
|
||||
|
||||
@@ -7,4 +7,22 @@
|
||||
#include <hpx/hpx_main.hpp>
|
||||
#endif
|
||||
|
||||
CV_TEST_MAIN("cv")
|
||||
#include "opencv2/core/utils/filesystem.hpp"
|
||||
|
||||
// The drawing tests (and their reference pictures) were made with the "italic"
|
||||
// (Rubik Italic) and "uni" (WenQuanYi Micro Hei) fonts compiled into OpenCV. Both
|
||||
// are optional now (WITH_ITALICFONT / WITH_UNIFONT, OFF by default), so provide
|
||||
// them as external files from opencv_extra when they are available.
|
||||
static void registerExternalTestFonts()
|
||||
{
|
||||
const std::string dir = cvtest::TS::ptr()->get_data_path() + "../highgui/drawing/";
|
||||
const char* fonts[][2] = {{"italic", "Rubik-Italic.ttf.gz"}, {"uni", "WenQuanYiMicroHei.ttf.gz"}};
|
||||
for (const auto& f : fonts)
|
||||
{
|
||||
const std::string path = dir + f[1];
|
||||
if (cv::utils::fs::exists(path))
|
||||
cv::FontFace::setBuiltinFont(f[0], path);
|
||||
}
|
||||
}
|
||||
|
||||
CV_TEST_MAIN("cv", registerExternalTestFonts())
|
||||
|
||||
Reference in New Issue
Block a user