/* * Copyright 2015 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "SkBitmapScaler.h" #include "SkBitmapFilter.h" #include "SkConvolver.h" #include "SkImageInfo.h" #include "SkPixmap.h" #include "SkRect.h" #include "SkTArray.h" // SkResizeFilter ---------------------------------------------------------------- // Encapsulates computation and storage of the filters required for one complete // resize operation. class SkResizeFilter { public: SkResizeFilter(SkBitmapScaler::ResizeMethod method, int srcFullWidth, int srcFullHeight, float destWidth, float destHeight, const SkRect& destSubset); ~SkResizeFilter() { delete fBitmapFilter; } // Returns the filled filter values. const SkConvolutionFilter1D& xFilter() { return fXFilter; } const SkConvolutionFilter1D& yFilter() { return fYFilter; } private: SkBitmapFilter* fBitmapFilter; // Computes one set of filters either horizontally or vertically. The caller // will specify the "min" and "max" rather than the bottom/top and // right/bottom so that the same code can be re-used in each dimension. // // |srcDependLo| and |srcDependSize| gives the range for the source // depend rectangle (horizontally or vertically at the caller's discretion // -- see above for what this means). // // Likewise, the range of destination values to compute and the scale factor // for the transform is also specified. void computeFilters(int srcSize, float destSubsetLo, float destSubsetSize, float scale, SkConvolutionFilter1D* output); SkConvolutionFilter1D fXFilter; SkConvolutionFilter1D fYFilter; }; SkResizeFilter::SkResizeFilter(SkBitmapScaler::ResizeMethod method, int srcFullWidth, int srcFullHeight, float destWidth, float destHeight, const SkRect& destSubset) { SkASSERT(method >= SkBitmapScaler::RESIZE_FirstMethod && method <= SkBitmapScaler::RESIZE_LastMethod); fBitmapFilter = nullptr; switch(method) { case SkBitmapScaler::RESIZE_BOX: fBitmapFilter = new SkBoxFilter; break; case SkBitmapScaler::RESIZE_TRIANGLE: fBitmapFilter = new SkTriangleFilter; break; case SkBitmapScaler::RESIZE_MITCHELL: fBitmapFilter = new SkMitchellFilter; break; case SkBitmapScaler::RESIZE_HAMMING: fBitmapFilter = new SkHammingFilter; break; case SkBitmapScaler::RESIZE_LANCZOS3: fBitmapFilter = new SkLanczosFilter; break; } float scaleX = destWidth / srcFullWidth; float scaleY = destHeight / srcFullHeight; this->computeFilters(srcFullWidth, destSubset.fLeft, destSubset.width(), scaleX, &fXFilter); if (srcFullWidth == srcFullHeight && destSubset.fLeft == destSubset.fTop && destSubset.width() == destSubset.height()&& scaleX == scaleY) { fYFilter = fXFilter; } else { this->computeFilters(srcFullHeight, destSubset.fTop, destSubset.height(), scaleY, &fYFilter); } } // TODO(egouriou): Take advantage of periods in the convolution. // Practical resizing filters are periodic outside of the border area. // For Lanczos, a scaling by a (reduced) factor of p/q (q pixels in the // source become p pixels in the destination) will have a period of p. // A nice consequence is a period of 1 when downscaling by an integral // factor. Downscaling from typical display resolutions is also bound // to produce interesting periods as those are chosen to have multiple // small factors. // Small periods reduce computational load and improve cache usage if // the coefficients can be shared. For periods of 1 we can consider // loading the factors only once outside the borders. void SkResizeFilter::computeFilters(int srcSize, float destSubsetLo, float destSubsetSize, float scale, SkConvolutionFilter1D* output) { float destSubsetHi = destSubsetLo + destSubsetSize; // [lo, hi) // When we're doing a magnification, the scale will be larger than one. This // means the destination pixels are much smaller than the source pixels, and // that the range covered by the filter won't necessarily cover any source // pixel boundaries. Therefore, we use these clamped values (max of 1) for // some computations. float clampedScale = SkTMin(1.0f, scale); // This is how many source pixels from the center we need to count // to support the filtering function. float srcSupport = fBitmapFilter->width() / clampedScale; float invScale = 1.0f / scale; SkSTArray<64, float, true> filterValuesArray; SkSTArray<64, SkConvolutionFilter1D::ConvolutionFixed, true> fixedFilterValuesArray; // Loop over all pixels in the output range. We will generate one set of // filter values for each one. Those values will tell us how to blend the // source pixels to compute the destination pixel. // This is the pixel in the source directly under the pixel in the dest. // Note that we base computations on the "center" of the pixels. To see // why, observe that the destination pixel at coordinates (0, 0) in a 5.0x // downscale should "cover" the pixels around the pixel with *its center* // at coordinates (2.5, 2.5) in the source, not those around (0, 0). // Hence we need to scale coordinates (0.5, 0.5), not (0, 0). destSubsetLo = SkScalarFloorToScalar(destSubsetLo); destSubsetHi = SkScalarCeilToScalar(destSubsetHi); float srcPixel = (destSubsetLo + 0.5f) * invScale; int destLimit = SkScalarTruncToInt(destSubsetHi - destSubsetLo); output->reserveAdditional(destLimit, SkScalarCeilToInt(destLimit * srcSupport * 2)); for (int destI = 0; destI < destLimit; srcPixel += invScale, destI++) { // Compute the (inclusive) range of source pixels the filter covers. float srcBegin = SkTMax(0.f, SkScalarFloorToScalar(srcPixel - srcSupport)); float srcEnd = SkTMin(srcSize - 1.f, SkScalarCeilToScalar(srcPixel + srcSupport)); // Compute the unnormalized filter value at each location of the source // it covers. // Sum of the filter values for normalizing. // Distance from the center of the filter, this is the filter coordinate // in source space. We also need to consider the center of the pixel // when comparing distance against 'srcPixel'. In the 5x downscale // example used above the distance from the center of the filter to // the pixel with coordinates (2, 2) should be 0, because its center // is at (2.5, 2.5). float destFilterDist = (srcBegin + 0.5f - srcPixel) * clampedScale; int filterCount = SkScalarTruncToInt(srcEnd - srcBegin) + 1; if (filterCount <= 0) { // true when srcSize is equal to srcPixel - srcSupport; this may be a bug return; } filterValuesArray.reset(filterCount); float filterSum = fBitmapFilter->evaluate_n(destFilterDist, clampedScale, filterCount, filterValuesArray.begin()); // The filter must be normalized so that we don't affect the brightness of // the image. Convert to normalized fixed point. int fixedSum = 0; fixedFilterValuesArray.reset(filterCount); const float* filterValues = filterValuesArray.begin(); SkConvolutionFilter1D::ConvolutionFixed* fixedFilterValues = fixedFilterValuesArray.begin(); float invFilterSum = 1 / filterSum; for (int fixedI = 0; fixedI < filterCount; fixedI++) { int curFixed = SkConvolutionFilter1D::FloatToFixed(filterValues[fixedI] * invFilterSum); fixedSum += curFixed; fixedFilterValues[fixedI] = SkToS16(curFixed); } SkASSERT(fixedSum <= 0x7FFF); // The conversion to fixed point will leave some rounding errors, which // we add back in to avoid affecting the brightness of the image. We // arbitrarily add this to the center of the filter array (this won't always // be the center of the filter function since it could get clipped on the // edges, but it doesn't matter enough to worry about that case). int leftovers = SkConvolutionFilter1D::FloatToFixed(1) - fixedSum; fixedFilterValues[filterCount / 2] += leftovers; // Now it's ready to go. output->AddFilter(SkScalarFloorToInt(srcBegin), fixedFilterValues, filterCount); } } /////////////////////////////////////////////////////////////////////////////////////////////////// static bool valid_for_resize(const SkPixmap& source, int dstW, int dstH) { // TODO: Seems like we shouldn't care about the swizzle of source, just that it's 8888 return source.addr() && source.colorType() == kN32_SkColorType && source.width() >= 1 && source.height() >= 1 && dstW >= 1 && dstH >= 1; } bool SkBitmapScaler::Resize(const SkPixmap& result, const SkPixmap& source, ResizeMethod method) { if (!valid_for_resize(source, result.width(), result.height())) { return false; } if (!result.addr() || result.colorType() != source.colorType()) { return false; } SkRect destSubset = SkRect::MakeIWH(result.width(), result.height()); SkResizeFilter filter(method, source.width(), source.height(), result.width(), result.height(), destSubset); // Get a subset 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* sourceSubset = reinterpret_cast(source.addr()); return BGRAConvolve2D(sourceSubset, static_cast(source.rowBytes()), !source.isOpaque(), filter.xFilter(), filter.yFilter(), static_cast(result.rowBytes()), static_cast(result.writable_addr())); } bool SkBitmapScaler::Resize(SkBitmap* resultPtr, const SkPixmap& source, ResizeMethod method, int destWidth, int destHeight, SkBitmap::Allocator* allocator) { // Preflight some of the checks, to avoid allocating the result if we don't need it. if (!valid_for_resize(source, destWidth, destHeight)) { return false; } SkBitmap result; // Note: pass along the profile information even thought this is no the right answer because // this could be scaling in sRGB. result.setInfo(SkImageInfo::MakeN32(destWidth, destHeight, source.alphaType(), sk_ref_sp(source.info().colorSpace()))); result.allocPixels(allocator, nullptr); SkPixmap resultPM; if (!result.peekPixels(&resultPM) || !Resize(resultPM, source, method)) { return false; } *resultPtr = result; SkASSERT(resultPtr->getPixels()); return true; }