From 0cdc7c1cf79342c4edb27fd5bba53be71c22828c Mon Sep 17 00:00:00 2001 From: Giacomo Debidda Date: Mon, 25 Jan 2021 20:54:24 +0100 Subject: [PATCH] WIP add more pango examples --- build.zig | 2 + examples/pango_shape.zig | 187 +++++++++++++++++++ examples/pango_twisted.zig | 366 +++++++++++++++++++++++++++++++++++++ 3 files changed, 555 insertions(+) create mode 100644 examples/pango_shape.zig create mode 100644 examples/pango_twisted.zig diff --git a/build.zig b/build.zig index fcab090..6ded9c4 100644 --- a/build.zig +++ b/build.zig @@ -25,7 +25,9 @@ const EXAMPLES = [_][]const u8{ "image_pattern", "mask", "multi_segment_caps", + "pango_shape", "pango_simple", + "pango_twisted", "rounded_rectangle", "save_and_restore", "set_line_cap", diff --git a/examples/pango_shape.zig b/examples/pango_shape.zig new file mode 100644 index 0000000..3770065 --- /dev/null +++ b/examples/pango_shape.zig @@ -0,0 +1,187 @@ +//! Example code to show how to use pangocairo to render arbitrary shapes inside +//! a text layout, positioned by Pango. +//! https://gitlab.gnome.org/GNOME/pango/-/blob/master/examples/cairoshape.c +const std = @import("std"); +const log = std.log; +const pi = std.math.pi; +const cos = std.math.cos; +const cairo = @import("cairo"); +const pc = @import("pangocairo"); + +const BULLET = "•"; + +const text = + \\The GNOME project provides two things: + \\ + \\ • The GNOME desktop environment + \\ • The GNOME development platform + \\ • Planet GNOME +; + +const path = + \\M 86.068,1 C 61.466,0 56.851,35.041 70.691,35.041 C 84.529,35.041 110.671,0 86.068,0 z + \\M 45.217,30.699 C 52.586,31.149 60.671,2.577 46.821,4.374 C 32.976,6.171 37.845,30.249 45.217,30.699 z + \\M 11.445,48.453 C 16.686,46.146 12.12,23.581 3.208,29.735 C -5.7,35.89 6.204,50.759 11.445,48.453 z + \\M 26.212,36.642 C 32.451,35.37 32.793,9.778 21.667,14.369 C 10.539,18.961 19.978,37.916 26.212,36.642 L 26.212,36.642 z + \\M 58.791,93.913 C 59.898,102.367 52.589,106.542 45.431,101.092 C 22.644,83.743 83.16,75.088 79.171,51.386 C 75.86,31.712 15.495,37.769 8.621,68.553 C 3.968,89.374 27.774,118.26 52.614,118.26 C 64.834,118.26 78.929,107.226 81.566,93.248 C 83.58,82.589 57.867,86.86 58.791,93.913 L 58.791,93.913 z +; + +// I don't know if the multiline string works or I have to use a single string +// const path = "M 86.068,1 C 61.466,0 56.851,35.041 70.691,35.041 C 84.529,35.041 110.671,0 86.068,0 z M 45.217,30.699 C 52.586,31.149 60.671,2.577 46.821,4.374 C 32.976,6.171 37.845,30.249 45.217,30.699 z M 11.445,48.453 C 16.686,46.146 12.12,23.581 3.208,29.735 C -5.7,35.89 6.204,50.759 11.445,48.453 z M 26.212,36.642 C 32.451,35.37 32.793,9.778 21.667,14.369 C 10.539,18.961 19.978,37.916 26.212,36.642 L 26.212,36.642 z M 58.791,93.913 C 59.898,102.367 52.589,106.542 45.431,101.092 C 22.644,83.743 83.16,75.088 79.171,51.386 C 75.86,31.712 15.495,37.769 8.621,68.553 C 3.968,89.374 27.774,118.26 52.614,118.26 C 64.834,118.26 78.929,107.226 81.566,93.248 C 83.58,82.589 57.867,86.86 58.791,93.913 L 58.791,93.913 z "; + +// TODO: add render method? (it would replace the miniSvgRender function) +const MiniSvg = struct { + width: f64, + height: f64, + path: []const u8, +}; + +var gnome_foot_logo = MiniSvg{ + .width = 96.2152, + .height = 118.26, + .path = path, +}; + +// TODO: miniSvgRender +fn miniSvgRender(shape: *MiniSvg, cr: ?cairo.CContext, do_path: c_int) void { + log.debug("miniSvgRender", .{}); + if (do_path == 1) { + log.debug("do_path is TRUE", .{}); + } else { + log.debug("do_path is FALSE", .{}); + } +} + +fn shapeRenderer(cr: ?cairo.CContext, attr: pc.PangoAttrShape, do_path: c_int, data: ?*c_void) callconv(.C) void { + log.debug("shapeRenderer", .{}); + if (do_path == 1) { + log.debug("do_path is TRUE", .{}); + } else { + log.debug("do_path is FALSE", .{}); + } + // log.debug("attr {}", .{ attr }); + // log.debug("PangoAttrShape {}", .{ attr.* }); + log.debug("PangoAttrShape.data {}", .{attr.*.data}); + // TODO: how to cast PangoAttrShape.data into *MiniSvg ? The alignment + // doesn't match. + // const shape = @ptrCast(*MiniSvg, attr.*.data.?); + // const shape = (MiniSvg *) attr.data; // @ptrCast a MiniSvg ? + + log.debug("ink_rect.width {}", .{attr.*.ink_rect.width}); + log.debug("ink_rect.height {}", .{attr.*.ink_rect.height}); + + // scale_x = (double) attr->ink_rect.width / (PANGO_SCALE * shape->width ); + // scale_y = (double) attr->ink_rect.height / (PANGO_SCALE * shape->height); + + // cr.relMoveTo((double) attr->ink_rect.x / PANGO_SCALE, (double) attr->ink_rect.y / PANGO_SCALE); + // cr.scale(scale_x, scale_y); + // miniSvgRender(shape, cr, do_path); + // this should probably be: + // miniSvg.render(cr, do_path); + + // @panic("TODO: remove me when shapeRenderer is correct"); +} + +// TODO: how to return a ?*c_void type? +// fn pangoAttrDataCopyFunc(user_data: ?*const c_void) callconv(.C) ?*c_void { +// log.debug("pangoAttrDataCopyFunc", .{}); +// log.debug("user_data {}", .{user_data}); +// return @ptrCast(?*c_void, user_data); +// } +const pangoAttrDataCopyFunc: ?pc.PangoAttrDataCopyFunc = null; + +fn gDestroyNotify(data: ?*c_void) callconv(.C) void { + log.debug("gDestroyNotify", .{}); + // log.debug("data {}", .{data.?.*}); // data.? is opaque +} +// const gDestroyNotify: ?pc.GDestroyNotify = null; + +fn getLayout(cr: *cairo.Context) !pc.Layout { + var ink_rect = try pc.Rectangle.new(1 * pc.SCALE, -11 * pc.SCALE, 8 * pc.SCALE, 10 * pc.SCALE); + var logical_rect = try pc.Rectangle.new(0 * pc.SCALE, -12 * pc.SCALE, 10 * pc.SCALE, 12 * pc.SCALE); + + var layout = try pc.Layout.create(cr); + + var ctx = try layout.getContext(); + ctx.setShapeRenderer(shapeRenderer, null, null); + layout.setText(text); + + var attrs = try pc.AttrList.new(); + defer attrs.destroy(); + + var idx = std.mem.indexOf(u8, text[0..], BULLET); + while (idx != null) { + const str = text[idx.?..]; + // log.debug("idx {} str.len {}", .{idx, str.len}); + var attr = try pc.Attribute.newShapeWithData(MiniSvg, &ink_rect, &logical_rect, &gnome_foot_logo, pangoAttrDataCopyFunc, gDestroyNotify); + // TODO: move this ptrCast and intCast to pango.zig + // https://stackoverflow.com/questions/18659120/subtracting-two-strings-in-c + const str_addr = @intCast(c_uint, @ptrToInt(str.ptr)); + const text_addr = @intCast(c_uint, @ptrToInt(text)); + attr.c_ptr.*.start_index = str_addr - text_addr; + attr.c_ptr.*.end_index = attr.c_ptr.*.start_index + @intCast(c_uint, BULLET.len); + attrs.insert(&attr); + + // find index for next iteration + const i_rel = std.mem.indexOf(u8, text[idx.? + BULLET.len ..], BULLET); + if (i_rel == null) { + idx = null; + } else { + idx = idx.? + i_rel.? + 1; + } + } + + layout.setAttributes(&attrs); + return layout; +} + +fn drawText(cr: *cairo.Context, width: ?f64, height: ?f64) !pc.Size { + log.info("=== drawText ===", .{}); + var layout = try getLayout(cr); + defer layout.destroy(); + + var size = pc.Size{ .width = 0.0, .height = 0.0 }; + const margin = 10.0; + + if ((width != null) or (height != null)) { + const original_size = layout.getPixelSize(); + if (width != null) { + size.width = original_size.width + 2.0 * margin; + } + if (height != null) { + size.height = original_size.height + 2.0 * margin; + } + } + + cr.moveTo(margin, margin); + layout.show(cr); + + return size; +} + +pub fn main() !void { + // First create and use a 0x0 surface, to measure how large the final + // surface needs to be. + var surface = try cairo.Surface.image(0.0, 0.0); + defer surface.destroy(); + + var cr = try cairo.Context.create(&surface); + defer cr.destroy(); + + const size = try drawText(&cr, null, null); + + // TODO: Now create the final surface and draw to it. Reuse surface and cr? + var surface2 = try cairo.Surface.image(@floatToInt(u16, size.width), @floatToInt(u16, size.height)); + defer surface2.destroy(); + + var cr2 = try cairo.Context.create(&surface); + defer cr2.destroy(); + + cr2.setSourceRgb(1.0, 1.0, 1.0); // white + cr2.paint(); + + cr2.setSourceRgb(0.0, 0.0, 0.5); + const size2 = try drawText(&cr2, size.width, size.height); + + _ = surface2.writeToPng("examples/generated/pango_shape.png"); +} diff --git a/examples/pango_twisted.zig b/examples/pango_twisted.zig new file mode 100644 index 0000000..00fece9 --- /dev/null +++ b/examples/pango_twisted.zig @@ -0,0 +1,366 @@ +//! Example code to show how to use pangocairo to render text projected on a path. +//! https://gitlab.gnome.org/GNOME/pango/-/blob/master/examples/cairotwisted.c +const std = @import("std"); +const log = std.log; +const pi = std.math.pi; +const cos = std.math.cos; +const cairo = @import("cairo"); +const pc = @import("pangocairo"); + +/// A fancy cairo_stroke that draws points and control points, and connects them +/// together. +fn _fancyCairoStroke(cr: *cairo.Context, should_preserve: bool) !void { + cr.save(); + + cr.setSourceRgb(1.0, 0.0, 0.0); // red + const line_width = cr.getLineWidth(); + var path = try cr.copyPath(); + defer path.destroy(); + + cr.newPath(); + cr.save(); + cr.setLineWidth(line_width / 3.0); + + var dash = [_]f64{ 10.0, 10.0 }; // ink, skip + const offset = 0.0; + cr.setDash(dash[0..], offset); + + var iter = path.iterator(); + while (iter.next()) |pdt| { + switch (pdt) { + cairo.PathDataType.MoveTo, cairo.PathDataType.LineTo => { + cr.moveTo(iter.data[1].point.x, iter.data[1].point.y); + }, + cairo.PathDataType.CurveTo => { + cr.lineTo(iter.data[1].point.x, iter.data[1].point.y); + cr.moveTo(iter.data[2].point.x, iter.data[2].point.y); + cr.lineTo(iter.data[3].point.x, iter.data[3].point.y); + }, + cairo.PathDataType.ClosePath => {}, + } + } + + cr.stroke(); + cr.restore(); + + cr.save(); + cr.setLineWidth(line_width * 4.0); + cr.setLineCap(cairo.LineCap.Round); + + iter = path.iterator(); + while (iter.next()) |pdt| { + switch (pdt) { + cairo.PathDataType.MoveTo => { + cr.moveTo(iter.data[1].point.x, iter.data[1].point.y); + }, + cairo.PathDataType.LineTo => { + cr.relLineTo(0.0, 0.0); + cr.moveTo(iter.data[1].point.x, iter.data[1].point.y); + }, + cairo.PathDataType.CurveTo => { + cr.relLineTo(0.0, 0.0); + cr.moveTo(iter.data[1].point.x, iter.data[1].point.y); + cr.relLineTo(0.0, 0.0); + cr.moveTo(iter.data[2].point.x, iter.data[2].point.y); + cr.relLineTo(0.0, 0.0); + cr.moveTo(iter.data[3].point.x, iter.data[3].point.y); + }, + cairo.PathDataType.ClosePath => { + cr.relLineTo(0.0, 0.0); + }, + } + } + + cr.relLineTo(0.0, 0.0); + cr.stroke(); + cr.restore(); + + iter = path.iterator(); + while (iter.next()) |pdt| { + switch (pdt) { + cairo.PathDataType.MoveTo => { + cr.moveTo(iter.data[1].point.x, iter.data[1].point.y); + }, + cairo.PathDataType.LineTo => { + cr.lineTo(iter.data[1].point.x, iter.data[1].point.y); + }, + cairo.PathDataType.CurveTo => { + cr.curveTo( + iter.data[1].point.x, + iter.data[1].point.y, + iter.data[2].point.x, + iter.data[2].point.y, + iter.data[3].point.x, + iter.data[3].point.y, + ); + }, + cairo.PathDataType.ClosePath => { + cr.closePath(); + }, + } + } + cr.stroke(); + + if (should_preserve) { + cr.appendPath(path); + } + + cr.restore(); +} + +fn fancyCairoStroke(cr: *cairo.Context) !void { + try _fancyCairoStroke(cr, false); +} + +fn fancyCairoStrokePreserve(cr: *cairo.Context) !void { + try _fancyCairoStroke(cr, true); +} + +// DONE +fn drawText(cr: *cairo.Context, x: f64, y: f64, font: []const u8, text: []const u8) !void { + var font_options = try cairo.FontOptions.create(); + defer font_options.destroy(); + + _ = try cairo.FontOptions.status(font_options.c_ptr); + + font_options.setHintStyle(cairo.HintStyle.None); + font_options.setHintMetrics(cairo.HintMetrics.Off); + + cr.setFontOptions(font_options); + + var layout = try pc.Layout.create(cr); + defer layout.destroy(); + + var desc = try pc.FontDescription.fromString(font); + defer desc.destroy(); + + layout.setFontDescription(desc); + layout.setText(text); + + var line = try layout.getLineReadonly(0); + cr.moveTo(x, y); + pc.linePath(cr, line); +} + +const ParametrizedPath = struct { + path: *cairo.Path, + parametrization: []f64, +}; + +/// Euclidean distance between two points. +fn twoPointsDistance(a: *cairo.CUnionPathData, b: *cairo.CUnionPathData) f64 { + const dx = b.point.x - a.point.x; + const dy = b.point.y - a.point.y; + return std.math.sqrt(dx * dx + dy * dy); +} + +fn curveLength( + x0: f64, + y0: f64, + x1: f64, + y1: f64, + x2: f64, + y2: f64, + x3: f64, + y3: f64, +) !f64 { + var c_ptr = try cairo.image_surface.create(cairo.image_surface.Format.A8, 0, 0); + var surface = cairo.Surface{ .surface = c_ptr }; + defer surface.destroy(); + + var cr = try cairo.Context.create(&surface); + defer cr.destroy(); + + cr.moveTo(x0, y0); + cr.curveTo(x1, y1, x2, y2, x3, y3); + + var length: f64 = 0.0; + var current_point: cairo.CUnionPathData = undefined; + var path = try cr.copyPathFlat(); + var iter = path.iterator(); + while (iter.next()) |pdt| { + switch (pdt) { + cairo.PathDataType.MoveTo => { + current_point = iter.data[1]; + }, + cairo.PathDataType.LineTo => { + length += twoPointsDistance(¤t_point, &iter.data[1]); + current_point = iter.data[1]; + }, + cairo.PathDataType.CurveTo, cairo.PathDataType.ClosePath => unreachable, + } + } + return length; +} + +/// Compute parametrization info. That is, for each part of the cairo path, +/// tag it with its length. The caller owns the returned slice. +fn parametrizePath(allocator: *std.mem.Allocator, path: *cairo.Path) ![]f64 { + const parametrization = try allocator.alloc(f64, @intCast(usize, path.c_ptr.num_data)); + // log.debug("slice.len {} {}", .{slice.len, @typeInfo(@TypeOf(slice))}); + + var last_move_to: cairo.CUnionPathData = undefined; + var current_point: cairo.CUnionPathData = undefined; + + var iter = path.iterator(); + for (parametrization) |p, i| { + // log.debug("param {}", .{i}); + const pdt = iter.next(); + // log.debug("PathDataType {}", .{pdt}); + if (pdt != null) { + switch (pdt.?) { + cairo.PathDataType.MoveTo => { + last_move_to = iter.data[1]; + current_point = iter.data[1]; + }, + cairo.PathDataType.ClosePath => { + // Make it look like it's a line_to to last_move_to + @panic("TODO"); + // data = (&last_move_to) - 1; + // G_GNUC_FALLTHROUGH; + // log.debug("last_move_to {} {}", .{&last_move_to, last_move_to}); + }, + cairo.PathDataType.LineTo => { + const d = twoPointsDistance(¤t_point, &iter.data[1]); + parametrization[i] = d; + // log.debug("parametrization[{}] {}", .{i, parametrization[i]}); + current_point = iter.data[1]; + }, + cairo.PathDataType.CurveTo => { + // naive curve-length, treating bezier as three line segments: + // parametrization[i] = two_points_distance (¤t_point, &data[1]) + // + two_points_distance (&data[1], &data[2]) + // + two_points_distance (&data[2], &data[3]); + // + const curve_length = try curveLength( + current_point.point.x, + current_point.point.y, + iter.data[1].point.x, + iter.data[1].point.y, + iter.data[2].point.x, + iter.data[2].point.y, + iter.data[3].point.x, + iter.data[3].point.y, + ); + log.debug("curve_length {}", .{curve_length}); + current_point = iter.data[3]; + }, + } + } + } + return parametrization; +} + +// TODO: I don't understand what to do here... review original C code. +fn pointOnPath(param_path: *ParametrizedPath) void { + var last_move_to: cairo.CUnionPathData = undefined; + var current_point: cairo.CUnionPathData = undefined; + + var iter = param_path.path.iterator(); + while (iter.next()) |pdt| { + // log.debug("pdt {}", .{pdt}); + switch (pdt) { + cairo.PathDataType.MoveTo => { + last_move_to = iter.data[1]; + current_point = iter.data[1]; + }, + cairo.PathDataType.LineTo => { + current_point = iter.data[1]; + }, + cairo.PathDataType.CurveTo => { + current_point = iter.data[3]; + }, + cairo.PathDataType.ClosePath => {}, + } + } +} + +/// Project the current path of cr onto the provided path. +fn mapPathOnto(allocator: *std.mem.Allocator, cr: *cairo.Context, path: *cairo.Path) !void { + log.debug("TODO mapPathOnto", .{}); + var parametrization = try parametrizePath(allocator, path); + defer allocator.free(parametrization); + + var param_path = ParametrizedPath{ + .path = path, + .parametrization = parametrization, + }; + // log.debug("path parametrized in {} (segments?)", .{param_path.parametrization.len}); + + var current_path = try cr.copyPath(); + defer current_path.destroy(); + + cr.newPath(); + log.debug("transform current_path {}", .{current_path}); + pointOnPath(¶m_path); + // transform_path (current_path, (transform_point_func_t) point_on_path, ¶m); + cr.appendPath(current_path); +} + +// DONE +fn drawTwisted(cr: *cairo.Context, x: f64, y: f64, font: []const u8, text: []const u8) !void { + cr.save(); + + // decrease tolerance a bit, since it's going to be magnified + cr.setTolerance(0.01); + + // Using cairo_copy_path() here shows our deficiency in handling Bezier + // curves, specially around sharper curves. + // Using cairo_copy_path_flat() on the other hand, magnifies the flattening + // error with large off-path values. We decreased tolerance for that reason. + // Increase tolerance to see that artifact. + var path = try cr.copyPathFlat(); + // var path = try cr.copyPath(); + defer path.destroy(); + + cr.newPath(); + + var allocator = std.testing.allocator; + try drawText(cr, x, y + 100.0, font, text); // TODO: remove 100.0 from y. It's just for now to see the text + try mapPathOnto(allocator, cr, &path); + + cr.fillPreserve(); + + cr.save(); + cr.setSourceRgb(0.1, 0.1, 0.1); + cr.stroke(); + cr.restore(); + + cr.restore(); +} + +fn drawDream(cr: *cairo.Context) !void { + cr.moveTo(50, 650); + cr.relLineTo(250, 50); + cr.relCurveTo(250, 50, 600, -50, 600, -250); + cr.relCurveTo(0, -400, -300, -100, -800, -300); + cr.setLineWidth(1.5); + cr.setSourceRgba(0.3, 0.3, 1.0, 0.3); + try fancyCairoStrokePreserve(cr); + try drawTwisted(cr, 0.0, 0.0, "Serif 72", "It was a dream... Oh Just a dream..."); +} + +fn drawWow(cr: *cairo.Context) !void { + cr.moveTo(400, 780); + cr.relCurveTo(50, -50, 150, -50, 200, 0); + cr.scale(1.0, 2.0); + cr.setLineWidth(2.0); + cr.setSourceRgba(0.3, 1.0, 0.3, 1.0); + try fancyCairoStrokePreserve(cr); + try drawTwisted(cr, -20.0, -150.0, "Serif 60", "WOW!"); +} + +pub fn main() !void { + var surface = try cairo.Surface.image(1000, 800); + defer surface.destroy(); + + var cr = try cairo.Context.create(&surface); + defer cr.destroy(); + + cr.setSourceRgb(1.0, 1.0, 1.0); // white + cr.paint(); + + try drawDream(&cr); + try drawWow(&cr); + _ = surface.writeToPng("examples/generated/pango_twisted.png"); +}