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