blob: 6e253023fd996f33738703730af4dbc6b4329c25 [file] [log] [blame]
reed92fc2ae2015-05-22 08:06:21 -07001/*
2 * Copyright 2015 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
humper@google.com138ebc32013-07-19 20:20:04 +00008#include "SkBitmapScaler.h"
9#include "SkBitmapFilter.h"
reedfb8c1fc2015-08-04 18:44:56 -070010#include "SkConvolver.h"
bungemand3ebb482015-08-05 13:57:49 -070011#include "SkImageInfo.h"
12#include "SkPixmap.h"
13#include "SkRect.h"
14#include "SkScalar.h"
15#include "SkTArray.h"
humper@google.com138ebc32013-07-19 20:20:04 +000016
17// SkResizeFilter ----------------------------------------------------------------
18
19// Encapsulates computation and storage of the filters required for one complete
20// resize operation.
21class SkResizeFilter {
22public:
23 SkResizeFilter(SkBitmapScaler::ResizeMethod method,
24 int srcFullWidth, int srcFullHeight,
commit-bot@chromium.orgf4491562014-05-28 17:30:02 +000025 float destWidth, float destHeight,
26 const SkRect& destSubset,
reed@google.comfed04b32013-09-05 20:31:17 +000027 const SkConvolutionProcs& convolveProcs);
skia.committer@gmail.com1f3c7382013-07-20 07:00:58 +000028 ~SkResizeFilter() {
29 SkDELETE( fBitmapFilter );
humper@google.com138ebc32013-07-19 20:20:04 +000030 }
skia.committer@gmail.com1f3c7382013-07-20 07:00:58 +000031
humper@google.com138ebc32013-07-19 20:20:04 +000032 // Returns the filled filter values.
33 const SkConvolutionFilter1D& xFilter() { return fXFilter; }
34 const SkConvolutionFilter1D& yFilter() { return fYFilter; }
35
36private:
skia.committer@gmail.com1f3c7382013-07-20 07:00:58 +000037
humper@google.com138ebc32013-07-19 20:20:04 +000038 SkBitmapFilter* fBitmapFilter;
39
40 // Computes one set of filters either horizontally or vertically. The caller
41 // will specify the "min" and "max" rather than the bottom/top and
42 // right/bottom so that the same code can be re-used in each dimension.
43 //
44 // |srcDependLo| and |srcDependSize| gives the range for the source
45 // depend rectangle (horizontally or vertically at the caller's discretion
46 // -- see above for what this means).
47 //
48 // Likewise, the range of destination values to compute and the scale factor
49 // for the transform is also specified.
skia.committer@gmail.com1f3c7382013-07-20 07:00:58 +000050
humper@google.com138ebc32013-07-19 20:20:04 +000051 void computeFilters(int srcSize,
commit-bot@chromium.orgf4491562014-05-28 17:30:02 +000052 float destSubsetLo, float destSubsetSize,
humper@google.com138ebc32013-07-19 20:20:04 +000053 float scale,
54 SkConvolutionFilter1D* output,
reed@google.comfed04b32013-09-05 20:31:17 +000055 const SkConvolutionProcs& convolveProcs);
humper@google.com138ebc32013-07-19 20:20:04 +000056
humper@google.com138ebc32013-07-19 20:20:04 +000057 SkConvolutionFilter1D fXFilter;
58 SkConvolutionFilter1D fYFilter;
59};
60
61SkResizeFilter::SkResizeFilter(SkBitmapScaler::ResizeMethod method,
62 int srcFullWidth, int srcFullHeight,
commit-bot@chromium.orgf4491562014-05-28 17:30:02 +000063 float destWidth, float destHeight,
64 const SkRect& destSubset,
reed@google.comfed04b32013-09-05 20:31:17 +000065 const SkConvolutionProcs& convolveProcs) {
skia.committer@gmail.com1f3c7382013-07-20 07:00:58 +000066
humper@google.com138ebc32013-07-19 20:20:04 +000067 // method will only ever refer to an "algorithm method".
68 SkASSERT((SkBitmapScaler::RESIZE_FIRST_ALGORITHM_METHOD <= method) &&
69 (method <= SkBitmapScaler::RESIZE_LAST_ALGORITHM_METHOD));
70
71 switch(method) {
72 case SkBitmapScaler::RESIZE_BOX:
73 fBitmapFilter = SkNEW(SkBoxFilter);
74 break;
75 case SkBitmapScaler::RESIZE_TRIANGLE:
76 fBitmapFilter = SkNEW(SkTriangleFilter);
77 break;
78 case SkBitmapScaler::RESIZE_MITCHELL:
79 fBitmapFilter = SkNEW_ARGS(SkMitchellFilter, (1.f/3.f, 1.f/3.f));
80 break;
81 case SkBitmapScaler::RESIZE_HAMMING:
82 fBitmapFilter = SkNEW(SkHammingFilter);
83 break;
84 case SkBitmapScaler::RESIZE_LANCZOS3:
85 fBitmapFilter = SkNEW(SkLanczosFilter);
86 break;
87 default:
88 // NOTREACHED:
89 fBitmapFilter = SkNEW_ARGS(SkMitchellFilter, (1.f/3.f, 1.f/3.f));
90 break;
91 }
skia.committer@gmail.com1f3c7382013-07-20 07:00:58 +000092
humper@google.com138ebc32013-07-19 20:20:04 +000093
commit-bot@chromium.orgf4491562014-05-28 17:30:02 +000094 float scaleX = destWidth / srcFullWidth;
95 float scaleY = destHeight / srcFullHeight;
humper@google.com138ebc32013-07-19 20:20:04 +000096
97 this->computeFilters(srcFullWidth, destSubset.fLeft, destSubset.width(),
98 scaleX, &fXFilter, convolveProcs);
commit-bot@chromium.orgd10913b2014-03-06 19:10:44 +000099 if (srcFullWidth == srcFullHeight &&
100 destSubset.fLeft == destSubset.fTop &&
101 destSubset.width() == destSubset.height()&&
102 scaleX == scaleY) {
103 fYFilter = fXFilter;
104 } else {
105 this->computeFilters(srcFullHeight, destSubset.fTop, destSubset.height(),
106 scaleY, &fYFilter, convolveProcs);
107 }
humper@google.com138ebc32013-07-19 20:20:04 +0000108}
109
110// TODO(egouriou): Take advantage of periods in the convolution.
111// Practical resizing filters are periodic outside of the border area.
112// For Lanczos, a scaling by a (reduced) factor of p/q (q pixels in the
113// source become p pixels in the destination) will have a period of p.
114// A nice consequence is a period of 1 when downscaling by an integral
115// factor. Downscaling from typical display resolutions is also bound
116// to produce interesting periods as those are chosen to have multiple
117// small factors.
118// Small periods reduce computational load and improve cache usage if
119// the coefficients can be shared. For periods of 1 we can consider
120// loading the factors only once outside the borders.
121void SkResizeFilter::computeFilters(int srcSize,
commit-bot@chromium.orgf4491562014-05-28 17:30:02 +0000122 float destSubsetLo, float destSubsetSize,
humper@google.com138ebc32013-07-19 20:20:04 +0000123 float scale,
124 SkConvolutionFilter1D* output,
reed@google.comfed04b32013-09-05 20:31:17 +0000125 const SkConvolutionProcs& convolveProcs) {
commit-bot@chromium.orgf4491562014-05-28 17:30:02 +0000126 float destSubsetHi = destSubsetLo + destSubsetSize; // [lo, hi)
humper@google.com138ebc32013-07-19 20:20:04 +0000127
128 // When we're doing a magnification, the scale will be larger than one. This
129 // means the destination pixels are much smaller than the source pixels, and
130 // that the range covered by the filter won't necessarily cover any source
131 // pixel boundaries. Therefore, we use these clamped values (max of 1) for
132 // some computations.
133 float clampedScale = SkTMin(1.0f, scale);
134
135 // This is how many source pixels from the center we need to count
136 // to support the filtering function.
137 float srcSupport = fBitmapFilter->width() / clampedScale;
138
139 // Speed up the divisions below by turning them into multiplies.
140 float invScale = 1.0f / scale;
141
142 SkTArray<float> filterValues(64);
143 SkTArray<short> fixedFilterValues(64);
144
145 // Loop over all pixels in the output range. We will generate one set of
146 // filter values for each one. Those values will tell us how to blend the
147 // source pixels to compute the destination pixel.
commit-bot@chromium.orgf4491562014-05-28 17:30:02 +0000148 for (int destSubsetI = SkScalarFloorToInt(destSubsetLo); destSubsetI < SkScalarCeilToInt(destSubsetHi);
humper@google.com138ebc32013-07-19 20:20:04 +0000149 destSubsetI++) {
150 // Reset the arrays. We don't declare them inside so they can re-use the
151 // same malloc-ed buffer.
152 filterValues.reset();
153 fixedFilterValues.reset();
154
155 // This is the pixel in the source directly under the pixel in the dest.
156 // Note that we base computations on the "center" of the pixels. To see
157 // why, observe that the destination pixel at coordinates (0, 0) in a 5.0x
158 // downscale should "cover" the pixels around the pixel with *its center*
159 // at coordinates (2.5, 2.5) in the source, not those around (0, 0).
160 // Hence we need to scale coordinates (0.5, 0.5), not (0, 0).
161 float srcPixel = (static_cast<float>(destSubsetI) + 0.5f) * invScale;
162
163 // Compute the (inclusive) range of source pixels the filter covers.
164 int srcBegin = SkTMax(0, SkScalarFloorToInt(srcPixel - srcSupport));
165 int srcEnd = SkTMin(srcSize - 1, SkScalarCeilToInt(srcPixel + srcSupport));
166
167 // Compute the unnormalized filter value at each location of the source
168 // it covers.
169 float filterSum = 0.0f; // Sub of the filter values for normalizing.
170 for (int curFilterPixel = srcBegin; curFilterPixel <= srcEnd;
171 curFilterPixel++) {
172 // Distance from the center of the filter, this is the filter coordinate
173 // in source space. We also need to consider the center of the pixel
174 // when comparing distance against 'srcPixel'. In the 5x downscale
175 // example used above the distance from the center of the filter to
176 // the pixel with coordinates (2, 2) should be 0, because its center
177 // is at (2.5, 2.5).
178 float srcFilterDist =
179 ((static_cast<float>(curFilterPixel) + 0.5f) - srcPixel);
180
181 // Since the filter really exists in dest space, map it there.
182 float destFilterDist = srcFilterDist * clampedScale;
183
184 // Compute the filter value at that location.
185 float filterValue = fBitmapFilter->evaluate(destFilterDist);
186 filterValues.push_back(filterValue);
187
188 filterSum += filterValue;
189 }
190 SkASSERT(!filterValues.empty());
191
192 // The filter must be normalized so that we don't affect the brightness of
193 // the image. Convert to normalized fixed point.
194 short fixedSum = 0;
195 for (int i = 0; i < filterValues.count(); i++) {
196 short curFixed = output->FloatToFixed(filterValues[i] / filterSum);
197 fixedSum += curFixed;
198 fixedFilterValues.push_back(curFixed);
199 }
200
201 // The conversion to fixed point will leave some rounding errors, which
202 // we add back in to avoid affecting the brightness of the image. We
203 // arbitrarily add this to the center of the filter array (this won't always
204 // be the center of the filter function since it could get clipped on the
205 // edges, but it doesn't matter enough to worry about that case).
206 short leftovers = output->FloatToFixed(1.0f) - fixedSum;
207 fixedFilterValues[fixedFilterValues.count() / 2] += leftovers;
208
209 // Now it's ready to go.
210 output->AddFilter(srcBegin, &fixedFilterValues[0],
211 static_cast<int>(fixedFilterValues.count()));
212 }
213
reed@google.comfed04b32013-09-05 20:31:17 +0000214 if (convolveProcs.fApplySIMDPadding) {
215 convolveProcs.fApplySIMDPadding( output );
humper@google.com138ebc32013-07-19 20:20:04 +0000216 }
217}
218
219static SkBitmapScaler::ResizeMethod ResizeMethodToAlgorithmMethod(
220 SkBitmapScaler::ResizeMethod method) {
221 // Convert any "Quality Method" into an "Algorithm Method"
222 if (method >= SkBitmapScaler::RESIZE_FIRST_ALGORITHM_METHOD &&
223 method <= SkBitmapScaler::RESIZE_LAST_ALGORITHM_METHOD) {
224 return method;
225 }
226 // The call to SkBitmapScalerGtv::Resize() above took care of
227 // GPU-acceleration in the cases where it is possible. So now we just
228 // pick the appropriate software method for each resize quality.
229 switch (method) {
230 // Users of RESIZE_GOOD are willing to trade a lot of quality to
231 // get speed, allowing the use of linear resampling to get hardware
232 // acceleration (SRB). Hence any of our "good" software filters
233 // will be acceptable, so we use a triangle.
234 case SkBitmapScaler::RESIZE_GOOD:
235 return SkBitmapScaler::RESIZE_TRIANGLE;
236 // Users of RESIZE_BETTER are willing to trade some quality in order
237 // to improve performance, but are guaranteed not to devolve to a linear
238 // resampling. In visual tests we see that Hamming-1 is not as good as
239 // Lanczos-2, however it is about 40% faster and Lanczos-2 itself is
240 // about 30% faster than Lanczos-3. The use of Hamming-1 has been deemed
241 // an acceptable trade-off between quality and speed.
242 case SkBitmapScaler::RESIZE_BETTER:
243 return SkBitmapScaler::RESIZE_HAMMING;
244 default:
commit-bot@chromium.orgdcfaa732014-02-14 18:46:08 +0000245#ifdef SK_HIGH_QUALITY_IS_LANCZOS
246 return SkBitmapScaler::RESIZE_LANCZOS3;
247#else
humper@google.com138ebc32013-07-19 20:20:04 +0000248 return SkBitmapScaler::RESIZE_MITCHELL;
commit-bot@chromium.orgdcfaa732014-02-14 18:46:08 +0000249#endif
humper@google.com138ebc32013-07-19 20:20:04 +0000250 }
251}
252
reed3c834322015-06-12 07:09:59 -0700253bool SkBitmapScaler::Resize(SkBitmap* resultPtr, const SkPixmap& source, ResizeMethod method,
commit-bot@chromium.orgf4491562014-05-28 17:30:02 +0000254 float destWidth, float destHeight,
reed@google.com1e182252013-07-24 20:10:42 +0000255 SkBitmap::Allocator* allocator) {
reed3c834322015-06-12 07:09:59 -0700256 if (NULL == source.addr() || source.colorType() != kN32_SkColorType ||
257 source.width() < 1 || source.height() < 1)
258 {
259 return false;
260 }
commit-bot@chromium.orgf4491562014-05-28 17:30:02 +0000261
reed3c834322015-06-12 07:09:59 -0700262 if (destWidth < 1 || destHeight < 1) {
263 // todo: seems like we could handle negative dstWidth/Height, since that
264 // is just a negative scale (flip)
265 return false;
266 }
humper4f96ab32014-06-27 11:27:03 -0700267
reed3c834322015-06-12 07:09:59 -0700268 SkConvolutionProcs convolveProcs= { 0, NULL, NULL, NULL, NULL };
269 PlatformConvolutionProcs(&convolveProcs);
reed5bcef1b2015-05-26 13:57:04 -0700270
reed3c834322015-06-12 07:09:59 -0700271 SkRect destSubset = { 0, 0, destWidth, destHeight };
272
273 // Ensure that the ResizeMethod enumeration is sound.
274 SkASSERT(((RESIZE_FIRST_QUALITY_METHOD <= method) &&
humperaae1a262015-02-26 11:07:23 -0800275 (method <= RESIZE_LAST_QUALITY_METHOD)) ||
276 ((RESIZE_FIRST_ALGORITHM_METHOD <= method) &&
277 (method <= RESIZE_LAST_ALGORITHM_METHOD)));
humper@google.com138ebc32013-07-19 20:20:04 +0000278
reed3c834322015-06-12 07:09:59 -0700279 method = ResizeMethodToAlgorithmMethod(method);
humper@google.com138ebc32013-07-19 20:20:04 +0000280
reed3c834322015-06-12 07:09:59 -0700281 // Check that we deal with an "algorithm methods" from this point onward.
282 SkASSERT((SkBitmapScaler::RESIZE_FIRST_ALGORITHM_METHOD <= method) &&
humperaae1a262015-02-26 11:07:23 -0800283 (method <= SkBitmapScaler::RESIZE_LAST_ALGORITHM_METHOD));
humper@google.com138ebc32013-07-19 20:20:04 +0000284
reed3c834322015-06-12 07:09:59 -0700285 SkResizeFilter filter(method, source.width(), source.height(),
reed5bcef1b2015-05-26 13:57:04 -0700286 destWidth, destHeight, destSubset, convolveProcs);
287
reed3c834322015-06-12 07:09:59 -0700288 // Get a subset encompassing this touched area. We construct the
289 // offsets and row strides such that it looks like a new bitmap, while
290 // referring to the old data.
291 const uint8_t* sourceSubset = reinterpret_cast<const uint8_t*>(source.addr());
reed5bcef1b2015-05-26 13:57:04 -0700292
reed3c834322015-06-12 07:09:59 -0700293 // Convolve into the result.
294 SkBitmap result;
295 result.setInfo(SkImageInfo::MakeN32(SkScalarCeilToInt(destSubset.width()),
reed5bcef1b2015-05-26 13:57:04 -0700296 SkScalarCeilToInt(destSubset.height()),
297 source.alphaType()));
reed3c834322015-06-12 07:09:59 -0700298 result.allocPixels(allocator, NULL);
299 if (!result.readyToDraw()) {
reed5bcef1b2015-05-26 13:57:04 -0700300 return false;
reed3c834322015-06-12 07:09:59 -0700301 }
reed5bcef1b2015-05-26 13:57:04 -0700302
reed3c834322015-06-12 07:09:59 -0700303 BGRAConvolve2D(sourceSubset, static_cast<int>(source.rowBytes()),
reed5bcef1b2015-05-26 13:57:04 -0700304 !source.isOpaque(), filter.xFilter(), filter.yFilter(),
305 static_cast<int>(result.rowBytes()),
306 static_cast<unsigned char*>(result.getPixels()),
307 convolveProcs, true);
308
reed3c834322015-06-12 07:09:59 -0700309 *resultPtr = result;
310 resultPtr->lockPixels();
311 SkASSERT(resultPtr->getPixels());
312 return true;
humper@google.com138ebc32013-07-19 20:20:04 +0000313}
humperd92f5b82014-06-28 20:12:45 -0700314