From 208a80f975072d938ea4e8811b9fb8b523fda869 Mon Sep 17 00:00:00 2001 From: Chris Rudd Date: Wed, 13 Dec 2023 17:50:34 -0600 Subject: [PATCH 1/3] Implement missing canvas functions --- lib/impl/cairo/canvas.cpp | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/impl/cairo/canvas.cpp b/lib/impl/cairo/canvas.cpp index 1a211019..3910d37a 100755 --- a/lib/impl/cairo/canvas.cpp +++ b/lib/impl/cairo/canvas.cpp @@ -25,6 +25,7 @@ namespace cycfi::artist void apply_fill_style(); void apply_stroke_style(); + float pre_scale = 1.0f; using state_stack = std::stack; @@ -60,8 +61,10 @@ namespace cycfi::artist { } - void canvas::pre_scale(float scale) + void canvas::pre_scale(float sc) { + scale({sc,sc}); + _state->pre_scale = sc; } void canvas::translate(point p) @@ -79,6 +82,20 @@ namespace cycfi::artist cairo_scale(_context, p.x, p.y); } + point canvas::device_to_user(point p) { + double x = p.x * _state->pre_scale; + double y = p.y * _state->pre_scale; + cairo_device_to_user_distance(_context, &x, &y); + return {float(x),float(y)}; + } + + point canvas::user_to_device(point p) { + double x = p.x; + double y = p.y; + cairo_user_to_device_distance(_context, &x, &y); + return {float(x / _state->pre_scale),float(y / _state->pre_scale)}; + } + void canvas::save() { cairo_save(_context); @@ -102,6 +119,10 @@ namespace cycfi::artist cairo_close_path(_context); } + void canvas::fill_rule(path::fill_rule_enum rule) { + //TODO + } + void canvas::fill() { _state->apply_fill_style(); @@ -131,6 +152,11 @@ namespace cycfi::artist cairo_clip(_context); } + bool canvas::point_in_path(point p) const { + // not sure how to implement this on cairo + return false; + } + void canvas::move_to(point p) { cairo_move_to(_context, p.x, p.y); From 24a8ae6941e7bd45c18aca6e09f6d8ce1d1ff6b1 Mon Sep 17 00:00:00 2001 From: Chris Rudd Date: Wed, 13 Dec 2023 17:52:51 -0600 Subject: [PATCH 2/3] basic implementation of font based on elements Cairo implementation --- lib/impl/cairo/canvas.cpp | 6 + lib/impl/cairo/font.cpp | 449 +++++++++++++++++++++++++++++++++++- lib/impl/cairo/font_impl.h | 27 +++ lib/include/artist/font.hpp | 3 + 4 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 lib/impl/cairo/font_impl.h diff --git a/lib/impl/cairo/canvas.cpp b/lib/impl/cairo/canvas.cpp index 3910d37a..73ed6a1a 100755 --- a/lib/impl/cairo/canvas.cpp +++ b/lib/impl/cairo/canvas.cpp @@ -7,6 +7,8 @@ #include #include +#include "font_impl.h" + namespace cycfi::artist { class canvas::canvas_state @@ -425,6 +427,10 @@ namespace cycfi::artist void canvas::font(class font const& font_) { + if( font_.impl() ) { + cairo_set_font_face(_context,font_.impl()->font_face); + cairo_set_font_size(_context,font_.impl()->size); + } } namespace diff --git a/lib/impl/cairo/font.cpp b/lib/impl/cairo/font.cpp index 9c81103d..b89aabb3 100644 --- a/lib/impl/cairo/font.cpp +++ b/lib/impl/cairo/font.cpp @@ -4,19 +4,463 @@ Distributed under the MIT License [ https://opensource.org/licenses/MIT ] =============================================================================*/ #include +#include +#include +#include + +#include +#include + +# include +# include +# include + +#include "font_impl.h" + +#ifdef __APPLE__ +# include +#endif namespace cycfi::artist { + namespace + { + inline void ltrim(std::string& s) + { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), + [](int ch) { return ch != ' ' && ch != '"'; } + )); + } + + inline void rtrim(std::string& s) + { + s.erase(std::find_if(s.rbegin(), s.rend(), + [](int ch) { return ch != ' ' && ch != '"'; } + ).base(), s.end()); + } + + inline void trim(std::string& s) + { + ltrim(s); + rtrim(s); + } + } + + namespace // FONT CONFIG -- possibly move to impl/fontconfig + { + inline float lerp(float a, float b, float f) + { + return (a * (1.0 - f)) + (b * f); + } + + struct font_entry + { + //sk_sp cached_typeface; + cairo_font_face_t* cached_typeface; + std::string file; + std::string full_name; + int index = 0; + uint8_t weight = font_constants::weight_normal; + uint8_t slant = font_constants::slant_normal; + uint8_t stretch = font_constants::stretch_normal; + }; + + using font_map_type = std::map>; + std::pair get_font_map() + { + static font_map_type font_map_; + static std::mutex mutex_; + return {font_map_, mutex_}; + } + + constexpr auto font_map_default_font_family = ""; + + enum + { + fc_thin = 0, + fc_extralight = 40, + fc_light = 50, + fc_semilight = 55, + fc_book = 75, + fc_normal = 80, + fc_medium = 100, + fc_semibold = 180, + fc_bold = 200, + fc_extrabold = 205, + fc_black = 210 + }; + + int map_fc_weight(int w) + { + auto&& map = [](double mina, double maxa, double minb, double maxb, double val) + { + return lerp(mina, maxa, (val-minb)/(maxb-minb)); + }; + + namespace fc = font_constants; + + if (w < fc_extralight) + return map(fc::thin, fc::extra_light, fc_thin, fc_extralight, w); + if (w < fc_light) + return map(fc::extra_light, fc::light, fc_extralight, fc_light, w); + if (w < fc_normal) + return map(fc::light, fc::weight_normal, fc_light, fc_normal, w); + if (w < fc_medium) + return map(fc::weight_normal, fc::medium, fc_normal, fc_medium, w); + if (w < fc_semibold) + return map(fc::medium, fc::semi_bold, fc_medium, fc_semibold, w); + if (w < fc_bold) + return map(fc::semi_bold, fc::bold, fc_semibold, fc_bold, w); + if (w < fc_extrabold) + return map(fc::bold, fc::extra_bold, fc_bold, fc_extrabold, w); + if (w < fc_black) + return map(fc::extra_bold, fc::black, fc_extrabold, fc_black, w); + return map(fc::black, 100, fc_black, 220, std::min(w, 220)); + } + + using fc_config_ptr = std::unique_ptr>; + using fc_patern_ptr = std::unique_ptr>; + using fc_object_set_ptr = std::unique_ptr>; + using fc_font_set_ptr = std::unique_ptr>; + + void init_font_map() + { + FcInit(); + FcConfig* config = FcConfigGetCurrent(); + auto user_fonts_path = get_user_fonts_directory(); + FcConfigAppFontAddDir(config, (FcChar8 const*)user_fonts_path.string().c_str()); + auto pat = fc_patern_ptr{FcPatternCreate()}; + auto os = fc_object_set_ptr{ + FcObjectSetBuild( + FC_FAMILY, FC_FULLNAME, FC_WIDTH, FC_WEIGHT + , FC_SLANT, FC_FILE, FC_INDEX, nullptr) + }; + auto fs = fc_font_set_ptr{FcFontList(config, pat.get(), os.get())}; + + // The lock is not needed, since this is run on static initialization. + auto [font_map, font_map_mutex] = get_font_map(); + + for (int i=0; fs && i < fs->nfont; ++i) + { + FcPattern* font = fs->fonts[i]; + FcChar8 *file, *family, *full_name; + int index; + if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch && + FcPatternGetString(font, FC_FAMILY, 0, &family) == FcResultMatch && + FcPatternGetString(font, FC_FULLNAME, 0, &full_name) == FcResultMatch && + FcPatternGetInteger(font, FC_INDEX, 0, &index) == FcResultMatch + ) + { + font_entry entry; + entry.cached_typeface = nullptr; + entry.file = (const char*) file; + entry.full_name = (const char*) full_name; + entry.index = index; + + int weight; + if (FcPatternGetInteger(font, FC_WEIGHT, 0, &weight) == FcResultMatch) + entry.weight = map_fc_weight(weight); // map the weight (normalized 0 to 100) + + int slant; + if (FcPatternGetInteger(font, FC_SLANT, 0, &slant) == FcResultMatch) + entry.slant = (slant * 100) / 110; // normalize 0 to 100 + + int width; + if (FcPatternGetInteger(font, FC_WIDTH, 0, &width) == FcResultMatch) + entry.stretch = (width * 100) / 200; // normalize 0 to 100 + + std::string key = (const char*) family; + trim(key); + + font_map[key].push_back(std::move(entry)); + } + } + } + + font_entry* match(font_map_type& font_map, font_descr descr) + { + struct font_init + { + font_init() + { + init_font_map(); + } + }; + static font_init init; + + std::istringstream str( + std::string{descr._families} + ", " + font_map_default_font_family); + std::string family; + while (getline(str, family, ',')) + { + trim(family); + if (auto i = font_map.find(family); i != font_map.end()) + { + int min = 10000; + std::vector::iterator best_match = i->second.end(); + for (auto j = i->second.begin(); j != i->second.end(); ++j) + { + auto const& item = *j; + + // Get biased score (lower is better). Give `slant` attribute + // the highest bias (3.0), followed by `weight` (1.0) and then + // `stretch` (0.25). + auto diff = + (std::abs(int(descr._weight) - int(item.weight)) * 1.0) + + (std::abs(int(descr._slant) - int(item.slant)) * 3.0) + + (std::abs(int(descr._stretch) - int(item.stretch)) * 0.25) + ; + if (diff < min) + { + min = diff; + best_match = j; + if (diff == 0) + break; + } + } + if (best_match != i->second.end()) + return &*best_match; + } + } + return nullptr; + } + } + + namespace { // FREE_TYPE + class free_type_face + { + public: + free_type_face() = default; + + free_type_face(FT_Face face) + : _face(face) {} + + ~free_type_face() + { + if (_face) + FT_Done_Face(_face); + } + + free_type_face(free_type_face const& other) = delete; + free_type_face& operator=(free_type_face const& other) = delete; + + free_type_face(free_type_face&& other) noexcept + : free_type_face() + { + *this = std::move(other); + } + + free_type_face& operator=(free_type_face&& other) noexcept + { + std::swap(_face, other._face); + return *this; + } + + FT_Face handle() + { + return _face; + } + + FT_Face release() + { + return std::exchange(_face, nullptr); + } + + explicit operator bool() const + { + return _face != nullptr; + } + + private: + FT_Face _face = nullptr; + }; + + void destroy_free_type_face(void* face) + { + FT_Done_Face(reinterpret_cast(face)); + } + + class free_type_library + { + public: + free_type_library() + { + FT_Error status = FT_Init_FreeType(&_ft_lib); + //CYCFI_ASSERT(status == 0, "Error: failed to initialize free type library."); + } + + ~free_type_library() + { + if (!_ft_lib) + return; + + FT_Error status = FT_Done_FreeType(_ft_lib); + //CYCFI_ASSERT(status == 0, "Error: failed to destroy free type library."); + } + + free_type_library(free_type_library const& other) = delete; + free_type_library& operator=(free_type_library const& other) = delete; + + free_type_library(free_type_library&& other) noexcept + { + swap(*this, other); + } + + free_type_library& operator=(free_type_library&& other) noexcept + { + swap(*this, other); + return *this; + } + + friend void swap(free_type_library& lhs, free_type_library& rhs) noexcept + { + std::swap(lhs._ft_lib, rhs._ft_lib); + } + + free_type_face load_face(char const* font_path) + { + FT_Face ft_face; + FT_Error ft_status = FT_New_Face(_ft_lib, font_path, 0, &ft_face); + + if (ft_status == 0) + return free_type_face(ft_face); + else + return free_type_face(nullptr); + } + + private: + FT_Library _ft_lib = nullptr; + }; + } + + auto const& cairo_user_data_key() + { + static const cairo_user_data_key_t key = {}; + return key; + } + + cairo_font_face_t* load_free_type_font(char const* font_path) + { + static free_type_library ft_lib; + + free_type_face ft_face = ft_lib.load_face(font_path); + if (!ft_face) + return nullptr; + + cairo_font_face_t* cairo_face = cairo_ft_font_face_create_for_ft_face(ft_face.handle(), 0); + if (cairo_face == nullptr) + return nullptr; + + // extend the freetype font face lifetime to cairo's font face lifetime + cairo_status_t cairo_status = CAIRO_STATUS_SUCCESS; + cairo_user_data_key_t const& key = cairo_user_data_key(); + if (cairo_font_face_get_user_data(cairo_face, &key) != ft_face.handle()) + { + cairo_status = cairo_font_face_set_user_data( + cairo_face, &key, ft_face.handle(), &destroy_free_type_face); + } + + if (cairo_status == CAIRO_STATUS_SUCCESS) + { + ft_face.release(); + } + else + { + cairo_font_face_destroy(cairo_face); + return nullptr; + } + + return cairo_face; + } + + cairo_font_face_t* load_apple_font(const char* name) { + auto cfstr = CFStringCreateWithCString( + kCFAllocatorDefault + , name + , kCFStringEncodingUTF8 + ); + auto cgfont = CGFontCreateWithFontName(cfstr); + auto cairo_face = cairo_quartz_font_face_create_for_cgfont(cgfont); + if (cgfont) + CFRelease(cgfont); + if (cfstr) + CFRelease(cfstr); + return cairo_face; + } + font::font() + : _ptr(std::make_shared()) { } font::font(font_descr descr) { + auto [font_map, font_map_mutex] = get_font_map(); + std::lock_guard lock(font_map_mutex); + + auto match_ptr = match(font_map, descr); + if (match_ptr) + { + if( !match_ptr->cached_typeface ) { +#ifdef __APPLE__ + match_ptr->cached_typeface = load_apple_font(match_ptr->full_name.c_str()); +#else + match_ptr->cached_typeface = load_free_type_font(match_ptr->file.c_str()); +#endif + } + + if (match_ptr->cached_typeface) + { + _ptr = std::make_shared(cairo_font_face_reference(match_ptr->cached_typeface), descr._size); + } + } + + if (_ptr) + return; +/* + using namespace font_constants; + int stretch = int(descr._stretch) / 10; + SkFontStyle style( + descr._weight * 10 + , (descr._stretch < condensed)? stretch-1 : stretch + , (descr._slant == italic)? SkFontStyle::kItalic_Slant : + (descr._slant == oblique)? SkFontStyle::kOblique_Slant : + SkFontStyle::kUpright_Slant + ); + + auto default_face = SkTypeface::MakeFromName(nullptr, style); + std::istringstream str(std::string{descr._families}); + std::string family; + + while (getline(str, family, ',')) + { + trim(family); + auto face = SkTypeface::MakeFromName(family.c_str(), style); + if (face && face != default_face) + { + _ptr = std::make_shared(face, descr._size); + break; + } + } + if (!_ptr) + { + family = font_map_default_font_family; + _ptr = std::make_shared(default_face, descr._size); + } + + font_entry entry; + entry.cached_typeface = sk_ref_sp(_ptr->getTypeface()); + entry.weight = descr._weight; + entry.slant = descr._slant; + entry.stretch = descr._stretch; + + font_map[family].push_back(std::move(entry)); +*/ } font::font(font const& rhs) { + _ptr = rhs._ptr; } font::font(font&& rhs) noexcept @@ -42,11 +486,14 @@ namespace cycfi::artist font::metrics_info font::metrics() const { - return {}; + // Create a temp cairo context, set font and get extents? + return {0,0,0}; } float font::measure_text(std::string_view str) const { + // not sure what the intent of this measure is + // but seems like it also needs a temp cairo context return 0; } } diff --git a/lib/impl/cairo/font_impl.h b/lib/impl/cairo/font_impl.h new file mode 100644 index 00000000..5420d0df --- /dev/null +++ b/lib/impl/cairo/font_impl.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace cycfi::artist { + +struct font_impl { + cairo_font_face_t* font_face = nullptr; + float size = 0; + + font_impl() + :font_impl(nullptr,0) + {} + + font_impl(cairo_font_face_t* f, float s) + :font_face(f) + ,size(s) + {} + + ~font_impl() { + if( font_face ) { + cairo_font_face_destroy(font_face); + } + } +}; + +} diff --git a/lib/include/artist/font.hpp b/lib/include/artist/font.hpp index ad9f7966..d317de43 100644 --- a/lib/include/artist/font.hpp +++ b/lib/include/artist/font.hpp @@ -20,6 +20,9 @@ namespace cycfi::artist #if defined(ARTIST_SKIA) using font_impl = SkFont; using font_impl_ptr = std::shared_ptr; +#elif defined(ARTIST_CAIRO) + struct font_impl; + using font_impl_ptr = std::shared_ptr; #else struct font_impl; using font_impl_ptr = font_impl*; From b79bcdf8c83cd0be291589567866d1901b040647 Mon Sep 17 00:00:00 2001 From: Chris Rudd Date: Wed, 13 Dec 2023 17:53:38 -0600 Subject: [PATCH 3/3] Implement offscreen_image and image fixes --- lib/impl/cairo/image.cpp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/impl/cairo/image.cpp b/lib/impl/cairo/image.cpp index a832e323..8e8f896e 100755 --- a/lib/impl/cairo/image.cpp +++ b/lib/impl/cairo/image.cpp @@ -68,14 +68,20 @@ namespace cycfi::artist } stbi_image_free(src_data); + + cairo_surface_flush(_impl); + // Flag the surface as dirty + cairo_surface_mark_dirty(_impl); } } - + + if( cairo_surface_status(_impl) != CAIRO_STATUS_SUCCESS ) { + cairo_surface_destroy(_impl); + _impl = nullptr; + } + if (!_impl) throw std::runtime_error{ "Failed to load pixmap: " + path_.string() }; - - // Flag the surface as dirty - cairo_surface_mark_dirty(_impl); } image::~image() @@ -116,18 +122,29 @@ namespace cycfi::artist return {}; } + struct offscreen_image::state { + cairo_t* _context = nullptr; + }; + offscreen_image::offscreen_image(image& img) : _image(img) + , _state(new offscreen_image::state{}) { } offscreen_image::~offscreen_image() { + if( _state->_context ) { + cairo_destroy( _state->_context ); + } } canvas_impl* offscreen_image::context() const { - return nullptr; + if( !_state->_context ) { + _state->_context = cairo_create(_image.impl()); + } + return _state->_context; } }