1 /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 // SPDX-FileCopyrightText: 2010 litl, LLC.
4 
5 #include <config.h>
6 
7 #include <cairo.h>
8 #include <girepository.h>
9 #include <glib.h>
10 
11 #include <js/Array.h>
12 #include <js/CallArgs.h>
13 #include <js/Class.h>
14 #include <js/PropertyDescriptor.h>  // for JSPROP_READONLY
15 #include <js/PropertySpec.h>
16 #include <js/RootingAPI.h>
17 #include <js/TypeDecls.h>
18 #include <js/Value.h>
19 #include <js/ValueArray.h>
20 #include <jsapi.h>  // for JS_GetPrivate, JS_GetClass, ...
21 
22 #include "gi/arg-inl.h"
23 #include "gi/arg.h"
24 #include "gi/cwrapper.h"
25 #include "gi/foreign.h"
26 #include "gjs/enum-utils.h"
27 #include "gjs/jsapi-class.h"
28 #include "gjs/jsapi-util-args.h"
29 #include "gjs/jsapi-util.h"
30 #include "gjs/macros.h"
31 #include "modules/cairo-private.h"
32 
33 /* Properties */
34 // clang-format off
35 const JSPropertySpec CairoSurface::proto_props[] = {
36     JS_STRING_SYM_PS(toStringTag, "Surface", JSPROP_READONLY),
37     JS_PS_END};
38 // clang-format on
39 
40 /* Methods */
41 GJS_JSAPI_RETURN_CONVENTION
42 static bool
writeToPNG_func(JSContext * context,unsigned argc,JS::Value * vp)43 writeToPNG_func(JSContext *context,
44                 unsigned   argc,
45                 JS::Value *vp)
46 {
47     GJS_GET_THIS(context, argc, vp, argv, obj);
48     GjsAutoChar filename;
49 
50     if (!gjs_parse_call_args(context, "writeToPNG", argv, "F",
51                              "filename", &filename))
52         return false;
53 
54     cairo_surface_t* surface = CairoSurface::for_js(context, obj);
55     if (!surface)
56         return false;
57 
58     cairo_surface_write_to_png(surface, filename);
59     if (!gjs_cairo_check_status(context, cairo_surface_status(surface),
60                                 "surface"))
61         return false;
62     argv.rval().setUndefined();
63     return true;
64 }
65 
66 GJS_JSAPI_RETURN_CONVENTION
getType_func(JSContext * context,unsigned argc,JS::Value * vp)67 bool CairoSurface::getType_func(JSContext* context, unsigned argc,
68                                 JS::Value* vp) {
69     GJS_GET_THIS(context, argc, vp, rec, obj);
70     cairo_surface_type_t type;
71 
72     if (argc > 1) {
73         gjs_throw(context, "Surface.getType() takes no arguments");
74         return false;
75     }
76 
77     cairo_surface_t* surface = CairoSurface::for_js(context, obj);
78     if (!surface)
79         return false;
80 
81     type = cairo_surface_get_type(surface);
82     if (!gjs_cairo_check_status(context, cairo_surface_status(surface),
83                                 "surface"))
84         return false;
85 
86     rec.rval().setInt32(type);
87     return true;
88 }
89 
90 GJS_JSAPI_RETURN_CONVENTION
setDeviceOffset_func(JSContext * cx,unsigned argc,JS::Value * vp)91 static bool setDeviceOffset_func(JSContext* cx, unsigned argc, JS::Value* vp) {
92     GJS_GET_THIS(cx, argc, vp, args, obj);
93     double x_offset = 0.0, y_offset = 0.0;
94     if (!gjs_parse_call_args(cx, "setDeviceOffset", args, "ff", "x_offset",
95                              &x_offset, "y_offset", &y_offset))
96         return false;
97 
98     cairo_surface_t* surface = CairoSurface::for_js(cx, obj);
99     if (!surface)
100         return false;
101 
102     cairo_surface_set_device_offset(surface, x_offset, y_offset);
103     if (!gjs_cairo_check_status(cx, cairo_surface_status(surface), "surface"))
104         return false;
105 
106     args.rval().setUndefined();
107     return true;
108 }
109 
110 GJS_JSAPI_RETURN_CONVENTION
getDeviceOffset_func(JSContext * cx,unsigned argc,JS::Value * vp)111 static bool getDeviceOffset_func(JSContext* cx, unsigned argc, JS::Value* vp) {
112     GJS_GET_THIS(cx, argc, vp, args, obj);
113 
114     if (argc > 0) {
115         gjs_throw(cx, "Surface.getDeviceOffset() takes no arguments");
116         return false;
117     }
118 
119     cairo_surface_t* surface = CairoSurface::for_js(cx, obj);
120     if (!surface)
121         return false;
122 
123     double x_offset, y_offset;
124     cairo_surface_get_device_offset(surface, &x_offset, &y_offset);
125     // cannot error
126 
127     JS::RootedValueArray<2> elements(cx);
128     elements[0].setNumber(x_offset);
129     elements[1].setNumber(y_offset);
130     JS::RootedObject retval(cx, JS::NewArrayObject(cx, elements));
131     if (!retval)
132         return false;
133 
134     args.rval().setObject(*retval);
135     return true;
136 }
137 
138 GJS_JSAPI_RETURN_CONVENTION
setDeviceScale_func(JSContext * cx,unsigned argc,JS::Value * vp)139 static bool setDeviceScale_func(JSContext* cx, unsigned argc, JS::Value* vp) {
140     GJS_GET_THIS(cx, argc, vp, args, obj);
141     double x_scale = 1.0, y_scale = 1.0;
142 
143     if (!gjs_parse_call_args(cx, "setDeviceScale", args, "ff", "x_scale",
144                              &x_scale, "y_scale", &y_scale))
145         return false;
146 
147     cairo_surface_t* surface = CairoSurface::for_js(cx, obj);
148     if (!surface)
149         return false;
150 
151     cairo_surface_set_device_scale(surface, x_scale, y_scale);
152     if (!gjs_cairo_check_status(cx, cairo_surface_status(surface),
153                                 "surface"))
154         return false;
155 
156     args.rval().setUndefined();
157     return true;
158 }
159 
160 GJS_JSAPI_RETURN_CONVENTION
getDeviceScale_func(JSContext * cx,unsigned argc,JS::Value * vp)161 static bool getDeviceScale_func(JSContext* cx, unsigned argc, JS::Value* vp) {
162     GJS_GET_THIS(cx, argc, vp, args, obj);
163 
164     if (argc > 0) {
165         gjs_throw(cx, "Surface.getDeviceScale() takes no arguments");
166         return false;
167     }
168 
169     cairo_surface_t* surface = CairoSurface::for_js(cx, obj);
170     if (!surface)
171         return false;
172 
173     double x_scale, y_scale;
174     cairo_surface_get_device_scale(surface, &x_scale, &y_scale);
175     // cannot error
176 
177     JS::RootedValueArray<2> elements(cx);
178     elements[0].setNumber(x_scale);
179     elements[1].setNumber(y_scale);
180     JS::RootedObject retval(cx, JS::NewArrayObject(cx, elements));
181     if (!retval)
182         return false;
183 
184     args.rval().setObject(*retval);
185     return true;
186 }
187 
188 const JSFunctionSpec CairoSurface::proto_funcs[] = {
189     // flush
190     // getContent
191     // getFontOptions
192     JS_FN("getType", getType_func, 0, 0),
193     // markDirty
194     // markDirtyRectangle
195     JS_FN("setDeviceOffset", setDeviceOffset_func, 2, 0),
196     JS_FN("getDeviceOffset", getDeviceOffset_func, 0, 0),
197     JS_FN("setDeviceScale", setDeviceScale_func, 2, 0),
198     JS_FN("getDeviceScale", getDeviceScale_func, 0, 0),
199     // setFallbackResolution
200     // getFallbackResolution
201     // copyPage
202     // showPage
203     // hasShowTextGlyphs
204     JS_FN("writeToPNG", writeToPNG_func, 0, 0), JS_FS_END};
205 
206 /* Public API */
207 
208 /**
209  * CairoSurface::finalize_impl:
210  * @fop: the free op
211  * @surface: the pointer to finalize
212  *
213  * Destroys the resources associated with a surface wrapper.
214  *
215  * This is mainly used for subclasses.
216  */
finalize_impl(JSFreeOp *,cairo_surface_t * surface)217 void CairoSurface::finalize_impl(JSFreeOp*, cairo_surface_t* surface) {
218     if (!surface)
219         return;
220     cairo_surface_destroy(surface);
221 }
222 
223 /**
224  * CairoSurface::from_c_ptr:
225  * @context: the context
226  * @surface: cairo_surface to attach to the object
227  *
228  * Constructs a surface wrapper given cairo surface.
229  * A reference to @surface will be taken.
230  *
231  */
from_c_ptr(JSContext * context,cairo_surface_t * surface)232 JSObject* CairoSurface::from_c_ptr(JSContext* context,
233                                    cairo_surface_t* surface) {
234     g_return_val_if_fail(context, nullptr);
235     g_return_val_if_fail(surface, nullptr);
236 
237     cairo_surface_type_t type = cairo_surface_get_type(surface);
238     if (type == CAIRO_SURFACE_TYPE_IMAGE)
239         return CairoImageSurface::from_c_ptr(context, surface);
240     if (type == CAIRO_SURFACE_TYPE_PDF)
241         return CairoPDFSurface::from_c_ptr(context, surface);
242     if (type == CAIRO_SURFACE_TYPE_PS)
243         return CairoPSSurface::from_c_ptr(context, surface);
244     if (type == CAIRO_SURFACE_TYPE_SVG)
245         return CairoSVGSurface::from_c_ptr(context, surface);
246     return CairoSurface::CWrapper::from_c_ptr(context, surface);
247 }
248 
249 /**
250  * CairoSurface::for_js:
251  * @cx: the context
252  * @surface_wrapper: surface wrapper
253  *
254  * Overrides NativeObject::for_js().
255  *
256  * Returns: the surface attached to the wrapper.
257  */
for_js(JSContext * cx,JS::HandleObject surface_wrapper)258 cairo_surface_t* CairoSurface::for_js(JSContext* cx,
259                                       JS::HandleObject surface_wrapper) {
260     g_return_val_if_fail(cx, nullptr);
261     g_return_val_if_fail(surface_wrapper, nullptr);
262 
263     JS::RootedObject proto(cx, CairoSurface::prototype(cx));
264 
265     bool is_surface_subclass = false;
266     if (!gjs_object_in_prototype_chain(cx, proto, surface_wrapper,
267                                        &is_surface_subclass))
268         return nullptr;
269     if (!is_surface_subclass) {
270         gjs_throw(cx, "Expected Cairo.Surface but got %s",
271                   JS_GetClass(surface_wrapper)->name);
272         return nullptr;
273     }
274 
275     return static_cast<cairo_surface_t*>(JS_GetPrivate(surface_wrapper));
276 }
277 
surface_to_g_argument(JSContext * context,JS::Value value,const char * arg_name,GjsArgumentType argument_type,GITransfer transfer,GjsArgumentFlags flags,GIArgument * arg)278 [[nodiscard]] static bool surface_to_g_argument(
279     JSContext* context, JS::Value value, const char* arg_name,
280     GjsArgumentType argument_type, GITransfer transfer, GjsArgumentFlags flags,
281     GIArgument* arg) {
282     if (value.isNull()) {
283         if (!(flags & GjsArgumentFlags::MAY_BE_NULL)) {
284             GjsAutoChar display_name =
285                 gjs_argument_display_name(arg_name, argument_type);
286             gjs_throw(context, "%s may not be null", display_name.get());
287             return false;
288         }
289 
290         gjs_arg_unset<void*>(arg);
291         return true;
292     }
293 
294     if (!value.isObject()) {
295         GjsAutoChar display_name =
296             gjs_argument_display_name(arg_name, argument_type);
297         gjs_throw(context, "%s is not a Cairo.Surface", display_name.get());
298         return false;
299     }
300 
301     JS::RootedObject surface_wrapper(context, &value.toObject());
302     cairo_surface_t* s = CairoSurface::for_js(context, surface_wrapper);
303     if (!s)
304         return false;
305     if (transfer == GI_TRANSFER_EVERYTHING)
306         cairo_surface_destroy(s);
307 
308     gjs_arg_set(arg, s);
309     return true;
310 }
311 
312 GJS_JSAPI_RETURN_CONVENTION
surface_from_g_argument(JSContext * cx,JS::MutableHandleValue value_p,GIArgument * arg)313 static bool surface_from_g_argument(JSContext* cx,
314                                     JS::MutableHandleValue value_p,
315                                     GIArgument* arg) {
316     JSObject* obj =
317         CairoSurface::from_c_ptr(cx, gjs_arg_get<cairo_surface_t*>(arg));
318     if (!obj)
319         return false;
320 
321     value_p.setObject(*obj);
322     return true;
323 }
324 
surface_release_argument(JSContext *,GITransfer transfer,GIArgument * arg)325 static bool surface_release_argument(JSContext*, GITransfer transfer,
326                                      GIArgument* arg) {
327     if (transfer != GI_TRANSFER_NOTHING)
328         cairo_surface_destroy(gjs_arg_get<cairo_surface_t*>(arg));
329     return true;
330 }
331 
gjs_cairo_surface_init(void)332 void gjs_cairo_surface_init(void) {
333     static GjsForeignInfo foreign_info = {surface_to_g_argument,
334                                           surface_from_g_argument,
335                                           surface_release_argument};
336     gjs_struct_foreign_register("cairo", "Surface", &foreign_info);
337 }
338