#include "Canvas.h" #include "Blend2D_Convert.h" #include "Font_Face_p.h" #include "Image_p.h" #include "blend2d/blend2d.h" #include #include #include #include namespace renderive { namespace { std::string& default_font_source_storage() { static std::string source; return source; } } // namespace std::shared_ptr Canvas::default_font_face() { static std::shared_ptr face = []() -> std::shared_ptr { static constexpr std::array candidates{ "C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/simsun.ttc", "C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/tahoma.ttf" }; for (const char* path : candidates) { std::error_code error; if (!std::filesystem::exists(path, error)) continue; auto loaded = std::make_shared(); if (loaded->impl_->face.create_from_file(path) == BL_SUCCESS) { default_font_source_storage() = path; return loaded; } } return {}; }(); return face; } Font Canvas::resolve_font(Font font) { if (font.size <= 0.0) font.size = 12.0; if (!font.face || !font.face->valid()) font.face = default_font_face(); return font; } bool Canvas::can_use_font(const Font& font) { return font.size > 0.0 && font.face && font.face->valid(); } struct Canvas::Impl { Image& image; BLContext ctx; Font current_font = Canvas::resolve_font({}); bool antialias = true; Pen current_pen; Brush current_brush; Impl(Image& img) : image(img) { if (!img.impl_->image.is_empty()) { ctx.begin(img.impl_->image); if (ctx) ctx.set_comp_op(BL_COMP_OP_SRC_OVER); } } ~Impl() { if (ctx) ctx.end(); } void apply_brush() { if (!current_brush.enabled()) { ctx.set_fill_style(BLRgba(0.0, 0.0, 0.0, 0.0)); return; } ctx.set_fill_style(Convert::to_bl(current_brush.color)); } void apply_text_fill() { if (!current_pen.enabled()) { ctx.set_fill_style(BLRgba(0.0, 0.0, 0.0, 0.0)); return; } ctx.set_fill_style(Convert::to_bl(current_pen.color)); } void apply_pen(double stroke_width = -1.0) { if (!current_pen.enabled()) { ctx.set_stroke_style(BLRgba(0.0, 0.0, 0.0, 0.0)); return; } ctx.set_stroke_style(Convert::to_bl(current_pen.color)); double w = stroke_width > 0.0 ? stroke_width : current_pen.width; ctx.set_stroke_width(w); if (current_pen.style == Line_Style::Dash || current_pen.style == Line_Style::Dot) { BLArray pattern; if (current_pen.style == Line_Style::Dash) { pattern.append(w * 4.0); pattern.append(w * 2.0); } else { pattern.append(w * 1.0); pattern.append(w * 2.0); } ctx.set_stroke_dash_array(pattern); } else { ctx.set_stroke_dash_array(BLArray()); } ctx.set_stroke_caps(static_cast(Convert::blend2d_line_cap(current_pen.cap))); ctx.set_stroke_join(static_cast(Convert::blend2d_line_join(current_pen.join))); } }; Canvas::Canvas(Image& image) : impl_(std::make_unique(image)) {} Canvas::~Canvas() = default; void Canvas::save() { impl_->ctx.save(); } void Canvas::restore() { impl_->ctx.restore(); } void Canvas::set_pen(const Pen& pen) { impl_->current_pen = pen; } void Canvas::set_brush(const Brush& brush) { impl_->current_brush = brush; } void Canvas::set_font(const Font& font) { impl_->current_font = resolve_font(font); } void Canvas::set_antialias(bool enabled) { impl_->antialias = enabled; } void Canvas::set_image_interpolation(Image_Interpolation_Mode mode) { switch (mode) { case Image_Interpolation_Mode::Nearest: impl_->ctx.set_pattern_quality(BL_PATTERN_QUALITY_NEAREST); break; case Image_Interpolation_Mode::Bilinear: impl_->ctx.set_pattern_quality(BL_PATTERN_QUALITY_BILINEAR); break; case Image_Interpolation_Mode::Bicubic: impl_->ctx.set_pattern_quality(BL_PATTERN_QUALITY_BILINEAR); break; } } void Canvas::translate(double x, double y) { impl_->ctx.translate(x, y); } void Canvas::scale(double x, double y) { impl_->ctx.scale(x, y); } void Canvas::rotate(double radians) { impl_->ctx.rotate(radians); } void Canvas::transform(double m00, double m01, double m10, double m11, double m20, double m21) { impl_->ctx.apply_transform(BLMatrix2D(m00, m01, m10, m11, m20, m21)); } void Canvas::reset_transform() { impl_->ctx.reset_transform(); } void Canvas::set_clip_rect(const RectF& rect) { impl_->ctx.clip_to_rect(Convert::to_bl(rect)); } void Canvas::reset_clip() { impl_->ctx.restore_clipping(); } void Canvas::draw_line(PointF first, PointF second) { impl_->apply_pen(); impl_->ctx.stroke_line(first.x, first.y, second.x, second.y); } void Canvas::draw_polyline(std::span points) { if (points.size() < 2) return; impl_->apply_pen(); BLPath path; path.move_to(points[0].x, points[0].y); for (std::size_t i = 1; i < points.size(); ++i) path.line_to(points[i].x, points[i].y); impl_->ctx.stroke_path(path); } void Canvas::draw_point(PointF point) { impl_->apply_pen(); impl_->ctx.stroke_line(point.x, point.y, point.x + 0.001, point.y); } void Canvas::draw_points(std::span points) { for (auto& p : points) draw_point(p); } void Canvas::draw_rect(const RectF& rect) { impl_->apply_pen(); auto r = Convert::to_bl(rect); impl_->ctx.stroke_rect(r); } void Canvas::fill_rect(const RectF& rect) { impl_->apply_brush(); auto r = Convert::to_bl(rect); impl_->ctx.fill_rect(r); } void Canvas::draw_ellipse(PointF center, double radius_x, double radius_y) { impl_->apply_pen(); BLPath path; path.add_ellipse(BLEllipse(center.x, center.y, radius_x, radius_y)); impl_->ctx.stroke_path(path); } void Canvas::draw_polygon(std::span points, bool fill) { if (points.size() < 3) return; BLPath path; path.move_to(points[0].x, points[0].y); for (std::size_t i = 1; i < points.size(); ++i) path.line_to(points[i].x, points[i].y); path.close(); if (fill) { impl_->apply_brush(); impl_->ctx.fill_path(path); } impl_->apply_pen(); impl_->ctx.stroke_path(path); } void Canvas::fill_polygon(std::span points) { draw_polygon(points, true); } void Canvas::draw_image(const RectF& target, const Image& image) { auto src = RectF{0, 0, static_cast(image.width()), static_cast(image.height())}; draw_image(target, image, src); } void Canvas::draw_image(const RectF& target, const Image& image, const RectF& source) { if (image.empty()) return; impl_->ctx.blit_image( BLRect(target.x, target.y, target.width, target.height), image.impl_->image, BLRectI(static_cast(source.x), static_cast(source.y), static_cast(source.width), static_cast(source.height))); } void Canvas::draw_text(PointF position, std::string_view text) { if (text.empty() || !can_use_font(impl_->current_font)) return; BLFont bl_font; bl_font.create_from_face(impl_->current_font.face->impl_->face, static_cast(impl_->current_font.size)); const BLFontMetrics& metrics = bl_font.metrics(); impl_->apply_text_fill(); impl_->ctx.set_stroke_style(BLRgba(0.0, 0.0, 0.0, 0.0)); impl_->ctx.fill_utf8_text(BLPoint(position.x, position.y + metrics.ascent), bl_font, text.data(), text.size()); } void Canvas::draw_text(const RectF& rect, Text_Align align, std::string_view text) { if (text.empty() || !can_use_font(impl_->current_font)) return; auto metrics = measure_text(text); double x = rect.x; double y = rect.y; if ((align & Text_Align::Horizontal_Center) == Text_Align::Horizontal_Center) x += (rect.width - metrics.width) / 2.0; else if ((align & Text_Align::Right) == Text_Align::Right) x += rect.width - metrics.width; if ((align & Text_Align::Vertical_Center) == Text_Align::Vertical_Center) y += (rect.height - metrics.height) / 2.0; else if ((align & Text_Align::Bottom) == Text_Align::Bottom) y += rect.height - metrics.height; draw_text(PointF{x, y}, text); } Text_Metrics Canvas::measure_text(std::string_view text) const { return measure_text(impl_->current_font, text); } Text_Metrics Canvas::measure_text(const Font& font, std::string_view text) const { Font resolved = resolve_font(font); if (text.empty() || !can_use_font(resolved)) return {}; BLFont bl_font; bl_font.create_from_face(resolved.face->impl_->face, static_cast(resolved.size)); BLGlyphBuffer gb; gb.set_utf8_text(text.data(), text.size()); bl_font.shape(gb); BLTextMetrics tm{}; bl_font.get_text_metrics(gb, tm); const BLFontMetrics& fm = bl_font.metrics(); return { static_cast(tm.bounding_box.x1 - tm.bounding_box.x0), static_cast(fm.ascent + fm.descent), static_cast(fm.ascent), static_cast(fm.descent), static_cast(fm.line_gap) }; } Image& Canvas::image() const { return impl_->image; } Canvas::Font_Diagnostics Canvas::default_font_diagnostics() { Font_Diagnostics diagnostics; auto face = default_font_face(); diagnostics.source = default_font_source_storage(); diagnostics.face_valid = face && face->valid(); Font font; font.face = face; font.size = 12.0; diagnostics.font_valid = can_use_font(font); if (!diagnostics.font_valid) return diagnostics; Image image(1, 1); image.fill(Color::transparent()); Canvas canvas(image); canvas.set_font(font); Text_Metrics metrics = canvas.measure_text("0123456789 ABC \xE4\xB8\xAD\xE6\x96\x87"); diagnostics.test_width = metrics.width; diagnostics.test_height = metrics.height; return diagnostics; } void Canvas::diagnose_default_font_once() { static bool done = false; if (done) return; done = true; Font_Diagnostics diagnostics = default_font_diagnostics(); std::cout << "Renderive default font: source=" << (diagnostics.source.empty() ? "" : diagnostics.source) << " face_valid=" << (diagnostics.face_valid ? "true" : "false") << " font_valid=" << (diagnostics.font_valid ? "true" : "false") << " test_width=" << diagnostics.test_width << " test_height=" << diagnostics.test_height << std::endl; } } // namespace renderive