DIY 一个用于显示 PDF 页面的 Clutter Actor
最近打算基于 Clutter 做一个 PDF 文档的演示工具,首先要解决在 Clutter 场景中渲染 PDF 页面的问题。可以通过 Poppler 库将 PDF 页面转换为 Cairo 图形,进而利用 Clutter 的 CairoTexture Actor 进行 Cairo 图形渲染解决这一问题 [1]。本文将上述过程中的一些细节(例如尺寸与位置的控制)封装为一个便于使用的 Clutter Actor。
ClutterActor 子类的声明
下面的代码声明了 CkdPage 类,继承自 ClutterActor 类。相关知识可参考 GObject 子类化文档 [2, 3]。
#ifndef CKD_PAGE_H #define CKD_PAGE_H #include <glib.h> #include <clutter/clutter.h> #define CKD_TYPE_PAGE (ckd_page_get_type ()) #define CKD_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CKD_TYPE_PAGE, CkdPage)) #define CKD_IS_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CKD_TYPE_PAGE)) #define CKD_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CKD_TYPE_PAGE, CkdPageClass)) #define CKD_IS_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CKD_TYPE_PAGE)) #define CKD_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CKD_TYPE_PAGE, CkdPageClass)) typedef struct _CkdPage CkdPage; typedef struct _CkdPageClass CkdPageClass; struct _CkdPage { ClutterActor parent_instance; }; struct _CkdPageClass { ClutterActorClass parent_class; }; GType ckd_page_get_type (void); /* Public functons */ ClutterActor *ckd_page_new (PopplerPage *page); #endif
属性
CkdPage 类有 3 个属性:
typedef struct _CkdPagePriv { PopplerPage *pdf_page; ClutterActor *cairo_texture; gfloat scale; } CkdPagePriv; enum { PROP_0, PROP_PDF_PAGE };
其中 pdf_page 是 PDF 页面实例,cairo_texture 为 Clutter 的 Cairo 纹理实例,scale 是 cairo_texture 相对 pdf_page 的缩放比例。
上述的三个属性中,只有 pdf_page 可被外部访问,为了实现这一点,需要以下代码:
static void ckd_page_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) { CkdPage *self = CKD_PAGE (obj); CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self); gdouble w, h; switch (prop_id) { case PROP_PDF_PAGE: priv->pdf_page = g_value_get_pointer (value); if (priv->cairo_texture) { clutter_actor_destroy (priv->cairo_texture); } poppler_page_get_size (priv->pdf_page, &w, &h); priv->cairo_texture = clutter_cairo_texture_new (w, h); clutter_actor_set_parent (priv->cairo_texture, CLUTTER_ACTOR (self)); clutter_cairo_texture_set_auto_resize (CLUTTER_CAIRO_TEXTURE (priv->cairo_texture), TRUE); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); break; } } static void ckd_page_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) { CkdPage *self = CKD_PAGE (obj); CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self); switch (prop_id) { case PROP_PDF_PAGE: g_value_set_pointer (value, priv->pdf_page); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); break; } } static void ckd_page_class_init (CkdPageClass *klass) { g_type_class_add_private (klass, sizeof (CkdPagePriv)); GObjectClass *base_class = G_OBJECT_CLASS (klass); base_class->set_property = ckd_page_set_property; base_class->get_property = ckd_page_get_property; GParamSpec *pspec; pspec = g_param_spec_pointer ("pdf-page", "PDF Page", "A PDF Page which to be convert to CairoTexture", G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT); g_object_class_install_property (base_class, PROP_PDF_PAGE, pspec); }
其中的要点是每次为 CkdPage 实例设置 pdf_page 属性时,都要释放已有的 cairo_texture 并重新分配。还要将 cairo_texture 的尺寸设置为自动与其所包含的 Cairo 图形(Cairo Surface) 的尺寸同步,如果 cairo_texture 与 Cairo 图形的尺寸不一致,会导致 Cairo 图形显示不完全或过小。另外也不要忘了将 CkdPage 实例设为 cairo_texture 的父 Actor。
如果看不明白上述代码,那说明你需要了解有关 GObject 子类属性方面的知识,可参考 [4]。
ClutterActor 子类的内存管理
在谈论 ClutterActor 子类的内存管理之前,我们需要先搞明白 ClutterActor 类是如何管理内存的。但是,在搞明白 ClutterActor 类的内存管理之前,你需要明白 GObject 的内存管理方法,可能你需要阅读 [5]。
通常,要释放一个 ClutterActor 占用的资源,可使用 clutter_actor_destroy()。虽然使用 g_object_unref() 有时也可以成功,但是几率非常之小。这是因为在 Clutter 中,各个 Actor 之间均存在某种联系,构成一个有向无环图,因此存在许多的引用计数环(Reference Count Cycle),若想释放某个 Actor 的资源,那么必须要打破与它相关的引用计数环。clutter_actor_destroy() 可以借助 g_object_run_dispose() 解决这一问题。如果一个 Actor 示例的父 Actor 是容器,那么 g_object_run_dispose() 会调用 ClutterActor 的 dispose() 方法,后者又会调用 clutter_container_remove_actor() 释放子 Actor 的资源。对于非容器 Actor,ClutterActor 的 dispose() 方法中会调用 clutter_actor_unparent() 解除它与父 Actor 之间的联系,并消减其引用计数。在 clutter_actor_destroy() 快要结束时,Actor 的引用计数会变为 0,于是其资源便会被释放。
上述过程可以简述为:
clutter_actor_destroy (self) { g_object_ref (self); clutter_actor_dispose (self) { if (self 的父 Actor 是容器) clutter_container_remove_actor() 将 slef 从其父 Actor 中移除; else clutter_actor_unparent() 将 self 从其父 Actor 中脱离,并消减 self 的引用计数,此时 self 的引用计数为 1; } g_object_unref (self) 释放 self 自身,即调用 clutter_actor_finalize(); }
但是,上述过程中还有一个有趣的环节,那就是在 clutter_actor_dispose() 的最后会发射一个 "destroy" 信号,该信号默认的处理器是 ClutterActor 类的 destroy() 虚函数。也就是说如果我们在一个 ClutterActor 的子类中提供了 destroy() 函数的实现,那么该函数会在 clutter_actor_dispose() 的执行中被调用。
在 [6] 给出的示例程序中指出,对于自定义的复合 Actor,应当提供 destroy() 函数。那么在这个 destroy() 函数中应当处理哪些事情?自然是释放复合 Actor 所包含的那些 Actor。对于 CkdPage 这个 Actor 而言,要释放的 Actor 是 cairo_texture。因此 CkdPage 的 destroy() 函数的实现如下:
static void ckd_page_destroy (ClutterActor *self) { CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self); if (priv->cairo_texture) { clutter_actor_destroy (priv->cairo_texture); priv->cairo_texture = NULL; } if (CLUTTER_ACTOR_CLASS (ckd_page_parent_class)->destroy) CLUTTER_ACTOR_CLASS (ckd_page_parent_class)->destroy (self); }
由于 ckd_page_destroy() 是 ClutterActor 的虚函数的具体实现,那么需要在 ckd_page_class_init() 中将其作为 destroy 的实例,如下:
static void ckd_page_class_init (CkdPageClass *klass) { /* 省略了前面已出现的部分代码 */ ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); actor_class->destroy = ckd_page_destroy; }
将自定义的 Actor 嵌入场景
上面的内容虽然是与 Clutter 自定义 Actor 相关,但实质上它们是 GObject 层面的知识。真正与 Clutter 相关的是我们自定义的 Actor 如何嵌入到 Clutter 场景并显示。
为了便于实现自定义的 Actor 嵌入场景,Clutter 为 ClutterActor 类提供了三个虚函数,即 get_preferred_width(), get_preferred_height() 以及 allocate(),每个 ClutterActor 的子类通常需要实现它们。但是如果你想显式的控制自定义 Actor 的尺寸与位置,像 ClutterRecangle、ClutterTexture 这样的 Actor,就没必要实现 get_preferred_width() 和 get_preferred_height(),或者自定义的 Actor 既非容器也非复合对象,那么就没必要实现 allocate()。
显然,CkdPage 是一个复合对象,因为它包含了一个 Cairo 纹理 Actor,同时我们也期望它能够自适应父 Actor 的变化,例如拖曳窗口时,Cairo 纹理 Actor 能自适应的放大或缩小。所以,CkdPage 必须实现上述的三个虚函数。
首先来看 get_preferred_width() 与 get_preferred_height() 的实现:
static void ckd_page_get_size (PopplerPage *page, gfloat *width, gfloat *height, gfloat *aspect) { gdouble w, h, a; poppler_page_get_size (page, &w, &h); if (h < G_MINFLOAT) h = G_MINFLOAT; a = w / h; if (width) *width = w; if (height) *height = h; if (aspect) *aspect = a; } static void ckd_page_get_parent_size (ClutterActor *actor, gfloat *width, gfloat *height, gfloat *aspect) { gfloat w, h, a; ClutterActor *parent = clutter_actor_get_parent (actor); ClutterActorBox box; clutter_actor_get_size (parent, &w, &h); if (h < G_MINFLOAT) h = G_MINFLOAT; a = w / h; if (width) *width = w; if (height) *height = h; if (aspect) *aspect = a; } static void ckd_page_get_preferred_width (ClutterActor *actor, gfloat for_height, gfloat *min_width, gfloat *natural_width) { CkdPage *self = CKD_PAGE (actor); CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self); gfloat w, h, a; ckd_page_get_parent_size (actor, &w, &h, &a); gfloat pw, pa; ckd_page_get_size (priv->pdf_page, &pw, NULL, &pa); if (a > pa) w = h * pa; *min_width = 0; *natural_width = w; priv->scale = w / pw; } static void ckd_page_get_preferred_height (ClutterActor *actor, gfloat for_width, gfloat *min_height, gfloat *natural_height) { CkdPage *self = CKD_PAGE (actor); CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self); gfloat w, h, a; ckd_page_get_parent_size (actor, &w, &h, &a); gfloat ph, pa; ckd_page_get_size (priv->pdf_page, NULL, &ph, &pa); if (a <= pa) h = w / pa; *min_height = 0; *natural_height = h; priv->scale = h / ph; } static void ckd_page_class_init (CkdPageClass *klass) { /* ==> 此处省略了前面已出现的部分代码 */ ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); /* ==> 此处省略了前面已出现的部分代码 */ actor_class->get_preferred_height = ckd_page_get_preferred_height; actor_class->get_preferred_width = ckd_page_get_preferred_width; }
由于 ckd_page_get_preferred_width() 与 ckd_page_get_preferred_height() 在实现上比较相似,因此仅以 ckd_page_get_preferred_width() 为例讲述 CkdPage 在 get_preferred_width() 实现中所体现的逻辑。
在 ckd_page_get_preferred_width(),首先调用自定义的 ckd_page_get_parent_size() 函数(基于 clutter_actor_get_size() 实现)获取 CkdPage Actor 实例的父 Actor 实例的尺寸,并调用自定义的 ckd_page_get_size() 函数,获取 PDF 页面(CkdPage 的 pdf_page 属性)尺寸,然后根据父 Actor 实例的尺寸以及 PDF 页面宽高比确定 CkdPage 实例的宽度,并记录该宽度相对 PDF 页面尺寸的缩放因子(CkdPage 的 scale 属性)。
上面虽然回答了 get_preferred_width / height() 的实现,但是还没有指出这两个函数究竟有什么用。其实很简单,当你将 CkdPage 实例添加到某个容器实例中的时候,这个容器实例便会调用 CkdPage 的这两个函数为 CkdPage 实例打造一个包围盒,并根据这个包围盒来在 Clutter 场景中为 CkdPage 实例分配一定的空间。
如果说 get_preferred_width / height() 是面向上级 Actor 的,那么 allocate() 则是面向下级的。例如 CkdPage 实例包含着一个 Cairo 纹理实例(ClutterCairoTexture),在 CkdPage 实例的上层 Actor 为 CkdPage 实例安排好了空间,那么 CkdPage 实例便应当在这个空间内为 Cairo 纹理实例分配一定的空间,而且分配方法也不固定,需要结合实际需要。例如可以在 CkdPage 的 allocate() 中通过调用 Cairo 纹理 Actor 的 get_preferred_width / height() 为其分配空间;也可以直接将 CkdPage 实例所得空间赋予 Cairo 纹理实例,下面的 ckd_page_allocate() 便是这样做的。
static void ckd_page_allocate (ClutterActor *actor, const ClutterActorBox *box, ClutterAllocationFlags flags) { CkdPage *self = CKD_PAGE (actor); CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self); CLUTTER_ACTOR_CLASS (ckd_page_parent_class)->allocate (actor, box, flags); gfloat w = clutter_actor_box_get_width (box); gfloat h = clutter_actor_box_get_height (box); ClutterActorBox page_box = { 0, 0, w, h }; clutter_actor_allocate (priv->cairo_texture, &page_box, flags); } static void ckd_page_class_init (CkdPageClass *klass) { /* ==> 此处省略了前面已出现的部分代码 */ ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); /* ==> 此处省略了前面已出现的部分代码 */ actor_class->allocate = ckd_page_allocate; }
渲染
对于自定义的 Actor,若在场景中显示它,那么还需要实现 paint() 虚函数,除非这个 Actor 可被父类的 paint() 函数渲染。例如,你定义的 Actor 是 ClutterRectangle 的子类,而你在这个子类中所添加的功能与渲染无关,那么就没必要再覆盖 ClutterRectangle 所提供的 paint() 函数了。
CkdPage 是个复合 Actor,其父类是 ClutterActor,没有提供 paint() 虚函数的实现,因此我们必须自行实现。如下:
static void ckd_page_paint (ClutterActor *actor) { CkdPage *self = CKD_PAGE (actor); CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self); clutter_actor_paint (priv->cairo_texture); } static void ckd_page_class_init (CkdPageClass *klass) { /* ==> 此处省略了前面已出现的部分代码 */ ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); /* ==> 此处省略了前面已出现的部分代码 */ actor_class->paint = ckd_page_paint; }
这个 paint() 的实现是非常简单的,只是调用了 ClutterCairoTexture 所实现的 paint() 函数。
真正的复杂的是在前面设置 CkdPage 的 pdf_page 属性之处,即:
static void ckd_page_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) { /* ==> 此处省略前面出现的部分代码 */ switch (prop_id) { case PROP_PDF_PAGE: priv->pdf_page = g_value_get_pointer (value); if (priv->cairo_texture) { clutter_actor_destroy (priv->cairo_texture); } poppler_page_get_size (priv->pdf_page, &w, &h); priv->cairo_texture = clutter_cairo_texture_new (w, h); clutter_actor_set_parent (priv->cairo_texture, CLUTTER_ACTOR (self)); clutter_cairo_texture_set_auto_resize (CLUTTER_CAIRO_TEXTURE (priv->cairo_texture), TRUE); break; /* ==> 此处省略前面出现的部分代码 */ }
上述代码表示每次为 CkdPage 实例设置 pdf_page 属性时,都要创建一个 ClutterCairoTexture 实例(即 CkdPage 实例的 cairo_texture 属性)。从 Clutter 1.7.6 版本开始,ClutterCairoTexture 实例的渲染改为通过 "draw" 信号实现。因此在上面代码中,每次创建 ClutterCairoTexture 实例之后,需要将它与 "draw" 信号连接,并实现相应的信号处理函数,具体如下:
static gboolean ckd_page_render (ClutterCairoTexture *actor, cairo_t *cr, gpointer user_data) { CkdPage *self = CKD_PAGE (user_data); CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self); clutter_cairo_texture_clear (actor); if (priv->pdf_page) { cairo_scale (cr, priv->scale, priv->scale); poppler_page_render (priv->pdf_page, cr); } return TRUE; } static void ckd_page_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) { /* ==> 此处省略前面出现的部分代码 */ switch (prop_id) { case PROP_PDF_PAGE: priv->pdf_page = g_value_get_pointer (value); if (priv->cairo_texture) { clutter_actor_destroy (priv->cairo_texture); } poppler_page_get_size (priv->pdf_page, &w, &h); priv->cairo_texture = clutter_cairo_texture_new (w, h); clutter_actor_set_parent (priv->cairo_texture, CLUTTER_ACTOR (self)); clutter_cairo_texture_set_auto_resize (CLUTTER_CAIRO_TEXTURE (priv->cairo_texture), TRUE); /* ==> 将 ClutterCairoTexture 实例与 "draw" 信号连接 */ g_signal_connect (priv->cairo_texture, "draw", G_CALLBACK (ckd_page_render), self); break; /* ==> 此处省略前面出现的部分代码 */ }
值得注意的是,在 "draw" 信号处理器,即 ckd_page_render() 中,需要使用 ckd_page_get_preferred_width / height() 函数中记录的 CkdPage 实例的 scale 属性对 Cairo 图形进行缩放,以充满 CkdPage 在场景中所占据的空间。
Demo
上文中所实现的 CkdPage Actor 的全部源码见 ckd-page.tar.gz。
下面是一个 demo 程序。
#include <poppler.h> #include "ckd-page.h" int main(int argc, char *argv[]) { if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS) return 1; /* 使用 Poppler 库函数获得 PDF 页面,需要自己设定 PDF 的 uri 路径 */ gchar *file = "file:///path/to/your/pdf/file"; PopplerDocument *doc = poppler_document_new_from_file (file, NULL, NULL); PopplerPage *page = poppler_document_get_page (doc, 0); /* 创建场景 */ ClutterColor stage_color = { 0x21, 0x43, 0x5e, 0xff }; ClutterActor *stage = clutter_stage_get_default (); clutter_stage_set_title (CLUTTER_STAGE (stage), "CkdPage Demo!"); clutter_actor_set_size (stage, 400, 400); clutter_stage_set_color (CLUTTER_STAGE (stage), &stage_color); clutter_stage_set_user_resizable (CLUTTER_STAGE (stage), TRUE); /* 创建 CkdPage 实例 */ ClutterActor *ckd_page = ckd_page_new (page); clutter_container_add_actor (CLUTTER_CONTAINER(stage), ckd_page); /* 使用约束机制将 CkdPage 实例居于场景中央 */ ClutterConstraint *x_center, *y_center; x_center = clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, 0.5); y_center = clutter_align_constraint_new (stage, CLUTTER_ALIGN_Y_AXIS, 0.5); clutter_actor_add_constraint (ckd_page, x_center); clutter_actor_add_constraint (ckd_page, y_center); g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL); clutter_actor_show_all (stage); clutter_main (); /* 释放资源,虽然本例中没必要,只是表达了这样一种行为 :) */ clutter_actor_destroy (ckd_page); g_object_unref (G_OBJECT (page)); g_object_unref (G_OBJECT (doc)); return 0; }
参考资料
[3] GObject 常用的宏
转载时,希望不要链接文中图片,另外请保留本文原始出处:http://garfileo.is-programmer.com