• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * This file defines SkpDebugPlayer, a class which loads a SKP or MSKP file and draws it
3  * to an SkSurface with annotation, and detailed playback controls. It holds as many DebugCanvases
4  * as there are frames in the file.
5  *
6  * It also defines emscripten bindings for SkpDebugPlayer and other classes necessary to us it.
7  *
8  * Copyright 2019 Google LLC
9  *
10  * Use of this source code is governed by a BSD-style license that can be
11  * found in the LICENSE file.
12  */
13 
14 #include "include/codec/SkCodec.h"
15 #include "include/core/SkImage.h"
16 #include "include/core/SkPicture.h"
17 #include "include/core/SkString.h"
18 #include "include/core/SkSurface.h"
19 #include "include/docs/SkMultiPictureDocument.h"
20 #include "include/encode/SkPngEncoder.h"
21 #include "src/base/SkBase64.h"
22 #include "src/core/SkPicturePriv.h"
23 #include "src/ports/SkTypeface_FreeType.h"
24 #include "src/utils/SkJSONWriter.h"
25 #include "tools/SkSharingProc.h"
26 #include "tools/UrlDataManager.h"
27 #include "tools/debugger/DebugCanvas.h"
28 #include "tools/debugger/DebugLayerManager.h"
29 #include "tools/debugger/DrawCommand.h"
30 
31 #include <memory>
32 #include <string>
33 #include <string_view>
34 #include <vector>
35 #include <map>
36 #include <emscripten.h>
37 #include <emscripten/bind.h>
38 
39 #ifdef CK_ENABLE_WEBGL
40 #include "include/gpu/ganesh/GrBackendSurface.h"
41 #include "include/gpu/ganesh/GrDirectContext.h"
42 #include "include/gpu/ganesh/SkSurfaceGanesh.h"
43 #include "include/gpu/ganesh/gl/GrGLInterface.h"
44 #include "include/gpu/ganesh/gl/GrGLTypes.h"
45 
46 #include <GL/gl.h>
47 #include <emscripten/html5.h>
48 #endif
49 
50 #include "modules/canvaskit/WasmCommon.h"
51 
52 // file signature for SkMultiPictureDocument
53 // TODO(nifong): make public and include from SkMultiPictureDocument.h
54 static constexpr char kMultiMagic[] = "Skia Multi-Picture Doc\n\n";
55 
MinVersion()56 uint32_t MinVersion() { return SkPicturePriv::kMin_Version; }
57 
58 struct ImageInfoNoColorspace {
59     int width;
60     int height;
61     SkColorType colorType;
62     SkAlphaType alphaType;
63 };
64 
65 // TODO(kjlubick) Should this handle colorspace
toImageInfoNoColorspace(const SkImageInfo & ii)66 ImageInfoNoColorspace toImageInfoNoColorspace(const SkImageInfo& ii) {
67   return (ImageInfoNoColorspace){ii.width(), ii.height(), ii.colorType(), ii.alphaType()};
68 }
69 
deserializeImage(sk_sp<SkData> data,std::optional<SkAlphaType>,void *)70 static sk_sp<SkImage> deserializeImage(sk_sp<SkData> data, std::optional<SkAlphaType>, void*) {
71   std::unique_ptr<SkCodec> codec = DecodeImageData(std::move(data));
72   if (!codec) {
73     SkDebugf("Could not decode an image\n");
74     return nullptr;
75   }
76   sk_sp<SkImage> img = std::get<0>(codec->getImage());
77   if (!img) {
78     SkDebugf("Could not make an image from a codec\n");
79     return nullptr;
80   }
81   return img;
82 }
83 
register_typeface()84 static void register_typeface() {
85   static SkOnce once;
86   once([] {
87     SkTypeface::Register(SkTypeface_FreeType::FactoryId,
88                          SkTypeface_FreeType::MakeFromStream );
89   });
90 }
91 
92 class SkpDebugPlayer {
93   public:
SkpDebugPlayer()94     SkpDebugPlayer() :
95     udm(UrlDataManager(SkString("/data"))){}
96 
97     /* loadSkp deserializes a skp file that has been copied into the shared WASM memory.
98      * cptr - a pointer to the data to deserialize.
99      * length - length of the data in bytes.
100      * The caller must allocate the memory with M._malloc where M is the wasm module in javascript
101      * and copy the data into M.buffer at the pointer returned by malloc.
102      *
103      * uintptr_t is used here because emscripten will not allow binding of functions with pointers
104      * to primitive types. We can instead pass a number and cast it to whatever kind of
105      * pointer we're expecting.
106      *
107      * Returns an error string which is populated in the case that the file cannot be read.
108      */
loadSkp(uintptr_t cptr,int length)109     std::string loadSkp(uintptr_t cptr, int length) {
110       const uint8_t* data = reinterpret_cast<const uint8_t*>(cptr);
111       // Both traditional and multi-frame skp files have a magic word
112       SkMemoryStream stream(data, length);
113       SkDebugf("make stream at %p, with %d bytes\n",data, length);
114       const bool isMulti = memcmp(data, kMultiMagic, sizeof(kMultiMagic) - 1) == 0;
115 
116 
117       if (isMulti) {
118         SkDebugf("Try reading as a multi-frame skp\n");
119         const auto& error = loadMultiFrame(&stream);
120         if (!error.empty()) { return error; }
121       } else {
122         SkDebugf("Try reading as single-frame skp\n");
123         // TODO(nifong): Rely on SkPicture's return errors once it provides some.
124         std::unique_ptr<DebugCanvas> canvas = loadSingleFrame(&stream);
125         if (!canvas) {
126           return "Error loading single frame";
127         }
128         frames.push_back(std::move(canvas));
129       }
130       return "";
131     }
132 
133     /* drawTo asks the debug canvas to draw from the beginning of the picture
134      * to the given command and flush the canvas.
135      */
drawTo(SkSurface * surface,int32_t index)136     void drawTo(SkSurface* surface, int32_t index) {
137       // Set the command within the frame or layer event being drawn.
138       if (fInspectedLayer >= 0) {
139         fLayerManager->setCommand(fInspectedLayer, fp, index);
140       } else {
141         index = constrainFrameCommand(index);
142       }
143 
144       auto* canvas = surface->getCanvas();
145       canvas->clear(SK_ColorTRANSPARENT);
146       if (fInspectedLayer >= 0) {
147         // when it's a layer event we're viewing, we use the layer manager to render it.
148         fLayerManager->drawLayerEventTo(surface, fInspectedLayer, fp);
149       } else {
150         // otherwise, its a frame at the top level.
151         frames[fp]->drawTo(surface->getCanvas(), index);
152       }
153 #ifdef CK_ENABLE_WEBGL
154       skgpu::ganesh::Flush(surface);
155 #endif
156     }
157 
158     // Draws to the end of the current frame.
draw(SkSurface * surface)159     void draw(SkSurface* surface) {
160       auto* canvas = surface->getCanvas();
161       canvas->clear(SK_ColorTRANSPARENT);
162       frames[fp]->draw(surface->getCanvas());
163 #ifdef CK_ENABLE_WEBGL
164       skgpu::ganesh::Flush(surface);
165 #endif
166     }
167 
168     // Gets the bounds for the given frame
169     // (or layer update, assuming there is one at that frame for fInspectedLayer)
getBoundsForFrame(int32_t frame)170     const SkIRect getBoundsForFrame(int32_t frame) {
171       if (fInspectedLayer < 0) {
172         return fBoundsArray[frame];
173       }
174       auto summary = fLayerManager->event(fInspectedLayer, fp);
175       return SkIRect::MakeWH(summary.layerWidth, summary.layerHeight);
176     }
177 
178     // Gets the bounds for the current frame
getBounds()179     const SkIRect getBounds() {
180       return getBoundsForFrame(fp);
181     }
182 
183     // returns the debugcanvas of the current frame, or the current draw event when inspecting
184     // a layer.
visibleCanvas()185     DebugCanvas* visibleCanvas() {
186       if (fInspectedLayer >=0) {
187         return fLayerManager->getEventDebugCanvas(fInspectedLayer, fp);
188       } else {
189         return frames[fp].get();
190       }
191     }
192 
193     // The following three operations apply to every debugcanvas because they are overdraw features.
194     // There is only one toggle for them on the app, they are global settings.
195     // However, there's not a simple way to make the debugcanvases pull settings from a central
196     // location so we set it on all of them at once.
setOverdrawVis(bool on)197     void setOverdrawVis(bool on) {
198       for (size_t i=0; i < frames.size(); i++) {
199         frames[i]->setOverdrawViz(on);
200       }
201       fLayerManager->setOverdrawViz(on);
202     }
setGpuOpBounds(bool on)203     void setGpuOpBounds(bool on) {
204       for (size_t i=0; i < frames.size(); i++) {
205         frames[i]->setDrawGpuOpBounds(on);
206       }
207       fLayerManager->setDrawGpuOpBounds(on);
208     }
setClipVizColor(JSColor color)209     void setClipVizColor(JSColor color) {
210       for (size_t i=0; i < frames.size(); i++) {
211         frames[i]->setClipVizColor(SkColor(color));
212       }
213       fLayerManager->setClipVizColor(SkColor(color));
214     }
setAndroidClipViz(bool on)215     void setAndroidClipViz(bool on) {
216       for (size_t i=0; i < frames.size(); i++) {
217         frames[i]->setAndroidClipViz(on);
218       }
219       // doesn't matter in layers
220     }
setOriginVisible(bool on)221     void setOriginVisible(bool on) {
222       for (size_t i=0; i < frames.size(); i++) {
223         frames[i]->setOriginVisible(on);
224       }
225     }
226     // The two operations below only apply to the current frame, because they concern the command
227     // list, which is unique to each frame.
deleteCommand(int index)228     void deleteCommand(int index) {
229       visibleCanvas()->deleteDrawCommandAt(index);
230     }
setCommandVisibility(int index,bool visible)231     void setCommandVisibility(int index, bool visible) {
232       visibleCanvas()->toggleCommand(index, visible);
233     }
getSize() const234     int getSize() const {
235       if (fInspectedLayer >=0) {
236         return fLayerManager->event(fInspectedLayer, fp).commandCount;
237       } else {
238         return frames[fp]->getSize();
239       }
240     }
getFrameCount() const241     int getFrameCount() const {
242       return frames.size();
243     }
244 
245     // Return the command list in JSON representation as a string
jsonCommandList(sk_sp<SkSurface> surface)246     std::string jsonCommandList(sk_sp<SkSurface> surface) {
247       SkDynamicMemoryWStream stream;
248       SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast);
249       writer.beginObject(); // root
250       visibleCanvas()->toJSON(writer, udm, surface->getCanvas());
251       writer.endObject(); // root
252       writer.flush();
253       auto skdata = stream.detachAsData();
254       // Convert skdata to string_view, which accepts a length
255       std::string_view data_view(reinterpret_cast<const char*>(skdata->data()), skdata->size());
256       // and string_view to string, which emscripten understands.
257       return std::string(data_view);
258     }
259 
260     // Gets the clip and matrix of the last command drawn
lastCommandInfo()261     std::string lastCommandInfo() {
262       SkM44 vm = visibleCanvas()->getCurrentMatrix();
263       SkIRect clip = visibleCanvas()->getCurrentClip();
264 
265       SkDynamicMemoryWStream stream;
266       SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast);
267       writer.beginObject(); // root
268 
269       writer.appendName("ViewMatrix");
270       DrawCommand::MakeJsonMatrix44(writer, vm);
271       writer.appendName("ClipRect");
272       DrawCommand::MakeJsonIRect(writer, clip);
273 
274       writer.endObject(); // root
275       writer.flush();
276       auto skdata = stream.detachAsData();
277       // Convert skdata to string_view, which accepts a length
278       std::string_view data_view(reinterpret_cast<const char*>(skdata->data()), skdata->size());
279       // and string_view to string, which emscripten understands.
280       return std::string(data_view);
281     }
282 
changeFrame(int index)283     void changeFrame(int index) {
284       fp = index;
285     }
286 
287     // Return the png file at the requested index in
288     // the skp file's vector of shared images. this is the set of images referred to by the
289     // filenames like "\\1" in DrawImage commands.
290     // Return type is the PNG data as a base64 encoded string with prepended URI.
getImageResource(int index)291     std::string getImageResource(int index) {
292       sk_sp<SkData> pngData = SkPngEncoder::Encode(nullptr, fImages[index].get(), {});
293       size_t len = SkBase64::EncodedSize(pngData->size());
294       SkString dst;
295       dst.resize(len);
296       SkBase64::Encode(pngData->data(), pngData->size(), dst.data());
297       dst.prepend("data:image/png;base64,");
298       return std::string(dst.c_str());
299     }
300 
getImageCount()301     int getImageCount() {
302       return fImages.size();
303     }
304 
305     // Get the image info of one of the resource images.
getImageInfo(int index)306     ImageInfoNoColorspace getImageInfo(int index) {
307       return toImageInfoNoColorspace(fImages[index]->imageInfo());
308     }
309 
310     // return data on which commands each image is used in.
311     // (frame, -1) returns info for the given frame,
312     // (frame, nodeid) return info for a layer update
313     // { imageid: [commandid, commandid, ...], ... }
imageUseInfo(int framenumber,int nodeid)314     JSObject imageUseInfo(int framenumber, int nodeid) {
315       JSObject result = emscripten::val::object();
316       DebugCanvas* debugCanvas = frames[framenumber].get();
317       if (nodeid >= 0) {
318         debugCanvas = fLayerManager->getEventDebugCanvas(nodeid, framenumber);
319       }
320       const auto& map = debugCanvas->getImageIdToCommandMap(udm);
321       for (auto it = map.begin(); it != map.end(); ++it) {
322         JSArray list = emscripten::val::array();
323         for (const int commandId : it->second) {
324           list.call<void>("push", commandId);
325         }
326         result.set(std::to_string(it->first), list);
327       }
328       return result;
329     }
330 
331     // Return information on every layer (offscreeen buffer) that is available for drawing at
332     // the current frame.
getLayerSummariesJs()333     JSArray getLayerSummariesJs() {
334       JSArray result = emscripten::val::array();
335       for (auto summary : fLayerManager->summarizeLayers(fp)) {
336           result.call<void>("push", summary);
337       }
338       return result;
339     }
340 
getLayerKeys()341     JSArray getLayerKeys() {
342       JSArray result = emscripten::val::array();
343       for (auto key : fLayerManager->getKeys()) {
344         JSObject item = emscripten::val::object();
345         item.set("frame", key.frame);
346         item.set("nodeId", key.nodeId);
347         result.call<void>("push", item);
348       }
349       return result;
350     }
351 
352     // When set to a valid layer index, causes this class to playback the layer draw event at nodeId
353     // on frame fp. No validation of nodeId or fp is performed, this must be valid values obtained
354     // from either fLayerManager.listNodesForFrame or fLayerManager.summarizeEvents
355     // Set to -1 to return to viewing the top level animation
setInspectedLayer(int nodeId)356     void setInspectedLayer(int nodeId) {
357       fInspectedLayer = nodeId;
358     }
359 
360     // Finds a command that left the given pixel in it's current state.
361     // Note that this method may fail to find the absolute last command that leaves a pixel
362     // the given color, but there is probably only one candidate in most cases, and the log(n)
363     // makes it worth it.
findCommandByPixel(SkSurface * surface,int x,int y,int commandIndex)364     int findCommandByPixel(SkSurface* surface, int x, int y, int commandIndex) {
365       // What color is the pixel now?
366       SkColor finalColor = evaluateCommandColor(surface, commandIndex, x, y);
367 
368       int lowerBound = 0;
369       int upperBound = commandIndex;
370 
371       while (upperBound - lowerBound > 1) {
372         int command = (upperBound - lowerBound) / 2 + lowerBound;
373         auto c = evaluateCommandColor(surface, command, x, y);
374         if (c == finalColor) {
375           upperBound = command;
376         } else {
377           lowerBound = command;
378         }
379       }
380       // clean up after side effects
381       drawTo(surface, commandIndex);
382       return upperBound;
383     }
384 
385   private:
386 
387       // Helper for findCommandByPixel.
388       // Has side effect of flushing to surface.
389       // TODO(nifong) eliminate side effect.
evaluateCommandColor(SkSurface * surface,int command,int x,int y)390       SkColor evaluateCommandColor(SkSurface* surface, int command, int x, int y) {
391         drawTo(surface, command);
392 
393         SkColor c;
394         SkImageInfo info = SkImageInfo::Make(1, 1, kRGBA_8888_SkColorType, kOpaque_SkAlphaType);
395         SkPixmap pixmap(info, &c, 4);
396         surface->readPixels(pixmap, x, y);
397         return c;
398       }
399 
400       // Loads a single frame (traditional) skp file from the provided data stream and returns
401       // a newly allocated DebugCanvas initialized with the SkPicture that was in the file.
loadSingleFrame(SkMemoryStream * stream)402       std::unique_ptr<DebugCanvas> loadSingleFrame(SkMemoryStream* stream) {
403         register_typeface();
404         SkDeserialProcs procs;
405         procs.fImageDataProc = deserializeImage;
406         // note overloaded = operator that actually does a move
407         sk_sp<SkPicture> picture = SkPicture::MakeFromStream(stream, &procs);
408         if (!picture) {
409           SkDebugf("Unable to deserialze frame.\n");
410           return nullptr;
411         }
412         SkDebugf("Parsed SKP file.\n");
413         // Make debug canvas using bounds from SkPicture
414         fBoundsArray.push_back(picture->cullRect().roundOut());
415         std::unique_ptr<DebugCanvas> debugCanvas = std::make_unique<DebugCanvas>(fBoundsArray.back());
416 
417         // Only draw picture to the debug canvas once.
418         debugCanvas->drawPicture(picture);
419         return debugCanvas;
420       }
421 
loadMultiFrame(SkMemoryStream * stream)422       std::string loadMultiFrame(SkMemoryStream* stream) {
423         register_typeface();
424         // Attempt to deserialize with an image sharing serial proc.
425         auto deserialContext = std::make_unique<SkSharingDeserialContext>();
426         SkDeserialProcs procs;
427         procs.fImageProc = SkSharingDeserialContext::deserializeImage;
428         procs.fImageCtx = deserialContext.get();
429 
430         int page_count = SkMultiPictureDocument::ReadPageCount(stream);
431         if (!page_count) {
432           // MSKP's have a version separate from the SKP subpictures they contain.
433           return "Not a MultiPictureDocument, MultiPictureDocument file version too old, or MultiPictureDocument contained 0 frames.";
434         }
435         SkDebugf("Expecting %d frames\n", page_count);
436 
437         std::vector<SkDocumentPage> pages(page_count);
438         if (!SkMultiPictureDocument::Read(stream, pages.data(), page_count, &procs)) {
439           return "Reading frames from MultiPictureDocument failed";
440         }
441 
442         fLayerManager = std::make_unique<DebugLayerManager>();
443 
444         int i = 0;
445         for (const auto& page : pages) {
446           // Make debug canvas using bounds from SkPicture
447           fBoundsArray.push_back(page.fPicture->cullRect().roundOut());
448           std::unique_ptr<DebugCanvas> debugCanvas = std::make_unique<DebugCanvas>(fBoundsArray.back());
449           debugCanvas->setLayerManagerAndFrame(fLayerManager.get(), i);
450 
451           // Only draw picture to the debug canvas once.
452           debugCanvas->drawPicture(page.fPicture);
453 
454           if (debugCanvas->getSize() <=0 ){
455             SkDebugf("Skipped corrupted frame, had %d commands \n", debugCanvas->getSize());
456             continue;
457           }
458           // If you don't set these, they're undefined.
459           debugCanvas->setOverdrawViz(false);
460           debugCanvas->setDrawGpuOpBounds(false);
461           debugCanvas->setClipVizColor(SK_ColorTRANSPARENT);
462           debugCanvas->setAndroidClipViz(false);
463           frames.push_back(std::move(debugCanvas));
464           i++;
465         }
466         fImages = deserialContext->fImages;
467 
468         udm.indexImages(fImages);
469         return "";
470       }
471 
472       // constrains the draw command index to the frame's command list length.
constrainFrameCommand(int index)473       int constrainFrameCommand(int index) {
474         int cmdlen = frames[fp]->getSize();
475         if (index >= cmdlen) {
476           return cmdlen-1;
477         }
478         return index;
479       }
480 
481       // A vector of DebugCanvas, each one initialized to a frame of the animation.
482       std::vector<std::unique_ptr<DebugCanvas>> frames;
483       // The index of the current frame (into the vector above)
484       int fp = 0;
485       // The width and height of every frame.
486       // frame sizes are known to change in Android Skia RenderEngine because it interleves pictures from different applications.
487       std::vector<SkIRect> fBoundsArray;
488       // image resources from a loaded file
489       std::vector<sk_sp<SkImage>> fImages;
490 
491       // The URLDataManager here is a cache that accepts encoded data (pngs) and puts
492       // numbers on them. We have our own collection of images (fImages) that was populated by the
493       // SkSharingDeserialContext when mskp files are loaded which it can use for IDing images
494       // without having to serialize them.
495       UrlDataManager udm;
496 
497       // A structure holding the picture information needed to draw any layers used in an mskp file
498       // individual frames hold a pointer to it, store draw events, and request images from it.
499       // it is stateful and is set to the current frame at all times.
500       std::unique_ptr<DebugLayerManager> fLayerManager;
501 
502       // The node id of a layer being inspected, if any.
503       // -1 means we are viewing the top level animation, not a layer.
504       // the exact draw event being inspected depends also on the selected frame `fp`.
505       int fInspectedLayer = -1;
506 };
507 
508 using namespace emscripten;
EMSCRIPTEN_BINDINGS(my_module)509 EMSCRIPTEN_BINDINGS(my_module) {
510 
511   function("MinVersion", &MinVersion);
512 
513   // The main class that the JavaScript in index.html uses
514   class_<SkpDebugPlayer>("SkpDebugPlayer")
515     .constructor<>()
516     .function("changeFrame",          &SkpDebugPlayer::changeFrame)
517     .function("deleteCommand",        &SkpDebugPlayer::deleteCommand)
518     .function("draw",                 &SkpDebugPlayer::draw, allow_raw_pointers())
519     .function("drawTo",               &SkpDebugPlayer::drawTo, allow_raw_pointers())
520     .function("findCommandByPixel",   &SkpDebugPlayer::findCommandByPixel, allow_raw_pointers())
521     .function("getBounds",            &SkpDebugPlayer::getBounds)
522     .function("getBoundsForFrame",    &SkpDebugPlayer::getBoundsForFrame)
523     .function("getFrameCount",        &SkpDebugPlayer::getFrameCount)
524     .function("getImageResource",     &SkpDebugPlayer::getImageResource)
525     .function("getImageCount",        &SkpDebugPlayer::getImageCount)
526     .function("getImageInfo",         &SkpDebugPlayer::getImageInfo)
527     .function("getLayerKeys",         &SkpDebugPlayer::getLayerKeys)
528     .function("getLayerSummariesJs",  &SkpDebugPlayer::getLayerSummariesJs)
529     .function("getSize",              &SkpDebugPlayer::getSize)
530     .function("imageUseInfo",         &SkpDebugPlayer::imageUseInfo)
531     .function("imageUseInfoForFrameJs", optional_override([](SkpDebugPlayer& self, const int frame)->JSObject {
532        // -1 as a node id is used throughout the application to mean no layer inspected.
533       return self.imageUseInfo(frame, -1);
534     }))
535     .function("jsonCommandList",      &SkpDebugPlayer::jsonCommandList, allow_raw_pointers())
536     .function("lastCommandInfo",      &SkpDebugPlayer::lastCommandInfo)
537     .function("loadSkp",              &SkpDebugPlayer::loadSkp, allow_raw_pointers())
538     .function("setClipVizColor",      &SkpDebugPlayer::setClipVizColor)
539     .function("setCommandVisibility", &SkpDebugPlayer::setCommandVisibility)
540     .function("setGpuOpBounds",       &SkpDebugPlayer::setGpuOpBounds)
541     .function("setInspectedLayer",    &SkpDebugPlayer::setInspectedLayer)
542     .function("setOriginVisible",     &SkpDebugPlayer::setOriginVisible)
543     .function("setOverdrawVis",       &SkpDebugPlayer::setOverdrawVis)
544     .function("setAndroidClipViz",    &SkpDebugPlayer::setAndroidClipViz);
545 
546   // Structs used as arguments or returns to the functions above
547   // TODO(kjlubick) handle this rect like the ones in CanvasKit
548   value_object<SkIRect>("SkIRect")
549       .field("fLeft",   &SkIRect::fLeft)
550       .field("fTop",    &SkIRect::fTop)
551       .field("fRight",  &SkIRect::fRight)
552       .field("fBottom", &SkIRect::fBottom);
553   // emscripten provided the following convenience function for binding vector<T>
554   // https://emscripten.org/docs/api_reference/bind.h.html#_CPPv415register_vectorPKc
555   register_vector<DebugLayerManager::LayerSummary>("VectorLayerSummary");
556   value_object<DebugLayerManager::LayerSummary>("LayerSummary")
557     .field("nodeId",            &DebugLayerManager::LayerSummary::nodeId)
558     .field("frameOfLastUpdate", &DebugLayerManager::LayerSummary::frameOfLastUpdate)
559     .field("fullRedraw",        &DebugLayerManager::LayerSummary::fullRedraw)
560     .field("layerWidth",        &DebugLayerManager::LayerSummary::layerWidth)
561     .field("layerHeight",       &DebugLayerManager::LayerSummary::layerHeight);
562 
563   value_object<ImageInfoNoColorspace>("ImageInfoNoColorspace")
564     .field("width",     &ImageInfoNoColorspace::width)
565     .field("height",    &ImageInfoNoColorspace::height)
566     .field("colorType", &ImageInfoNoColorspace::colorType)
567     .field("alphaType", &ImageInfoNoColorspace::alphaType);
568 }
569