BlendMode.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.pdmodel.graphics.blend;
import java.util.HashMap;
import java.util.Map;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSName;
/**
* Blend mode.
*
* @author Kühn & Weyh Software GmbH
*/
public class BlendMode
{
@FunctionalInterface
public interface BlendChannelFunction
{
/**
* BlendChannel function for separable blend modes.
*
* @param src the source value
* @param dest the destination value
* @return the function result
*/
float blendChannel(float src, float dest);
}
@FunctionalInterface
public interface BlendFunction
{
/**
* Blend function for non separable blend modes.
*
* @param src the source values
* @param dest the destination values
* @param result the function result values
*/
void blend(float[] src, float[] dest, float[] result);
}
/**
* Functions for the blend operation of separable blend modes
*/
private static BlendChannelFunction fNormal = (src, dest) -> src;
private static BlendChannelFunction fMultiply = (src, dest) -> src * dest;
private static BlendChannelFunction fScreen = (src, dest) -> src + dest - src * dest;
private static BlendChannelFunction fOverlay = (src, dest) -> (dest <= 0.5) ? 2 * dest * src
: 2 * (src + dest - src * dest) - 1;
private static BlendChannelFunction fDarken = Math::min;
private static BlendChannelFunction fLighten = Math::max;
private static BlendChannelFunction fColorDodge = (src, dest) -> {
// See PDF 2.0 specification
if (Float.compare(dest, 0) == 0)
{
return 0f;
}
if (dest >= 1 - src)
{
return 1f;
}
return dest / (1 - src);
};
private static BlendChannelFunction fColorBurn = (src, dest) -> {
// See PDF 2.0 specification
if (Float.compare(dest, 1) == 0)
{
return 1f;
}
if (1 - dest >= src)
{
return 0f;
}
return 1 - (1 - dest) / src;
};
private static BlendChannelFunction fHardLight = (src, dest) -> (src <= 0.5) ? 2 * dest * src
: 2 * (src + dest - src * dest) - 1;
private static BlendChannelFunction fSoftLight = (src, dest) -> {
if (src <= 0.5)
{
return dest - (1 - 2 * src) * dest * (1 - dest);
}
else
{
float d = (dest <= 0.25) ? ((16 * dest - 12) * dest + 4) * dest
: (float) Math.sqrt(dest);
return dest + (2 * src - 1) * (d - dest);
}
};
private static BlendChannelFunction fDifference = (src, dest) -> Math.abs(dest - src);
private static BlendChannelFunction fExclusion = (src, dest) -> dest + src - 2 * dest * src;
/**
* Functions for the blend operation of non-separable blend modes
*/
private static BlendFunction fHue = (src, dest, result) -> {
float[] temp = new float[3];
getSaturationRGB(dest, src, temp);
getLuminosityRGB(dest, temp, result);
};
private static BlendFunction fSaturation = BlendMode::getSaturationRGB;
private static BlendFunction fColor = (src, dest, result) -> getLuminosityRGB(dest, src,
result);
private static BlendFunction fLuminosity = BlendMode::getLuminosityRGB;
/**
* Separable blend modes as defined in the PDF specification
*/
public static final BlendMode NORMAL = new BlendMode(COSName.NORMAL, fNormal, null);
public static final BlendMode COMPATIBLE = BlendMode.NORMAL;
public static final BlendMode MULTIPLY = new BlendMode(COSName.MULTIPLY, fMultiply, null);
public static final BlendMode SCREEN = new BlendMode(COSName.SCREEN, fScreen, null);
public static final BlendMode OVERLAY = new BlendMode(COSName.OVERLAY, fOverlay, null);
public static final BlendMode DARKEN = new BlendMode(COSName.DARKEN, fDarken, null);
public static final BlendMode LIGHTEN = new BlendMode(COSName.LIGHTEN, fLighten, null);
public static final BlendMode COLOR_DODGE = new BlendMode(COSName.COLOR_DODGE, fColorDodge,
null);
public static final BlendMode COLOR_BURN = new BlendMode(COSName.COLOR_BURN, fColorBurn, null);
public static final BlendMode HARD_LIGHT = new BlendMode(COSName.HARD_LIGHT, fHardLight, null);
public static final BlendMode SOFT_LIGHT = new BlendMode(COSName.SOFT_LIGHT, fSoftLight, null);
public static final BlendMode DIFFERENCE = new BlendMode(COSName.DIFFERENCE, fDifference, null);
public static final BlendMode EXCLUSION = new BlendMode(COSName.EXCLUSION, fExclusion, null);
/**
* Non-separable blend modes as defined in the PDF specification
*/
public static final BlendMode HUE = new BlendMode(COSName.HUE, null, fHue);
public static final BlendMode SATURATION = new BlendMode(COSName.SATURATION, null, fSaturation);
public static final BlendMode COLOR = new BlendMode(COSName.COLOR, null, fColor);
public static final BlendMode LUMINOSITY = new BlendMode(COSName.LUMINOSITY, null, fLuminosity);
private static final Map<COSName, BlendMode> BLEND_MODES = createBlendModeMap();
private static Map<COSName, BlendMode> createBlendModeMap()
{
Map<COSName, BlendMode> map = new HashMap<>(13);
map.put(COSName.NORMAL, NORMAL);
// BlendMode.COMPATIBLE should not be used
map.put(COSName.COMPATIBLE, NORMAL);
map.put(COSName.MULTIPLY, MULTIPLY);
map.put(COSName.SCREEN, SCREEN);
map.put(COSName.OVERLAY, OVERLAY);
map.put(COSName.DARKEN, DARKEN);
map.put(COSName.LIGHTEN, LIGHTEN);
map.put(COSName.COLOR_DODGE, COLOR_DODGE);
map.put(COSName.COLOR_BURN, COLOR_BURN);
map.put(COSName.HARD_LIGHT, HARD_LIGHT);
map.put(COSName.SOFT_LIGHT, SOFT_LIGHT);
map.put(COSName.DIFFERENCE, DIFFERENCE);
map.put(COSName.EXCLUSION, EXCLUSION);
map.put(COSName.HUE, HUE);
map.put(COSName.SATURATION, SATURATION);
map.put(COSName.LUMINOSITY, LUMINOSITY);
map.put(COSName.COLOR, COLOR);
return map;
}
private final COSName name;
private final BlendChannelFunction blendChannel;
private final BlendFunction blend;
private final boolean isSeparable;
/**
* Private constructor due to the limited set of possible blend modes.
*
* @param name the corresponding COSName of the blend mode
* @param blendChannel the blend function for separable blend modes
* @param blend the blend function for non-separable blend modes
*/
private BlendMode(COSName name, BlendChannelFunction blendChannel, BlendFunction blend)
{
this.name = name;
this.blendChannel = blendChannel;
this.blend = blend;
isSeparable = blendChannel != null;
}
/**
* The blend mode name from the BM object.
*
* @return name of blend mode.
*/
public COSName getCOSName()
{
return name;
}
/**
* Determines if the blend mode is a separable blend mode.
*
* @return true for separable blend modes
*/
public boolean isSeparableBlendMode()
{
return isSeparable;
}
/**
* Returns the blend channel function, only available for separable blend modes.
*
* @return the blend channel function
*/
public BlendChannelFunction getBlendChannelFunction()
{
return blendChannel;
}
/**
* Returns the blend function, only available for non separable blend modes.
*
* @return the blend function
*/
public BlendFunction getBlendFunction()
{
return blend;
}
/**
* Determines the blend mode from the BM entry in the COS ExtGState.
*
* @param cosBlendMode name or array
* @return blending mode
*/
public static BlendMode getInstance(COSBase cosBlendMode)
{
BlendMode result = null;
if (cosBlendMode instanceof COSName)
{
result = BLEND_MODES.get(cosBlendMode);
}
else if (cosBlendMode instanceof COSArray)
{
COSArray cosBlendModeArray = (COSArray) cosBlendMode;
for (int i = 0; i < cosBlendModeArray.size(); i++)
{
COSBase cosBase = cosBlendModeArray.getObject(i);
if (cosBase instanceof COSName)
{
result = BLEND_MODES.get(cosBase);
if (result != null)
{
break;
}
}
}
}
return result != null ? result : BlendMode.NORMAL;
}
private static int get255Value(float val)
{
return (int) Math.floor(val >= 1.0 ? 255 : val * 255.0);
}
private static void getSaturationRGB(float[] srcValues, float[] dstValues, float[] result)
{
int rd = get255Value(dstValues[0]);
int gd = get255Value(dstValues[1]);
int bd = get255Value(dstValues[2]);
int minb = Math.min(rd, Math.min(gd, bd));
int maxb = Math.max(rd, Math.max(gd, bd));
if (minb == maxb)
{
/* backdrop has zero saturation, avoid divide by 0 */
result[0] = gd / 255.0f;
result[1] = gd / 255.0f;
result[2] = gd / 255.0f;
return;
}
int rs = get255Value(srcValues[0]);
int gs = get255Value(srcValues[1]);
int bs = get255Value(srcValues[2]);
int mins = Math.min(rs, Math.min(gs, bs));
int maxs = Math.max(rs, Math.max(gs, bs));
int scale = ((maxs - mins) << 16) / (maxb - minb);
int y = (rd * 77 + gd * 151 + bd * 28 + 0x80) >> 8;
int r = y + ((((rd - y) * scale) + 0x8000) >> 16);
int g = y + ((((gd - y) * scale) + 0x8000) >> 16);
int b = y + ((((bd - y) * scale) + 0x8000) >> 16);
if (((r | g | b) & 0x100) == 0x100)
{
int scalemin;
int scalemax;
int min = Math.min(r, Math.min(g, b));
int max = Math.max(r, Math.max(g, b));
if (min < 0)
{
scalemin = (y << 16) / (y - min);
}
else
{
scalemin = 0x10000;
}
if (max > 255)
{
scalemax = ((255 - y) << 16) / (max - y);
}
else
{
scalemax = 0x10000;
}
scale = Math.min(scalemin, scalemax);
r = y + (((r - y) * scale + 0x8000) >> 16);
g = y + (((g - y) * scale + 0x8000) >> 16);
b = y + (((b - y) * scale + 0x8000) >> 16);
}
result[0] = r / 255.0f;
result[1] = g / 255.0f;
result[2] = b / 255.0f;
}
private static void getLuminosityRGB(float[] srcValues, float[] dstValues, float[] result)
{
int rd = get255Value(dstValues[0]);
int gd = get255Value(dstValues[1]);
int bd = get255Value(dstValues[2]);
int rs = get255Value(srcValues[0]);
int gs = get255Value(srcValues[1]);
int bs = get255Value(srcValues[2]);
int delta = ((rs - rd) * 77 + (gs - gd) * 151 + (bs - bd) * 28 + 0x80) >> 8;
int r = rd + delta;
int g = gd + delta;
int b = bd + delta;
if (((r | g | b) & 0x100) == 0x100)
{
int scale;
int y = (rs * 77 + gs * 151 + bs * 28 + 0x80) >> 8;
if (delta > 0)
{
int max;
max = Math.max(r, Math.max(g, b));
scale = max == y ? 0 : ((255 - y) << 16) / (max - y);
}
else
{
int min;
min = Math.min(r, Math.min(g, b));
scale = y == min ? 0 : (y << 16) / (y - min);
}
r = y + (((r - y) * scale + 0x8000) >> 16);
g = y + (((g - y) * scale + 0x8000) >> 16);
b = y + (((b - y) * scale + 0x8000) >> 16);
}
result[0] = r / 255.0f;
result[1] = g / 255.0f;
result[2] = b / 255.0f;
}
}