Dominic Mazzoni | 394d414 | 2017-02-14 11:15:31 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2017 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 "SkHighContrastFilter.h" |
| 9 | |
| 10 | #include "SkArenaAlloc.h" |
| 11 | #include "SkRasterPipeline.h" |
| 12 | #include "SkReadBuffer.h" |
| 13 | #include "SkString.h" |
| 14 | #include "SkWriteBuffer.h" |
| 15 | |
| 16 | #if SK_SUPPORT_GPU |
| 17 | #include "GrContext.h" |
| 18 | #include "glsl/GrGLSLFragmentProcessor.h" |
| 19 | #include "glsl/GrGLSLFragmentShaderBuilder.h" |
| 20 | #endif |
| 21 | |
| 22 | using InvertStyle = SkHighContrastConfig::InvertStyle; |
| 23 | |
| 24 | namespace { |
| 25 | |
| 26 | SkScalar Hue2RGB(SkScalar p, SkScalar q, SkScalar t) { |
| 27 | if (t < 0) { |
| 28 | t += 1; |
| 29 | } else if (t > 1) { |
| 30 | t -= 1; |
| 31 | } |
| 32 | |
| 33 | if (t < 1/6.f) { |
| 34 | return p + (q - p) * 6 * t; |
| 35 | } |
| 36 | |
| 37 | if (t < 1/2.f) { |
| 38 | return q; |
| 39 | } |
| 40 | |
| 41 | if (t < 2/3.f) { |
| 42 | return p + (q - p) * (2/3.f - t) * 6; |
| 43 | } |
| 44 | |
| 45 | return p; |
| 46 | } |
| 47 | |
| 48 | uint8_t SkScalarToUint8Clamp(SkScalar f) { |
| 49 | if (f <= 0) { |
| 50 | return 0; |
| 51 | } else if (f >= 1) { |
| 52 | return 255; |
| 53 | } |
| 54 | return static_cast<unsigned char>(255 * f); |
| 55 | } |
| 56 | |
| 57 | SkScalar IncreaseContrast(SkScalar f, SkScalar contrast) { |
| 58 | SkScalar m = (1 + contrast) / (1 - contrast); |
| 59 | SkScalar b = (-0.5f * m + 0.5f); |
| 60 | return m * f + b; |
| 61 | } |
| 62 | |
| 63 | static SkPMColor ApplyHighContrastFilter(const SkHighContrastConfig& config, |
| 64 | SkPMColor pmColor) { |
| 65 | SkColor color = SkUnPreMultiply::PMColorToColor(pmColor); |
| 66 | SkScalar rf = SkColorGetR(color) / 255.f; |
| 67 | SkScalar gf = SkColorGetG(color) / 255.f; |
| 68 | SkScalar bf = SkColorGetB(color) / 255.f; |
| 69 | |
| 70 | // Apply a gamma of 2.0 so that the rest of the calculations |
| 71 | // happen roughly in linear space. |
| 72 | rf *= rf; |
| 73 | gf *= gf; |
| 74 | bf *= bf; |
| 75 | |
| 76 | // Convert to grayscale using luminance coefficients. |
| 77 | if (config.fGrayscale) { |
| 78 | SkScalar lum = |
| 79 | rf * SK_LUM_COEFF_R + gf * SK_LUM_COEFF_G + bf * SK_LUM_COEFF_B; |
| 80 | rf = lum; |
| 81 | gf = lum; |
| 82 | bf = lum; |
| 83 | } |
| 84 | |
| 85 | // Now invert. |
| 86 | if (config.fInvertStyle == InvertStyle::kInvertBrightness) { |
| 87 | rf = 1 - rf; |
| 88 | gf = 1 - gf; |
| 89 | bf = 1 - bf; |
| 90 | } else if (config.fInvertStyle == InvertStyle::kInvertLightness) { |
| 91 | // Convert to HSL |
| 92 | SkScalar max = SkTMax(SkTMax(rf, gf), bf); |
| 93 | SkScalar min = SkTMin(SkTMin(rf, gf), bf); |
| 94 | SkScalar l = (max + min) / 2; |
| 95 | SkScalar h, s; |
| 96 | |
| 97 | if (max == min) { |
| 98 | h = 0; |
| 99 | s = 0; |
| 100 | } else { |
| 101 | SkScalar d = max - min; |
| 102 | s = l > 0.5f ? d / (2 - max - min) : d / (max + min); |
| 103 | if (max == rf) { |
| 104 | h = (gf - bf) / d + (gf < bf ? 6 : 0); |
| 105 | } else if (max == gf) { |
| 106 | h = (bf - rf) / d + 2; |
| 107 | } else { |
| 108 | h = (rf - gf) / d + 4; |
| 109 | } |
| 110 | h /= 6; |
| 111 | } |
| 112 | |
| 113 | // Invert lightness. |
| 114 | l = 1 - l; |
| 115 | |
| 116 | // Now convert back to RGB. |
| 117 | if (s == 0) { |
| 118 | // Grayscale |
| 119 | rf = l; |
| 120 | gf = l; |
| 121 | bf = l; |
| 122 | } else { |
| 123 | SkScalar q = l < 0.5f ? l * (1 + s) : l + s - l * s; |
| 124 | SkScalar p = 2 * l - q; |
| 125 | rf = Hue2RGB(p, q, h + 1/3.f); |
| 126 | gf = Hue2RGB(p, q, h); |
| 127 | bf = Hue2RGB(p, q, h - 1/3.f); |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | // Increase contrast. |
| 132 | if (config.fContrast != 0.0f) { |
| 133 | rf = IncreaseContrast(rf, config.fContrast); |
| 134 | gf = IncreaseContrast(gf, config.fContrast); |
| 135 | bf = IncreaseContrast(bf, config.fContrast); |
| 136 | } |
| 137 | |
| 138 | // Convert back from linear to a color space with a gamma of ~2.0. |
| 139 | rf = SkScalarSqrt(rf); |
| 140 | gf = SkScalarSqrt(gf); |
| 141 | bf = SkScalarSqrt(bf); |
| 142 | |
| 143 | return SkPremultiplyARGBInline(SkColorGetA(color), |
| 144 | SkScalarToUint8Clamp(rf), |
| 145 | SkScalarToUint8Clamp(gf), |
| 146 | SkScalarToUint8Clamp(bf)); |
| 147 | } |
| 148 | |
| 149 | } // namespace |
| 150 | |
| 151 | class SkHighContrast_Filter : public SkColorFilter { |
| 152 | public: |
| 153 | SkHighContrast_Filter(const SkHighContrastConfig& config) { |
| 154 | fConfig = config; |
| 155 | // Clamp contrast to just inside -1 to 1 to avoid division by zero. |
| 156 | fConfig.fContrast = SkScalarPin(fConfig.fContrast, |
| 157 | -1.0f + FLT_EPSILON, |
| 158 | 1.0f - FLT_EPSILON); |
| 159 | } |
| 160 | |
| 161 | virtual ~SkHighContrast_Filter() { } |
| 162 | |
| 163 | #if SK_SUPPORT_GPU |
| 164 | sk_sp<GrFragmentProcessor> asFragmentProcessor(GrContext*, SkColorSpace*) const override; |
| 165 | #endif |
| 166 | |
| 167 | void filterSpan(const SkPMColor src[], int count, SkPMColor dst[]) const |
| 168 | override; |
| 169 | bool onAppendStages(SkRasterPipeline* p, |
| 170 | SkColorSpace* dst, |
| 171 | SkArenaAlloc* scratch, |
| 172 | bool shaderIsOpaque) const override; |
| 173 | |
| 174 | SK_TO_STRING_OVERRIDE() |
| 175 | |
| 176 | SK_DECLARE_PUBLIC_FLATTENABLE_DESERIALIZATION_PROCS(SkHighContrast_Filter) |
| 177 | |
| 178 | protected: |
| 179 | void flatten(SkWriteBuffer&) const override; |
| 180 | |
| 181 | private: |
| 182 | SkHighContrastConfig fConfig; |
| 183 | |
| 184 | friend class SkHighContrastFilter; |
| 185 | |
| 186 | typedef SkColorFilter INHERITED; |
| 187 | }; |
| 188 | |
| 189 | void SkHighContrast_Filter::filterSpan(const SkPMColor src[], int count, |
| 190 | SkPMColor dst[]) const { |
| 191 | for (int i = 0; i < count; ++i) |
| 192 | dst[i] = ApplyHighContrastFilter(fConfig, src[i]); |
| 193 | } |
| 194 | |
| 195 | bool SkHighContrast_Filter::onAppendStages(SkRasterPipeline* p, |
| 196 | SkColorSpace* dst, |
| 197 | SkArenaAlloc* scratch, |
| 198 | bool shaderIsOpaque) const { |
| 199 | if (!shaderIsOpaque) { |
| 200 | p->append(SkRasterPipeline::unpremul); |
| 201 | } |
| 202 | |
| 203 | if (fConfig.fGrayscale) { |
| 204 | float r = SK_LUM_COEFF_R; |
| 205 | float g = SK_LUM_COEFF_G; |
| 206 | float b = SK_LUM_COEFF_B; |
| 207 | float* matrix = scratch->makeArray<float>(12); |
| 208 | matrix[0] = matrix[1] = matrix[2] = r; |
| 209 | matrix[3] = matrix[4] = matrix[5] = g; |
| 210 | matrix[6] = matrix[7] = matrix[8] = b; |
| 211 | p->append(SkRasterPipeline::matrix_3x4, matrix); |
| 212 | } |
| 213 | |
| 214 | if (fConfig.fInvertStyle == InvertStyle::kInvertBrightness) { |
| 215 | float* matrix = scratch->makeArray<float>(12); |
| 216 | matrix[0] = matrix[4] = matrix[8] = -1; |
| 217 | matrix[9] = matrix[10] = matrix[11] = 1; |
| 218 | p->append(SkRasterPipeline::matrix_3x4, matrix); |
| 219 | } else if (fConfig.fInvertStyle == InvertStyle::kInvertLightness) { |
| 220 | p->append(SkRasterPipeline::rgb_to_hsl); |
| 221 | float* matrix = scratch->makeArray<float>(12); |
| 222 | matrix[0] = matrix[4] = matrix[11] = 1; |
| 223 | matrix[8] = -1; |
| 224 | p->append(SkRasterPipeline::matrix_3x4, matrix); |
| 225 | p->append(SkRasterPipeline::hsl_to_rgb); |
| 226 | } |
| 227 | |
| 228 | if (fConfig.fContrast != 0.0) { |
| 229 | float* matrix = scratch->makeArray<float>(12); |
| 230 | float c = fConfig.fContrast; |
| 231 | float m = (1 + c) / (1 - c); |
| 232 | float b = (-0.5f * m + 0.5f); |
| 233 | matrix[0] = matrix[4] = matrix[8] = m; |
| 234 | matrix[9] = matrix[10] = matrix[11] = b; |
| 235 | p->append(SkRasterPipeline::matrix_3x4, matrix); |
| 236 | } |
| 237 | |
| 238 | p->append(SkRasterPipeline::clamp_0); |
| 239 | p->append(SkRasterPipeline::clamp_1); |
| 240 | |
| 241 | if (!shaderIsOpaque) { |
| 242 | p->append(SkRasterPipeline::premul); |
| 243 | } |
| 244 | |
| 245 | return true; |
| 246 | } |
| 247 | |
| 248 | void SkHighContrast_Filter::flatten(SkWriteBuffer& buffer) const { |
| 249 | buffer.writeBool(fConfig.fGrayscale); |
| 250 | buffer.writeInt(static_cast<int>(fConfig.fInvertStyle)); |
| 251 | buffer.writeScalar(fConfig.fContrast); |
| 252 | } |
| 253 | |
| 254 | sk_sp<SkFlattenable> SkHighContrast_Filter::CreateProc(SkReadBuffer& buffer) { |
| 255 | SkHighContrastConfig config; |
| 256 | config.fGrayscale = buffer.readBool(); |
| 257 | config.fInvertStyle = static_cast<InvertStyle>(buffer.readInt()); |
| 258 | config.fContrast = buffer.readScalar(); |
| 259 | return SkHighContrastFilter::Make(config); |
| 260 | } |
| 261 | |
| 262 | sk_sp<SkColorFilter> SkHighContrastFilter::Make( |
| 263 | const SkHighContrastConfig& config) { |
| 264 | if (!config.isValid()) |
| 265 | return nullptr; |
| 266 | return sk_make_sp<SkHighContrast_Filter>(config); |
| 267 | } |
| 268 | |
| 269 | #ifndef SK_IGNORE_TO_STRING |
| 270 | void SkHighContrast_Filter::toString(SkString* str) const { |
| 271 | str->append("SkHighContrastColorFilter "); |
| 272 | } |
| 273 | #endif |
| 274 | |
| 275 | SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_START(SkHighContrastFilter) |
| 276 | SK_DEFINE_FLATTENABLE_REGISTRAR_ENTRY(SkHighContrast_Filter) |
| 277 | SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_END |
| 278 | |
| 279 | #if SK_SUPPORT_GPU |
| 280 | class HighContrastFilterEffect : public GrFragmentProcessor { |
| 281 | public: |
| 282 | static sk_sp<GrFragmentProcessor> Make(const SkHighContrastConfig& config) { |
| 283 | return sk_sp<GrFragmentProcessor>(new HighContrastFilterEffect(config)); |
| 284 | } |
| 285 | |
| 286 | const char* name() const override { return "HighContrastFilter"; } |
| 287 | |
| 288 | const SkHighContrastConfig& config() const { return fConfig; } |
| 289 | |
| 290 | private: |
| 291 | HighContrastFilterEffect(const SkHighContrastConfig& config) |
| 292 | : INHERITED(kNone_OptimizationFlags) |
| 293 | , fConfig(config) { |
| 294 | this->initClassID<HighContrastFilterEffect>(); |
| 295 | } |
| 296 | |
| 297 | GrGLSLFragmentProcessor* onCreateGLSLInstance() const override; |
| 298 | |
| 299 | virtual void onGetGLSLProcessorKey(const GrShaderCaps& caps, |
| 300 | GrProcessorKeyBuilder* b) const override; |
| 301 | |
| 302 | bool onIsEqual(const GrFragmentProcessor& other) const override { |
| 303 | const HighContrastFilterEffect& that = other.cast<HighContrastFilterEffect>(); |
| 304 | return fConfig.fGrayscale == that.fConfig.fGrayscale && |
| 305 | fConfig.fInvertStyle == that.fConfig.fInvertStyle && |
| 306 | fConfig.fContrast == that.fConfig.fContrast; |
| 307 | } |
| 308 | |
| 309 | SkHighContrastConfig fConfig; |
| 310 | |
| 311 | typedef GrFragmentProcessor INHERITED; |
| 312 | }; |
| 313 | |
| 314 | class GLHighContrastFilterEffect : public GrGLSLFragmentProcessor { |
| 315 | public: |
| 316 | static void GenKey(const GrProcessor&, const GrShaderCaps&, GrProcessorKeyBuilder*); |
| 317 | |
| 318 | GLHighContrastFilterEffect(const SkHighContrastConfig& config); |
| 319 | |
| 320 | protected: |
| 321 | void onSetData(const GrGLSLProgramDataManager&, const GrProcessor&) override; |
| 322 | void emitCode(EmitArgs& args) override; |
| 323 | |
| 324 | private: |
| 325 | UniformHandle fContrastUni; |
| 326 | SkHighContrastConfig fConfig; |
| 327 | |
| 328 | typedef GrGLSLFragmentProcessor INHERITED; |
| 329 | }; |
| 330 | |
| 331 | GrGLSLFragmentProcessor* HighContrastFilterEffect::onCreateGLSLInstance() const { |
| 332 | return new GLHighContrastFilterEffect(fConfig); |
| 333 | } |
| 334 | |
| 335 | void HighContrastFilterEffect::onGetGLSLProcessorKey(const GrShaderCaps& caps, |
| 336 | GrProcessorKeyBuilder* b) const { |
| 337 | GLHighContrastFilterEffect::GenKey(*this, caps, b); |
| 338 | } |
| 339 | |
| 340 | void GLHighContrastFilterEffect::onSetData(const GrGLSLProgramDataManager& pdm, const GrProcessor& proc) { |
| 341 | const HighContrastFilterEffect& hcfe = proc.cast<HighContrastFilterEffect>(); |
| 342 | pdm.set1f(fContrastUni, hcfe.config().fContrast); |
| 343 | } |
| 344 | |
| 345 | GLHighContrastFilterEffect::GLHighContrastFilterEffect(const SkHighContrastConfig& config) |
| 346 | : INHERITED() |
| 347 | , fConfig(config) { |
| 348 | } |
| 349 | |
| 350 | void GLHighContrastFilterEffect::GenKey( |
| 351 | const GrProcessor& proc, const GrShaderCaps&, GrProcessorKeyBuilder* b) { |
| 352 | const HighContrastFilterEffect& hcfe = proc.cast<HighContrastFilterEffect>(); |
| 353 | b->add32(static_cast<uint32_t>(hcfe.config().fGrayscale)); |
| 354 | b->add32(static_cast<uint32_t>(hcfe.config().fInvertStyle)); |
| 355 | } |
| 356 | |
| 357 | void GLHighContrastFilterEffect::emitCode(EmitArgs& args) { |
| 358 | const char* contrast; |
| 359 | fContrastUni = args.fUniformHandler->addUniform(kFragment_GrShaderFlag, |
| 360 | kFloat_GrSLType, kDefault_GrSLPrecision, |
| 361 | "contrast", &contrast); |
| 362 | |
| 363 | if (nullptr == args.fInputColor) { |
| 364 | args.fInputColor = "vec4(1)"; |
| 365 | } |
| 366 | |
| 367 | GrGLSLFPFragmentBuilder* fragBuilder = args.fFragBuilder; |
| 368 | |
| 369 | fragBuilder->codeAppendf("vec4 color = %s;", args.fInputColor); |
| 370 | |
| 371 | // Unpremultiply. The max() is to guard against 0 / 0. |
| 372 | fragBuilder->codeAppendf("float nonZeroAlpha = max(color.a, 0.00001);"); |
| 373 | fragBuilder->codeAppendf("color = vec4(color.rgb / nonZeroAlpha, nonZeroAlpha);"); |
| 374 | |
| 375 | // Grayscale. |
| 376 | if (fConfig.fGrayscale) { |
| 377 | fragBuilder->codeAppendf("float luma = dot(color, vec4(%f, %f, %f, 0));", |
| 378 | SK_LUM_COEFF_R, SK_LUM_COEFF_G, SK_LUM_COEFF_B); |
| 379 | fragBuilder->codeAppendf("color = vec4(luma, luma, luma, 0);"); |
| 380 | } |
| 381 | |
| 382 | if (fConfig.fInvertStyle == InvertStyle::kInvertBrightness) { |
| 383 | fragBuilder->codeAppendf("color = vec4(1, 1, 1, 1) - color;"); |
| 384 | } |
| 385 | |
| 386 | if (fConfig.fInvertStyle == InvertStyle::kInvertLightness) { |
| 387 | // Convert from RGB to HSL. |
| 388 | fragBuilder->codeAppendf("float fmax = max(color.r, max(color.g, color.b));"); |
| 389 | fragBuilder->codeAppendf("float fmin = min(color.r, min(color.g, color.b));"); |
| 390 | fragBuilder->codeAppendf("float l = (fmax + fmin) / 2;"); |
| 391 | |
| 392 | fragBuilder->codeAppendf("float h;"); |
| 393 | fragBuilder->codeAppendf("float s;"); |
| 394 | |
| 395 | fragBuilder->codeAppendf("if (fmax == fmin) {"); |
| 396 | fragBuilder->codeAppendf(" h = 0;"); |
| 397 | fragBuilder->codeAppendf(" s = 0;"); |
| 398 | fragBuilder->codeAppendf("} else {"); |
| 399 | fragBuilder->codeAppendf(" float d = fmax - fmin;"); |
| 400 | fragBuilder->codeAppendf(" s = l > 0.5 ?"); |
| 401 | fragBuilder->codeAppendf(" d / (2 - fmax - fmin) :"); |
| 402 | fragBuilder->codeAppendf(" d / (fmax + fmin);"); |
| 403 | fragBuilder->codeAppendf(" if (fmax == color.r) {"); |
| 404 | fragBuilder->codeAppendf(" h = (color.g - color.b) / d + "); |
| 405 | fragBuilder->codeAppendf(" (color.g < color.b ? 6 : 0);"); |
| 406 | fragBuilder->codeAppendf(" } else if (fmax == color.g) {"); |
| 407 | fragBuilder->codeAppendf(" h = (color.b - color.r) / d + 2;"); |
| 408 | fragBuilder->codeAppendf(" } else {"); |
| 409 | fragBuilder->codeAppendf(" h = (color.r - color.g) / d + 4;"); |
| 410 | fragBuilder->codeAppendf(" }"); |
| 411 | fragBuilder->codeAppendf("}"); |
| 412 | fragBuilder->codeAppendf("h /= 6;"); |
| 413 | fragBuilder->codeAppendf("l = 1.0 - l;"); |
| 414 | // Convert back from HSL to RGB. |
| 415 | SkString hue2rgbFuncName; |
| 416 | static const GrShaderVar gHue2rgbArgs[] = { |
| 417 | GrShaderVar("p", kFloat_GrSLType), |
| 418 | GrShaderVar("q", kFloat_GrSLType), |
| 419 | GrShaderVar("t", kFloat_GrSLType), |
| 420 | }; |
| 421 | fragBuilder->emitFunction(kFloat_GrSLType, |
| 422 | "hue2rgb", |
| 423 | SK_ARRAY_COUNT(gHue2rgbArgs), |
| 424 | gHue2rgbArgs, |
| 425 | "if (t < 0)" |
| 426 | " t += 1;" |
| 427 | "if (t > 1)" |
| 428 | " t -= 1;" |
| 429 | "if (t < 1/6.)" |
| 430 | " return p + (q - p) * 6 * t;" |
| 431 | "if (t < 1/2.)" |
| 432 | " return q;" |
| 433 | "if (t < 2/3.)" |
| 434 | " return p + (q - p) * (2/3. - t) * 6;" |
| 435 | "return p;", |
| 436 | &hue2rgbFuncName); |
| 437 | fragBuilder->codeAppendf("if (s == 0) {"); |
| 438 | fragBuilder->codeAppendf(" color = vec4(l, l, l, 0);"); |
| 439 | fragBuilder->codeAppendf("} else {"); |
| 440 | fragBuilder->codeAppendf(" float q = l < 0.5 ? l * (1 + s) : l + s - l * s;"); |
| 441 | fragBuilder->codeAppendf(" float p = 2 * l - q;"); |
| 442 | fragBuilder->codeAppendf(" color.r = %s(p, q, h + 1/3.);", hue2rgbFuncName.c_str()); |
| 443 | fragBuilder->codeAppendf(" color.g = %s(p, q, h);", hue2rgbFuncName.c_str()); |
| 444 | fragBuilder->codeAppendf(" color.b = %s(p, q, h - 1/3.);", hue2rgbFuncName.c_str()); |
| 445 | fragBuilder->codeAppendf("}"); |
| 446 | } |
| 447 | |
| 448 | // Contrast. |
| 449 | fragBuilder->codeAppendf("if (%s != 0) {", contrast); |
| 450 | fragBuilder->codeAppendf(" float m = (1 + %s) / (1 - %s);", contrast, contrast); |
| 451 | fragBuilder->codeAppendf(" float off = (-0.5 * m + 0.5);"); |
| 452 | fragBuilder->codeAppendf(" color = m * color + off;"); |
| 453 | fragBuilder->codeAppendf("}"); |
| 454 | |
| 455 | // Clamp. |
| 456 | fragBuilder->codeAppendf("color = clamp(color, 0, 1);"); |
| 457 | |
| 458 | // Restore the original alpha and premultiply. |
| 459 | fragBuilder->codeAppendf("color.a = %s.a;", args.fInputColor); |
| 460 | fragBuilder->codeAppendf("color.rgb *= color.a;"); |
| 461 | |
| 462 | // Copy to the output color. |
| 463 | fragBuilder->codeAppendf("%s = color;", args.fOutputColor); |
| 464 | } |
| 465 | |
| 466 | sk_sp<GrFragmentProcessor> SkHighContrast_Filter::asFragmentProcessor(GrContext*, SkColorSpace*) const { |
| 467 | return HighContrastFilterEffect::Make(fConfig); |
| 468 | } |
| 469 | #endif |