/* * Copyright 2023 Google LLC * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "gm/gm.h" #include "include/codec/SkPngDecoder.h" #include "include/core/SkBitmap.h" #include "include/core/SkCanvas.h" #include "include/core/SkPicture.h" #include "include/core/SkPictureRecorder.h" #include "include/core/SkSerialProcs.h" #include "include/core/SkStream.h" #include "include/core/SkString.h" #include "include/core/SkSurface.h" #include "include/encode/SkPngEncoder.h" #include "include/private/base/SkAssert.h" #include "src/base/SkBase64.h" #include "tools/testrunners/gm/vias/Draw.h" #include #include // Implements the "direct" via. It draws the GM directly on the surface under test without any // additional behaviors. Equivalent to running a GM with DM without using any vias. static GMOutput draw_direct(skiagm::GM* gm, SkSurface* surface) { // Run the GM. SkString msg; skiagm::GM::DrawResult result = gm->draw(surface->getCanvas(), &msg); if (result != skiagm::DrawResult::kOk) { return {result, msg.c_str()}; } // Extract a bitmap from the surface. SkBitmap bitmap; bitmap.allocPixelsFlags(surface->getCanvas()->imageInfo(), SkBitmap::kZeroPixels_AllocFlag); if (!surface->readPixels(bitmap, 0, 0)) { return {skiagm::DrawResult::kFail, "Could not read pixels from surface"}; } return {result, msg.c_str(), bitmap}; } // Encodes a bitmap as a base-64 image/png URI. In the presence of errors, the returned string will // contain a human-friendly error message. Otherwise the string will start with "data:image/png". // // Based on // https://skia.googlesource.com/skia/+/650c980daa72d887602e701db8f84072e26d4d48/tests/TestUtils.cpp#127. static std::string bitmap_to_base64_data_uri(const SkBitmap& bitmap) { SkPixmap pm; if (!bitmap.peekPixels(&pm)) { return "peekPixels failed"; } // We're going to embed this PNG in a data URI, so make it as small as possible. SkPngEncoder::Options options; options.fFilterFlags = SkPngEncoder::FilterFlag::kAll; options.fZLibLevel = 9; SkDynamicMemoryWStream wStream; if (!SkPngEncoder::Encode(&wStream, pm, options)) { return "SkPngEncoder::Encode failed"; } sk_sp pngData = wStream.detachAsData(); size_t len = SkBase64::EncodedSize(pngData->size()); // The PNG can be almost arbitrarily large. We don't want to fill our logs with enormous URLs // and should only output them on failure. static const size_t kMaxBase64Length = 1024 * 1024; if (len > kMaxBase64Length) { return SkStringPrintf("Encoded image too large (%zu bytes)", len).c_str(); } std::string out; out.resize(len); SkBase64::Encode(pngData->data(), pngData->size(), out.data()); return "data:image/png;base64," + out; } // Implements the "picture" and "picture_serialization" vias. The "serialize" argument determines // which of the two vias is used. // // The "picture" via is based on DM's ViaPicture class here: // https://skia.googlesource.com/skia/+/dcc56df202cca129edda3f6f8bae04ec306b264e/dm/DMSrcSink.cpp#2310. // // The "picture_serialization" via is based on DM's ViaSerialization class here: // https://skia.googlesource.com/skia/+/dcc56df202cca129edda3f6f8bae04ec306b264e/dm/DMSrcSink.cpp#2281. static GMOutput draw_via_picture(skiagm::GM* gm, SkSurface* surface, bool serialize) { // Draw GM on a recording canvas. SkPictureRecorder recorder; SkCanvas* recordingCanvas = recorder.beginRecording(gm->getISize().width(), gm->getISize().height()); SkString msg; skiagm::DrawResult result = gm->draw(recordingCanvas, &msg); if (result != skiagm::DrawResult::kOk) { return {result, msg.c_str()}; } // Finish recording, and optionally serialize and then deserialize the resulting picture using // the PNG encoder/decoder. sk_sp pic = recorder.finishRecordingAsPicture(); if (serialize) { SkSerialProcs serialProcs = {.fImageProc = [](SkImage* img, void*) -> sk_sp { SkASSERT_RELEASE(!img->isTextureBacked()); return SkPngEncoder::Encode(nullptr, img, SkPngEncoder::Options{}); }}; SkDeserialProcs deserialProcs = {.fImageDataProc = [](sk_sp data, std::optional, void* ctx) -> sk_sp { SkCodec::Result decodeResult; std::unique_ptr codec = SkPngDecoder::Decode(data, &decodeResult); SkASSERT(decodeResult == SkCodec::Result::kSuccess); auto [image, getImageResult] = codec->getImage(); SkASSERT(getImageResult == SkCodec::Result::kSuccess); return image; }}; pic = SkPicture::MakeFromData(pic->serialize(&serialProcs).get(), &deserialProcs); } // Draw the recorded picture on the surface under test and extract it as a bitmap. surface->getCanvas()->drawPicture(pic); SkBitmap recordedBitmap; recordedBitmap.allocPixelsFlags(surface->getCanvas()->imageInfo(), SkBitmap::kZeroPixels_AllocFlag); if (!surface->readPixels(recordedBitmap, 0, 0)) { return {skiagm::DrawResult::kFail, "Could not read recorded picture pixels from surface"}; } // Draw GM on the surface under test and extract the reference bitmap. result = gm->draw(surface->getCanvas(), &msg); if (result != skiagm::DrawResult::kOk) { return {result, msg.c_str()}; } SkBitmap referenceBitmap; referenceBitmap.allocPixelsFlags(surface->getCanvas()->imageInfo(), SkBitmap::kZeroPixels_AllocFlag); if (!surface->readPixels(referenceBitmap, 0, 0)) { return {skiagm::DrawResult::kFail, "Could not read reference picture pixels from surface"}; } // The recorded and reference bitmaps should be identical. if (recordedBitmap.computeByteSize() != referenceBitmap.computeByteSize()) { return {skiagm::DrawResult::kFail, SkStringPrintf("Recorded and reference bitmap dimensions do not match: " "expected byte size %zu, width %d and height %d; " "got %zu, %d and %d", referenceBitmap.computeByteSize(), referenceBitmap.bounds().width(), referenceBitmap.bounds().height(), recordedBitmap.computeByteSize(), recordedBitmap.bounds().width(), recordedBitmap.bounds().height()) .c_str()}; } if (0 != memcmp(recordedBitmap.getPixels(), referenceBitmap.getPixels(), referenceBitmap.computeByteSize())) { return {skiagm::DrawResult::kFail, SkStringPrintf("Recorded and reference bitmap pixels do not match.\n" "Recorded image:\n%s\nReference image:\n%s", bitmap_to_base64_data_uri(recordedBitmap).c_str(), bitmap_to_base64_data_uri(referenceBitmap).c_str()) .c_str()}; } return {result, msg.c_str(), referenceBitmap}; } // This draw() implementation supports the "direct", "picture" and "picture_serialization" vias. // // The "direct" via draws the GM directly on the surface under test with no additional behaviors. // It is equivalent to running a GM with DM without using a via. // // The "picture" via tests that if we record a GM using an SkPictureRecorder, the bitmap produced // by drawing the recorded picture on the surface under test is the same as the bitmap obtained by // drawing the GM directly on the surface under test. // // The "picture_serialization" via is identical to the "picture" via, except that the recorded // picture is serialized and then deserialized before being drawn on the surface under test. GMOutput draw(skiagm::GM* gm, SkSurface* surface, std::string via) { if (via == "direct") { return draw_direct(gm, surface); } else if (via == "picture") { return draw_via_picture(gm, surface, /* serialize= */ false); } else if (via == "picture_serialization") { return draw_via_picture(gm, surface, /* serialize= */ true); } SK_ABORT("unknown --via flag value: %s", via.c_str()); }