robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 1 | /* |
| 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 | |
| 8 | #include "GrBlurUtils.h" |
| 9 | #include "GrDrawContext.h" |
bsalomon | 7622863 | 2015-05-29 08:02:10 -0700 | [diff] [blame] | 10 | #include "GrCaps.h" |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 11 | #include "GrContext.h" |
| 12 | #include "effects/GrSimpleTextureEffect.h" |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 13 | #include "GrStyle.h" |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 14 | #include "GrTexture.h" |
| 15 | #include "GrTextureProvider.h" |
| 16 | #include "SkDraw.h" |
bsalomon | f1b7a1d | 2015-09-28 06:26:28 -0700 | [diff] [blame] | 17 | #include "SkGrPriv.h" |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 18 | #include "SkMaskFilter.h" |
| 19 | #include "SkPaint.h" |
| 20 | |
| 21 | static bool clip_bounds_quick_reject(const SkIRect& clipBounds, const SkIRect& rect) { |
| 22 | return clipBounds.isEmpty() || rect.isEmpty() || !SkIRect::Intersects(clipBounds, rect); |
| 23 | } |
| 24 | |
| 25 | // Draw a mask using the supplied paint. Since the coverage/geometry |
| 26 | // is already burnt into the mask this boils down to a rect draw. |
| 27 | // Return true if the mask was successfully drawn. |
| 28 | static bool draw_mask(GrDrawContext* drawContext, |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 29 | const GrClip& clip, |
| 30 | const SkMatrix& viewMatrix, |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 31 | const SkIRect& maskRect, |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 32 | GrPaint* grp, |
| 33 | GrTexture* mask) { |
| 34 | SkMatrix matrix; |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 35 | matrix.setTranslate(-SkIntToScalar(maskRect.fLeft), -SkIntToScalar(maskRect.fTop)); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 36 | matrix.postIDiv(mask->width(), mask->height()); |
| 37 | |
bungeman | 06ca8ec | 2016-06-09 08:01:03 -0700 | [diff] [blame] | 38 | grp->addCoverageFragmentProcessor(GrSimpleTextureEffect::Make(mask, matrix, |
| 39 | kDevice_GrCoordSet)); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 40 | |
| 41 | SkMatrix inverse; |
| 42 | if (!viewMatrix.invert(&inverse)) { |
| 43 | return false; |
| 44 | } |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 45 | drawContext->fillRectWithLocalMatrix(clip, *grp, SkMatrix::I(), |
| 46 | SkRect::Make(maskRect), inverse); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 47 | return true; |
| 48 | } |
| 49 | |
robertphillips | 0e7029e | 2015-11-30 05:45:06 -0800 | [diff] [blame] | 50 | static bool sw_draw_with_mask_filter(GrDrawContext* drawContext, |
| 51 | GrTextureProvider* textureProvider, |
| 52 | const GrClip& clipData, |
| 53 | const SkMatrix& viewMatrix, |
| 54 | const SkPath& devPath, |
| 55 | const SkMaskFilter* filter, |
| 56 | const SkIRect& clipBounds, |
| 57 | GrPaint* grp, |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 58 | SkStrokeRec::InitStyle fillOrHairline) { |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 59 | SkMask srcM, dstM; |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 60 | if (!SkDraw::DrawToMask(devPath, &clipBounds, filter, &viewMatrix, &srcM, |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 61 | SkMask::kComputeBoundsAndRenderImage_CreateMode, fillOrHairline)) { |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 62 | return false; |
| 63 | } |
| 64 | SkAutoMaskFreeImage autoSrc(srcM.fImage); |
| 65 | |
halcanary | 96fcdcc | 2015-08-27 07:41:13 -0700 | [diff] [blame] | 66 | if (!filter->filterMask(&dstM, srcM, viewMatrix, nullptr)) { |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 67 | return false; |
| 68 | } |
| 69 | // this will free-up dstM when we're done (allocated in filterMask()) |
| 70 | SkAutoMaskFreeImage autoDst(dstM.fImage); |
| 71 | |
| 72 | if (clip_bounds_quick_reject(clipBounds, dstM.fBounds)) { |
| 73 | return false; |
| 74 | } |
| 75 | |
| 76 | // we now have a device-aligned 8bit mask in dstM, ready to be drawn using |
| 77 | // the current clip (and identity matrix) and GrPaint settings |
| 78 | GrSurfaceDesc desc; |
| 79 | desc.fWidth = dstM.fBounds.width(); |
| 80 | desc.fHeight = dstM.fBounds.height(); |
| 81 | desc.fConfig = kAlpha_8_GrPixelConfig; |
| 82 | |
bsalomon | eae6200 | 2015-07-31 13:59:30 -0700 | [diff] [blame] | 83 | SkAutoTUnref<GrTexture> texture(textureProvider->createApproxTexture(desc)); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 84 | if (!texture) { |
| 85 | return false; |
| 86 | } |
| 87 | texture->writePixels(0, 0, desc.fWidth, desc.fHeight, desc.fConfig, |
| 88 | dstM.fImage, dstM.fRowBytes); |
| 89 | |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 90 | return draw_mask(drawContext, clipData, viewMatrix, dstM.fBounds, grp, texture); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 91 | } |
| 92 | |
| 93 | // Create a mask of 'devPath' and place the result in 'mask'. |
robertphillips | d4c741e | 2016-04-28 09:55:15 -0700 | [diff] [blame] | 94 | static sk_sp<GrTexture> create_mask_GPU(GrContext* context, |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 95 | const SkIRect& maskRect, |
robertphillips | d4c741e | 2016-04-28 09:55:15 -0700 | [diff] [blame] | 96 | const SkPath& devPath, |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 97 | SkStrokeRec::InitStyle fillOrHairline, |
robertphillips | d4c741e | 2016-04-28 09:55:15 -0700 | [diff] [blame] | 98 | bool doAA, |
| 99 | int sampleCnt) { |
robertphillips | d4c741e | 2016-04-28 09:55:15 -0700 | [diff] [blame] | 100 | if (!doAA) { |
| 101 | // Don't need MSAA if mask isn't AA |
| 102 | sampleCnt = 0; |
| 103 | } |
| 104 | |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 105 | // We actually only need A8, but it often isn't supported as a |
| 106 | // render target so default to RGBA_8888 |
robertphillips | d4c741e | 2016-04-28 09:55:15 -0700 | [diff] [blame] | 107 | GrPixelConfig config = kRGBA_8888_GrPixelConfig; |
| 108 | if (context->caps()->isConfigRenderable(kAlpha_8_GrPixelConfig, sampleCnt > 0)) { |
| 109 | config = kAlpha_8_GrPixelConfig; |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 110 | } |
| 111 | |
robertphillips | 76948d4 | 2016-05-04 12:47:41 -0700 | [diff] [blame] | 112 | sk_sp<GrDrawContext> drawContext(context->newDrawContext(SkBackingFit::kApprox, |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 113 | maskRect.width(), |
| 114 | maskRect.height(), |
robertphillips | d4c741e | 2016-04-28 09:55:15 -0700 | [diff] [blame] | 115 | config, |
| 116 | sampleCnt)); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 117 | if (!drawContext) { |
halcanary | 96fcdcc | 2015-08-27 07:41:13 -0700 | [diff] [blame] | 118 | return nullptr; |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 119 | } |
| 120 | |
robertphillips | 2e1e51f | 2015-10-15 08:01:48 -0700 | [diff] [blame] | 121 | drawContext->clear(nullptr, 0x0, true); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 122 | |
| 123 | GrPaint tempPaint; |
| 124 | tempPaint.setAntiAlias(doAA); |
| 125 | tempPaint.setCoverageSetOpXPFactory(SkRegion::kReplace_Op); |
| 126 | |
| 127 | // setup new clip |
cdalton | 846c051 | 2016-05-13 10:25:00 -0700 | [diff] [blame] | 128 | const SkIRect clipRect = SkIRect::MakeWH(maskRect.width(), maskRect.height()); |
| 129 | GrFixedClip clip(clipRect); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 130 | |
robertphillips | 3833daa | 2015-09-14 11:18:13 -0700 | [diff] [blame] | 131 | // Draw the mask into maskTexture with the path's integerized top-left at |
| 132 | // the origin using tempPaint. |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 133 | SkMatrix translate; |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 134 | translate.setTranslate(-SkIntToScalar(maskRect.fLeft), -SkIntToScalar(maskRect.fTop)); |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 135 | drawContext->drawPath(clip, tempPaint, translate, devPath, GrStyle(fillOrHairline)); |
robertphillips | d4c741e | 2016-04-28 09:55:15 -0700 | [diff] [blame] | 136 | return drawContext->asTexture();; |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 137 | } |
| 138 | |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 139 | static void draw_path_with_mask_filter(GrContext* context, |
| 140 | GrDrawContext* drawContext, |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 141 | const GrClip& clip, |
| 142 | GrPaint* paint, |
| 143 | const SkMatrix& viewMatrix, |
| 144 | const SkMaskFilter* maskFilter, |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 145 | const GrStyle& style, |
| 146 | const SkPath* path, |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 147 | bool pathIsMutable) { |
| 148 | SkASSERT(maskFilter); |
| 149 | |
| 150 | SkIRect clipBounds; |
robertphillips | 7bceedc | 2015-12-01 12:51:26 -0800 | [diff] [blame] | 151 | clip.getConservativeBounds(drawContext->width(), drawContext->height(), &clipBounds); |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 152 | SkTLazy<SkPath> tmpPath; |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 153 | SkStrokeRec::InitStyle fillOrHairline; |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 154 | |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 155 | // We just fully apply the style here. |
| 156 | if (style.applies()) { |
| 157 | if (!style.applyToPath(tmpPath.init(), &fillOrHairline, *path, |
| 158 | GrStyle::MatrixToScaleFactor(viewMatrix))) { |
| 159 | return; |
| 160 | } |
| 161 | pathIsMutable = true; |
| 162 | path = tmpPath.get(); |
| 163 | } else if (style.isSimpleHairline()) { |
| 164 | fillOrHairline = SkStrokeRec::kHairline_InitStyle; |
bsalomon | 055e192 | 2016-05-06 07:22:58 -0700 | [diff] [blame] | 165 | } else { |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 166 | SkASSERT(style.isSimpleFill()); |
| 167 | fillOrHairline = SkStrokeRec::kFill_InitStyle; |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 168 | } |
| 169 | |
| 170 | // transform the path into device space |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 171 | if (!viewMatrix.isIdentity()) { |
| 172 | SkPath* result; |
| 173 | if (pathIsMutable) { |
| 174 | result = const_cast<SkPath*>(path); |
| 175 | } else { |
| 176 | if (!tmpPath.isValid()) { |
| 177 | tmpPath.init(); |
| 178 | } |
| 179 | result = tmpPath.get(); |
| 180 | } |
| 181 | path->transform(viewMatrix, result); |
| 182 | path = result; |
| 183 | result->setIsVolatile(true); |
| 184 | pathIsMutable = true; |
| 185 | } |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 186 | |
| 187 | SkRect maskRect; |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 188 | if (maskFilter->canFilterMaskGPU(SkRRect::MakeRect(path->getBounds()), |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 189 | clipBounds, |
| 190 | viewMatrix, |
| 191 | &maskRect)) { |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 192 | // This mask will ultimately be drawn as a non-AA rect (see draw_mask). |
| 193 | // Non-AA rects have a bad habit of snapping arbitrarily. Integerize here |
| 194 | // so the mask draws in a reproducible manner. |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 195 | SkIRect finalIRect; |
| 196 | maskRect.roundOut(&finalIRect); |
| 197 | if (clip_bounds_quick_reject(clipBounds, finalIRect)) { |
| 198 | // clipped out |
| 199 | return; |
| 200 | } |
| 201 | |
| 202 | if (maskFilter->directFilterMaskGPU(context->textureProvider(), |
| 203 | drawContext, |
| 204 | paint, |
| 205 | clip, |
| 206 | viewMatrix, |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 207 | SkStrokeRec(fillOrHairline), |
| 208 | *path)) { |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 209 | // the mask filter was able to draw itself directly, so there's nothing |
| 210 | // left to do. |
| 211 | return; |
| 212 | } |
| 213 | |
robertphillips | d4c741e | 2016-04-28 09:55:15 -0700 | [diff] [blame] | 214 | sk_sp<GrTexture> mask(create_mask_GPU(context, |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 215 | finalIRect, |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 216 | *path, |
| 217 | fillOrHairline, |
robertphillips | d4c741e | 2016-04-28 09:55:15 -0700 | [diff] [blame] | 218 | paint->isAntiAlias(), |
| 219 | drawContext->numColorSamples())); |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 220 | if (mask) { |
| 221 | GrTexture* filtered; |
| 222 | |
robertphillips | 8bad3ac | 2016-06-27 11:11:05 -0700 | [diff] [blame] | 223 | if (maskFilter->filterMaskGPU(mask.get(), viewMatrix, finalIRect, &filtered)) { |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 224 | // filterMaskGPU gives us ownership of a ref to the result |
| 225 | SkAutoTUnref<GrTexture> atu(filtered); |
robertphillips | f054b17 | 2016-05-13 05:06:19 -0700 | [diff] [blame] | 226 | if (draw_mask(drawContext, clip, viewMatrix, finalIRect, paint, filtered)) { |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 227 | // This path is completely drawn |
| 228 | return; |
| 229 | } |
| 230 | } |
| 231 | } |
| 232 | } |
| 233 | |
robertphillips | 0e7029e | 2015-11-30 05:45:06 -0800 | [diff] [blame] | 234 | sw_draw_with_mask_filter(drawContext, context->textureProvider(), |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 235 | clip, viewMatrix, *path, |
| 236 | maskFilter, clipBounds, paint, fillOrHairline); |
| 237 | } |
| 238 | |
| 239 | void GrBlurUtils::drawPathWithMaskFilter(GrContext* context, |
| 240 | GrDrawContext* drawContext, |
| 241 | const GrClip& clip, |
| 242 | const SkPath& path, |
| 243 | GrPaint* paint, |
| 244 | const SkMatrix& viewMatrix, |
| 245 | const SkMaskFilter* mf, |
| 246 | const GrStyle& style, |
| 247 | bool pathIsMutable) { |
| 248 | draw_path_with_mask_filter(context, drawContext, clip, paint, viewMatrix, mf, |
| 249 | style, &path, pathIsMutable); |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 250 | } |
| 251 | |
| 252 | void GrBlurUtils::drawPathWithMaskFilter(GrContext* context, |
| 253 | GrDrawContext* drawContext, |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 254 | const GrClip& clip, |
| 255 | const SkPath& origPath, |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 256 | const SkPaint& paint, |
| 257 | const SkMatrix& origViewMatrix, |
| 258 | const SkMatrix* prePathMatrix, |
| 259 | const SkIRect& clipBounds, |
| 260 | bool pathIsMutable) { |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 261 | SkASSERT(!pathIsMutable || origPath.isVolatile()); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 262 | |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 263 | GrStyle style(paint); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 264 | // If we have a prematrix, apply it to the path, optimizing for the case |
| 265 | // where the original path can in fact be modified in place (even though |
| 266 | // its parameter type is const). |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 267 | |
| 268 | const SkPath* path = &origPath; |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 269 | SkTLazy<SkPath> tmpPath; |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 270 | |
| 271 | SkMatrix viewMatrix = origViewMatrix; |
| 272 | |
| 273 | if (prePathMatrix) { |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 274 | // Styling, blurs, and shading are supposed to be applied *after* the prePathMatrix. |
| 275 | if (!paint.getMaskFilter() && !paint.getShader() && !style.applies()) { |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 276 | viewMatrix.preConcat(*prePathMatrix); |
| 277 | } else { |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 278 | SkPath* result = pathIsMutable ? const_cast<SkPath*>(path) : tmpPath.init(); |
| 279 | pathIsMutable = true; |
| 280 | path->transform(*prePathMatrix, result); |
| 281 | path = result; |
| 282 | result->setIsVolatile(true); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 283 | } |
| 284 | } |
| 285 | // at this point we're done with prePathMatrix |
| 286 | SkDEBUGCODE(prePathMatrix = (const SkMatrix*)0x50FF8001;) |
| 287 | |
| 288 | GrPaint grPaint; |
brianosman | b461d34 | 2016-04-13 13:10:14 -0700 | [diff] [blame] | 289 | if (!SkPaintToGrPaint(context, paint, viewMatrix, drawContext->isGammaCorrect(), |
brianosman | 898235c | 2016-04-06 07:38:23 -0700 | [diff] [blame] | 290 | &grPaint)) { |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 291 | return; |
| 292 | } |
| 293 | |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 294 | if (paint.getMaskFilter()) { |
robertphillips | 7bceedc | 2015-12-01 12:51:26 -0800 | [diff] [blame] | 295 | draw_path_with_mask_filter(context, drawContext, clip, &grPaint, viewMatrix, |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 296 | paint.getMaskFilter(), style, |
| 297 | path, pathIsMutable); |
bsalomon | c55271f | 2015-11-09 11:55:57 -0800 | [diff] [blame] | 298 | } else { |
bsalomon | 6663acf | 2016-05-10 09:14:17 -0700 | [diff] [blame] | 299 | drawContext->drawPath(clip, grPaint, viewMatrix, *path, style); |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 300 | } |
robertphillips | ccb1b57 | 2015-05-27 11:02:55 -0700 | [diff] [blame] | 301 | } |