// static SkBitmap ImageOperations::ResizeBasic(const SkBitmap& source, ResizeMethod method, int dest_width, int dest_height, const SkIRect& dest_subset, void* dest_pixels /* = nullptr */) { // Ensure that the ResizeMethod enumeration is sound. SkASSERT(((RESIZE_FIRST_QUALITY_METHOD <= method) && (method <= RESIZE_LAST_QUALITY_METHOD)) || ((RESIZE_FIRST_ALGORITHM_METHOD <= method) && (method <= RESIZE_LAST_ALGORITHM_METHOD))); // If the size of source or destination is 0, i.e. 0x0, 0xN or Nx0, just // return empty. if (source.width() < 1 || source.height() < 1 || dest_width < 1 || dest_height < 1) return SkBitmap(); method = ResizeMethodToAlgorithmMethod(method); // Check that we deal with an "algorithm methods" from this point onward. SkASSERT((ImageOperations::RESIZE_FIRST_ALGORITHM_METHOD <= method) && (method <= ImageOperations::RESIZE_LAST_ALGORITHM_METHOD)); SkAutoLockPixels locker(source); if (!source.readyToDraw()) return SkBitmap(); ConvolutionFilter1D x_filter; ConvolutionFilter1D y_filter; resize::ComputeFilters(method, source.width(), dest_width, dest_subset.fLeft, dest_subset.width(), &x_filter); resize::ComputeFilters(method, source.height(), dest_height, dest_subset.fTop, dest_subset.height(), &y_filter); // Get a source bitmap encompassing this touched area. We construct the // offsets and row strides such that it looks like a new bitmap, while // referring to the old data. const uint8_t* source_subset = reinterpret_cast<const uint8_t*>(source.getPixels()); // Convolve into the result. SkBitmap result; SkImageInfo info = SkImageInfo::Make(dest_subset.width(), dest_subset.height(), kBGRA_8888_SkColorType, kPremul_SkAlphaType); if (dest_pixels) { result.installPixels(info, dest_pixels, info.minRowBytes()); } else { result.allocPixels(info); } if (!result.readyToDraw()) return SkBitmap(); BGRAConvolve2D(source_subset, static_cast<int>(source.rowBytes()), !source.isOpaque(), x_filter, y_filter, static_cast<int>(result.rowBytes()), static_cast<unsigned char*>(result.getPixels())); // Preserve the "opaque" flag for use as an optimization later. result.setAlphaType(source.alphaType()); return result; }
// static SkBitmap ImageOperations::ResizeSubpixel(const SkBitmap& source, int dest_width, int dest_height, const SkIRect& dest_subset) { // Currently only works on Linux/BSD because these are the only platforms // where SkFontHost::GetSubpixelOrder is defined. #if defined(XP_UNIX) // Understand the display. const SkFontHost::LCDOrder order = SkFontHost::GetSubpixelOrder(); const SkFontHost::LCDOrientation orientation = SkFontHost::GetSubpixelOrientation(); // Decide on which dimension, if any, to deploy subpixel rendering. int w = 1; int h = 1; switch (orientation) { case SkFontHost::kHorizontal_LCDOrientation: w = dest_width < source.width() ? 3 : 1; break; case SkFontHost::kVertical_LCDOrientation: h = dest_height < source.height() ? 3 : 1; break; } // Resize the image. const int width = dest_width * w; const int height = dest_height * h; SkIRect subset = { dest_subset.fLeft, dest_subset.fTop, dest_subset.fLeft + dest_subset.width() * w, dest_subset.fTop + dest_subset.height() * h }; SkBitmap img = ResizeBasic(source, ImageOperations::RESIZE_LANCZOS3, width, height, subset); const int row_words = img.rowBytes() / 4; if (w == 1 && h == 1) return img; // Render into subpixels. SkBitmap result; SkImageInfo info = SkImageInfo::Make(dest_subset.width(), dest_subset.height(), kBGRA_8888_SkColorType, kPremul_SkAlphaType); result.allocPixels(info); if (!result.readyToDraw()) return img; SkAutoLockPixels locker(img); if (!img.readyToDraw()) return img; uint32_t* src_row = img.getAddr32(0, 0); uint32_t* dst_row = result.getAddr32(0, 0); for (int y = 0; y < dest_subset.height(); y++) { uint32_t* src = src_row; uint32_t* dst = dst_row; for (int x = 0; x < dest_subset.width(); x++, src += w, dst++) { uint8_t r = 0, g = 0, b = 0, a = 0; switch (order) { case SkFontHost::kRGB_LCDOrder: switch (orientation) { case SkFontHost::kHorizontal_LCDOrientation: r = SkGetPackedR32(src[0]); g = SkGetPackedG32(src[1]); b = SkGetPackedB32(src[2]); a = SkGetPackedA32(src[1]); break; case SkFontHost::kVertical_LCDOrientation: r = SkGetPackedR32(src[0 * row_words]); g = SkGetPackedG32(src[1 * row_words]); b = SkGetPackedB32(src[2 * row_words]); a = SkGetPackedA32(src[1 * row_words]); break; } break; case SkFontHost::kBGR_LCDOrder: switch (orientation) { case SkFontHost::kHorizontal_LCDOrientation: b = SkGetPackedB32(src[0]); g = SkGetPackedG32(src[1]); r = SkGetPackedR32(src[2]); a = SkGetPackedA32(src[1]); break; case SkFontHost::kVertical_LCDOrientation: b = SkGetPackedB32(src[0 * row_words]); g = SkGetPackedG32(src[1 * row_words]); r = SkGetPackedR32(src[2 * row_words]); a = SkGetPackedA32(src[1 * row_words]); break; } break; case SkFontHost::kNONE_LCDOrder: break; } // Premultiplied alpha is very fragile. a = a > r ? a : r; a = a > g ? a : g; a = a > b ? a : b; *dst = SkPackARGB32(a, r, g, b); } src_row += h * row_words; dst_row += result.rowBytes() / 4; } result.setAlphaType(img.alphaType()); return result; #else return SkBitmap(); #endif // OS_POSIX && !OS_MACOSX && !defined(OS_ANDROID) }
sk_sp<SkSpecialImage> SkMatrixConvolutionImageFilter::onFilterImage(SkSpecialImage* source, const Context& ctx, SkIPoint* offset) const { SkIPoint inputOffset = SkIPoint::Make(0, 0); sk_sp<SkSpecialImage> input(this->filterInput(0, source, ctx, &inputOffset)); if (!input) { return nullptr; } SkIRect bounds; input = this->applyCropRect(this->mapContext(ctx), input.get(), &inputOffset, &bounds); if (!input) { return nullptr; } #if SK_SUPPORT_GPU // Note: if the kernel is too big, the GPU path falls back to SW if (source->isTextureBacked() && fKernelSize.width() * fKernelSize.height() <= MAX_KERNEL_SIZE) { GrContext* context = source->getContext(); sk_sp<GrTexture> inputTexture(input->asTextureRef(context)); SkASSERT(inputTexture); offset->fX = bounds.left(); offset->fY = bounds.top(); bounds.offset(-inputOffset); // SRGBTODO: handle sRGB here sk_sp<GrFragmentProcessor> fp(GrMatrixConvolutionEffect::Make(inputTexture.get(), bounds, fKernelSize, fKernel, fGain, fBias, fKernelOffset, convert_tilemodes(fTileMode), fConvolveAlpha)); if (!fp) { return nullptr; } return DrawWithFP(context, std::move(fp), bounds, ctx.outputProperties()); } #endif SkBitmap inputBM; if (!input->getROPixels(&inputBM)) { return nullptr; } if (inputBM.colorType() != kN32_SkColorType) { return nullptr; } if (!fConvolveAlpha && !inputBM.isOpaque()) { inputBM = unpremultiply_bitmap(inputBM); } SkAutoLockPixels alp(inputBM); if (!inputBM.getPixels()) { return nullptr; } const SkImageInfo info = SkImageInfo::MakeN32(bounds.width(), bounds.height(), inputBM.alphaType()); SkBitmap dst; if (!dst.tryAllocPixels(info)) { return nullptr; } SkAutoLockPixels dstLock(dst); offset->fX = bounds.fLeft; offset->fY = bounds.fTop; bounds.offset(-inputOffset); SkIRect interior = SkIRect::MakeXYWH(bounds.left() + fKernelOffset.fX, bounds.top() + fKernelOffset.fY, bounds.width() - fKernelSize.fWidth + 1, bounds.height() - fKernelSize.fHeight + 1); SkIRect top = SkIRect::MakeLTRB(bounds.left(), bounds.top(), bounds.right(), interior.top()); SkIRect bottom = SkIRect::MakeLTRB(bounds.left(), interior.bottom(), bounds.right(), bounds.bottom()); SkIRect left = SkIRect::MakeLTRB(bounds.left(), interior.top(), interior.left(), interior.bottom()); SkIRect right = SkIRect::MakeLTRB(interior.right(), interior.top(), bounds.right(), interior.bottom()); this->filterBorderPixels(inputBM, &dst, top, bounds); this->filterBorderPixels(inputBM, &dst, left, bounds); this->filterInteriorPixels(inputBM, &dst, interior, bounds); this->filterBorderPixels(inputBM, &dst, right, bounds); this->filterBorderPixels(inputBM, &dst, bottom, bounds); return SkSpecialImage::MakeFromRaster(SkIRect::MakeWH(bounds.width(), bounds.height()), dst); }
// static bool SkBitmapScaler::Resize(SkBitmap* resultPtr, const SkBitmap& source, ResizeMethod method, float destWidth, float destHeight, SkBitmap::Allocator* allocator) { SkConvolutionProcs convolveProcs= { 0, NULL, NULL, NULL, NULL }; PlatformConvolutionProcs(&convolveProcs); SkRect destSubset = { 0, 0, destWidth, destHeight }; // Ensure that the ResizeMethod enumeration is sound. SkASSERT(((RESIZE_FIRST_QUALITY_METHOD <= method) && (method <= RESIZE_LAST_QUALITY_METHOD)) || ((RESIZE_FIRST_ALGORITHM_METHOD <= method) && (method <= RESIZE_LAST_ALGORITHM_METHOD))); SkRect dest = { 0, 0, destWidth, destHeight }; if (!dest.contains(destSubset)) { SkErrorInternals::SetError( kInvalidArgument_SkError, "Sorry, the destination bitmap scale subset " "falls outside the full destination bitmap." ); return false; } // If the size of source or destination is 0, i.e. 0x0, 0xN or Nx0, just // return empty. if (source.width() < 1 || source.height() < 1 || destWidth < 1 || destHeight < 1) { // todo: seems like we could handle negative dstWidth/Height, since that // is just a negative scale (flip) return false; } method = ResizeMethodToAlgorithmMethod(method); // Check that we deal with an "algorithm methods" from this point onward. SkASSERT((SkBitmapScaler::RESIZE_FIRST_ALGORITHM_METHOD <= method) && (method <= SkBitmapScaler::RESIZE_LAST_ALGORITHM_METHOD)); SkAutoLockPixels locker(source); if (!source.readyToDraw() || source.colorType() != kN32_SkColorType) { return false; } SkResizeFilter filter(method, source.width(), source.height(), destWidth, destHeight, destSubset, convolveProcs); // Get a source bitmap encompassing this touched area. We construct the // offsets and row strides such that it looks like a new bitmap, while // referring to the old data. const unsigned char* sourceSubset = reinterpret_cast<const unsigned char*>(source.getPixels()); // Convolve into the result. SkBitmap result; result.setInfo(SkImageInfo::MakeN32(SkScalarCeilToInt(destSubset.width()), SkScalarCeilToInt(destSubset.height()), source.alphaType())); result.allocPixels(allocator, NULL); if (!result.readyToDraw()) { return false; } BGRAConvolve2D(sourceSubset, static_cast<int>(source.rowBytes()), !source.isOpaque(), filter.xFilter(), filter.yFilter(), static_cast<int>(result.rowBytes()), static_cast<unsigned char*>(result.getPixels()), convolveProcs, true); *resultPtr = result; resultPtr->lockPixels(); SkASSERT(NULL != resultPtr->getPixels()); return true; }
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options ) { int sampleSize = 1; SkImageDecoder::Mode decodeMode = SkImageDecoder::kDecodePixels_Mode; SkColorType prefColorType = kN32_SkColorType; bool doDither = true; bool isMutable = false; float scale = 1.0f; bool preferQualityOverSpeed = false; bool requireUnpremultiplied = false; jobject javaBitmap = NULL; if (options != NULL) { sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID); if (optionsJustBounds(env, options)) { decodeMode = SkImageDecoder::kDecodeBounds_Mode; } // initialize these, in case we fail later on env->SetIntField(options, gOptions_widthFieldID, -1); env->SetIntField(options, gOptions_heightFieldID, -1); env->SetObjectField(options, gOptions_mimeFieldID, 0); jobject jconfig = env->GetObjectField(options, gOptions_configFieldID); prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig); isMutable = env->GetBooleanField(options, gOptions_mutableFieldID); doDither = env->GetBooleanField(options, gOptions_ditherFieldID); preferQualityOverSpeed = env->GetBooleanField(options, gOptions_preferQualityOverSpeedFieldID); requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID); javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID); if (env->GetBooleanField(options, gOptions_scaledFieldID)) { const int density = env->GetIntField(options, gOptions_densityFieldID); const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID); const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID); if (density != 0 && targetDensity != 0 && density != screenDensity) { scale = (float) targetDensity / density; } } } const bool willScale = scale != 1.0f; SkImageDecoder* decoder = SkImageDecoder::Factory(stream); if (decoder == NULL) { return nullObjectReturn("SkImageDecoder::Factory returned null"); } decoder->setSampleSize(sampleSize); decoder->setDitherImage(doDither); decoder->setPreferQualityOverSpeed(preferQualityOverSpeed); decoder->setRequireUnpremultipliedColors(requireUnpremultiplied); SkBitmap* outputBitmap = NULL; unsigned int existingBufferSize = 0; if (javaBitmap != NULL) { outputBitmap = (SkBitmap*) env->GetLongField(javaBitmap, gBitmap_nativeBitmapFieldID); if (outputBitmap->isImmutable()) { ALOGW("Unable to reuse an immutable bitmap as an image decoder target."); javaBitmap = NULL; outputBitmap = NULL; } else { existingBufferSize = GraphicsJNI::getBitmapAllocationByteCount(env, javaBitmap); } } SkAutoTDelete<SkBitmap> adb(outputBitmap == NULL ? new SkBitmap : NULL); if (outputBitmap == NULL) outputBitmap = adb.get(); NinePatchPeeker peeker(decoder); decoder->setPeeker(&peeker); JavaPixelAllocator javaAllocator(env); RecyclingPixelAllocator recyclingAllocator(outputBitmap->pixelRef(), existingBufferSize); ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize); SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ? (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator; if (decodeMode != SkImageDecoder::kDecodeBounds_Mode) { if (!willScale) { // If the java allocator is being used to allocate the pixel memory, the decoder // need not write zeroes, since the memory is initialized to 0. decoder->setSkipWritingZeroes(outputAllocator == &javaAllocator); decoder->setAllocator(outputAllocator); } else if (javaBitmap != NULL) { // check for eventual scaled bounds at allocation time, so we don't decode the bitmap // only to find the scaled result too large to fit in the allocation decoder->setAllocator(&scaleCheckingAllocator); } } // Only setup the decoder to be deleted after its stack-based, refcounted // components (allocators, peekers, etc) are declared. This prevents RefCnt // asserts from firing due to the order objects are deleted from the stack. SkAutoTDelete<SkImageDecoder> add(decoder); AutoDecoderCancel adc(options, decoder); // To fix the race condition in case "requestCancelDecode" // happens earlier than AutoDecoderCancel object is added // to the gAutoDecoderCancelMutex linked list. if (options != NULL && env->GetBooleanField(options, gOptions_mCancelID)) { return nullObjectReturn("gOptions_mCancelID"); } SkBitmap decodingBitmap; if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode) != SkImageDecoder::kSuccess) { return nullObjectReturn("decoder->decode returned false"); } int scaledWidth = decodingBitmap.width(); int scaledHeight = decodingBitmap.height(); if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) { scaledWidth = int(scaledWidth * scale + 0.5f); scaledHeight = int(scaledHeight * scale + 0.5f); } // update options (if any) if (options != NULL) { jstring mimeType = getMimeTypeString(env, decoder->getFormat()); if (env->ExceptionCheck()) { return nullObjectReturn("OOM in getMimeTypeString()"); } env->SetIntField(options, gOptions_widthFieldID, scaledWidth); env->SetIntField(options, gOptions_heightFieldID, scaledHeight); env->SetObjectField(options, gOptions_mimeFieldID, mimeType); } // if we're in justBounds mode, return now (skip the java bitmap) if (decodeMode == SkImageDecoder::kDecodeBounds_Mode) { return NULL; } jbyteArray ninePatchChunk = NULL; if (peeker.mPatch != NULL) { if (willScale) { scaleNinePatchChunk(peeker.mPatch, scale, scaledWidth, scaledHeight); } size_t ninePatchArraySize = peeker.mPatch->serializedSize(); ninePatchChunk = env->NewByteArray(ninePatchArraySize); if (ninePatchChunk == NULL) { return nullObjectReturn("ninePatchChunk == null"); } jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL); if (array == NULL) { return nullObjectReturn("primitive array == null"); } memcpy(array, peeker.mPatch, peeker.mPatchSize); env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0); } jobject ninePatchInsets = NULL; if (peeker.mHasInsets) { ninePatchInsets = env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID, peeker.mOpticalInsets[0], peeker.mOpticalInsets[1], peeker.mOpticalInsets[2], peeker.mOpticalInsets[3], peeker.mOutlineInsets[0], peeker.mOutlineInsets[1], peeker.mOutlineInsets[2], peeker.mOutlineInsets[3], peeker.mOutlineRadius, peeker.mOutlineAlpha, scale); if (ninePatchInsets == NULL) { return nullObjectReturn("nine patch insets == null"); } if (javaBitmap != NULL) { env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID, ninePatchInsets); } } if (willScale) { // This is weird so let me explain: we could use the scale parameter // directly, but for historical reasons this is how the corresponding // Dalvik code has always behaved. We simply recreate the behavior here. // The result is slightly different from simply using scale because of // the 0.5f rounding bias applied when computing the target image size const float sx = scaledWidth / float(decodingBitmap.width()); const float sy = scaledHeight / float(decodingBitmap.height()); // TODO: avoid copying when scaled size equals decodingBitmap size SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType()); // FIXME: If the alphaType is kUnpremul and the image has alpha, the // colors may not be correct, since Skia does not yet support drawing // to/from unpremultiplied bitmaps. outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight, colorType, decodingBitmap.alphaType())); if (!outputBitmap->allocPixels(outputAllocator, NULL)) { return nullObjectReturn("allocation failed for scaled bitmap"); } // If outputBitmap's pixels are newly allocated by Java, there is no need // to erase to 0, since the pixels were initialized to 0. if (outputAllocator != &javaAllocator) { outputBitmap->eraseColor(0); } SkPaint paint; paint.setFilterLevel(SkPaint::kLow_FilterLevel); SkCanvas canvas(*outputBitmap); canvas.scale(sx, sy); canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint); } else { outputBitmap->swap(decodingBitmap); } if (padding) { if (peeker.mPatch != NULL) { GraphicsJNI::set_jrect(env, padding, peeker.mPatch->paddingLeft, peeker.mPatch->paddingTop, peeker.mPatch->paddingRight, peeker.mPatch->paddingBottom); } else { GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1); } } // if we get here, we're in kDecodePixels_Mode and will therefore // already have a pixelref installed. if (outputBitmap->pixelRef() == NULL) { return nullObjectReturn("Got null SkPixelRef"); } if (!isMutable && javaBitmap == NULL) { // promise we will never change our pixels (great for sharing and pictures) outputBitmap->setImmutable(); } // detach bitmap from its autodeleter, since we want to own it now adb.detach(); if (javaBitmap != NULL) { bool isPremultiplied = !requireUnpremultiplied; GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap, isPremultiplied); outputBitmap->notifyPixelsChanged(); // If a java bitmap was passed in for reuse, pass it back return javaBitmap; } int bitmapCreateFlags = 0x0; if (isMutable) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Mutable; if (!requireUnpremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied; // now create the java bitmap return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); }
sk_sp<SkSpecialImage> SkMorphologyImageFilter::onFilterImage(SkSpecialImage* source, const Context& ctx, SkIPoint* offset) const { SkIPoint inputOffset = SkIPoint::Make(0, 0); sk_sp<SkSpecialImage> input(this->filterInput(0, source, ctx, &inputOffset)); if (!input) { return nullptr; } SkIRect bounds; input = this->applyCropRectAndPad(this->mapContext(ctx), input.get(), &inputOffset, &bounds); if (!input) { return nullptr; } SkVector radius = SkVector::Make(SkIntToScalar(this->radius().width()), SkIntToScalar(this->radius().height())); ctx.ctm().mapVectors(&radius, 1); int width = SkScalarFloorToInt(radius.fX); int height = SkScalarFloorToInt(radius.fY); if (width < 0 || height < 0) { return nullptr; } SkIRect srcBounds = bounds; srcBounds.offset(-inputOffset); if (0 == width && 0 == height) { offset->fX = bounds.left(); offset->fY = bounds.top(); return input->makeSubset(srcBounds); } #if SK_SUPPORT_GPU if (source->isTextureBacked()) { GrContext* context = source->getContext(); // Ensure the input is in the destination color space. Typically applyCropRect will have // called pad_image to account for our dilation of bounds, so the result will already be // moved to the destination color space. If a filter DAG avoids that, then we use this // fall-back, which saves us from having to do the xform during the filter itself. input = ImageToColorSpace(input.get(), ctx.outputProperties()); auto type = (kDilate_Op == this->op()) ? GrMorphologyEffect::Type::kDilate : GrMorphologyEffect::Type::kErode; sk_sp<SkSpecialImage> result(apply_morphology(context, input.get(), srcBounds, type, SkISize::Make(width, height), ctx.outputProperties())); if (result) { offset->fX = bounds.left(); offset->fY = bounds.top(); } return result; } #endif SkBitmap inputBM; if (!input->getROPixels(&inputBM)) { return nullptr; } if (inputBM.colorType() != kN32_SkColorType) { return nullptr; } SkImageInfo info = SkImageInfo::Make(bounds.width(), bounds.height(), inputBM.colorType(), inputBM.alphaType()); SkBitmap dst; if (!dst.tryAllocPixels(info)) { return nullptr; } SkMorphologyImageFilter::Proc procX, procY; if (kDilate_Op == this->op()) { procX = SkOpts::dilate_x; procY = SkOpts::dilate_y; } else { procX = SkOpts::erode_x; procY = SkOpts::erode_y; } if (width > 0 && height > 0) { SkBitmap tmp; if (!tmp.tryAllocPixels(info)) { return nullptr; } call_proc_X(procX, inputBM, &tmp, width, srcBounds); SkIRect tmpBounds = SkIRect::MakeWH(srcBounds.width(), srcBounds.height()); call_proc_Y(procY, tmp.getAddr32(tmpBounds.left(), tmpBounds.top()), tmp.rowBytesAsPixels(), &dst, height, tmpBounds); } else if (width > 0) { call_proc_X(procX, inputBM, &dst, width, srcBounds); } else if (height > 0) { call_proc_Y(procY, inputBM.getAddr32(srcBounds.left(), srcBounds.top()), inputBM.rowBytesAsPixels(), &dst, height, srcBounds); } offset->fX = bounds.left(); offset->fY = bounds.top(); return SkSpecialImage::MakeFromRaster(SkIRect::MakeWH(bounds.width(), bounds.height()), dst, &source->props()); }