diff --git a/AndroidCompat/build.gradle.kts b/AndroidCompat/build.gradle.kts
index df6d19dd..e5889b14 100644
--- a/AndroidCompat/build.gradle.kts
+++ b/AndroidCompat/build.gradle.kts
@@ -47,4 +47,5 @@ dependencies {
// OpenJDK lacks native JPEG encoder and native WEBP decoder
implementation(libs.bundles.twelvemonkeys)
+ implementation(libs.sejda.webp)
}
diff --git a/AndroidCompat/src/main/java/android/annotation/ColorLong.java b/AndroidCompat/src/main/java/android/annotation/ColorLong.java
new file mode 100644
index 00000000..95df7d6d
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/annotation/ColorLong.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed 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 android.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+/**
+ *
Denotes that the annotated element represents a packed color
+ * long. If applied to a long array, every element in the array
+ * represents a color long. For more information on how colors
+ * are packed in a long, please refer to the documentation of
+ * the {@link android.graphics.Color} class.
+ *
+ * Example:
+ *
+ * {@code
+ * public void setFillColor(@ColorLong long color);
+ * }
+ *
+ * @see android.graphics.Color
+ *
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({PARAMETER,METHOD,LOCAL_VARIABLE,FIELD})
+public @interface ColorLong {
+}
diff --git a/AndroidCompat/src/main/java/android/annotation/HalfFloat.java b/AndroidCompat/src/main/java/android/annotation/HalfFloat.java
new file mode 100644
index 00000000..256008c5
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/annotation/HalfFloat.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed 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 android.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+/**
+ * Denotes that the annotated element represents a half-precision floating point
+ * value. Such values are stored in short data types and can be manipulated with
+ * the {@link android.util.Half} class. If applied to an array of short, every
+ * element in the array represents a half-precision float.
+ *
+ * Example:
+ *
+ * {@code
+ * public abstract void setPosition(@HalfFloat short x, @HalfFloat short y, @HalfFloat short z);
+ * }
+ *
+ * @see android.util.Half
+ * @see android.util.Half#toHalf(float)
+ * @see android.util.Half#toFloat(short)
+ *
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
+public @interface HalfFloat {
+}
diff --git a/AndroidCompat/src/main/java/android/graphics/Bitmap.java b/AndroidCompat/src/main/java/android/graphics/Bitmap.java
index 3dbc1656..7101bb75 100644
--- a/AndroidCompat/src/main/java/android/graphics/Bitmap.java
+++ b/AndroidCompat/src/main/java/android/graphics/Bitmap.java
@@ -2,17 +2,17 @@ package android.graphics;
import android.annotation.ColorInt;
import android.annotation.NonNull;
-
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Iterator;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
-import java.awt.image.BufferedImage;
-import java.awt.image.Raster;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Iterator;
+
public final class Bitmap {
private final int width;
@@ -75,6 +75,19 @@ public final class Bitmap {
}
}
+ private static int configToBufferedImageType(Config config) {
+ switch (config) {
+ case ALPHA_8:
+ return BufferedImage.TYPE_BYTE_GRAY;
+ case RGB_565:
+ return BufferedImage.TYPE_USHORT_565_RGB;
+ case ARGB_8888:
+ return BufferedImage.TYPE_INT_ARGB;
+ default:
+ throw new UnsupportedOperationException("Bitmap.Config(" + config + ") not supported");
+ }
+ }
+
/**
* Common code for checking that x and y are >= 0
*
@@ -106,7 +119,7 @@ public final class Bitmap {
}
public static Bitmap createBitmap(int width, int height, Config config) {
- BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ BufferedImage image = new BufferedImage(width, height, configToBufferedImageType(config));
return new Bitmap(image);
}
@@ -144,8 +157,10 @@ public final class Bitmap {
formatString = "png";
} else if (format == Bitmap.CompressFormat.JPEG) {
formatString = "jpg";
+ } else if (format == Bitmap.CompressFormat.WEBP || format == Bitmap.CompressFormat.WEBP_LOSSY) {
+ formatString = "webp";
} else {
- throw new IllegalArgumentException("unsupported compression format!");
+ throw new IllegalArgumentException("unsupported compression format! " + format);
}
Iterator writers = ImageIO.getImageWritersByFormatName(formatString);
@@ -162,14 +177,19 @@ public final class Bitmap {
}
writer.setOutput(ios);
+ BufferedImage img = image;
+
ImageWriteParam param = writer.getDefaultWriteParam();
if ("jpg".equals(formatString)) {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(qualityFloat);
+
+ img = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
+ img.getGraphics().drawImage(image, 0, 0, null);
}
try {
- writer.write(null, new IIOImage(image, null, null), param);
+ writer.write(null, new IIOImage(img, null, null), param);
ios.close();
writer.dispose();
} catch (IOException ex) {
@@ -179,6 +199,12 @@ public final class Bitmap {
return true;
}
+ public Bitmap copy(Config config, boolean isMutable) {
+ Bitmap ret = createBitmap(width, height, config);
+ ret.image.getGraphics().drawImage(image, 0, 0, null);
+ return ret;
+ }
+
/**
* Shared code to check for illegal arguments passed to getPixels()
* or setPixels()
@@ -224,12 +250,18 @@ public final class Bitmap {
int x, int y, int width, int height) {
checkPixelsAccess(x, y, width, height, offset, stride, pixels);
- Raster raster = image.getData();
- int[] rasterPixels = raster.getPixels(x, y, width, height, (int[]) null);
+ image.getRGB(x, y, width, height, pixels, offset, stride);
+ }
- for (int ht = 0; ht < height; ht++) {
- int rowOffset = offset + stride * ht;
- System.arraycopy(rasterPixels, ht * width, pixels, rowOffset, width);
- }
+ public void eraseColor(int c) {
+ java.awt.Color color = Color.valueOf(c).toJavaColor();
+ Graphics2D graphics = image.createGraphics();
+ graphics.setColor(color);
+ graphics.fillRect(0, 0, width, height);
+ graphics.dispose();
+ }
+
+ public void recycle() {
+ // do nothing
}
}
diff --git a/AndroidCompat/src/main/java/android/graphics/Canvas.java b/AndroidCompat/src/main/java/android/graphics/Canvas.java
index 3f2c522a..8d203dab 100644
--- a/AndroidCompat/src/main/java/android/graphics/Canvas.java
+++ b/AndroidCompat/src/main/java/android/graphics/Canvas.java
@@ -1,21 +1,184 @@
package android.graphics;
+import android.annotation.NonNull;
+import android.util.Log;
+import java.awt.BasicStroke;
+import java.awt.Font;
import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.font.GlyphVector;
+import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.List;
import javax.imageio.ImageIO;
public final class Canvas {
private BufferedImage canvasImage;
private Graphics2D canvas;
+ private List transformStack = new ArrayList();
+
+ private static final String TAG = "Canvas";
public Canvas(Bitmap bitmap) {
canvasImage = bitmap.getImage();
canvas = canvasImage.createGraphics();
}
- public void drawBitmap(Bitmap sourceBitmap, Rect src, Rect dst, Paint paint) {
+ public void drawBitmap(Bitmap sourceBitmap, Rect src, Rect dst, Paint paint) {
BufferedImage sourceImage = sourceBitmap.getImage();
BufferedImage sourceImageCropped = sourceImage.getSubimage(src.left, src.top, src.getWidth(), src.getHeight());
- canvas.drawImage(sourceImageCropped, null, dst.left, dst.top);
+ canvas.drawImage(sourceImageCropped, dst.left, dst.top, dst.getWidth(), dst.getHeight(), null);
+ }
+
+ public void drawBitmap(Bitmap sourceBitmap, float left, float top, Paint paint) {
+ BufferedImage sourceImage = sourceBitmap.getImage();
+ canvas.drawImage(sourceImage, null, (int) left, (int) top);
+ }
+
+ public void drawText(@NonNull char[] text, int index, int count, float x, float y,
+ @NonNull Paint paint) {
+ drawText(new String(text, index, count), x, y, paint);
+ }
+
+ public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
+ applyPaint(paint);
+ GlyphVector glyphVector = paint.getFont().createGlyphVector(canvas.getFontRenderContext(), text);
+ Shape textShape = glyphVector.getOutline();
+ switch (paint.getStyle()) {
+ case Paint.Style.FILL:
+ canvas.drawString(text, x, y);
+ break;
+ case Paint.Style.STROKE:
+ save();
+ translate(x, y);
+ canvas.draw(textShape);
+ restore();
+ break;
+ case Paint.Style.FILL_AND_STROKE:
+ save();
+ translate(x, y);
+ canvas.draw(textShape);
+ canvas.fill(textShape);
+ restore();
+ break;
+ }
+ }
+
+ public void drawText(@NonNull String text, int start, int end, float x, float y,
+ @NonNull Paint paint) {
+ drawText(text.substring(start, end), x, y, paint);
+ }
+
+ public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
+ @NonNull Paint paint) {
+ String str = text.subSequence(start, end).toString();
+ drawText(str, x, y, paint);
+ }
+
+ public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,
+ @NonNull Paint paint) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void drawPath(@NonNull Path path, @NonNull Paint paint) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void translate(float dx, float dy) {
+ if (dx == 0.0f && dy == 0.0f) return;
+ // TODO: check this, should translations stack?
+ canvas.translate(dx, dy);
+ }
+
+ public void scale(float sx, float sy) {
+ if (sx == 1.0f && sy == 1.0f) return;
+ canvas.scale(sx, sy);
+ }
+
+ public final void scale(float sx, float sy, float px, float py) {
+ if (sx == 1.0f && sy == 1.0f) return;
+ translate(px, py);
+ scale(sx, sy);
+ translate(-px, -py);
+ }
+
+ public void rotate(float degrees) {
+ if (degrees == 0.0f) return;
+ canvas.rotate(degrees);
+ }
+
+ public final void rotate(float degrees, float px, float py) {
+ if (degrees == 0.0f) return;
+ canvas.rotate(degrees, px, py);
+ }
+
+ public int getSaveCount() {
+ return transformStack.size();
+ }
+
+ public int save() {
+ transformStack.add(canvas.getTransform());
+ return getSaveCount();
+ }
+
+ public void restoreToCount(int saveCount) {
+ if (saveCount < 1) {
+ throw new IllegalArgumentException(
+ "Underflow in restoreToCount - more restores than saves");
+ }
+ if (saveCount > getSaveCount()) {
+ throw new IllegalArgumentException("Overflow in restoreToCount");
+
+ }
+ AffineTransform ts = transformStack.get(saveCount - 1);
+ canvas.setTransform(ts);
+ while (transformStack.size() >= saveCount) {
+ transformStack.remove(transformStack.size() - 1);
+ }
+ }
+
+ public void restore() {
+ restoreToCount(getSaveCount());
+ }
+
+ public boolean getClipBounds(@NonNull Rect bounds) {
+ Rectangle r = canvas.getClipBounds();
+ if (r == null) {
+ bounds.left = 0;
+ bounds.top = 0;
+ bounds.right = canvasImage.getWidth();
+ bounds.bottom = canvasImage.getHeight();
+ return true;
+ }
+ bounds.left = r.x;
+ bounds.top = r.y;
+ bounds.right = r.x + r.width;
+ bounds.bottom = r.y + r.height;
+ return r.width != 0 && r.height != 0;
+ }
+
+ private void applyPaint(Paint paint) {
+ canvas.setFont(paint.getFont());
+ java.awt.Color color = Color.valueOf(paint.getColorLong()).toJavaColor();
+ canvas.setColor(color);
+ canvas.setStroke(new BasicStroke(paint.getStrokeWidth(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+ if (paint.isAntiAlias()) {
+ canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ } else {
+ canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
+ }
+ if (paint.isDither()) {
+ canvas.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
+ } else {
+ canvas.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
+ }
+ // TODO: use more from paint?
}
}
diff --git a/AndroidCompat/src/main/java/android/graphics/Color.java b/AndroidCompat/src/main/java/android/graphics/Color.java
new file mode 100644
index 00000000..529a43c1
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/graphics/Color.java
@@ -0,0 +1,578 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed 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 android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.annotation.HalfFloat;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.util.Half;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.function.DoubleUnaryOperator;
+
+public class Color {
+ @ColorInt public static final int BLACK = 0xFF000000;
+ @ColorInt public static final int DKGRAY = 0xFF444444;
+ @ColorInt public static final int GRAY = 0xFF888888;
+ @ColorInt public static final int LTGRAY = 0xFFCCCCCC;
+ @ColorInt public static final int WHITE = 0xFFFFFFFF;
+ @ColorInt public static final int RED = 0xFFFF0000;
+ @ColorInt public static final int GREEN = 0xFF00FF00;
+ @ColorInt public static final int BLUE = 0xFF0000FF;
+ @ColorInt public static final int YELLOW = 0xFFFFFF00;
+ @ColorInt public static final int CYAN = 0xFF00FFFF;
+ @ColorInt public static final int MAGENTA = 0xFFFF00FF;
+ @ColorInt public static final int TRANSPARENT = 0;
+
+ @NonNull
+ @Size(min = 4, max = 5)
+ private final float[] mComponents;
+
+ @NonNull
+ private final ColorSpace mColorSpace;
+
+ public Color() {
+ // This constructor is required for compatibility with previous APIs
+ mComponents = new float[] { 0.0f, 0.0f, 0.0f, 1.0f };
+ mColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+ }
+
+ private Color(float r, float g, float b, float a) {
+ this(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+
+ private Color(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) {
+ mComponents = new float[] { r, g, b, a };
+ mColorSpace = colorSpace;
+ }
+
+ private Color(@Size(min = 4, max = 5) float[] components, @NonNull ColorSpace colorSpace) {
+ mComponents = components;
+ mColorSpace = colorSpace;
+ }
+
+ public java.awt.Color toJavaColor() {
+ return new java.awt.Color(red(), green(), blue(), alpha());
+ }
+
+ @NonNull
+ public ColorSpace getColorSpace() {
+ return mColorSpace;
+ }
+
+ public ColorSpace.Model getModel() {
+ return mColorSpace.getModel();
+ }
+
+ public boolean isWideGamut() {
+ return getColorSpace().isWideGamut();
+ }
+
+ public boolean isSrgb() {
+ return getColorSpace().isSrgb();
+ }
+
+ @IntRange(from = 4, to = 5)
+ public int getComponentCount() {
+ return mColorSpace.getComponentCount() + 1;
+ }
+
+ @ColorLong
+ public long pack() {
+ return pack(mComponents[0], mComponents[1], mComponents[2], mComponents[3], mColorSpace);
+ }
+
+ @NonNull
+ public Color convert(@NonNull ColorSpace colorSpace) {
+ ColorSpace.Connector connector = ColorSpace.connect(mColorSpace, colorSpace);
+ float[] color = new float[] {
+ mComponents[0], mComponents[1], mComponents[2], mComponents[3]
+ };
+ connector.transform(color);
+ return new Color(color, colorSpace);
+ }
+
+ @ColorInt
+ public int toArgb() {
+ if (mColorSpace.isSrgb()) {
+ return ((int) (mComponents[3] * 255.0f + 0.5f) << 24) |
+ ((int) (mComponents[0] * 255.0f + 0.5f) << 16) |
+ ((int) (mComponents[1] * 255.0f + 0.5f) << 8) |
+ (int) (mComponents[2] * 255.0f + 0.5f);
+ }
+
+ float[] color = new float[] {
+ mComponents[0], mComponents[1], mComponents[2], mComponents[3]
+ };
+ // The transformation saturates the output
+ ColorSpace.connect(mColorSpace).transform(color);
+
+ return ((int) (color[3] * 255.0f + 0.5f) << 24) |
+ ((int) (color[0] * 255.0f + 0.5f) << 16) |
+ ((int) (color[1] * 255.0f + 0.5f) << 8) |
+ (int) (color[2] * 255.0f + 0.5f);
+ }
+
+ public float red() {
+ return mComponents[0];
+ }
+
+ public float green() {
+ return mComponents[1];
+ }
+
+ public float blue() {
+ return mComponents[2];
+ }
+
+ public float alpha() {
+ return mComponents[mComponents.length - 1];
+ }
+
+ @NonNull
+ @Size(min = 4, max = 5)
+ public float[] getComponents() {
+ return Arrays.copyOf(mComponents, mComponents.length);
+ }
+
+ @NonNull
+ @Size(min = 4)
+ public float[] getComponents(@Nullable @Size(min = 4) float[] components) {
+ if (components == null) {
+ return Arrays.copyOf(mComponents, mComponents.length);
+ }
+
+ if (components.length < mComponents.length) {
+ throw new IllegalArgumentException("The specified array's length must be at "
+ + "least " + mComponents.length);
+ }
+
+ System.arraycopy(mComponents, 0, components, 0, mComponents.length);
+ return components;
+ }
+
+ public float getComponent(@IntRange(from = 0, to = 4) int component) {
+ return mComponents[component];
+ }
+
+ public float luminance() {
+ if (mColorSpace.getModel() != ColorSpace.Model.RGB) {
+ throw new IllegalArgumentException("The specified color must be encoded in an RGB " +
+ "color space. The supplied color space is " + mColorSpace.getModel());
+ }
+
+ DoubleUnaryOperator eotf = ((ColorSpace.Rgb) mColorSpace).getEotf();
+ double r = eotf.applyAsDouble(mComponents[0]);
+ double g = eotf.applyAsDouble(mComponents[1]);
+ double b = eotf.applyAsDouble(mComponents[2]);
+
+ return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Color color = (Color) o;
+
+ //noinspection SimplifiableIfStatement
+ if (!Arrays.equals(mComponents, color.mComponents)) return false;
+ return mColorSpace.equals(color.mColorSpace);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Arrays.hashCode(mComponents);
+ result = 31 * result + mColorSpace.hashCode();
+ return result;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ StringBuilder b = new StringBuilder("Color(");
+ for (float c : mComponents) {
+ b.append(c).append(", ");
+ }
+ b.append(mColorSpace.getName());
+ b.append(')');
+ return b.toString();
+ }
+
+ @NonNull
+ public static ColorSpace colorSpace(@ColorLong long color) {
+ return ColorSpace.get((int) (color & 0x3fL));
+ }
+
+ public static float red(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 48) & 0xff) / 255.0f;
+ return Half.toFloat((short) ((color >> 48) & 0xffff));
+ }
+
+ public static float green(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 40) & 0xff) / 255.0f;
+ return Half.toFloat((short) ((color >> 32) & 0xffff));
+ }
+
+ public static float blue(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 32) & 0xff) / 255.0f;
+ return Half.toFloat((short) ((color >> 16) & 0xffff));
+ }
+
+ public static float alpha(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 56) & 0xff) / 255.0f;
+ return ((color >> 6) & 0x3ff) / 1023.0f;
+ }
+
+ public static boolean isSrgb(@ColorLong long color) {
+ return colorSpace(color).isSrgb();
+ }
+
+ public static boolean isWideGamut(@ColorLong long color) {
+ return colorSpace(color).isWideGamut();
+ }
+
+ public static boolean isInColorSpace(@ColorLong long color, @NonNull ColorSpace colorSpace) {
+ return (int) (color & 0x3fL) == colorSpace.getId();
+ }
+
+ @ColorInt
+ public static int toArgb(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return (int) (color >> 32);
+
+ float r = red(color);
+ float g = green(color);
+ float b = blue(color);
+ float a = alpha(color);
+
+ // The transformation saturates the output
+ float[] c = ColorSpace.connect(colorSpace(color)).transform(r, g, b);
+
+ return ((int) (a * 255.0f + 0.5f) << 24) |
+ ((int) (c[0] * 255.0f + 0.5f) << 16) |
+ ((int) (c[1] * 255.0f + 0.5f) << 8) |
+ (int) (c[2] * 255.0f + 0.5f);
+ }
+
+ @NonNull
+ public static Color valueOf(@ColorInt int color) {
+ float r = ((color >> 16) & 0xff) / 255.0f;
+ float g = ((color >> 8) & 0xff) / 255.0f;
+ float b = ((color ) & 0xff) / 255.0f;
+ float a = ((color >> 24) & 0xff) / 255.0f;
+ return new Color(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+
+ @NonNull
+ public static Color valueOf(@ColorLong long color) {
+ return new Color(red(color), green(color), blue(color), alpha(color), colorSpace(color));
+ }
+
+ @NonNull
+ public static Color valueOf(float r, float g, float b) {
+ return new Color(r, g, b, 1.0f);
+ }
+
+ @NonNull
+ public static Color valueOf(float r, float g, float b, float a) {
+ return new Color(saturate(r), saturate(g), saturate(b), saturate(a));
+ }
+
+ @NonNull
+ public static Color valueOf(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) {
+ if (colorSpace.getComponentCount() > 3) {
+ throw new IllegalArgumentException("The specified color space must use a color model " +
+ "with at most 3 color components");
+ }
+ return new Color(r, g, b, a, colorSpace);
+ }
+
+ @NonNull
+ public static Color valueOf(@NonNull @Size(min = 4, max = 5) float[] components,
+ @NonNull ColorSpace colorSpace) {
+ if (components.length < colorSpace.getComponentCount() + 1) {
+ throw new IllegalArgumentException("Received a component array of length " +
+ components.length + " but the color model requires " +
+ (colorSpace.getComponentCount() + 1) + " (including alpha)");
+ }
+ return new Color(Arrays.copyOf(components, colorSpace.getComponentCount() + 1), colorSpace);
+ }
+
+ @ColorLong
+ public static long pack(@ColorInt int color) {
+ return (color & 0xffffffffL) << 32;
+ }
+
+ @ColorLong
+ public static long pack(float red, float green, float blue) {
+ return pack(red, green, blue, 1.0f, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+
+ @ColorLong
+ public static long pack(float red, float green, float blue, float alpha) {
+ return pack(red, green, blue, alpha, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+
+ @ColorLong
+ public static long pack(float red, float green, float blue, float alpha,
+ @NonNull ColorSpace colorSpace) {
+ if (colorSpace.isSrgb()) {
+ int argb =
+ ((int) (alpha * 255.0f + 0.5f) << 24) |
+ ((int) (red * 255.0f + 0.5f) << 16) |
+ ((int) (green * 255.0f + 0.5f) << 8) |
+ (int) (blue * 255.0f + 0.5f);
+ return (argb & 0xffffffffL) << 32;
+ }
+
+ int id = colorSpace.getId();
+ if (id == ColorSpace.MIN_ID) {
+ throw new IllegalArgumentException(
+ "Unknown color space, please use a color space returned by ColorSpace.get()");
+ }
+ if (colorSpace.getComponentCount() > 3) {
+ throw new IllegalArgumentException(
+ "The color space must use a color model with at most 3 components");
+ }
+
+ @HalfFloat short r = Half.toHalf(red);
+ @HalfFloat short g = Half.toHalf(green);
+ @HalfFloat short b = Half.toHalf(blue);
+
+ int a = (int) (Math.max(0.0f, Math.min(alpha, 1.0f)) * 1023.0f + 0.5f);
+
+ // Suppress sign extension
+ return (r & 0xffffL) << 48 |
+ (g & 0xffffL) << 32 |
+ (b & 0xffffL) << 16 |
+ (a & 0x3ffL ) << 6 |
+ id & 0x3fL;
+ }
+
+ @ColorLong
+ public static long convert(@ColorInt int color, @NonNull ColorSpace colorSpace) {
+ float r = ((color >> 16) & 0xff) / 255.0f;
+ float g = ((color >> 8) & 0xff) / 255.0f;
+ float b = ((color ) & 0xff) / 255.0f;
+ float a = ((color >> 24) & 0xff) / 255.0f;
+ ColorSpace source = ColorSpace.get(ColorSpace.Named.SRGB);
+ return convert(r, g, b, a, source, colorSpace);
+ }
+
+ @ColorLong
+ public static long convert(@ColorLong long color, @NonNull ColorSpace colorSpace) {
+ float r = red(color);
+ float g = green(color);
+ float b = blue(color);
+ float a = alpha(color);
+ ColorSpace source = colorSpace(color);
+ return convert(r, g, b, a, source, colorSpace);
+ }
+
+ @ColorLong
+ public static long convert(float r, float g, float b, float a,
+ @NonNull ColorSpace source, @NonNull ColorSpace destination) {
+ float[] c = ColorSpace.connect(source, destination).transform(r, g, b);
+ return pack(c[0], c[1], c[2], a, destination);
+ }
+
+ @ColorLong
+ public static long convert(@ColorLong long color, @NonNull ColorSpace.Connector connector) {
+ float r = red(color);
+ float g = green(color);
+ float b = blue(color);
+ float a = alpha(color);
+ return convert(r, g, b, a, connector);
+ }
+
+ @ColorLong
+ public static long convert(float r, float g, float b, float a,
+ @NonNull ColorSpace.Connector connector) {
+ float[] c = connector.transform(r, g, b);
+ return pack(c[0], c[1], c[2], a, connector.getDestination());
+ }
+
+ public static float luminance(@ColorLong long color) {
+ ColorSpace colorSpace = colorSpace(color);
+ if (colorSpace.getModel() != ColorSpace.Model.RGB) {
+ throw new IllegalArgumentException("The specified color must be encoded in an RGB " +
+ "color space. The supplied color space is " + colorSpace.getModel());
+ }
+
+ DoubleUnaryOperator eotf = ((ColorSpace.Rgb) colorSpace).getEotf();
+ double r = eotf.applyAsDouble(red(color));
+ double g = eotf.applyAsDouble(green(color));
+ double b = eotf.applyAsDouble(blue(color));
+
+ return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)));
+ }
+
+ private static float saturate(float v) {
+ return v <= 0.0f ? 0.0f : (v >= 1.0f ? 1.0f : v);
+ }
+
+ @IntRange(from = 0, to = 255)
+ public static int alpha(int color) {
+ return color >>> 24;
+ }
+
+ @IntRange(from = 0, to = 255)
+ public static int red(int color) {
+ return (color >> 16) & 0xFF;
+ }
+
+ @IntRange(from = 0, to = 255)
+ public static int green(int color) {
+ return (color >> 8) & 0xFF;
+ }
+
+ @IntRange(from = 0, to = 255)
+ public static int blue(int color) {
+ return color & 0xFF;
+ }
+
+ @ColorInt
+ public static int rgb(
+ @IntRange(from = 0, to = 255) int red,
+ @IntRange(from = 0, to = 255) int green,
+ @IntRange(from = 0, to = 255) int blue) {
+ return 0xff000000 | (red << 16) | (green << 8) | blue;
+ }
+
+ @ColorInt
+ public static int rgb(float red, float green, float blue) {
+ return 0xff000000 |
+ ((int) (red * 255.0f + 0.5f) << 16) |
+ ((int) (green * 255.0f + 0.5f) << 8) |
+ (int) (blue * 255.0f + 0.5f);
+ }
+
+ @ColorInt
+ public static int argb(
+ @IntRange(from = 0, to = 255) int alpha,
+ @IntRange(from = 0, to = 255) int red,
+ @IntRange(from = 0, to = 255) int green,
+ @IntRange(from = 0, to = 255) int blue) {
+ return (alpha << 24) | (red << 16) | (green << 8) | blue;
+ }
+
+ @ColorInt
+ public static int argb(float alpha, float red, float green, float blue) {
+ return ((int) (alpha * 255.0f + 0.5f) << 24) |
+ ((int) (red * 255.0f + 0.5f) << 16) |
+ ((int) (green * 255.0f + 0.5f) << 8) |
+ (int) (blue * 255.0f + 0.5f);
+ }
+
+ public static float luminance(@ColorInt int color) {
+ ColorSpace.Rgb cs = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+ DoubleUnaryOperator eotf = cs.getEotf();
+
+ double r = eotf.applyAsDouble(red(color) / 255.0);
+ double g = eotf.applyAsDouble(green(color) / 255.0);
+ double b = eotf.applyAsDouble(blue(color) / 255.0);
+
+ return (float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b));
+ }
+
+ @ColorInt
+ public static int parseColor(@Size(min=1) String colorString) {
+ if (colorString.charAt(0) == '#') {
+ // Use a long to avoid rollovers on #ffXXXXXX
+ long color = Long.parseLong(colorString.substring(1), 16);
+ if (colorString.length() == 7) {
+ // Set the alpha value
+ color |= 0x00000000ff000000;
+ } else if (colorString.length() != 9) {
+ throw new IllegalArgumentException("Unknown color");
+ }
+ return (int)color;
+ } else {
+ Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.ROOT));
+ if (color != null) {
+ return color;
+ }
+ }
+ throw new IllegalArgumentException("Unknown color");
+ }
+
+ public static void RGBToHSV(
+ @IntRange(from = 0, to = 255) int red,
+ @IntRange(from = 0, to = 255) int green,
+ @IntRange(from = 0, to = 255) int blue, @Size(3) float hsv[]) {
+ if (hsv.length < 3) {
+ throw new RuntimeException("3 components required for hsv");
+ }
+ nativeRGBToHSV(red, green, blue, hsv);
+ }
+
+ public static void colorToHSV(@ColorInt int color, @Size(3) float hsv[]) {
+ RGBToHSV((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, hsv);
+ }
+
+ @ColorInt
+ public static int HSVToColor(@Size(3) float hsv[]) {
+ return HSVToColor(0xFF, hsv);
+ }
+
+ @ColorInt
+ public static int HSVToColor(@IntRange(from = 0, to = 255) int alpha, @Size(3) float hsv[]) {
+ if (hsv.length < 3) {
+ throw new RuntimeException("3 components required for hsv");
+ }
+ return nativeHSVToColor(alpha, hsv);
+ }
+
+ private static native void nativeRGBToHSV(int red, int greed, int blue, float hsv[]);
+ private static native int nativeHSVToColor(int alpha, float hsv[]);
+
+ private static final HashMap sColorNameMap;
+ static {
+ sColorNameMap = new HashMap<>();
+ sColorNameMap.put("black", BLACK);
+ sColorNameMap.put("darkgray", DKGRAY);
+ sColorNameMap.put("gray", GRAY);
+ sColorNameMap.put("lightgray", LTGRAY);
+ sColorNameMap.put("white", WHITE);
+ sColorNameMap.put("red", RED);
+ sColorNameMap.put("green", GREEN);
+ sColorNameMap.put("blue", BLUE);
+ sColorNameMap.put("yellow", YELLOW);
+ sColorNameMap.put("cyan", CYAN);
+ sColorNameMap.put("magenta", MAGENTA);
+ sColorNameMap.put("aqua", 0xFF00FFFF);
+ sColorNameMap.put("fuchsia", 0xFFFF00FF);
+ sColorNameMap.put("darkgrey", DKGRAY);
+ sColorNameMap.put("grey", GRAY);
+ sColorNameMap.put("lightgrey", LTGRAY);
+ sColorNameMap.put("lime", 0xFF00FF00);
+ sColorNameMap.put("maroon", 0xFF800000);
+ sColorNameMap.put("navy", 0xFF000080);
+ sColorNameMap.put("olive", 0xFF808000);
+ sColorNameMap.put("purple", 0xFF800080);
+ sColorNameMap.put("silver", 0xFFC0C0C0);
+ sColorNameMap.put("teal", 0xFF008080);
+
+ }
+}
+
diff --git a/AndroidCompat/src/main/java/android/graphics/ColorSpace.java b/AndroidCompat/src/main/java/android/graphics/ColorSpace.java
new file mode 100644
index 00000000..599adef7
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/graphics/ColorSpace.java
@@ -0,0 +1,1847 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed 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 android.graphics;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.SuppressLint;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.function.DoubleUnaryOperator;
+
+public abstract class ColorSpace {
+ public static final float[] ILLUMINANT_A = { 0.44757f, 0.40745f };
+ public static final float[] ILLUMINANT_B = { 0.34842f, 0.35161f };
+ public static final float[] ILLUMINANT_C = { 0.31006f, 0.31616f };
+ public static final float[] ILLUMINANT_D50 = { 0.34567f, 0.35850f };
+ public static final float[] ILLUMINANT_D55 = { 0.33242f, 0.34743f };
+ public static final float[] ILLUMINANT_D60 = { 0.32168f, 0.33767f };
+ public static final float[] ILLUMINANT_D65 = { 0.31271f, 0.32902f };
+ public static final float[] ILLUMINANT_D75 = { 0.29902f, 0.31485f };
+ public static final float[] ILLUMINANT_E = { 0.33333f, 0.33333f };
+
+ public static final int MIN_ID = -1; // Do not change
+ public static final int MAX_ID = 63; // Do not change, used to encode in longs
+
+ private static final float[] SRGB_PRIMARIES = { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f };
+ private static final float[] NTSC_1953_PRIMARIES = { 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f };
+ private static final float[] DCI_P3_PRIMARIES =
+ { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f };
+ private static final float[] BT2020_PRIMARIES =
+ { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f };
+ private static final float[] GRAY_PRIMARIES = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
+
+ private static final float[] ILLUMINANT_D50_XYZ = { 0.964212f, 1.0f, 0.825188f };
+
+ private static final Rgb.TransferParameters SRGB_TRANSFER_PARAMETERS =
+ new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4);
+
+ private static final Rgb.TransferParameters SMPTE_170M_TRANSFER_PARAMETERS =
+ new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45);
+
+ // HLG transfer with an SDR whitepoint of 203 nits
+ private static final Rgb.TransferParameters BT2020_HLG_TRANSFER_PARAMETERS =
+ new Rgb.TransferParameters(2.0, 2.0, 1 / 0.17883277, 0.28466892, 0.55991073,
+ -0.685490157, Rgb.TransferParameters.TYPE_HLGish);
+
+ // PQ transfer with an SDR whitepoint of 203 nits
+ private static final Rgb.TransferParameters BT2020_PQ_TRANSFER_PARAMETERS =
+ new Rgb.TransferParameters(-1.555223, 1.860454, 32 / 2523.0, 2413 / 128.0,
+ -2392 / 128.0, 8192 / 1305.0, Rgb.TransferParameters.TYPE_PQish);
+
+ // See static initialization block next to #get(Named)
+ private static final HashMap sNamedColorSpaceMap =
+ new HashMap<>();
+
+ @NonNull private final String mName;
+ @NonNull private final Model mModel;
+ @IntRange(from = MIN_ID, to = MAX_ID) private final int mId;
+
+ public enum Named {
+ // NOTE: Do NOT change the order of the enum
+ SRGB,
+ LINEAR_SRGB,
+ EXTENDED_SRGB,
+ LINEAR_EXTENDED_SRGB,
+ BT709,
+ BT2020,
+ DCI_P3,
+ DISPLAY_P3,
+ NTSC_1953,
+ SMPTE_C,
+ ADOBE_RGB,
+ PRO_PHOTO_RGB,
+ ACES,
+ ACESCG,
+ CIE_XYZ,
+ CIE_LAB,
+ BT2020_HLG,
+ BT2020_PQ,
+
+ OK_LAB,
+
+ DISPLAY_BT2020
+ // Update the initialization block next to #get(Named) when adding new values
+ }
+
+ public enum RenderIntent {
+ PERCEPTUAL,
+ RELATIVE,
+ SATURATION,
+ ABSOLUTE
+ }
+
+ public enum Adaptation {
+ BRADFORD(new float[] {
+ 0.8951f, -0.7502f, 0.0389f,
+ 0.2664f, 1.7135f, -0.0685f,
+ -0.1614f, 0.0367f, 1.0296f
+ }),
+ VON_KRIES(new float[] {
+ 0.40024f, -0.22630f, 0.00000f,
+ 0.70760f, 1.16532f, 0.00000f,
+ -0.08081f, 0.04570f, 0.91822f
+ }),
+ CIECAT02(new float[] {
+ 0.7328f, -0.7036f, 0.0030f,
+ 0.4296f, 1.6975f, 0.0136f,
+ -0.1624f, 0.0061f, 0.9834f
+ });
+
+ final float[] mTransform;
+
+ Adaptation(@NonNull @Size(9) float[] transform) {
+ mTransform = transform;
+ }
+ }
+
+ public enum Model {
+ RGB(3),
+ XYZ(3),
+ LAB(3),
+ CMYK(4);
+
+ private final int mComponentCount;
+
+ Model(@IntRange(from = 1, to = 4) int componentCount) {
+ mComponentCount = componentCount;
+ }
+
+ @IntRange(from = 1, to = 4)
+ public int getComponentCount() {
+ return mComponentCount;
+ }
+ }
+
+ /*package*/ ColorSpace(
+ @NonNull String name,
+ @NonNull Model model,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+
+ if (name == null || name.length() < 1) {
+ throw new IllegalArgumentException("The name of a color space cannot be null and " +
+ "must contain at least 1 character");
+ }
+
+ if (model == null) {
+ throw new IllegalArgumentException("A color space must have a model");
+ }
+
+ if (id < MIN_ID || id > MAX_ID) {
+ throw new IllegalArgumentException("The id must be between " +
+ MIN_ID + " and " + MAX_ID);
+ }
+
+ mName = name;
+ mModel = model;
+ mId = id;
+ }
+
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ @IntRange(from = MIN_ID, to = MAX_ID)
+ public int getId() {
+ return mId;
+ }
+
+ @NonNull
+ public Model getModel() {
+ return mModel;
+ }
+
+ @IntRange(from = 1, to = 4)
+ public int getComponentCount() {
+ return mModel.getComponentCount();
+ }
+
+ public abstract boolean isWideGamut();
+
+ public boolean isSrgb() {
+ return false;
+ }
+
+ public abstract float getMinValue(@IntRange(from = 0, to = 3) int component);
+
+ public abstract float getMaxValue(@IntRange(from = 0, to = 3) int component);
+
+ @NonNull
+ @Size(3)
+ public float[] toXyz(float r, float g, float b) {
+ return toXyz(new float[] { r, g, b });
+ }
+
+ @NonNull
+ @Size(min = 3)
+ public abstract float[] toXyz(@NonNull @Size(min = 3) float[] v);
+
+ @NonNull
+ @Size(min = 3)
+ public float[] fromXyz(float x, float y, float z) {
+ float[] xyz = new float[mModel.getComponentCount()];
+ xyz[0] = x;
+ xyz[1] = y;
+ xyz[2] = z;
+ return fromXyz(xyz);
+ }
+
+ @NonNull
+ @Size(min = 3)
+ public abstract float[] fromXyz(@NonNull @Size(min = 3) float[] v);
+
+ @Override
+ @NonNull
+ public String toString() {
+ return mName + " (id=" + mId + ", model=" + mModel + ")";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ColorSpace that = (ColorSpace) o;
+
+ if (mId != that.mId) return false;
+ //noinspection SimplifiableIfStatement
+ if (!mName.equals(that.mName)) return false;
+ return mModel == that.mModel;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mName.hashCode();
+ result = 31 * result + mModel.hashCode();
+ result = 31 * result + mId;
+ return result;
+ }
+
+ @NonNull
+ public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination) {
+ return connect(source, destination, RenderIntent.PERCEPTUAL);
+ }
+
+ @NonNull
+ @SuppressWarnings("ConstantConditions")
+ public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull RenderIntent intent) {
+ if (source.equals(destination)) return Connector.identity(source);
+
+ if (source.getModel() == Model.RGB && destination.getModel() == Model.RGB) {
+ return new Connector.Rgb((Rgb) source, (Rgb) destination, intent);
+ }
+
+ return new Connector(source, destination, intent);
+ }
+
+ @NonNull
+ public static Connector connect(@NonNull ColorSpace source) {
+ return connect(source, RenderIntent.PERCEPTUAL);
+ }
+
+ @NonNull
+ public static Connector connect(@NonNull ColorSpace source, @NonNull RenderIntent intent) {
+ if (source.isSrgb()) return Connector.identity(source);
+
+ if (source.getModel() == Model.RGB) {
+ return new Connector.Rgb((Rgb) source, (Rgb) get(Named.SRGB), intent);
+ }
+
+ return new Connector(source, get(Named.SRGB), intent);
+ }
+
+ @NonNull
+ public static ColorSpace adapt(@NonNull ColorSpace colorSpace,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint) {
+ return adapt(colorSpace, whitePoint, Adaptation.BRADFORD);
+ }
+
+ @NonNull
+ public static ColorSpace adapt(@NonNull ColorSpace colorSpace,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull Adaptation adaptation) {
+ if (colorSpace.getModel() == Model.RGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace;
+ if (compare(rgb.mWhitePoint, whitePoint)) return colorSpace;
+
+ float[] xyz = whitePoint.length == 3 ?
+ Arrays.copyOf(whitePoint, 3) : xyYToXyz(whitePoint);
+ float[] adaptationTransform = chromaticAdaptation(adaptation.mTransform,
+ xyYToXyz(rgb.getWhitePoint()), xyz);
+ float[] transform = mul3x3(adaptationTransform, rgb.mTransform);
+
+ return new ColorSpace.Rgb(rgb, transform, whitePoint);
+ }
+ return colorSpace;
+ }
+
+ @NonNull @Size(9)
+ private static float[] adaptToIlluminantD50(
+ @NonNull @Size(2) float[] origWhitePoint,
+ @NonNull @Size(9) float[] origTransform) {
+ float[] desired = ILLUMINANT_D50;
+ if (compare(origWhitePoint, desired)) return origTransform;
+
+ float[] xyz = xyYToXyz(desired);
+ float[] adaptationTransform = chromaticAdaptation(Adaptation.BRADFORD.mTransform,
+ xyYToXyz(origWhitePoint), xyz);
+ return mul3x3(adaptationTransform, origTransform);
+ }
+
+ @NonNull
+ static ColorSpace get(@IntRange(from = MIN_ID, to = MAX_ID) int index) {
+ ColorSpace colorspace = sNamedColorSpaceMap.get(index);
+ if (colorspace == null) {
+ throw new IllegalArgumentException("Invalid ID: " + index);
+ }
+ return colorspace;
+ }
+
+ @SuppressLint("MethodNameUnits")
+ @Nullable
+ public static ColorSpace getFromDataSpace(int dataSpace) {
+ return null;
+ }
+
+ @SuppressLint("MethodNameUnits")
+ public int getDataSpace() {
+ return -1;
+ }
+
+ @NonNull
+ public static ColorSpace get(@NonNull Named name) {
+ ColorSpace colorSpace = sNamedColorSpaceMap.get(name.ordinal());
+ if (colorSpace == null) {
+ return sNamedColorSpaceMap.get(Named.SRGB.ordinal());
+ }
+ return colorSpace;
+ }
+
+ @Nullable
+ public static ColorSpace match(
+ @NonNull @Size(9) float[] toXYZD50,
+ @NonNull Rgb.TransferParameters function) {
+
+ Collection colorspaces = sNamedColorSpaceMap.values();
+ for (ColorSpace colorSpace : colorspaces) {
+ if (colorSpace.getModel() == Model.RGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) adapt(colorSpace, ILLUMINANT_D50_XYZ);
+ if (compare(toXYZD50, rgb.mTransform) &&
+ compare(function, rgb.mTransferParameters)) {
+ return colorSpace;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ static {
+ sNamedColorSpaceMap.put(Named.SRGB.ordinal(), new ColorSpace.Rgb(
+ "sRGB IEC61966-2.1",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ SRGB_TRANSFER_PARAMETERS,
+ Named.SRGB.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.LINEAR_SRGB.ordinal(), new ColorSpace.Rgb(
+ "sRGB IEC61966-2.1 (Linear)",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ 1.0,
+ 0.0f, 1.0f,
+ Named.LINEAR_SRGB.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.EXTENDED_SRGB.ordinal(), new ColorSpace.Rgb(
+ "scRGB-nl IEC 61966-2-2:2003",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ x -> absRcpResponse(x, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
+ x -> absResponse(x, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
+ -0.799f, 2.399f,
+ SRGB_TRANSFER_PARAMETERS,
+ Named.EXTENDED_SRGB.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.LINEAR_EXTENDED_SRGB.ordinal(), new ColorSpace.Rgb(
+ "scRGB IEC 61966-2-2:2003",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ 1.0,
+ -0.5f, 7.499f,
+ Named.LINEAR_EXTENDED_SRGB.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.BT709.ordinal(), new ColorSpace.Rgb(
+ "Rec. ITU-R BT.709-5",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ SMPTE_170M_TRANSFER_PARAMETERS,
+ Named.BT709.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.BT2020.ordinal(), new ColorSpace.Rgb(
+ "Rec. ITU-R BT.2020-1",
+ BT2020_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45),
+ Named.BT2020.ordinal()
+ ));
+
+ sNamedColorSpaceMap.put(Named.DCI_P3.ordinal(), new ColorSpace.Rgb(
+ "SMPTE RP 431-2-2007 DCI (P3)",
+ DCI_P3_PRIMARIES,
+ new float[] { 0.314f, 0.351f },
+ 2.6,
+ 0.0f, 1.0f,
+ Named.DCI_P3.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.DISPLAY_P3.ordinal(), new ColorSpace.Rgb(
+ "Display P3",
+ DCI_P3_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ SRGB_TRANSFER_PARAMETERS,
+ Named.DISPLAY_P3.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.NTSC_1953.ordinal(), new ColorSpace.Rgb(
+ "NTSC (1953)",
+ NTSC_1953_PRIMARIES,
+ ILLUMINANT_C,
+ null,
+ SMPTE_170M_TRANSFER_PARAMETERS,
+ Named.NTSC_1953.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.SMPTE_C.ordinal(), new ColorSpace.Rgb(
+ "SMPTE-C RGB",
+ new float[] { 0.630f, 0.340f, 0.310f, 0.595f, 0.155f, 0.070f },
+ ILLUMINANT_D65,
+ null,
+ SMPTE_170M_TRANSFER_PARAMETERS,
+ Named.SMPTE_C.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.ADOBE_RGB.ordinal(), new ColorSpace.Rgb(
+ "Adobe RGB (1998)",
+ new float[] { 0.64f, 0.33f, 0.21f, 0.71f, 0.15f, 0.06f },
+ ILLUMINANT_D65,
+ 2.2,
+ 0.0f, 1.0f,
+ Named.ADOBE_RGB.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.PRO_PHOTO_RGB.ordinal(), new ColorSpace.Rgb(
+ "ROMM RGB ISO 22028-2:2013",
+ new float[] { 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f },
+ ILLUMINANT_D50,
+ null,
+ new Rgb.TransferParameters(1.0, 0.0, 1 / 16.0, 0.031248, 1.8),
+ Named.PRO_PHOTO_RGB.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.ACES.ordinal(), new ColorSpace.Rgb(
+ "SMPTE ST 2065-1:2012 ACES",
+ new float[] { 0.73470f, 0.26530f, 0.0f, 1.0f, 0.00010f, -0.0770f },
+ ILLUMINANT_D60,
+ 1.0,
+ -65504.0f, 65504.0f,
+ Named.ACES.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.ACESCG.ordinal(), new ColorSpace.Rgb(
+ "Academy S-2014-004 ACEScg",
+ new float[] { 0.713f, 0.293f, 0.165f, 0.830f, 0.128f, 0.044f },
+ ILLUMINANT_D60,
+ 1.0,
+ -65504.0f, 65504.0f,
+ Named.ACESCG.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.CIE_XYZ.ordinal(), new Xyz(
+ "Generic XYZ",
+ Named.CIE_XYZ.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.CIE_LAB.ordinal(), new ColorSpace.Lab(
+ "Generic L*a*b*",
+ Named.CIE_LAB.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.BT2020_HLG.ordinal(), new ColorSpace.Rgb(
+ "Hybrid Log Gamma encoding",
+ BT2020_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ x -> transferHLGOETF(BT2020_HLG_TRANSFER_PARAMETERS, x),
+ x -> transferHLGEOTF(BT2020_HLG_TRANSFER_PARAMETERS, x),
+ 0.0f, 1.0f,
+ BT2020_HLG_TRANSFER_PARAMETERS,
+ Named.BT2020_HLG.ordinal()
+ ));
+ sNamedColorSpaceMap.put(Named.BT2020_PQ.ordinal(), new ColorSpace.Rgb(
+ "Perceptual Quantizer encoding",
+ BT2020_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ x -> transferST2048OETF(BT2020_PQ_TRANSFER_PARAMETERS, x),
+ x -> transferST2048EOTF(BT2020_PQ_TRANSFER_PARAMETERS, x),
+ 0.0f, 1.0f,
+ BT2020_PQ_TRANSFER_PARAMETERS,
+ Named.BT2020_PQ.ordinal()
+ ));
+ }
+
+ private static double transferHLGOETF(Rgb.TransferParameters params, double x) {
+ double sign = x < 0 ? -1.0 : 1.0;
+ x *= sign;
+
+ // Unpack the transfer params matching skia's packing & invert R, G, and a
+ final double R = 1.0 / params.a;
+ final double G = 1.0 / params.b;
+ final double a = 1.0 / params.c;
+ final double b = params.d;
+ final double c = params.e;
+ final double K = params.f + 1.0;
+
+ x /= K;
+ return sign * (x <= 1 ? R * Math.pow(x, G) : a * Math.log(x - b) + c);
+ }
+
+ private static double transferHLGEOTF(Rgb.TransferParameters params, double x) {
+ double sign = x < 0 ? -1.0 : 1.0;
+ x *= sign;
+
+ // Unpack the transfer params matching skia's packing
+ final double R = params.a;
+ final double G = params.b;
+ final double a = params.c;
+ final double b = params.d;
+ final double c = params.e;
+ final double K = params.f + 1.0;
+
+ return K * sign * (x * R <= 1 ? Math.pow(x * R, G) : Math.exp((x - c) * a) + b);
+ }
+
+ private static double transferST2048OETF(Rgb.TransferParameters params, double x) {
+ double sign = x < 0 ? -1.0 : 1.0;
+ x *= sign;
+
+ double a = -params.a;
+ double b = params.d;
+ double c = 1.0 / params.f;
+ double d = params.b;
+ double e = -params.e;
+ double f = 1.0 / params.c;
+
+ double tmp = Math.max(a + b * Math.pow(x, c), 0);
+ return sign * Math.pow(tmp / (d + e * Math.pow(x, c)), f);
+ }
+
+ private static double transferST2048EOTF(Rgb.TransferParameters pq, double x) {
+ double sign = x < 0 ? -1.0 : 1.0;
+ x *= sign;
+
+ double tmp = Math.max(pq.a + pq.b * Math.pow(x, pq.c), 0);
+ return sign * Math.pow(tmp / (pq.d + pq.e * Math.pow(x, pq.c)), pq.f);
+ }
+
+ // Reciprocal piecewise gamma response
+ private static double rcpResponse(double x, double a, double b, double c, double d, double g) {
+ return x >= d * c ? (Math.pow(x, 1.0 / g) - b) / a : x / c;
+ }
+
+ // Piecewise gamma response
+ private static double response(double x, double a, double b, double c, double d, double g) {
+ return x >= d ? Math.pow(a * x + b, g) : c * x;
+ }
+
+ // Reciprocal piecewise gamma response
+ private static double rcpResponse(double x, double a, double b, double c, double d,
+ double e, double f, double g) {
+ return x >= d * c ? (Math.pow(x - e, 1.0 / g) - b) / a : (x - f) / c;
+ }
+
+ // Piecewise gamma response
+ private static double response(double x, double a, double b, double c, double d,
+ double e, double f, double g) {
+ return x >= d ? Math.pow(a * x + b, g) + e : c * x + f;
+ }
+
+ // Reciprocal piecewise gamma response, encoded as sign(x).f(abs(x)) for color
+ // spaces that allow negative values
+ @SuppressWarnings("SameParameterValue")
+ private static double absRcpResponse(double x, double a, double b, double c, double d, double g) {
+ return Math.copySign(rcpResponse(x < 0.0 ? -x : x, a, b, c, d, g), x);
+ }
+
+ // Piecewise gamma response, encoded as sign(x).f(abs(x)) for color spaces that
+ // allow negative values
+ @SuppressWarnings("SameParameterValue")
+ private static double absResponse(double x, double a, double b, double c, double d, double g) {
+ return Math.copySign(response(x < 0.0 ? -x : x, a, b, c, d, g), x);
+ }
+
+ private static boolean compare(
+ @Nullable Rgb.TransferParameters a,
+ @Nullable Rgb.TransferParameters b) {
+ //noinspection SimplifiableIfStatement
+ if (a == null && b == null) return true;
+ return a != null && b != null &&
+ Math.abs(a.a - b.a) < 1e-3 &&
+ Math.abs(a.b - b.b) < 1e-3 &&
+ Math.abs(a.c - b.c) < 1e-3 &&
+ Math.abs(a.d - b.d) < 2e-3 && // Special case for variations in sRGB OETF/EOTF
+ Math.abs(a.e - b.e) < 1e-3 &&
+ Math.abs(a.f - b.f) < 1e-3 &&
+ Math.abs(a.g - b.g) < 1e-3;
+ }
+
+ private static boolean compare(@NonNull float[] a, @NonNull float[] b) {
+ if (a == b) return true;
+ for (int i = 0; i < a.length; i++) {
+ if (Float.compare(a[i], b[i]) != 0 && Math.abs(a[i] - b[i]) > 1e-3f) return false;
+ }
+ return true;
+ }
+
+ @NonNull
+ @Size(9)
+ private static float[] inverse3x3(@NonNull @Size(9) float[] m) {
+ float a = m[0];
+ float b = m[3];
+ float c = m[6];
+ float d = m[1];
+ float e = m[4];
+ float f = m[7];
+ float g = m[2];
+ float h = m[5];
+ float i = m[8];
+
+ float A = e * i - f * h;
+ float B = f * g - d * i;
+ float C = d * h - e * g;
+
+ float det = a * A + b * B + c * C;
+
+ float inverted[] = new float[m.length];
+ inverted[0] = A / det;
+ inverted[1] = B / det;
+ inverted[2] = C / det;
+ inverted[3] = (c * h - b * i) / det;
+ inverted[4] = (a * i - c * g) / det;
+ inverted[5] = (b * g - a * h) / det;
+ inverted[6] = (b * f - c * e) / det;
+ inverted[7] = (c * d - a * f) / det;
+ inverted[8] = (a * e - b * d) / det;
+ return inverted;
+ }
+
+ @NonNull
+ @Size(9)
+ private static float[] mul3x3(@NonNull @Size(9) float[] lhs, @NonNull @Size(9) float[] rhs) {
+ float[] r = new float[9];
+ r[0] = lhs[0] * rhs[0] + lhs[3] * rhs[1] + lhs[6] * rhs[2];
+ r[1] = lhs[1] * rhs[0] + lhs[4] * rhs[1] + lhs[7] * rhs[2];
+ r[2] = lhs[2] * rhs[0] + lhs[5] * rhs[1] + lhs[8] * rhs[2];
+ r[3] = lhs[0] * rhs[3] + lhs[3] * rhs[4] + lhs[6] * rhs[5];
+ r[4] = lhs[1] * rhs[3] + lhs[4] * rhs[4] + lhs[7] * rhs[5];
+ r[5] = lhs[2] * rhs[3] + lhs[5] * rhs[4] + lhs[8] * rhs[5];
+ r[6] = lhs[0] * rhs[6] + lhs[3] * rhs[7] + lhs[6] * rhs[8];
+ r[7] = lhs[1] * rhs[6] + lhs[4] * rhs[7] + lhs[7] * rhs[8];
+ r[8] = lhs[2] * rhs[6] + lhs[5] * rhs[7] + lhs[8] * rhs[8];
+ return r;
+ }
+
+ @NonNull
+ @Size(min = 3)
+ private static float[] mul3x3Float3(
+ @NonNull @Size(9) float[] lhs, @NonNull @Size(min = 3) float[] rhs) {
+ float r0 = rhs[0];
+ float r1 = rhs[1];
+ float r2 = rhs[2];
+ rhs[0] = lhs[0] * r0 + lhs[3] * r1 + lhs[6] * r2;
+ rhs[1] = lhs[1] * r0 + lhs[4] * r1 + lhs[7] * r2;
+ rhs[2] = lhs[2] * r0 + lhs[5] * r1 + lhs[8] * r2;
+ return rhs;
+ }
+
+ @NonNull
+ @Size(9)
+ private static float[] mul3x3Diag(
+ @NonNull @Size(3) float[] lhs, @NonNull @Size(9) float[] rhs) {
+ return new float[] {
+ lhs[0] * rhs[0], lhs[1] * rhs[1], lhs[2] * rhs[2],
+ lhs[0] * rhs[3], lhs[1] * rhs[4], lhs[2] * rhs[5],
+ lhs[0] * rhs[6], lhs[1] * rhs[7], lhs[2] * rhs[8]
+ };
+ }
+
+ @NonNull
+ @Size(3)
+ private static float[] xyYToXyz(@NonNull @Size(2) float[] xyY) {
+ return new float[] { xyY[0] / xyY[1], 1.0f, (1 - xyY[0] - xyY[1]) / xyY[1] };
+ }
+
+ @NonNull
+ @Size(9)
+ private static float[] chromaticAdaptation(@NonNull @Size(9) float[] matrix,
+ @NonNull @Size(3) float[] srcWhitePoint, @NonNull @Size(3) float[] dstWhitePoint) {
+ float[] srcLMS = mul3x3Float3(matrix, srcWhitePoint);
+ float[] dstLMS = mul3x3Float3(matrix, dstWhitePoint);
+ // LMS is a diagonal matrix stored as a float[3]
+ float[] LMS = { dstLMS[0] / srcLMS[0], dstLMS[1] / srcLMS[1], dstLMS[2] / srcLMS[2] };
+ return mul3x3(inverse3x3(matrix), mul3x3Diag(LMS, matrix));
+ }
+
+ @NonNull
+ @Size(3)
+ public static float[] cctToXyz(@IntRange(from = 1) int cct) {
+ if (cct < 1) {
+ throw new IllegalArgumentException("Temperature must be greater than 0");
+ }
+
+ final float icct = 1e3f / cct;
+ final float icct2 = icct * icct;
+ final float x = cct <= 4000.0f ?
+ 0.179910f + 0.8776956f * icct - 0.2343589f * icct2 - 0.2661239f * icct2 * icct :
+ 0.240390f + 0.2226347f * icct + 2.1070379f * icct2 - 3.0258469f * icct2 * icct;
+
+ final float x2 = x * x;
+ final float y = cct <= 2222.0f ?
+ -0.20219683f + 2.18555832f * x - 1.34811020f * x2 - 1.1063814f * x2 * x :
+ cct <= 4000.0f ?
+ -0.16748867f + 2.09137015f * x - 1.37418593f * x2 - 0.9549476f * x2 * x :
+ -0.37001483f + 3.75112997f * x - 5.8733867f * x2 + 3.0817580f * x2 * x;
+
+ return xyYToXyz(new float[] {x, y});
+ }
+
+ @NonNull
+ @Size(9)
+ public static float[] chromaticAdaptation(@NonNull Adaptation adaptation,
+ @NonNull @Size(min = 2, max = 3) float[] srcWhitePoint,
+ @NonNull @Size(min = 2, max = 3) float[] dstWhitePoint) {
+ if ((srcWhitePoint.length != 2 && srcWhitePoint.length != 3)
+ || (dstWhitePoint.length != 2 && dstWhitePoint.length != 3)) {
+ throw new IllegalArgumentException("A white point array must have 2 or 3 floats");
+ }
+ float[] srcXyz = srcWhitePoint.length == 3 ?
+ Arrays.copyOf(srcWhitePoint, 3) : xyYToXyz(srcWhitePoint);
+ float[] dstXyz = dstWhitePoint.length == 3 ?
+ Arrays.copyOf(dstWhitePoint, 3) : xyYToXyz(dstWhitePoint);
+
+ if (compare(srcXyz, dstXyz)) {
+ return new float[] {
+ 1.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f
+ };
+ }
+ return chromaticAdaptation(adaptation.mTransform, srcXyz, dstXyz);
+ }
+
+ private static final class Xyz extends ColorSpace {
+ private Xyz(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ super(name, Model.XYZ, id);
+ }
+
+ @Override
+ public boolean isWideGamut() {
+ return true;
+ }
+
+ @Override
+ public float getMinValue(@IntRange(from = 0, to = 3) int component) {
+ return -2.0f;
+ }
+
+ @Override
+ public float getMaxValue(@IntRange(from = 0, to = 3) int component) {
+ return 2.0f;
+ }
+
+ @Override
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0]);
+ v[1] = clamp(v[1]);
+ v[2] = clamp(v[2]);
+ return v;
+ }
+
+ @Override
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0]);
+ v[1] = clamp(v[1]);
+ v[2] = clamp(v[2]);
+ return v;
+ }
+
+ private static float clamp(float x) {
+ return x < -2.0f ? -2.0f : x > 2.0f ? 2.0f : x;
+ }
+ }
+
+ private static final class Lab extends ColorSpace {
+ private static final float A = 216.0f / 24389.0f;
+ private static final float B = 841.0f / 108.0f;
+ private static final float C = 4.0f / 29.0f;
+ private static final float D = 6.0f / 29.0f;
+
+ private Lab(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ super(name, Model.LAB, id);
+ }
+
+ @Override
+ public boolean isWideGamut() {
+ return true;
+ }
+
+ @Override
+ public float getMinValue(@IntRange(from = 0, to = 3) int component) {
+ return component == 0 ? 0.0f : -128.0f;
+ }
+
+ @Override
+ public float getMaxValue(@IntRange(from = 0, to = 3) int component) {
+ return component == 0 ? 100.0f : 128.0f;
+ }
+
+ @Override
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0], 0.0f, 100.0f);
+ v[1] = clamp(v[1], -128.0f, 128.0f);
+ v[2] = clamp(v[2], -128.0f, 128.0f);
+
+ float fy = (v[0] + 16.0f) / 116.0f;
+ float fx = fy + (v[1] * 0.002f);
+ float fz = fy - (v[2] * 0.005f);
+ float X = fx > D ? fx * fx * fx : (1.0f / B) * (fx - C);
+ float Y = fy > D ? fy * fy * fy : (1.0f / B) * (fy - C);
+ float Z = fz > D ? fz * fz * fz : (1.0f / B) * (fz - C);
+
+ v[0] = X * ILLUMINANT_D50_XYZ[0];
+ v[1] = Y * ILLUMINANT_D50_XYZ[1];
+ v[2] = Z * ILLUMINANT_D50_XYZ[2];
+
+ return v;
+ }
+
+ @Override
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ float X = v[0] / ILLUMINANT_D50_XYZ[0];
+ float Y = v[1] / ILLUMINANT_D50_XYZ[1];
+ float Z = v[2] / ILLUMINANT_D50_XYZ[2];
+
+ float fx = X > A ? (float) Math.pow(X, 1.0 / 3.0) : B * X + C;
+ float fy = Y > A ? (float) Math.pow(Y, 1.0 / 3.0) : B * Y + C;
+ float fz = Z > A ? (float) Math.pow(Z, 1.0 / 3.0) : B * Z + C;
+
+ float L = 116.0f * fy - 16.0f;
+ float a = 500.0f * (fx - fy);
+ float b = 200.0f * (fy - fz);
+
+ v[0] = clamp(L, 0.0f, 100.0f);
+ v[1] = clamp(a, -128.0f, 128.0f);
+ v[2] = clamp(b, -128.0f, 128.0f);
+
+ return v;
+ }
+ }
+
+ private static float clamp(float x, float min, float max) {
+ return x < min ? min : x > max ? max : x;
+ }
+
+ private static final class OkLab extends ColorSpace {
+
+ private OkLab(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ super(name, Model.LAB, id);
+ }
+
+ @Override
+ public boolean isWideGamut() {
+ return true;
+ }
+
+ @Override
+ public float getMinValue(@IntRange(from = 0, to = 3) int component) {
+ return component == 0 ? 0.0f : -0.5f;
+ }
+
+ @Override
+ public float getMaxValue(@IntRange(from = 0, to = 3) int component) {
+ return component == 0 ? 1.0f : 0.5f;
+ }
+
+ @Override
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0], 0.0f, 1.0f);
+ v[1] = clamp(v[1], -0.5f, 0.5f);
+ v[2] = clamp(v[2], -0.5f, 0.5f);
+
+ mul3x3Float3(INVERSE_M2, v);
+ v[0] = v[0] * v[0] * v[0];
+ v[1] = v[1] * v[1] * v[1];
+ v[2] = v[2] * v[2] * v[2];
+
+ mul3x3Float3(INVERSE_M1, v);
+
+ return v;
+ }
+
+ @Override
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ mul3x3Float3(M1, v);
+
+ v[0] = (float) Math.cbrt(v[0]);
+ v[1] = (float) Math.cbrt(v[1]);
+ v[2] = (float) Math.cbrt(v[2]);
+
+ mul3x3Float3(M2, v);
+ return v;
+ }
+
+ private static final float[] M1TMP = {
+ 0.8189330101f, 0.0329845436f, 0.0482003018f,
+ 0.3618667424f, 0.9293118715f, 0.2643662691f,
+ -0.1288597137f, 0.0361456387f, 0.6338517070f
+ };
+
+ private static final float[] M1 = mul3x3(
+ M1TMP,
+ chromaticAdaptation(Adaptation.BRADFORD, ILLUMINANT_D50, ILLUMINANT_D65)
+ );
+
+ private static final float[] M2 = {
+ 0.2104542553f, 1.9779984951f, 0.0259040371f,
+ 0.7936177850f, -2.4285922050f, 0.7827717662f,
+ -0.0040720468f, 0.4505937099f, -0.8086757660f
+ };
+
+ private static final float[] INVERSE_M1 = inverse3x3(M1);
+
+ private static final float[] INVERSE_M2 = inverse3x3(M2);
+ }
+
+ public static class Rgb extends ColorSpace {
+ public static class TransferParameters {
+
+ private static final double TYPE_PQish = -2.0;
+ private static final double TYPE_HLGish = -3.0;
+
+ /** Variable \(a\) in the equation of the EOTF described above. */
+ public final double a;
+ /** Variable \(b\) in the equation of the EOTF described above. */
+ public final double b;
+ /** Variable \(c\) in the equation of the EOTF described above. */
+ public final double c;
+ /** Variable \(d\) in the equation of the EOTF described above. */
+ public final double d;
+ /** Variable \(e\) in the equation of the EOTF described above. */
+ public final double e;
+ /** Variable \(f\) in the equation of the EOTF described above. */
+ public final double f;
+ /** Variable \(g\) in the equation of the EOTF described above. */
+ public final double g;
+
+ private static boolean isSpecialG(double g) {
+ return g == TYPE_PQish || g == TYPE_HLGish;
+ }
+
+ public TransferParameters(double a, double b, double c, double d, double g) {
+ this(a, b, c, d, 0.0, 0.0, g);
+ }
+
+ public TransferParameters(double a, double b, double c, double d, double e,
+ double f, double g) {
+ if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c)
+ || Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f)
+ || Double.isNaN(g)) {
+ throw new IllegalArgumentException("Parameters cannot be NaN");
+ }
+ if (!isSpecialG(g)) {
+ // Next representable float after 1.0
+ // We use doubles here but the representation inside our native code
+ // is often floats
+ if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) {
+ throw new IllegalArgumentException(
+ "Parameter d must be in the range [0..1], " + "was " + d);
+ }
+
+ if (d == 0.0 && (a == 0.0 || g == 0.0)) {
+ throw new IllegalArgumentException(
+ "Parameter a or g is zero, the transfer function is constant");
+ }
+
+ if (d >= 1.0 && c == 0.0) {
+ throw new IllegalArgumentException(
+ "Parameter c is zero, the transfer function is constant");
+ }
+
+ if ((a == 0.0 || g == 0.0) && c == 0.0) {
+ throw new IllegalArgumentException("Parameter a or g is zero,"
+ + " and c is zero, the transfer function is constant");
+ }
+
+ if (c < 0.0) {
+ throw new IllegalArgumentException(
+ "The transfer function must be increasing");
+ }
+
+ if (a < 0.0 || g < 0.0) {
+ throw new IllegalArgumentException(
+ "The transfer function must be positive or increasing");
+ }
+ }
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ this.d = d;
+ this.e = e;
+ this.f = f;
+ this.g = g;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TransferParameters that = (TransferParameters) o;
+
+ if (Double.compare(that.a, a) != 0) return false;
+ if (Double.compare(that.b, b) != 0) return false;
+ if (Double.compare(that.c, c) != 0) return false;
+ if (Double.compare(that.d, d) != 0) return false;
+ if (Double.compare(that.e, e) != 0) return false;
+ if (Double.compare(that.f, f) != 0) return false;
+ return Double.compare(that.g, g) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ temp = Double.doubleToLongBits(a);
+ result = (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(b);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(c);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(d);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(e);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(f);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(g);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ private boolean isHLGish() {
+ return g == TYPE_HLGish;
+ }
+
+ private boolean isPQish() {
+ return g == TYPE_PQish;
+ }
+ }
+
+ @NonNull private final float[] mWhitePoint;
+ @NonNull private final float[] mPrimaries;
+ @NonNull private final float[] mTransform;
+ @NonNull private final float[] mInverseTransform;
+
+ @NonNull private final DoubleUnaryOperator mOetf;
+ @NonNull private final DoubleUnaryOperator mEotf;
+ @NonNull private final DoubleUnaryOperator mClampedOetf;
+ @NonNull private final DoubleUnaryOperator mClampedEotf;
+
+ private final float mMin;
+ private final float mMax;
+
+ private final boolean mIsWideGamut;
+ private final boolean mIsSrgb;
+
+ @Nullable private final TransferParameters mTransferParameters;
+
+
+ private static DoubleUnaryOperator generateOETF(TransferParameters function) {
+ if (function.isHLGish()) {
+ return x -> transferHLGOETF(function, x);
+ } else if (function.isPQish()) {
+ return x -> transferST2048OETF(function, x);
+ } else {
+ return function.e == 0.0 && function.f == 0.0
+ ? x -> rcpResponse(x, function.a, function.b,
+ function.c, function.d, function.g)
+ : x -> rcpResponse(x, function.a, function.b, function.c,
+ function.d, function.e, function.f, function.g);
+ }
+ }
+
+ private static DoubleUnaryOperator generateEOTF(TransferParameters function) {
+ if (function.isHLGish()) {
+ return x -> transferHLGEOTF(function, x);
+ } else if (function.isPQish()) {
+ return x -> transferST2048OETF(function, x);
+ } else {
+ return function.e == 0.0 && function.f == 0.0
+ ? x -> response(x, function.a, function.b,
+ function.c, function.d, function.g)
+ : x -> response(x, function.a, function.b, function.c,
+ function.d, function.e, function.f, function.g);
+ }
+ }
+
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(9) float[] toXYZ,
+ @NonNull DoubleUnaryOperator oetf,
+ @NonNull DoubleUnaryOperator eotf) {
+ this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), null,
+ oetf, eotf, 0.0f, 1.0f, null, MIN_ID);
+ }
+
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull DoubleUnaryOperator oetf,
+ @NonNull DoubleUnaryOperator eotf,
+ float min,
+ float max) {
+ this(name, primaries, whitePoint, null, oetf, eotf, min, max, null, MIN_ID);
+ }
+
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(9) float[] toXYZ,
+ @NonNull TransferParameters function) {
+ // Note: when isGray() returns false, this passes null for the transform for
+ // consistency with other constructors, which compute the transform from the primaries
+ // and white point.
+ this(name, isGray(toXYZ) ? GRAY_PRIMARIES : computePrimaries(toXYZ),
+ computeWhitePoint(toXYZ), isGray(toXYZ) ? toXYZ : null, function, MIN_ID);
+ }
+
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull TransferParameters function) {
+ this(name, primaries, whitePoint, null, function, MIN_ID);
+ }
+
+ private Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @Nullable @Size(9) float[] transform,
+ @NonNull TransferParameters function,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ this(name, primaries, whitePoint, transform,
+ generateOETF(function),
+ generateEOTF(function),
+ 0.0f, 1.0f, function, id);
+ }
+
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(9) float[] toXYZ,
+ double gamma) {
+ this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), gamma, 0.0f, 1.0f, MIN_ID);
+ }
+
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ double gamma) {
+ this(name, primaries, whitePoint, gamma, 0.0f, 1.0f, MIN_ID);
+ }
+
+ private Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ double gamma,
+ float min,
+ float max,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ this(name, primaries, whitePoint, null,
+ gamma == 1.0 ? DoubleUnaryOperator.identity() :
+ x -> Math.pow(x < 0.0 ? 0.0 : x, 1 / gamma),
+ gamma == 1.0 ? DoubleUnaryOperator.identity() :
+ x -> Math.pow(x < 0.0 ? 0.0 : x, gamma),
+ min, max, new TransferParameters(1.0, 0.0, 0.0, 0.0, gamma), id);
+ }
+
+ private Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @Nullable @Size(9) float[] transform,
+ @NonNull DoubleUnaryOperator oetf,
+ @NonNull DoubleUnaryOperator eotf,
+ float min,
+ float max,
+ @Nullable TransferParameters transferParameters,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+
+ super(name, Model.RGB, id);
+
+ if (primaries == null || (primaries.length != 6 && primaries.length != 9)) {
+ throw new IllegalArgumentException("The color space's primaries must be " +
+ "defined as an array of 6 floats in xyY or 9 floats in XYZ");
+ }
+
+ if (whitePoint == null || (whitePoint.length != 2 && whitePoint.length != 3)) {
+ throw new IllegalArgumentException("The color space's white point must be " +
+ "defined as an array of 2 floats in xyY or 3 float in XYZ");
+ }
+
+ if (oetf == null || eotf == null) {
+ throw new IllegalArgumentException("The transfer functions of a color space " +
+ "cannot be null");
+ }
+
+ if (min >= max) {
+ throw new IllegalArgumentException("Invalid range: min=" + min + ", max=" + max +
+ "; min must be strictly < max");
+ }
+
+ mWhitePoint = xyWhitePoint(whitePoint);
+ mPrimaries = xyPrimaries(primaries);
+
+ if (transform == null) {
+ mTransform = computeXYZMatrix(mPrimaries, mWhitePoint);
+ } else {
+ if (transform.length != 9) {
+ throw new IllegalArgumentException("Transform must have 9 entries! Has "
+ + transform.length);
+ }
+ mTransform = transform;
+ }
+ mInverseTransform = inverse3x3(mTransform);
+
+ mOetf = oetf;
+ mEotf = eotf;
+
+ mMin = min;
+ mMax = max;
+
+ DoubleUnaryOperator clamp = this::clamp;
+ mClampedOetf = oetf.andThen(clamp);
+ mClampedEotf = clamp.andThen(eotf);
+
+ mTransferParameters = transferParameters;
+
+ // A color space is wide-gamut if its area is >90% of NTSC 1953 and
+ // if it entirely contains the Color space definition in xyY
+ mIsWideGamut = isWideGamut(mPrimaries, min, max);
+ mIsSrgb = isSrgb(mPrimaries, mWhitePoint, oetf, eotf, min, max, id);
+ }
+
+ private Rgb(Rgb colorSpace,
+ @NonNull @Size(9) float[] transform,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint) {
+ this(colorSpace.getName(), colorSpace.mPrimaries, whitePoint, transform,
+ colorSpace.mOetf, colorSpace.mEotf, colorSpace.mMin, colorSpace.mMax,
+ colorSpace.mTransferParameters, MIN_ID);
+ }
+
+ @NonNull
+ @Size(min = 2)
+ public float[] getWhitePoint(@NonNull @Size(min = 2) float[] whitePoint) {
+ whitePoint[0] = mWhitePoint[0];
+ whitePoint[1] = mWhitePoint[1];
+ return whitePoint;
+ }
+
+ @NonNull
+ @Size(2)
+ public float[] getWhitePoint() {
+ return Arrays.copyOf(mWhitePoint, mWhitePoint.length);
+ }
+
+ @NonNull
+ @Size(min = 6)
+ public float[] getPrimaries(@NonNull @Size(min = 6) float[] primaries) {
+ System.arraycopy(mPrimaries, 0, primaries, 0, mPrimaries.length);
+ return primaries;
+ }
+
+ @NonNull
+ @Size(6)
+ public float[] getPrimaries() {
+ return Arrays.copyOf(mPrimaries, mPrimaries.length);
+ }
+
+ @NonNull
+ @Size(min = 9)
+ public float[] getTransform(@NonNull @Size(min = 9) float[] transform) {
+ System.arraycopy(mTransform, 0, transform, 0, mTransform.length);
+ return transform;
+ }
+
+ @NonNull
+ @Size(9)
+ public float[] getTransform() {
+ return Arrays.copyOf(mTransform, mTransform.length);
+ }
+
+ @NonNull
+ @Size(min = 9)
+ public float[] getInverseTransform(@NonNull @Size(min = 9) float[] inverseTransform) {
+ System.arraycopy(mInverseTransform, 0, inverseTransform, 0, mInverseTransform.length);
+ return inverseTransform;
+ }
+
+ @NonNull
+ @Size(9)
+ public float[] getInverseTransform() {
+ return Arrays.copyOf(mInverseTransform, mInverseTransform.length);
+ }
+
+ @NonNull
+ public DoubleUnaryOperator getOetf() {
+ return mClampedOetf;
+ }
+
+ @NonNull
+ public DoubleUnaryOperator getEotf() {
+ return mClampedEotf;
+ }
+
+ @Nullable
+ public TransferParameters getTransferParameters() {
+ if (mTransferParameters != null
+ && !mTransferParameters.equals(BT2020_PQ_TRANSFER_PARAMETERS)
+ && !mTransferParameters.equals(BT2020_HLG_TRANSFER_PARAMETERS)) {
+ return mTransferParameters;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isSrgb() {
+ return mIsSrgb;
+ }
+
+ @Override
+ public boolean isWideGamut() {
+ return mIsWideGamut;
+ }
+
+ @Override
+ public float getMinValue(int component) {
+ return mMin;
+ }
+
+ @Override
+ public float getMaxValue(int component) {
+ return mMax;
+ }
+
+ @NonNull
+ @Size(3)
+ public float[] toLinear(float r, float g, float b) {
+ return toLinear(new float[] { r, g, b });
+ }
+
+ @NonNull
+ @Size(min = 3)
+ public float[] toLinear(@NonNull @Size(min = 3) float[] v) {
+ v[0] = (float) mClampedEotf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedEotf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedEotf.applyAsDouble(v[2]);
+ return v;
+ }
+
+ @NonNull
+ @Size(3)
+ public float[] fromLinear(float r, float g, float b) {
+ return fromLinear(new float[] { r, g, b });
+ }
+
+ @NonNull
+ @Size(min = 3)
+ public float[] fromLinear(@NonNull @Size(min = 3) float[] v) {
+ v[0] = (float) mClampedOetf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedOetf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedOetf.applyAsDouble(v[2]);
+ return v;
+ }
+
+ @Override
+ @NonNull
+ @Size(min = 3)
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = (float) mClampedEotf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedEotf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedEotf.applyAsDouble(v[2]);
+ return mul3x3Float3(mTransform, v);
+ }
+
+ @Override
+ @NonNull
+ @Size(min = 3)
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ mul3x3Float3(mInverseTransform, v);
+ v[0] = (float) mClampedOetf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedOetf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedOetf.applyAsDouble(v[2]);
+ return v;
+ }
+
+ private double clamp(double x) {
+ return x < mMin ? mMin : x > mMax ? mMax : x;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ Rgb rgb = (Rgb) o;
+
+ if (Float.compare(rgb.mMin, mMin) != 0) return false;
+ if (Float.compare(rgb.mMax, mMax) != 0) return false;
+ if (!Arrays.equals(mWhitePoint, rgb.mWhitePoint)) return false;
+ if (!Arrays.equals(mPrimaries, rgb.mPrimaries)) return false;
+ if (mTransferParameters != null) {
+ return mTransferParameters.equals(rgb.mTransferParameters);
+ } else if (rgb.mTransferParameters == null) {
+ return true;
+ }
+ //noinspection SimplifiableIfStatement
+ if (!mOetf.equals(rgb.mOetf)) return false;
+ return mEotf.equals(rgb.mEotf);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + Arrays.hashCode(mWhitePoint);
+ result = 31 * result + Arrays.hashCode(mPrimaries);
+ result = 31 * result + (mMin != +0.0f ? Float.floatToIntBits(mMin) : 0);
+ result = 31 * result + (mMax != +0.0f ? Float.floatToIntBits(mMax) : 0);
+ result = 31 * result +
+ (mTransferParameters != null ? mTransferParameters.hashCode() : 0);
+ if (mTransferParameters == null) {
+ result = 31 * result + mOetf.hashCode();
+ result = 31 * result + mEotf.hashCode();
+ }
+ return result;
+ }
+
+ @SuppressWarnings("RedundantIfStatement")
+ private static boolean isSrgb(
+ @NonNull @Size(6) float[] primaries,
+ @NonNull @Size(2) float[] whitePoint,
+ @NonNull DoubleUnaryOperator OETF,
+ @NonNull DoubleUnaryOperator EOTF,
+ float min,
+ float max,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ if (id == 0) return true;
+ if (!ColorSpace.compare(primaries, SRGB_PRIMARIES)) {
+ return false;
+ }
+ if (!ColorSpace.compare(whitePoint, ILLUMINANT_D65)) {
+ return false;
+ }
+
+ if (min != 0.0f) return false;
+ if (max != 1.0f) return false;
+
+ // We would have already returned true if this was SRGB itself, so
+ // it is safe to reference it here.
+ ColorSpace.Rgb srgb = (ColorSpace.Rgb) get(Named.SRGB);
+
+ for (double x = 0.0; x <= 1.0; x += 1 / 255.0) {
+ if (!compare(x, OETF, srgb.mOetf)) return false;
+ if (!compare(x, EOTF, srgb.mEotf)) return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isGray(@NonNull @Size(9) float[] toXYZ) {
+ return toXYZ.length == 9 && toXYZ[1] == 0 && toXYZ[2] == 0 && toXYZ[3] == 0
+ && toXYZ[5] == 0 && toXYZ[6] == 0 && toXYZ[7] == 0;
+ }
+
+ private static boolean compare(double point, @NonNull DoubleUnaryOperator a,
+ @NonNull DoubleUnaryOperator b) {
+ double rA = a.applyAsDouble(point);
+ double rB = b.applyAsDouble(point);
+ return Math.abs(rA - rB) <= 1e-3;
+ }
+
+ private static boolean isWideGamut(@NonNull @Size(6) float[] primaries,
+ float min, float max) {
+ return (area(primaries) / area(NTSC_1953_PRIMARIES) > 0.9f &&
+ contains(primaries, SRGB_PRIMARIES)) || (min < 0.0f && max > 1.0f);
+ }
+
+ private static float area(@NonNull @Size(6) float[] primaries) {
+ float Rx = primaries[0];
+ float Ry = primaries[1];
+ float Gx = primaries[2];
+ float Gy = primaries[3];
+ float Bx = primaries[4];
+ float By = primaries[5];
+ float det = Rx * Gy + Ry * Bx + Gx * By - Gy * Bx - Ry * Gx - Rx * By;
+ float r = 0.5f * det;
+ return r < 0.0f ? -r : r;
+ }
+
+ private static float cross(float ax, float ay, float bx, float by) {
+ return ax * by - ay * bx;
+ }
+
+ @SuppressWarnings("RedundantIfStatement")
+ private static boolean contains(@NonNull @Size(6) float[] p1, @NonNull @Size(6) float[] p2) {
+ // Translate the vertices p1 in the coordinates system
+ // with the vertices p2 as the origin
+ float[] p0 = new float[] {
+ p1[0] - p2[0], p1[1] - p2[1],
+ p1[2] - p2[2], p1[3] - p2[3],
+ p1[4] - p2[4], p1[5] - p2[5],
+ };
+ // Check the first vertex of p1
+ if (cross(p0[0], p0[1], p2[0] - p2[4], p2[1] - p2[5]) < 0 ||
+ cross(p2[0] - p2[2], p2[1] - p2[3], p0[0], p0[1]) < 0) {
+ return false;
+ }
+ // Check the second vertex of p1
+ if (cross(p0[2], p0[3], p2[2] - p2[0], p2[3] - p2[1]) < 0 ||
+ cross(p2[2] - p2[4], p2[3] - p2[5], p0[2], p0[3]) < 0) {
+ return false;
+ }
+ // Check the third vertex of p1
+ if (cross(p0[4], p0[5], p2[4] - p2[2], p2[5] - p2[3]) < 0 ||
+ cross(p2[4] - p2[0], p2[5] - p2[1], p0[4], p0[5]) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ @NonNull
+ @Size(6)
+ private static float[] computePrimaries(@NonNull @Size(9) float[] toXYZ) {
+ float[] r = mul3x3Float3(toXYZ, new float[] { 1.0f, 0.0f, 0.0f });
+ float[] g = mul3x3Float3(toXYZ, new float[] { 0.0f, 1.0f, 0.0f });
+ float[] b = mul3x3Float3(toXYZ, new float[] { 0.0f, 0.0f, 1.0f });
+
+ float rSum = r[0] + r[1] + r[2];
+ float gSum = g[0] + g[1] + g[2];
+ float bSum = b[0] + b[1] + b[2];
+
+ return new float[] {
+ r[0] / rSum, r[1] / rSum,
+ g[0] / gSum, g[1] / gSum,
+ b[0] / bSum, b[1] / bSum,
+ };
+ }
+
+ @NonNull
+ @Size(2)
+ private static float[] computeWhitePoint(@NonNull @Size(9) float[] toXYZ) {
+ float[] w = mul3x3Float3(toXYZ, new float[] { 1.0f, 1.0f, 1.0f });
+ float sum = w[0] + w[1] + w[2];
+ return new float[] { w[0] / sum, w[1] / sum };
+ }
+
+ @NonNull
+ @Size(6)
+ private static float[] xyPrimaries(@NonNull @Size(min = 6, max = 9) float[] primaries) {
+ float[] xyPrimaries = new float[6];
+
+ // XYZ to xyY
+ if (primaries.length == 9) {
+ float sum;
+
+ sum = primaries[0] + primaries[1] + primaries[2];
+ xyPrimaries[0] = primaries[0] / sum;
+ xyPrimaries[1] = primaries[1] / sum;
+
+ sum = primaries[3] + primaries[4] + primaries[5];
+ xyPrimaries[2] = primaries[3] / sum;
+ xyPrimaries[3] = primaries[4] / sum;
+
+ sum = primaries[6] + primaries[7] + primaries[8];
+ xyPrimaries[4] = primaries[6] / sum;
+ xyPrimaries[5] = primaries[7] / sum;
+ } else {
+ System.arraycopy(primaries, 0, xyPrimaries, 0, 6);
+ }
+
+ return xyPrimaries;
+ }
+
+ @NonNull
+ @Size(2)
+ private static float[] xyWhitePoint(@Size(min = 2, max = 3) float[] whitePoint) {
+ float[] xyWhitePoint = new float[2];
+
+ // XYZ to xyY
+ if (whitePoint.length == 3) {
+ float sum = whitePoint[0] + whitePoint[1] + whitePoint[2];
+ xyWhitePoint[0] = whitePoint[0] / sum;
+ xyWhitePoint[1] = whitePoint[1] / sum;
+ } else {
+ System.arraycopy(whitePoint, 0, xyWhitePoint, 0, 2);
+ }
+
+ return xyWhitePoint;
+ }
+
+ @NonNull
+ @Size(9)
+ private static float[] computeXYZMatrix(
+ @NonNull @Size(6) float[] primaries,
+ @NonNull @Size(2) float[] whitePoint) {
+ float Rx = primaries[0];
+ float Ry = primaries[1];
+ float Gx = primaries[2];
+ float Gy = primaries[3];
+ float Bx = primaries[4];
+ float By = primaries[5];
+ float Wx = whitePoint[0];
+ float Wy = whitePoint[1];
+
+ float oneRxRy = (1 - Rx) / Ry;
+ float oneGxGy = (1 - Gx) / Gy;
+ float oneBxBy = (1 - Bx) / By;
+ float oneWxWy = (1 - Wx) / Wy;
+
+ float RxRy = Rx / Ry;
+ float GxGy = Gx / Gy;
+ float BxBy = Bx / By;
+ float WxWy = Wx / Wy;
+
+ float BY =
+ ((oneWxWy - oneRxRy) * (GxGy - RxRy) - (WxWy - RxRy) * (oneGxGy - oneRxRy)) /
+ ((oneBxBy - oneRxRy) * (GxGy - RxRy) - (BxBy - RxRy) * (oneGxGy - oneRxRy));
+ float GY = (WxWy - RxRy - BY * (BxBy - RxRy)) / (GxGy - RxRy);
+ float RY = 1 - GY - BY;
+
+ float RYRy = RY / Ry;
+ float GYGy = GY / Gy;
+ float BYBy = BY / By;
+
+ return new float[] {
+ RYRy * Rx, RY, RYRy * (1 - Rx - Ry),
+ GYGy * Gx, GY, GYGy * (1 - Gx - Gy),
+ BYBy * Bx, BY, BYBy * (1 - Bx - By)
+ };
+ }
+ }
+
+ public static class Connector {
+ @NonNull private final ColorSpace mSource;
+ @NonNull private final ColorSpace mDestination;
+ @NonNull private final ColorSpace mTransformSource;
+ @NonNull private final ColorSpace mTransformDestination;
+ @NonNull private final RenderIntent mIntent;
+ @NonNull @Size(3) private final float[] mTransform;
+
+ Connector(@NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull RenderIntent intent) {
+ this(source, destination,
+ source.getModel() == Model.RGB ? adapt(source, ILLUMINANT_D50_XYZ) : source,
+ destination.getModel() == Model.RGB ?
+ adapt(destination, ILLUMINANT_D50_XYZ) : destination,
+ intent, computeTransform(source, destination, intent));
+ }
+
+ private Connector(
+ @NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull ColorSpace transformSource, @NonNull ColorSpace transformDestination,
+ @NonNull RenderIntent intent, @Nullable @Size(3) float[] transform) {
+ mSource = source;
+ mDestination = destination;
+ mTransformSource = transformSource;
+ mTransformDestination = transformDestination;
+ mIntent = intent;
+ mTransform = transform;
+ }
+
+ @Nullable
+ private static float[] computeTransform(@NonNull ColorSpace source,
+ @NonNull ColorSpace destination, @NonNull RenderIntent intent) {
+ if (intent != RenderIntent.ABSOLUTE) return null;
+
+ boolean srcRGB = source.getModel() == Model.RGB;
+ boolean dstRGB = destination.getModel() == Model.RGB;
+
+ if (srcRGB && dstRGB) return null;
+
+ if (srcRGB || dstRGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) (srcRGB ? source : destination);
+ float[] srcXYZ = srcRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ;
+ float[] dstXYZ = dstRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ;
+ return new float[] {
+ srcXYZ[0] / dstXYZ[0],
+ srcXYZ[1] / dstXYZ[1],
+ srcXYZ[2] / dstXYZ[2],
+ };
+ }
+
+ return null;
+ }
+
+ @NonNull
+ public ColorSpace getSource() {
+ return mSource;
+ }
+
+ @NonNull
+ public ColorSpace getDestination() {
+ return mDestination;
+ }
+
+ public RenderIntent getRenderIntent() {
+ return mIntent;
+ }
+
+ @NonNull
+ @Size(3)
+ public float[] transform(float r, float g, float b) {
+ return transform(new float[] { r, g, b });
+ }
+
+ @NonNull
+ @Size(min = 3)
+ public float[] transform(@NonNull @Size(min = 3) float[] v) {
+ float[] xyz = mTransformSource.toXyz(v);
+ if (mTransform != null) {
+ xyz[0] *= mTransform[0];
+ xyz[1] *= mTransform[1];
+ xyz[2] *= mTransform[2];
+ }
+ return mTransformDestination.fromXyz(xyz);
+ }
+
+ private static class Rgb extends Connector {
+ @NonNull private final ColorSpace.Rgb mSource;
+ @NonNull private final ColorSpace.Rgb mDestination;
+ @NonNull private final float[] mTransform;
+
+ Rgb(@NonNull ColorSpace.Rgb source, @NonNull ColorSpace.Rgb destination,
+ @NonNull RenderIntent intent) {
+ super(source, destination, source, destination, intent, null);
+ mSource = source;
+ mDestination = destination;
+ mTransform = computeTransform(source, destination, intent);
+ }
+
+ @Override
+ public float[] transform(@NonNull @Size(min = 3) float[] rgb) {
+ rgb[0] = (float) mSource.mClampedEotf.applyAsDouble(rgb[0]);
+ rgb[1] = (float) mSource.mClampedEotf.applyAsDouble(rgb[1]);
+ rgb[2] = (float) mSource.mClampedEotf.applyAsDouble(rgb[2]);
+ mul3x3Float3(mTransform, rgb);
+ rgb[0] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[0]);
+ rgb[1] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[1]);
+ rgb[2] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[2]);
+ return rgb;
+ }
+
+ @NonNull
+ @Size(9)
+ private static float[] computeTransform(
+ @NonNull ColorSpace.Rgb source,
+ @NonNull ColorSpace.Rgb destination,
+ @NonNull RenderIntent intent) {
+ if (compare(source.mWhitePoint, destination.mWhitePoint)) {
+ // RGB->RGB using the PCS of both color spaces since they have the same
+ return mul3x3(destination.mInverseTransform, source.mTransform);
+ } else {
+ // RGB->RGB using CIE XYZ D50 as the PCS
+ float[] transform = source.mTransform;
+ float[] inverseTransform = destination.mInverseTransform;
+
+ float[] srcXYZ = xyYToXyz(source.mWhitePoint);
+ float[] dstXYZ = xyYToXyz(destination.mWhitePoint);
+
+ if (!compare(source.mWhitePoint, ILLUMINANT_D50)) {
+ float[] srcAdaptation = chromaticAdaptation(
+ Adaptation.BRADFORD.mTransform, srcXYZ,
+ Arrays.copyOf(ILLUMINANT_D50_XYZ, 3));
+ transform = mul3x3(srcAdaptation, source.mTransform);
+ }
+
+ if (!compare(destination.mWhitePoint, ILLUMINANT_D50)) {
+ float[] dstAdaptation = chromaticAdaptation(
+ Adaptation.BRADFORD.mTransform, dstXYZ,
+ Arrays.copyOf(ILLUMINANT_D50_XYZ, 3));
+ inverseTransform = inverse3x3(mul3x3(dstAdaptation, destination.mTransform));
+ }
+
+ if (intent == RenderIntent.ABSOLUTE) {
+ transform = mul3x3Diag(
+ new float[] {
+ srcXYZ[0] / dstXYZ[0],
+ srcXYZ[1] / dstXYZ[1],
+ srcXYZ[2] / dstXYZ[2],
+ }, transform);
+ }
+
+ return mul3x3(inverseTransform, transform);
+ }
+ }
+ }
+
+ static Connector identity(ColorSpace source) {
+ return new Connector(source, source, RenderIntent.RELATIVE) {
+ @Override
+ public float[] transform(@NonNull @Size(min = 3) float[] v) {
+ return v;
+ }
+ };
+ }
+ }
+}
+
diff --git a/AndroidCompat/src/main/java/android/graphics/Paint.java b/AndroidCompat/src/main/java/android/graphics/Paint.java
new file mode 100644
index 00000000..bf38edbc
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/graphics/Paint.java
@@ -0,0 +1,1549 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed 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 android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.os.Build;
+import android.os.LocaleList;
+import android.text.SpannableString;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.awt.Font;
+import java.awt.font.TextAttribute;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+public class Paint {
+ private static final String TAG = "Paint";
+
+ @ColorLong private long mColor;
+ private ColorFilter mColorFilter;
+ private MaskFilter mMaskFilter;
+ private PathEffect mPathEffect;
+ private Shader mShader;
+ private Typeface mTypeface;
+ private Xfermode mXfermode;
+
+ private boolean mHasCompatScaling;
+ private float mCompatScaling;
+ private float mInvCompatScaling;
+
+ private LocaleList mLocales;
+ private String mFontFeatureSettings;
+ private String mFontVariationSettings;
+
+ private float mShadowLayerRadius;
+ private float mShadowLayerDx;
+ private float mShadowLayerDy;
+ @ColorLong private long mShadowLayerColor;
+
+ private int mFlags;
+ private Font mFont = new Font(null);
+ private Style mStyle = Style.FILL;
+ private float mStrokeWidth = 1.0f;
+
+ private static final Object sCacheLock = new Object();
+
+ private static final HashMap sMinikinLocaleListIdCache = new HashMap<>();
+
+ public int mBidiFlags = BIDI_DEFAULT_LTR;
+
+ static final Style[] sStyleArray = {
+ Style.FILL, Style.STROKE, Style.FILL_AND_STROKE
+ };
+ static final Cap[] sCapArray = {
+ Cap.BUTT, Cap.ROUND, Cap.SQUARE
+ };
+ static final Join[] sJoinArray = {
+ Join.MITER, Join.ROUND, Join.BEVEL
+ };
+ static final Align[] sAlignArray = {
+ Align.LEFT, Align.CENTER, Align.RIGHT
+ };
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PaintFlag{}
+
+ public static final int ANTI_ALIAS_FLAG = 0x01;
+ public static final int FILTER_BITMAP_FLAG = 0x02;
+ public static final int DITHER_FLAG = 0x04;
+ public static final int UNDERLINE_TEXT_FLAG = 0x08;
+ public static final int STRIKE_THRU_TEXT_FLAG = 0x10;
+ public static final int FAKE_BOLD_TEXT_FLAG = 0x20;
+ public static final int LINEAR_TEXT_FLAG = 0x40;
+ public static final int SUBPIXEL_TEXT_FLAG = 0x80;
+ /** Legacy Paint flag, no longer used. */
+ public static final int DEV_KERN_TEXT_FLAG = 0x100;
+ /** @hide bit mask for the flag enabling subpixel glyph rendering for text */
+ public static final int LCD_RENDER_TEXT_FLAG = 0x200;
+ public static final int EMBEDDED_BITMAP_TEXT_FLAG = 0x400;
+ /** @hide bit mask for the flag forcing freetype's autohinter on for text */
+ public static final int AUTO_HINTING_TEXT_FLAG = 0x800;
+
+ public static final int VERTICAL_TEXT_FLAG = 0x1000;
+
+ public static final int TEXT_RUN_FLAG_LEFT_EDGE = 0x2000;
+
+
+ public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 0x4000;
+
+ // These flags are always set on a new/reset paint, even if flags 0 is passed.
+ static final int HIDDEN_DEFAULT_PAINT_FLAGS = DEV_KERN_TEXT_FLAG | EMBEDDED_BITMAP_TEXT_FLAG
+ | FILTER_BITMAP_FLAG;
+
+ public static final int HINTING_OFF = 0x0;
+
+ public static final int HINTING_ON = 0x1;
+
+ public static final int BIDI_LTR = 0x0;
+
+ public static final int BIDI_RTL = 0x1;
+
+ public static final int BIDI_DEFAULT_LTR = 0x2;
+
+ public static final int BIDI_DEFAULT_RTL = 0x3;
+
+ public static final int BIDI_FORCE_LTR = 0x4;
+
+ public static final int BIDI_FORCE_RTL = 0x5;
+
+ private static final int BIDI_MAX_FLAG_VALUE = BIDI_FORCE_RTL;
+
+ private static final int BIDI_FLAG_MASK = 0x7;
+
+ public static final int DIRECTION_LTR = 0;
+
+ public static final int DIRECTION_RTL = 1;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CursorOption {}
+
+ public static final int CURSOR_AFTER = 0;
+
+ public static final int CURSOR_AT_OR_AFTER = 1;
+
+ public static final int CURSOR_BEFORE = 2;
+
+ public static final int CURSOR_AT_OR_BEFORE = 3;
+
+ public static final int CURSOR_AT = 4;
+
+ private static final int CURSOR_OPT_MAX_VALUE = CURSOR_AT;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StartHyphenEdit {}
+
+ public static final int START_HYPHEN_EDIT_NO_EDIT = 0x00;
+
+ public static final int START_HYPHEN_EDIT_INSERT_HYPHEN = 0x01;
+
+ public static final int START_HYPHEN_EDIT_INSERT_ZWJ = 0x02;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EndHyphenEdit {}
+
+ public static final int END_HYPHEN_EDIT_NO_EDIT = 0x00;
+
+ public static final int END_HYPHEN_EDIT_REPLACE_WITH_HYPHEN = 0x01;
+
+ public static final int END_HYPHEN_EDIT_INSERT_HYPHEN = 0x02;
+
+ public static final int END_HYPHEN_EDIT_INSERT_ARMENIAN_HYPHEN = 0x03;
+
+ public static final int END_HYPHEN_EDIT_INSERT_MAQAF = 0x04;
+
+ public static final int END_HYPHEN_EDIT_INSERT_UCAS_HYPHEN = 0x05;
+
+ public static final int END_HYPHEN_EDIT_INSERT_ZWJ_AND_HYPHEN = 0x06;
+
+ public enum Style {
+ FILL (0),
+ STROKE (1),
+ FILL_AND_STROKE (2);
+
+ Style(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ public enum Cap {
+ BUTT (0),
+ ROUND (1),
+ SQUARE (2);
+
+ private Cap(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ public enum Join {
+ MITER (0),
+ ROUND (1),
+ BEVEL (2);
+
+ private Join(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ public enum Align {
+ LEFT (0),
+ CENTER (1),
+ RIGHT (2);
+
+ private Align(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+ public Paint() {
+ this(ANTI_ALIAS_FLAG);
+ }
+
+ public Paint(int flags) {
+ setFlags(flags | HIDDEN_DEFAULT_PAINT_FLAGS);
+ // TODO: Turning off hinting has undesirable side effects, we need to
+ // revisit hinting once we add support for subpixel positioning
+ // setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
+ // ? HINTING_OFF : HINTING_ON);
+ mCompatScaling = mInvCompatScaling = 1;
+ mColor = Color.pack(Color.BLACK);
+ }
+
+ public Paint(Paint paint) {
+ setClassVariablesFrom(paint);
+ }
+
+ /** Restores the paint to its default settings. */
+ public void reset() {
+ setFlags(HIDDEN_DEFAULT_PAINT_FLAGS | ANTI_ALIAS_FLAG);
+
+ // TODO: Turning off hinting has undesirable side effects, we need to
+ // revisit hinting once we add support for subpixel positioning
+ // setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
+ // ? HINTING_OFF : HINTING_ON);
+
+ mColor = Color.pack(Color.BLACK);
+ mColorFilter = null;
+ mMaskFilter = null;
+ mPathEffect = null;
+ mShader = null;
+ mTypeface = null;
+ mXfermode = null;
+
+ mHasCompatScaling = false;
+ mCompatScaling = 1;
+ mInvCompatScaling = 1;
+
+ mBidiFlags = BIDI_DEFAULT_LTR;
+ mFontFeatureSettings = null;
+ mFontVariationSettings = null;
+
+ mShadowLayerRadius = 0.0f;
+ mShadowLayerDx = 0.0f;
+ mShadowLayerDy = 0.0f;
+ mShadowLayerColor = Color.pack(0);
+
+ setFlags(ANTI_ALIAS_FLAG);
+ mFont = new Font(null);
+ mStyle = Style.FILL;
+ mStrokeWidth = 1.0f;
+ }
+
+ public void set(Paint src) {
+ if (this != src) {
+ // copy over the native settings
+ setClassVariablesFrom(src);
+ }
+ }
+
+ private void setClassVariablesFrom(Paint paint) {
+ mColor = paint.mColor;
+ mColorFilter = paint.mColorFilter;
+ mMaskFilter = paint.mMaskFilter;
+ mPathEffect = paint.mPathEffect;
+ mShader = paint.mShader;
+ mTypeface = paint.mTypeface;
+ mXfermode = paint.mXfermode;
+
+ mHasCompatScaling = paint.mHasCompatScaling;
+ mCompatScaling = paint.mCompatScaling;
+ mInvCompatScaling = paint.mInvCompatScaling;
+
+ mBidiFlags = paint.mBidiFlags;
+ mLocales = paint.mLocales;
+ mFontFeatureSettings = paint.mFontFeatureSettings;
+ mFontVariationSettings = paint.mFontVariationSettings;
+
+ mShadowLayerRadius = paint.mShadowLayerRadius;
+ mShadowLayerDx = paint.mShadowLayerDx;
+ mShadowLayerDy = paint.mShadowLayerDy;
+ mShadowLayerColor = paint.mShadowLayerColor;
+
+ mFlags = paint.mFlags;
+ mFont = paint.mFont;
+ mStyle = paint.mStyle;
+ mStrokeWidth = paint.mStrokeWidth;
+ }
+
+ /** @hide */
+ public void setCompatibilityScaling(float factor) {
+ if (factor == 1.0) {
+ mHasCompatScaling = false;
+ mCompatScaling = mInvCompatScaling = 1.0f;
+ } else {
+ mHasCompatScaling = true;
+ mCompatScaling = factor;
+ mInvCompatScaling = 1.0f/factor;
+ }
+ }
+
+ public int getBidiFlags() {
+ return mBidiFlags;
+ }
+
+ public void setBidiFlags(int flags) {
+ // only flag value is the 3-bit BIDI control setting
+ flags &= BIDI_FLAG_MASK;
+ if (flags > BIDI_MAX_FLAG_VALUE) {
+ throw new IllegalArgumentException("unknown bidi flag: " + flags);
+ }
+ mBidiFlags = flags;
+ }
+
+ public @PaintFlag int getFlags() {
+ return mFlags;
+ }
+
+ public void setFlags(@PaintFlag int flags) {
+ mFlags = flags;
+
+ Map fontAttributes = new HashMap();
+ if ((flags & UNDERLINE_TEXT_FLAG) != 0) {
+ fontAttributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
+ } else {
+ fontAttributes.put(TextAttribute.UNDERLINE, -1);
+ }
+ if ((flags & STRIKE_THRU_TEXT_FLAG) != 0) {
+ fontAttributes.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
+ } else {
+ fontAttributes.put(TextAttribute.STRIKETHROUGH, false);
+ }
+ if ((flags & FAKE_BOLD_TEXT_FLAG) != 0) {
+ fontAttributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
+ } else {
+ fontAttributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_REGULAR);
+ }
+
+ mFont = mFont.deriveFont(fontAttributes);
+ }
+
+ public int getHinting() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setHinting(int mode) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final boolean isAntiAlias() {
+ return (getFlags() & ANTI_ALIAS_FLAG) != 0;
+ }
+
+ public void setAntiAlias(boolean aa) {
+ setFlags(getFlags() | ANTI_ALIAS_FLAG);
+ }
+
+ public final boolean isDither() {
+ return (getFlags() & DITHER_FLAG) != 0;
+ }
+
+ public void setDither(boolean dither) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final boolean isLinearText() {
+ return (getFlags() & LINEAR_TEXT_FLAG) != 0;
+ }
+
+ public void setLinearText(boolean linearText) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final boolean isSubpixelText() {
+ return (getFlags() & SUBPIXEL_TEXT_FLAG) != 0;
+ }
+
+ public void setSubpixelText(boolean subpixelText) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final boolean isUnderlineText() {
+ return (getFlags() & UNDERLINE_TEXT_FLAG) != 0;
+ }
+
+ public float getUnderlinePosition() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getUnderlineThickness() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setUnderlineText(boolean underlineText) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final boolean isStrikeThruText() {
+ return (getFlags() & STRIKE_THRU_TEXT_FLAG) != 0;
+ }
+
+ public float getStrikeThruPosition() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getStrikeThruThickness() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setStrikeThruText(boolean strikeThruText) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final boolean isFakeBoldText() {
+ return (getFlags() & FAKE_BOLD_TEXT_FLAG) != 0;
+ }
+
+ public void setFakeBoldText(boolean fakeBoldText) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final boolean isFilterBitmap() {
+ return (getFlags() & FILTER_BITMAP_FLAG) != 0;
+ }
+
+ public void setFilterBitmap(boolean filter) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public Style getStyle() {
+ return mStyle;
+ }
+
+ public void setStyle(Style style) {
+ mStyle = style;
+ }
+
+ @ColorInt
+ public int getColor() {
+ return Color.toArgb(mColor);
+ }
+
+ @ColorLong
+ public long getColorLong() {
+ return mColor;
+ }
+
+ public void setColor(@ColorInt int color) {
+ mColor = Color.pack(color);
+ }
+
+ public void setColor(@ColorLong long color) {
+ mColor = color;
+ }
+
+ public int getAlpha() {
+ return Math.round(Color.alpha(mColor) * 255.0f);
+ }
+
+ public void setAlpha(int a) {
+ // FIXME: No need to unpack this. Instead, just update the alpha bits.
+ // b/122959599
+ ColorSpace cs = Color.colorSpace(mColor);
+ float r = Color.red(mColor);
+ float g = Color.green(mColor);
+ float b = Color.blue(mColor);
+ mColor = Color.pack(r, g, b, a * (1.0f / 255), cs);
+ }
+
+ public void setARGB(int a, int r, int g, int b) {
+ setColor((a << 24) | (r << 16) | (g << 8) | b);
+ }
+
+ public float getStrokeWidth() {
+ return mStrokeWidth;
+ }
+
+ public void setStrokeWidth(float width) {
+ mStrokeWidth = width;
+ }
+
+ public float getStrokeMiter() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setStrokeMiter(float miter) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public Cap getStrokeCap() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setStrokeCap(Cap cap) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public Join getStrokeJoin() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setStrokeJoin(Join join) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public boolean getFillPath(Path src, Path dst) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public Shader getShader() {
+ return mShader;
+ }
+
+ public Shader setShader(Shader shader) {
+ mShader = shader;
+ return shader;
+ }
+
+ public ColorFilter getColorFilter() {
+ return mColorFilter;
+ }
+
+ public ColorFilter setColorFilter(ColorFilter filter) {
+ mColorFilter = filter;
+ return filter;
+ }
+
+ public Xfermode getXfermode() {
+ return mXfermode;
+ }
+
+ @Nullable
+ public BlendMode getBlendMode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public Xfermode setXfermode(Xfermode xfermode) {
+ return installXfermode(xfermode);
+ }
+
+ @Nullable
+ private Xfermode installXfermode(Xfermode xfermode) {
+ mXfermode = xfermode;
+ return xfermode;
+ }
+
+ public void setBlendMode(@Nullable BlendMode blendmode) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public PathEffect getPathEffect() {
+ return mPathEffect;
+ }
+
+ public PathEffect setPathEffect(PathEffect effect) {
+ mPathEffect = effect;
+ return effect;
+ }
+
+ public MaskFilter getMaskFilter() {
+ return mMaskFilter;
+ }
+
+ public MaskFilter setMaskFilter(MaskFilter maskfilter) {
+ mMaskFilter = maskfilter;
+ return maskfilter;
+ }
+
+ public Typeface getTypeface() {
+ return mTypeface;
+ }
+
+ public Typeface setTypeface(Typeface typeface) {
+ mTypeface = typeface;
+ return typeface;
+ }
+
+ public void setShadowLayer(float radius, float dx, float dy, @ColorInt int shadowColor) {
+ setShadowLayer(radius, dx, dy, Color.pack(shadowColor));
+ }
+
+ public void setShadowLayer(float radius, float dx, float dy, @ColorLong long shadowColor) {
+ mShadowLayerRadius = radius;
+ mShadowLayerDx = dx;
+ mShadowLayerDy = dy;
+ mShadowLayerColor = shadowColor;
+ }
+
+ public void clearShadowLayer() {
+ setShadowLayer(0, 0, 0, 0);
+ }
+
+ public boolean hasShadowLayer() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getShadowLayerRadius() {
+ return mShadowLayerRadius;
+ }
+
+ public float getShadowLayerDx() {
+ return mShadowLayerDx;
+ }
+
+ public float getShadowLayerDy() {
+ return mShadowLayerDy;
+ }
+
+ public @ColorInt int getShadowLayerColor() {
+ return Color.toArgb(mShadowLayerColor);
+ }
+
+ public @ColorLong long getShadowLayerColorLong() {
+ return mShadowLayerColor;
+ }
+
+ public Align getTextAlign() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setTextAlign(Align align) {
+ throw new RuntimeException("Stub!");
+ }
+
+ @NonNull
+ public Locale getTextLocale() {
+ return mLocales.get(0);
+ }
+
+ @NonNull @Size(min=1)
+ public LocaleList getTextLocales() {
+ return mLocales;
+ }
+
+ public void setTextLocale(@NonNull Locale locale) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setTextLocales(@NonNull @Size(min=1) LocaleList locales) {
+ throw new RuntimeException("Stub!");
+ }
+
+ @Deprecated
+ public boolean isElegantTextHeight() {
+ throw new RuntimeException("Stub!");
+ }
+
+ @Deprecated
+ public void setElegantTextHeight(boolean elegant) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public Font getFont() {
+ return mFont;
+ }
+
+ public float getTextSize() {
+ return mFont.getSize2D();
+ }
+
+ public void setTextSize(float textSize) {
+ // convert px to pt using default DPI of 96
+ float fontSize = 72.0f * textSize / 96.0f;
+ mFont = mFont.deriveFont(fontSize);
+ }
+
+ public float getTextScaleX() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setTextScaleX(float scaleX) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getTextSkewX() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setTextSkewX(float skewX) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getLetterSpacing() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setLetterSpacing(float letterSpacing) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getWordSpacing() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setWordSpacing(float wordSpacing) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public String getFontFeatureSettings() {
+ return mFontFeatureSettings;
+ }
+
+ public void setFontFeatureSettings(String settings) {
+ if (settings != null && settings.equals("")) {
+ settings = null;
+ }
+ if ((settings == null && mFontFeatureSettings == null)
+ || (settings != null && settings.equals(mFontFeatureSettings))) {
+ return;
+ }
+ mFontFeatureSettings = settings;
+ }
+
+ public String getFontVariationSettings() {
+ return mFontVariationSettings;
+ }
+
+ public boolean setFontVariationSettings(String fontVariationSettings) {
+ return setFontVariationSettings(fontVariationSettings, 0 /* wght adjust */);
+ }
+
+ public boolean setFontVariationSettings(String fontVariationSettings, int wghtAdjust) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public @StartHyphenEdit int getStartHyphenEdit() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public @EndHyphenEdit int getEndHyphenEdit() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setStartHyphenEdit(@StartHyphenEdit int startHyphen) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void setEndHyphenEdit(@EndHyphenEdit int endHyphen) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float ascent() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float descent() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public static class FontMetrics {
+ public float top;
+ public float ascent;
+ public float descent;
+ public float bottom;
+ public float leading;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || !(o instanceof FontMetrics)) return false;
+ FontMetrics that = (FontMetrics) o;
+ return that.top == top && that.ascent == ascent && that.descent == descent
+ && that.bottom == bottom && that.leading == leading;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(top, ascent, descent, bottom, leading);
+ }
+
+ @Override
+ public String toString() {
+ return "FontMetrics{"
+ + "top=" + top
+ + ", ascent=" + ascent
+ + ", descent=" + descent
+ + ", bottom=" + bottom
+ + ", leading=" + leading
+ + '}';
+ }
+ }
+
+ /** @hide */
+ public static final class RunInfo {
+ private int mClusterCount = 0;
+
+ public int getClusterCount() {
+ return mClusterCount;
+ }
+
+ public void setClusterCount(int clusterCount) {
+ mClusterCount = clusterCount;
+ }
+ }
+
+ public float getFontMetrics(FontMetrics metrics) {
+ java.awt.Canvas c = new java.awt.Canvas();
+ java.awt.FontMetrics m = c.getFontMetrics(mFont);
+ metrics.top = m.getMaxDescent();
+ metrics.ascent = m.getAscent();
+ metrics.descent = m.getDescent();
+ metrics.bottom = m.getMaxAscent();
+ metrics.leading = m.getLeading();
+ return m.getLeading();
+ }
+
+ public FontMetrics getFontMetrics() {
+ FontMetrics fm = new FontMetrics();
+ getFontMetrics(fm);
+ return fm;
+ }
+
+ public void getFontMetricsForLocale(@NonNull FontMetrics metrics) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void getFontMetricsInt(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int start, @IntRange(from = 0) int count,
+ @IntRange(from = 0) int contextStart, @IntRange(from = 0) int contextCount,
+ boolean isRtl,
+ @NonNull FontMetricsInt outMetrics) {
+
+ if (text == null) {
+ throw new IllegalArgumentException("text must not be null");
+ }
+ if (start < 0 || start >= text.length()) {
+ throw new IllegalArgumentException("start argument is out of bounds.");
+ }
+ if (count < 0 || start + count > text.length()) {
+ throw new IllegalArgumentException("count argument is out of bounds.");
+ }
+ if (contextStart < 0 || contextStart >= text.length()) {
+ throw new IllegalArgumentException("ctxStart argument is out of bounds.");
+ }
+ if (contextCount < 0 || contextStart + contextCount > text.length()) {
+ throw new IllegalArgumentException("ctxCount argument is out of bounds.");
+ }
+ if (outMetrics == null) {
+ throw new IllegalArgumentException("outMetrics must not be null.");
+ }
+
+ if (count == 0) {
+ getFontMetricsInt(outMetrics);
+ return;
+ }
+
+ throw new RuntimeException("Stub!");
+ }
+
+ public void getFontMetricsInt(@NonNull char[] text,
+ @IntRange(from = 0) int start, @IntRange(from = 0) int count,
+ @IntRange(from = 0) int contextStart, @IntRange(from = 0) int contextCount,
+ boolean isRtl,
+ @NonNull FontMetricsInt outMetrics) {
+ if (text == null) {
+ throw new IllegalArgumentException("text must not be null");
+ }
+ if (start < 0 || start >= text.length) {
+ throw new IllegalArgumentException("start argument is out of bounds.");
+ }
+ if (count < 0 || start + count > text.length) {
+ throw new IllegalArgumentException("count argument is out of bounds.");
+ }
+ if (contextStart < 0 || contextStart >= text.length) {
+ throw new IllegalArgumentException("ctxStart argument is out of bounds.");
+ }
+ if (contextCount < 0 || contextStart + contextCount > text.length) {
+ throw new IllegalArgumentException("ctxCount argument is out of bounds.");
+ }
+ if (outMetrics == null) {
+ throw new IllegalArgumentException("outMetrics must not be null.");
+ }
+
+ if (count == 0) {
+ getFontMetricsInt(outMetrics);
+ return;
+ }
+
+ throw new RuntimeException("Stub!");
+ }
+
+ public static class FontMetricsInt {
+ public int top;
+ public int ascent;
+ public int descent;
+ public int bottom;
+ public int leading;
+
+ public void set(@NonNull FontMetricsInt fontMetricsInt) {
+ top = fontMetricsInt.top;
+ ascent = fontMetricsInt.ascent;
+ descent = fontMetricsInt.descent;
+ bottom = fontMetricsInt.bottom;
+ leading = fontMetricsInt.leading;
+ }
+
+ public void set(@NonNull FontMetrics fontMetrics) {
+ top = (int) Math.floor(fontMetrics.top);
+ ascent = Math.round(fontMetrics.ascent);
+ descent = Math.round(fontMetrics.descent);
+ bottom = (int) Math.ceil(fontMetrics.bottom);
+ leading = Math.round(fontMetrics.leading);
+ }
+
+ @Override public String toString() {
+ return "FontMetricsInt: top=" + top + " ascent=" + ascent +
+ " descent=" + descent + " bottom=" + bottom +
+ " leading=" + leading;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof FontMetricsInt)) return false;
+ FontMetricsInt that = (FontMetricsInt) o;
+ return top == that.top
+ && ascent == that.ascent
+ && descent == that.descent
+ && bottom == that.bottom
+ && leading == that.leading;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(top, ascent, descent, bottom, leading);
+ }
+ }
+
+ public int getFontMetricsInt(FontMetricsInt fmi) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public FontMetricsInt getFontMetricsInt() {
+ FontMetricsInt fm = new FontMetricsInt();
+ getFontMetricsInt(fm);
+ return fm;
+ }
+
+ public void getFontMetricsIntForLocale(@NonNull FontMetricsInt metrics) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getFontSpacing() {
+ return getFontMetrics(null);
+ }
+
+ public float measureText(char[] text, int index, int count) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((index | count) < 0 || index + count > text.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length == 0 || count == 0) {
+ return 0f;
+ }
+ int oldFlag = getFlags();
+ setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE));
+ try {
+
+ if (!mHasCompatScaling) {
+ throw new RuntimeException("Stub!");
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ throw new RuntimeException("Stub!");
+ // setTextSize(oldSize);
+ // return (float) Math.ceil(w * mInvCompatScaling);
+ } finally {
+ setFlags(oldFlag);
+ }
+ }
+
+ public float measureText(String text, int start, int end) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0f;
+ }
+ int oldFlag = getFlags();
+ setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE));
+ try {
+ if (!mHasCompatScaling) {
+ throw new RuntimeException("Stub!");
+ }
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ throw new RuntimeException("Stub!");
+ // setTextSize(oldSize);
+ // return (float) Math.ceil(w * mInvCompatScaling);
+ } finally {
+ setFlags(oldFlag);
+ }
+ }
+
+ public float measureText(String text) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ return measureText(text, 0, text.length());
+ }
+
+ public float measureText(CharSequence text, int start, int end) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0f;
+ }
+ if (text instanceof String) {
+ return measureText((String)text, start, end);
+ }
+ if (text instanceof SpannedString ||
+ text instanceof SpannableString) {
+ return measureText(text.toString(), start, end);
+ }
+
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ float result = measureText(buf, 0, end - start);
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ public int breakText(char[] text, int index, int count,
+ float maxWidth, float[] measuredWidth) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if (index < 0 || text.length - index < Math.abs(count)) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length == 0 || count == 0) {
+ return 0;
+ }
+ if (!mHasCompatScaling) {
+ throw new RuntimeException("Stub!");
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ throw new RuntimeException("Stub!");
+ // setTextSize(oldSize);
+ // if (measuredWidth != null) measuredWidth[0] *= mInvCompatScaling;
+ // return res;
+ }
+
+ public int breakText(CharSequence text, int start, int end,
+ boolean measureForwards,
+ float maxWidth, float[] measuredWidth) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0;
+ }
+ if (start == 0 && text instanceof String && end == text.length()) {
+ return breakText((String) text, measureForwards, maxWidth,
+ measuredWidth);
+ }
+
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ int result;
+
+ TextUtils.getChars(text, start, end, buf, 0);
+
+ if (measureForwards) {
+ result = breakText(buf, 0, end - start, maxWidth, measuredWidth);
+ } else {
+ result = breakText(buf, 0, -(end - start), maxWidth, measuredWidth);
+ }
+
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ public int breakText(String text, boolean measureForwards,
+ float maxWidth, float[] measuredWidth) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+
+ if (text.length() == 0) {
+ return 0;
+ }
+ if (!mHasCompatScaling) {
+ throw new RuntimeException("Stub!");
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize*mCompatScaling);
+ throw new RuntimeException("Stub!");
+ // setTextSize(oldSize);
+ // if (measuredWidth != null) measuredWidth[0] *= mInvCompatScaling;
+ // return res;
+ }
+
+ public int getTextWidths(char[] text, int index, int count,
+ float[] widths) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((index | count) < 0 || index + count > text.length
+ || count > widths.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length == 0 || count == 0) {
+ return 0;
+ }
+ int oldFlag = getFlags();
+ setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE));
+ try {
+ if (!mHasCompatScaling) {
+ throw new RuntimeException("Stub!");
+ // return count;
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ throw new RuntimeException("Stub!");
+ // setTextSize(oldSize);
+ // for (int i = 0; i < count; i++) {
+ // widths[i] *= mInvCompatScaling;
+ // }
+ // return count;
+ } finally {
+ setFlags(oldFlag);
+ }
+ }
+
+ public int getTextWidths(CharSequence text, int start, int end,
+ float[] widths) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (end - start > widths.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0;
+ }
+ if (text instanceof String) {
+ return getTextWidths((String) text, start, end, widths);
+ }
+ if (text instanceof SpannedString ||
+ text instanceof SpannableString) {
+ return getTextWidths(text.toString(), start, end, widths);
+ }
+
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ int result = getTextWidths(buf, 0, end - start, widths);
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ public int getTextWidths(String text, int start, int end, float[] widths) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (end - start > widths.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ if (text.length() == 0 || start == end) {
+ return 0;
+ }
+ int oldFlag = getFlags();
+ setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE));
+ try {
+ if (!mHasCompatScaling) {
+ throw new RuntimeException("Stub!");
+ // return end - start;
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ throw new RuntimeException("Stub!");
+ // setTextSize(oldSize);
+ // for (int i = 0; i < end - start; i++) {
+ // widths[i] *= mInvCompatScaling;
+ // }
+ // return end - start;
+ } finally {
+ setFlags(oldFlag);
+ }
+ }
+
+ public int getTextWidths(String text, float[] widths) {
+ return getTextWidths(text, 0, text.length(), widths);
+ }
+
+ public float getTextRunAdvances(@NonNull char[] chars, @IntRange(from = 0) int index,
+ @IntRange(from = 0) int count, @IntRange(from = 0) int contextIndex,
+ @IntRange(from = 0) int contextCount, boolean isRtl, @Nullable float[] advances,
+ @IntRange(from = 0) int advancesIndex) {
+ if (chars == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((index | count | contextIndex | contextCount | advancesIndex
+ | (index - contextIndex) | (contextCount - count)
+ | ((contextIndex + contextCount) - (index + count))
+ | (chars.length - (contextIndex + contextCount))
+ | (advances == null ? 0 :
+ (advances.length - (advancesIndex + count)))) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (chars.length == 0 || count == 0){
+ return 0f;
+ }
+ if (!mHasCompatScaling) {
+ throw new RuntimeException("Stub!");
+ }
+
+ final float oldSize = getTextSize();
+ setTextSize(oldSize * mCompatScaling);
+ float res = 0.0f;
+ throw new RuntimeException("Stub!");
+ // setTextSize(oldSize);
+
+ // if (advances != null) {
+ // for (int i = advancesIndex, e = i + count; i < e; i++) {
+ // advances[i] *= mInvCompatScaling;
+ // }
+ // }
+ // return res * mInvCompatScaling; // assume errors are not significant
+ }
+
+ public int getTextRunCursor(@NonNull char[] text, @IntRange(from = 0) int contextStart,
+ @IntRange(from = 0) int contextLength, boolean isRtl, @IntRange(from = 0) int offset,
+ @CursorOption int cursorOpt) {
+ int contextEnd = contextStart + contextLength;
+ if (((contextStart | contextEnd | offset | (contextEnd - contextStart)
+ | (offset - contextStart) | (contextEnd - offset)
+ | (text.length - contextEnd) | cursorOpt) < 0)
+ || cursorOpt > CURSOR_OPT_MAX_VALUE) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ throw new RuntimeException("Stub!");
+ }
+
+ public int getTextRunCursor(@NonNull CharSequence text, @IntRange(from = 0) int contextStart,
+ @IntRange(from = 0) int contextEnd, boolean isRtl, @IntRange(from = 0) int offset,
+ @CursorOption int cursorOpt) {
+
+ if (text instanceof String || text instanceof SpannedString ||
+ text instanceof SpannableString) {
+ return getTextRunCursor(text.toString(), contextStart, contextEnd,
+ isRtl, offset, cursorOpt);
+ }
+
+ int contextLen = contextEnd - contextStart;
+ char[] buf = TemporaryBuffer.obtain(contextLen);
+ TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ int relPos = getTextRunCursor(buf, 0, contextLen, isRtl, offset - contextStart, cursorOpt);
+ TemporaryBuffer.recycle(buf);
+ return (relPos == -1) ? -1 : relPos + contextStart;
+ }
+
+ public int getTextRunCursor(@NonNull String text, @IntRange(from = 0) int contextStart,
+ @IntRange(from = 0) int contextEnd, boolean isRtl, @IntRange(from = 0) int offset,
+ @CursorOption int cursorOpt) {
+ if (((contextStart | contextEnd | offset | (contextEnd - contextStart)
+ | (offset - contextStart) | (contextEnd - offset)
+ | (text.length() - contextEnd) | cursorOpt) < 0)
+ || cursorOpt > CURSOR_OPT_MAX_VALUE) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ throw new RuntimeException("Stub!");
+ }
+
+ public void getTextPath(char[] text, int index, int count,
+ float x, float y, Path path) {
+ if ((index | count) < 0 || index + count > text.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ throw new RuntimeException("Stub!");
+ }
+
+ public void getTextPath(String text, int start, int end,
+ float x, float y, Path path) {
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ throw new RuntimeException("Stub!");
+ }
+
+ public void getTextBounds(String text, int start, int end, Rect bounds) {
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (bounds == null) {
+ throw new NullPointerException("need bounds Rect");
+ }
+ throw new RuntimeException("Stub!");
+ }
+
+ public void getTextBounds(@NonNull CharSequence text, int start, int end,
+ @NonNull Rect bounds) {
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (bounds == null) {
+ throw new NullPointerException("need bounds Rect");
+ }
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ getTextBounds(buf, 0, end - start, bounds);
+ TemporaryBuffer.recycle(buf);
+ }
+
+ public void getTextBounds(char[] text, int index, int count, Rect bounds) {
+ if ((index | count) < 0 || index + count > text.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ if (bounds == null) {
+ throw new NullPointerException("need bounds Rect");
+ }
+ throw new RuntimeException("Stub!");
+ }
+
+ public boolean hasGlyph(String string) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getRunAdvance(char[] text, int start, int end, int contextStart, int contextEnd,
+ boolean isRtl, int offset) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((contextStart | start | offset | end | contextEnd
+ | start - contextStart | offset - start | end - offset
+ | contextEnd - end | text.length - contextEnd) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (end == start) {
+ return 0.0f;
+ }
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getRunAdvance(CharSequence text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, int offset) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((contextStart | start | offset | end | contextEnd
+ | start - contextStart | offset - start | end - offset
+ | contextEnd - end | text.length() - contextEnd) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (end == start) {
+ return 0.0f;
+ }
+ // TODO performance: specialized alternatives to avoid buffer copy, if win is significant
+ char[] buf = TemporaryBuffer.obtain(contextEnd - contextStart);
+ TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ float result = getRunAdvance(buf, start - contextStart, end - contextStart, 0,
+ contextEnd - contextStart, isRtl, offset - contextStart);
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+
+ public float getRunCharacterAdvance(@NonNull char[] text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, int offset,
+ @Nullable float[] advances, int advancesIndex) {
+ return getRunCharacterAdvance(text, start, end, contextStart, contextEnd, isRtl, offset,
+ advances, advancesIndex, null, null);
+ }
+
+ public float getRunCharacterAdvance(@NonNull char[] text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, int offset,
+ @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds,
+ @Nullable RunInfo runInfo) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if (contextStart < 0 || contextEnd > text.length) {
+ throw new IndexOutOfBoundsException("Invalid Context Range: " + contextStart + ", "
+ + contextEnd + " must be in 0, " + text.length);
+ }
+
+ if (start < contextStart || contextEnd < end) {
+ throw new IndexOutOfBoundsException("Invalid start/end range: " + start + ", " + end
+ + " must be in " + contextStart + ", " + contextEnd);
+ }
+
+ if (offset < start || end < offset) {
+ throw new IndexOutOfBoundsException("Invalid offset position: " + offset
+ + " must be in " + start + ", " + end);
+ }
+
+ if (advances != null && advances.length < advancesIndex - start + end) {
+ throw new IndexOutOfBoundsException("Given array doesn't have enough space to receive "
+ + "the result, advances.length: " + advances.length + " advanceIndex: "
+ + advancesIndex + " needed space: " + (offset - start));
+ }
+
+ if (end == start) {
+ if (runInfo != null) {
+ runInfo.setClusterCount(0);
+ }
+ return 0.0f;
+ }
+
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getRunCharacterAdvance(@NonNull CharSequence text, int start, int end,
+ int contextStart, int contextEnd, boolean isRtl, int offset,
+ @Nullable float[] advances, int advancesIndex) {
+ return getRunCharacterAdvance(text, start, end, contextStart, contextEnd, isRtl, offset,
+ advances, advancesIndex, null, null);
+ }
+
+ public float getRunCharacterAdvance(@NonNull CharSequence text, int start, int end,
+ int contextStart, int contextEnd, boolean isRtl, int offset,
+ @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds,
+ @Nullable RunInfo runInfo) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if (contextStart < 0 || contextEnd > text.length()) {
+ throw new IndexOutOfBoundsException("Invalid Context Range: " + contextStart + ", "
+ + contextEnd + " must be in 0, " + text.length());
+ }
+
+ if (start < contextStart || contextEnd < end) {
+ throw new IndexOutOfBoundsException("Invalid start/end range: " + start + ", " + end
+ + " must be in " + contextStart + ", " + contextEnd);
+ }
+
+ if (offset < start || end < offset) {
+ throw new IndexOutOfBoundsException("Invalid offset position: " + offset
+ + " must be in " + start + ", " + end);
+ }
+
+ if (advances != null && advances.length < advancesIndex - start + end) {
+ throw new IndexOutOfBoundsException("Given array doesn't have enough space to receive "
+ + "the result, advances.length: " + advances.length + " advanceIndex: "
+ + advancesIndex + " needed space: " + (offset - start));
+ }
+
+ if (end == start) {
+ return 0.0f;
+ }
+
+ char[] buf = TemporaryBuffer.obtain(contextEnd - contextStart);
+ TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ final float result = getRunCharacterAdvance(buf, start - contextStart, end - contextStart,
+ 0, contextEnd - contextStart, isRtl, offset - contextStart,
+ advances, advancesIndex, drawBounds, runInfo);
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ public int getOffsetForAdvance(char[] text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, float advance) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public int getOffsetForAdvance(CharSequence text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, float advance) {
+ if (text == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+ if ((contextStart | start | end | contextEnd
+ | start - contextStart | end - start | contextEnd - end
+ | text.length() - contextEnd) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ // TODO performance: specialized alternatives to avoid buffer copy, if win is significant
+ char[] buf = TemporaryBuffer.obtain(contextEnd - contextStart);
+ TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ int result = getOffsetForAdvance(buf, start - contextStart, end - contextStart, 0,
+ contextEnd - contextStart, isRtl, advance) + contextStart;
+ TemporaryBuffer.recycle(buf);
+ return result;
+ }
+
+ public boolean equalsForTextMeasurement(@NonNull Paint other) {
+ throw new RuntimeException("Stub!");
+ }
+}
+
diff --git a/AndroidCompat/src/main/java/android/graphics/Rect.java b/AndroidCompat/src/main/java/android/graphics/Rect.java
index 772bbcfb..67c28f13 100644
--- a/AndroidCompat/src/main/java/android/graphics/Rect.java
+++ b/AndroidCompat/src/main/java/android/graphics/Rect.java
@@ -1,15 +1,15 @@
package android.graphics;
-import android.os.Parcel;
import android.os.Parcelable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import android.os.Parcel;
public final class Rect {
- int left;
- int top;
- int right;
- int bottom;
+ public int left;
+ public int top;
+ public int right;
+ public int bottom;
private static final class UnflattenHelper {
private static final Pattern FLATTENED_PATTERN = Pattern.compile(
diff --git a/AndroidCompat/src/main/java/android/graphics/TemporaryBuffer.java b/AndroidCompat/src/main/java/android/graphics/TemporaryBuffer.java
new file mode 100644
index 00000000..72436c0b
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/graphics/TemporaryBuffer.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed 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 android.graphics;
+
+import com.android.internal.util.ArrayUtils;
+
+/**
+ * @hide
+ */
+public class TemporaryBuffer {
+ public static char[] obtain(int len) {
+ char[] buf;
+
+ synchronized (TemporaryBuffer.class) {
+ buf = sTemp;
+ sTemp = null;
+ }
+
+ if (buf == null || buf.length < len) {
+ buf = ArrayUtils.newUnpaddedCharArray(len);
+ }
+
+ return buf;
+ }
+
+ public static void recycle(char[] temp) {
+ if (temp.length > 1000) return;
+
+ synchronized (TemporaryBuffer.class) {
+ sTemp = temp;
+ }
+ }
+
+ private static char[] sTemp = null;
+}
+
diff --git a/AndroidCompat/src/main/java/android/text/Layout.java b/AndroidCompat/src/main/java/android/text/Layout.java
new file mode 100644
index 00000000..87c39fed
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/text/Layout.java
@@ -0,0 +1,1905 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed 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 android.text;
+
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.BlendMode;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.text.LineBreaker;
+import android.text.SpannableString;
+import android.text.style.AlignmentSpan;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
+import android.text.style.ParagraphStyle;
+import android.util.Log;
+import com.android.internal.util.ArrayUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+import android.annotation.ColorInt;
+
+
+
+public abstract class Layout {
+
+ private static final String TAG = "Layout";
+
+ // These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h
+ private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 0f;
+ private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f;
+ private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f;
+ // since we're not using soft light yet, this needs to be much lower than the spec'd 0.8
+ private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.5f;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BreakStrategy {}
+
+ public static final int BREAK_STRATEGY_SIMPLE = LineBreaker.BREAK_STRATEGY_SIMPLE;
+
+ public static final int BREAK_STRATEGY_HIGH_QUALITY = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY;
+
+ public static final int BREAK_STRATEGY_BALANCED = LineBreaker.BREAK_STRATEGY_BALANCED;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface HyphenationFrequency {}
+
+ public static final int HYPHENATION_FREQUENCY_NONE = 0;
+
+ public static final int HYPHENATION_FREQUENCY_NORMAL = 1;
+
+ public static final int HYPHENATION_FREQUENCY_FULL = 2;
+
+ public static final int HYPHENATION_FREQUENCY_NORMAL_FAST = 3;
+ public static final int HYPHENATION_FREQUENCY_FULL_FAST = 4;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface JustificationMode {}
+
+ public static final int JUSTIFICATION_MODE_NONE = LineBreaker.JUSTIFICATION_MODE_NONE;
+
+ public static final int JUSTIFICATION_MODE_INTER_WORD =
+ LineBreaker.JUSTIFICATION_MODE_INTER_WORD;
+
+ public static final int JUSTIFICATION_MODE_INTER_CHARACTER =
+ 0;
+
+ /*
+ * Line spacing multiplier for default line spacing.
+ */
+ public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f;
+
+ /*
+ * Line spacing addition for default line spacing.
+ */
+ public static final float DEFAULT_LINESPACING_ADDITION = 0.0f;
+
+ @NonNull
+ public static final TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP =
+ RectF::intersects;
+
+ @NonNull
+ public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER =
+ (segmentBounds, area) ->
+ area.contains(segmentBounds.centerX(), segmentBounds.centerY());
+
+ @NonNull
+ public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL =
+ (segmentBounds, area) -> area.contains(segmentBounds);
+
+ public static float getDesiredWidth(CharSequence source,
+ TextPaint paint) {
+ return getDesiredWidth(source, 0, source.length(), paint);
+ }
+
+ public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint) {
+ return getDesiredWidth(source, start, end, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
+ }
+
+ public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint,
+ TextDirectionHeuristic textDir) {
+ return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE, false);
+ }
+ public static float getDesiredWidthWithLimit(CharSequence source, int start, int end,
+ TextPaint paint, TextDirectionHeuristic textDir, float upperLimit,
+ boolean useBoundsForWidth) {
+ throw new RuntimeException("Stub!");
+ }
+
+ protected Layout(CharSequence text, TextPaint paint,
+ int width, Alignment align,
+ float spacingMult, float spacingAdd) {
+ this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
+ spacingMult, spacingAdd, false, false, 0, null, Integer.MAX_VALUE,
+ BREAK_STRATEGY_SIMPLE, HYPHENATION_FREQUENCY_NONE, null, null,
+ JUSTIFICATION_MODE_NONE, false, false, null);
+ }
+
+ protected Layout(
+ CharSequence text,
+ TextPaint paint,
+ int width,
+ Alignment align,
+ TextDirectionHeuristic textDir,
+ float spacingMult,
+ float spacingAdd,
+ boolean includePad,
+ boolean fallbackLineSpacing,
+ int ellipsizedWidth,
+ TextUtils.TruncateAt ellipsize,
+ int maxLines,
+ int breakStrategy,
+ int hyphenationFrequency,
+ int[] leftIndents,
+ int[] rightIndents,
+ int justificationMode,
+ boolean useBoundsForWidth,
+ boolean shiftDrawingOffsetForStartOverhang,
+ Paint.FontMetrics minimumFontMetrics
+ ) {
+
+ if (width < 0)
+ throw new IllegalArgumentException("Layout: " + width + " < 0");
+
+ // Ensure paint doesn't have baselineShift set.
+ // While normally we don't modify the paint the user passed in,
+ // we were already doing this in Styled.drawUniformRun with both
+ // baselineShift and bgColor. We probably should reevaluate bgColor.
+ if (paint != null) {
+ paint.bgColor = 0;
+ paint.baselineShift = 0;
+ }
+
+ mText = text;
+ mPaint = paint;
+ mWidth = width;
+ mAlignment = align;
+ mSpacingMult = spacingMult;
+ mSpacingAdd = spacingAdd;
+ mSpannedText = text instanceof Spanned;
+ mTextDir = textDir;
+ mIncludePad = includePad;
+ mFallbackLineSpacing = fallbackLineSpacing;
+ mEllipsizedWidth = ellipsize == null ? width : ellipsizedWidth;
+ mEllipsize = ellipsize;
+ mMaxLines = maxLines;
+ mBreakStrategy = breakStrategy;
+ mHyphenationFrequency = hyphenationFrequency;
+ mLeftIndents = leftIndents;
+ mRightIndents = rightIndents;
+ mJustificationMode = justificationMode;
+ mUseBoundsForWidth = useBoundsForWidth;
+ mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
+ mMinimumFontMetrics = minimumFontMetrics;
+ }
+
+ /* package */ void replaceWith(CharSequence text, TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd) {
+ if (width < 0) {
+ throw new IllegalArgumentException("Layout: " + width + " < 0");
+ }
+
+ mText = text;
+ mPaint = paint;
+ mWidth = width;
+ mAlignment = align;
+ mSpacingMult = spacingmult;
+ mSpacingAdd = spacingadd;
+ mSpannedText = text instanceof Spanned;
+ }
+
+ public void draw(Canvas c) {
+ draw(c, (Path) null, (Paint) null, 0);
+ }
+
+ public void draw(
+ Canvas canvas, Path selectionHighlight,
+ Paint selectionHighlightPaint, int cursorOffsetVertical) {
+ draw(canvas, null, null, selectionHighlight, selectionHighlightPaint, cursorOffsetVertical);
+ }
+
+ public void draw(@NonNull Canvas canvas,
+ @Nullable List highlightPaths,
+ @Nullable List highlightPaints,
+ @Nullable Path selectionPath,
+ @Nullable Paint selectionPaint,
+ int cursorOffsetVertical) {
+ float leftShift = 0;
+ if (mUseBoundsForWidth && mShiftDrawingOffsetForStartOverhang) {
+ RectF drawingRect = computeDrawingBoundingBox();
+ if (drawingRect.left < 0) {
+ leftShift = -drawingRect.left;
+ canvas.translate(leftShift, 0);
+ }
+ }
+ final long lineRange = getLineRangeForDraw(canvas);
+ int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
+ int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
+ if (lastLine < 0) return;
+
+ drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
+ cursorOffsetVertical, firstLine, lastLine);
+
+ drawText(canvas, firstLine, lastLine);
+
+ if (leftShift != 0) {
+ // Manually translate back to the original position because of b/324498002, using
+ // save/restore disappears the toggle switch drawables.
+ canvas.translate(-leftShift, 0);
+ }
+ }
+
+ private static Paint setToHighlightPaint(Paint p, BlendMode blendMode, Paint outPaint) {
+ if (p == null) return null;
+ outPaint.set(p);
+ outPaint.setBlendMode(blendMode);
+ // Yellow for maximum contrast
+ outPaint.setColor(Color.YELLOW);
+ return outPaint;
+ }
+
+ public void drawText(@NonNull Canvas canvas) {
+ final long lineRange = getLineRangeForDraw(canvas);
+ int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
+ int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
+ if (lastLine < 0) return;
+ drawText(canvas, firstLine, lastLine);
+ }
+
+ public void drawBackground(@NonNull Canvas canvas) {
+ final long lineRange = getLineRangeForDraw(canvas);
+ int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
+ int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
+ if (lastLine < 0) return;
+ drawBackground(canvas, firstLine, lastLine);
+ }
+
+ public void drawWithoutText(
+ @NonNull Canvas canvas,
+ @Nullable List highlightPaths,
+ @Nullable List highlightPaints,
+ @Nullable Path selectionPath,
+ @Nullable Paint selectionPaint,
+ int cursorOffsetVertical,
+ int firstLine,
+ int lastLine) {
+ drawBackground(canvas, firstLine, lastLine);
+ drawHighlights(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
+ cursorOffsetVertical, firstLine, lastLine);
+ }
+
+ public void drawHighlights(
+ @NonNull Canvas canvas,
+ @Nullable List highlightPaths,
+ @Nullable List highlightPaints,
+ @Nullable Path selectionPath,
+ @Nullable Paint selectionPaint,
+ int cursorOffsetVertical,
+ int firstLine,
+ int lastLine) {
+ if (highlightPaths == null && highlightPaints == null) {
+ return;
+ }
+ if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
+ try {
+ if (highlightPaths != null) {
+ if (highlightPaints == null) {
+ throw new IllegalArgumentException(
+ "if highlight is specified, highlightPaint must be specified.");
+ }
+ if (highlightPaints.size() != highlightPaths.size()) {
+ throw new IllegalArgumentException(
+ "The highlight path size is different from the size of highlight"
+ + " paints");
+ }
+ for (int i = 0; i < highlightPaths.size(); ++i) {
+ final Path highlight = highlightPaths.get(i);
+ Paint highlightPaint = highlightPaints.get(i);
+ if (highlight != null) {
+ canvas.drawPath(highlight, highlightPaint);
+ }
+ }
+ }
+
+ if (selectionPath != null) {
+ canvas.drawPath(selectionPath, selectionPaint);
+ }
+ } finally {
+ if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
+ }
+ }
+
+
+ private boolean isHighContrastTextDark(@ColorInt int color) {
+ int channelSum = Color.red(color) + Color.green(color) + Color.blue(color);
+ return channelSum < (128 * 3);
+ }
+
+ private boolean isJustificationRequired(int lineNum) {
+ if (mJustificationMode == JUSTIFICATION_MODE_NONE) return false;
+ final int lineEnd = getLineEnd(lineNum);
+ return lineEnd < mText.length() && mText.charAt(lineEnd - 1) != '\n';
+ }
+
+ private float getJustifyWidth(int lineNum) {
+ Alignment paraAlign = mAlignment;
+
+ int left = 0;
+ int right = mWidth;
+
+ final int dir = getParagraphDirection(lineNum);
+
+ ParagraphStyle[] spans = new ParagraphStyle[0];
+ if (mSpannedText) {
+ Spanned sp = (Spanned) mText;
+ final int start = getLineStart(lineNum);
+
+ final boolean isFirstParaLine = (start == 0 || mText.charAt(start - 1) == '\n');
+
+ if (isFirstParaLine) {
+ final int spanEnd = sp.nextSpanTransition(start, mText.length(),
+ ParagraphStyle.class);
+ spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
+
+ for (int n = spans.length - 1; n >= 0; n--) {
+ if (spans[n] instanceof AlignmentSpan) {
+ paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
+ break;
+ }
+ }
+ }
+
+ final int length = spans.length;
+ boolean useFirstLineMargin = isFirstParaLine;
+ for (int n = 0; n < length; n++) {
+ if (spans[n] instanceof LeadingMarginSpan2) {
+ int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
+ int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
+ if (lineNum < startLine + count) {
+ useFirstLineMargin = true;
+ break;
+ }
+ }
+ }
+ for (int n = 0; n < length; n++) {
+ if (spans[n] instanceof LeadingMarginSpan) {
+ LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ right -= margin.getLeadingMargin(useFirstLineMargin);
+ } else {
+ left += margin.getLeadingMargin(useFirstLineMargin);
+ }
+ }
+ }
+ }
+
+ final Alignment align;
+ if (paraAlign == Alignment.ALIGN_LEFT) {
+ align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
+ } else if (paraAlign == Alignment.ALIGN_RIGHT) {
+ align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
+ } else {
+ align = paraAlign;
+ }
+
+ final int indentWidth;
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ } else {
+ indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ }
+ } else if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ } else {
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ }
+ } else { // Alignment.ALIGN_CENTER
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
+ }
+
+ return right - left - indentWidth;
+ }
+
+ public void drawText(Canvas canvas, int firstLine, int lastLine) {
+ int previousLineBottom = getLineTop(firstLine);
+ int previousLineEnd = getLineStart(firstLine);
+ ParagraphStyle[] spans = new ParagraphStyle[0];
+ int spanEnd = 0;
+ final TextPaint paint = mWorkPaint;
+ paint.set(mPaint);
+ CharSequence buf = mText;
+
+ Alignment paraAlign = mAlignment;
+ TabStops tabStops = null;
+ boolean tabStopsIsInitialized = false;
+
+ TextLine tl = TextLine.obtain();
+
+ // Draw the lines, one at a time.
+ // The baseline is the top of the following line minus the current line's descent.
+ for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
+ int start = previousLineEnd;
+ previousLineEnd = getLineStart(lineNum + 1);
+ final boolean justify = isJustificationRequired(lineNum);
+ int end = getLineVisibleEnd(lineNum, start, previousLineEnd,
+ true /* trailingSpaceAtLastLineIsVisible */);
+ // TODO: not supported
+ // paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
+ // paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));
+
+ int ltop = previousLineBottom;
+ int lbottom = getLineTop(lineNum + 1);
+ previousLineBottom = lbottom;
+ int lbaseline = lbottom - getLineDescent(lineNum);
+
+ int dir = getParagraphDirection(lineNum);
+ int left = 0;
+ int right = mWidth;
+
+ if (mSpannedText) {
+ Spanned sp = (Spanned) buf;
+ int textLength = buf.length();
+ boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');
+
+ // New batch of paragraph styles, collect into spans array.
+ // Compute the alignment, last alignment style wins.
+ // Reset tabStops, we'll rebuild if we encounter a line with
+ // tabs.
+ // We expect paragraph spans to be relatively infrequent, use
+ // spanEnd so that we can check less frequently. Since
+ // paragraph styles ought to apply to entire paragraphs, we can
+ // just collect the ones present at the start of the paragraph.
+ // If spanEnd is before the end of the paragraph, that's not
+ // our problem.
+ if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
+ spanEnd = sp.nextSpanTransition(start, textLength,
+ ParagraphStyle.class);
+ spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
+
+ paraAlign = mAlignment;
+ for (int n = spans.length - 1; n >= 0; n--) {
+ if (spans[n] instanceof AlignmentSpan) {
+ paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
+ break;
+ }
+ }
+
+ tabStopsIsInitialized = false;
+ }
+
+ // Draw all leading margin spans. Adjust left or right according
+ // to the paragraph direction of the line.
+ final int length = spans.length;
+ boolean useFirstLineMargin = isFirstParaLine;
+ for (int n = 0; n < length; n++) {
+ if (spans[n] instanceof LeadingMarginSpan2) {
+ int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
+ int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
+ // if there is more than one LeadingMarginSpan2, use
+ // the count that is greatest
+ if (lineNum < startLine + count) {
+ useFirstLineMargin = true;
+ break;
+ }
+ }
+ }
+ for (int n = 0; n < length; n++) {
+ if (spans[n] instanceof LeadingMarginSpan) {
+ LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
+ lbaseline, lbottom, buf,
+ start, end, isFirstParaLine, this);
+ right -= margin.getLeadingMargin(useFirstLineMargin);
+ } else {
+ margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
+ lbaseline, lbottom, buf,
+ start, end, isFirstParaLine, this);
+ left += margin.getLeadingMargin(useFirstLineMargin);
+ }
+ }
+ }
+ }
+
+ boolean hasTab = getLineContainsTab(lineNum);
+ // Can't tell if we have tabs for sure, currently
+ if (hasTab && !tabStopsIsInitialized) {
+ if (tabStops == null) {
+ tabStops = new TabStops(TAB_INCREMENT, spans);
+ } else {
+ tabStops.reset(TAB_INCREMENT, spans);
+ }
+ tabStopsIsInitialized = true;
+ }
+
+ // Determine whether the line aligns to normal, opposite, or center.
+ Alignment align = paraAlign;
+ if (align == Alignment.ALIGN_LEFT) {
+ align = (dir == DIR_LEFT_TO_RIGHT) ?
+ Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
+ } else if (align == Alignment.ALIGN_RIGHT) {
+ align = (dir == DIR_LEFT_TO_RIGHT) ?
+ Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
+ }
+
+ int x;
+ final int indentWidth;
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ x = left + indentWidth;
+ } else {
+ indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ x = right - indentWidth;
+ }
+ } else {
+ int max = (int)getLineExtent(lineNum, tabStops, false);
+ if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ x = right - max - indentWidth;
+ } else {
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ x = left - max + indentWidth;
+ }
+ } else { // Alignment.ALIGN_CENTER
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
+ max = max & ~1;
+ x = ((right + left - max) >> 1) + indentWidth;
+ }
+ }
+
+ Directions directions = getLineDirections(lineNum);
+ if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) {
+ // XXX: assumes there's nothing additional to be done
+ canvas.drawText(buf, start, end, x, lbaseline, paint);
+ } else {
+ tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops,
+ getEllipsisStart(lineNum),
+ getEllipsisStart(lineNum) + getEllipsisCount(lineNum),
+ isFallbackLineSpacingEnabled());
+ if (justify) {
+ tl.justify(mJustificationMode, right - left - indentWidth);
+ }
+ tl.draw(canvas, x, ltop, lbaseline, lbottom);
+ }
+ }
+
+ TextLine.recycle(tl);
+ }
+
+ public void drawBackground(
+ @NonNull Canvas canvas,
+ int firstLine, int lastLine) {
+ // TODO: do we need this?
+ }
+
+
+ public long getLineRangeForDraw(Canvas canvas) {
+ int dtop, dbottom;
+
+ synchronized (sTempRect) {
+ if (!canvas.getClipBounds(sTempRect)) {
+ // Negative range end used as a special flag
+ return TextUtils.packRangeInLong(0, -1);
+ }
+
+ dtop = sTempRect.top;
+ dbottom = sTempRect.bottom;
+ }
+
+ final int top = Math.max(dtop, 0);
+ final int bottom = Math.min(getLineTop(getLineCount()), dbottom);
+
+ if (top >= bottom) return TextUtils.packRangeInLong(0, -1);
+ return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom));
+ }
+
+ public final void increaseWidthTo(int wid) {
+ if (wid < mWidth) {
+ throw new RuntimeException("attempted to reduce Layout width");
+ }
+
+ mWidth = wid;
+ }
+
+ public int getHeight() {
+ return getLineTop(getLineCount());
+ }
+
+ public int getHeight(boolean cap) {
+ return getHeight();
+ }
+
+ public abstract int getLineCount();
+
+ @NonNull
+ public RectF computeDrawingBoundingBox() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public int getLineBounds(int line, Rect bounds) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public abstract int getLineTop(int line);
+
+ public abstract int getLineDescent(int line);
+
+ public abstract int getLineStart(int line);
+
+ public abstract int getParagraphDirection(int line);
+
+ public abstract boolean getLineContainsTab(int line);
+
+ public abstract Directions getLineDirections(int line);
+
+ public abstract int getTopPadding();
+
+ public abstract int getBottomPadding();
+
+ public @Paint.StartHyphenEdit int getStartHyphenEdit(int line) {
+ return Paint.START_HYPHEN_EDIT_NO_EDIT;
+ }
+
+ public @Paint.EndHyphenEdit int getEndHyphenEdit(int line) {
+ return Paint.END_HYPHEN_EDIT_NO_EDIT;
+ }
+
+ public int getIndentAdjust(int line, Alignment alignment) {
+ return 0;
+ }
+
+ public boolean isLevelBoundary(int offset) {
+ int line = getLineForOffset(offset);
+ Directions dirs = getLineDirections(line);
+ if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
+ return false;
+ }
+
+ int[] runs = dirs.mDirections;
+ int lineStart = getLineStart(line);
+ int lineEnd = getLineEnd(line);
+ if (offset == lineStart || offset == lineEnd) {
+ int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1;
+ int runIndex = offset == lineStart ? 0 : runs.length - 2;
+ return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel;
+ }
+
+ offset -= lineStart;
+ for (int i = 0; i < runs.length; i += 2) {
+ if (offset == runs[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isRtlCharAt(int offset) {
+ int line = getLineForOffset(offset);
+ Directions dirs = getLineDirections(line);
+ if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
+ return false;
+ }
+ if (dirs == DIRS_ALL_RIGHT_TO_LEFT) {
+ return true;
+ }
+ int[] runs = dirs.mDirections;
+ int lineStart = getLineStart(line);
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
+ if (offset >= start && offset < limit) {
+ int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
+ return ((level & 1) != 0);
+ }
+ }
+ // Should happen only if the offset is "out of bounds"
+ return false;
+ }
+
+ public long getRunRange(int offset) {
+ int line = getLineForOffset(offset);
+ Directions dirs = getLineDirections(line);
+ if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
+ return TextUtils.packRangeInLong(0, getLineEnd(line));
+ }
+ int[] runs = dirs.mDirections;
+ int lineStart = getLineStart(line);
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
+ if (offset >= start && offset < limit) {
+ return TextUtils.packRangeInLong(start, limit);
+ }
+ }
+ // Should happen only if the offset is "out of bounds"
+ return TextUtils.packRangeInLong(0, getLineEnd(line));
+ }
+
+ public boolean primaryIsTrailingPrevious(int offset) {
+ int line = getLineForOffset(offset);
+ int lineStart = getLineStart(line);
+ int lineEnd = getLineEnd(line);
+ int[] runs = getLineDirections(line).mDirections;
+
+ int levelAt = -1;
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
+ if (limit > lineEnd) {
+ limit = lineEnd;
+ }
+ if (offset >= start && offset < limit) {
+ if (offset > start) {
+ // Previous character is at same level, so don't use trailing.
+ return false;
+ }
+ levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
+ break;
+ }
+ }
+ if (levelAt == -1) {
+ // Offset was limit of line.
+ levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
+ }
+
+ // At level boundary, check previous level.
+ int levelBefore = -1;
+ if (offset == lineStart) {
+ levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
+ } else {
+ offset -= 1;
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
+ if (limit > lineEnd) {
+ limit = lineEnd;
+ }
+ if (offset >= start && offset < limit) {
+ levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
+ break;
+ }
+ }
+ }
+
+ return levelBefore < levelAt;
+ }
+
+ public boolean[] primaryIsTrailingPreviousAllLineOffsets(int line) {
+ int lineStart = getLineStart(line);
+ int lineEnd = getLineEnd(line);
+ int[] runs = getLineDirections(line).mDirections;
+
+ boolean[] trailing = new boolean[lineEnd - lineStart + 1];
+
+ byte[] level = new byte[lineEnd - lineStart + 1];
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ int limit = start + (runs[i + 1] & RUN_LENGTH_MASK);
+ if (limit > lineEnd) {
+ limit = lineEnd;
+ }
+ if (limit == start) {
+ continue;
+ }
+ level[limit - lineStart - 1] =
+ (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
+ }
+
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ byte currentLevel = (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
+ trailing[start - lineStart] = currentLevel > (start == lineStart
+ ? (getParagraphDirection(line) == 1 ? 0 : 1)
+ : level[start - lineStart - 1]);
+ }
+
+ return trailing;
+ }
+
+ public float getPrimaryHorizontal(int offset) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getPrimaryHorizontal(int offset, boolean clamped) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getSecondaryHorizontal(int offset) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getSecondaryHorizontal(int offset, boolean clamped) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void fillCharacterBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
+ @NonNull float[] bounds, @IntRange(from = 0) int boundsStart) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getLineLeft(int line) {
+ final int dir = getParagraphDirection(line);
+ Alignment align = getParagraphAlignment(line);
+ // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment
+ // is null. And when it is null, the old behavior is the same as ALIGN_CENTER.
+ // To keep consistency, we convert a null alignment to ALIGN_CENTER.
+ if (align == null) {
+ align = Alignment.ALIGN_CENTER;
+ }
+
+ // First convert combinations of alignment and direction settings to
+ // three basic cases: ALIGN_LEFT, ALIGN_RIGHT and ALIGN_CENTER.
+ // For unexpected cases, it will fallback to ALIGN_LEFT.
+ final Alignment resultAlign;
+ switch(align) {
+ case ALIGN_NORMAL:
+ resultAlign =
+ dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT;
+ break;
+ case ALIGN_OPPOSITE:
+ resultAlign =
+ dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT;
+ break;
+ case ALIGN_CENTER:
+ resultAlign = Alignment.ALIGN_CENTER;
+ break;
+ case ALIGN_RIGHT:
+ resultAlign = Alignment.ALIGN_RIGHT;
+ break;
+ default: /* align == Alignment.ALIGN_LEFT */
+ resultAlign = Alignment.ALIGN_LEFT;
+ }
+
+ // Here we must use getLineMax() to do the computation, because it maybe overridden by
+ // derived class. And also note that line max equals the width of the text in that line
+ // plus the leading margin.
+ switch (resultAlign) {
+ case ALIGN_CENTER:
+ final int left = getParagraphLeft(line);
+ final float max = getLineMax(line);
+ // This computation only works when mWidth equals leadingMargin plus
+ // the width of text in this line. If this condition doesn't meet anymore,
+ // please change here too.
+ return (float) Math.floor(left + (mWidth - max) / 2);
+ case ALIGN_RIGHT:
+ return mWidth - getLineMax(line);
+ default: /* resultAlign == Alignment.ALIGN_LEFT */
+ return 0;
+ }
+ }
+
+ public float getLineRight(int line) {
+ final int dir = getParagraphDirection(line);
+ Alignment align = getParagraphAlignment(line);
+ // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment
+ // is null. And when it is null, the old behavior is the same as ALIGN_CENTER.
+ // To keep consistency, we convert a null alignment to ALIGN_CENTER.
+ if (align == null) {
+ align = Alignment.ALIGN_CENTER;
+ }
+
+ final Alignment resultAlign;
+ switch(align) {
+ case ALIGN_NORMAL:
+ resultAlign =
+ dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT;
+ break;
+ case ALIGN_OPPOSITE:
+ resultAlign =
+ dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT;
+ break;
+ case ALIGN_CENTER:
+ resultAlign = Alignment.ALIGN_CENTER;
+ break;
+ case ALIGN_RIGHT:
+ resultAlign = Alignment.ALIGN_RIGHT;
+ break;
+ default: /* align == Alignment.ALIGN_LEFT */
+ resultAlign = Alignment.ALIGN_LEFT;
+ }
+
+ switch (resultAlign) {
+ case ALIGN_CENTER:
+ final int right = getParagraphRight(line);
+ final float max = getLineMax(line);
+ // This computation only works when mWidth equals leadingMargin plus width of the
+ // text in this line. If this condition doesn't meet anymore, please change here.
+ return (float) Math.ceil(right - (mWidth - max) / 2);
+ case ALIGN_RIGHT:
+ return mWidth;
+ default: /* resultAlign == Alignment.ALIGN_LEFT */
+ return getLineMax(line);
+ }
+ }
+
+ public float getLineMax(int line) {
+ float margin = getParagraphLeadingMargin(line);
+ float signedExtent = getLineExtent(line, false);
+ return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
+ }
+
+ public float getLineWidth(int line) {
+ float margin = getParagraphLeadingMargin(line);
+ float signedExtent = getLineExtent(line, true);
+ return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
+ }
+
+ @IntRange(from = 0)
+ public int getLineLetterSpacingUnitCount(@IntRange(from = 0) int line,
+ boolean includeTrailingWhitespace) {
+ final int start = getLineStart(line);
+ final int end = includeTrailingWhitespace ? getLineEnd(line)
+ : getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
+ false // trailingSpaceAtLastLineIsVisible: Treating trailing whitespaces at
+ // the last line as a invisible chars for single line justification.
+ );
+
+ final Directions directions = getLineDirections(line);
+ // Returned directions can actually be null
+ if (directions == null) {
+ return 0;
+ }
+ final int dir = getParagraphDirection(line);
+
+ final TextLine tl = TextLine.obtain();
+ final TextPaint paint = mWorkPaint;
+ paint.set(mPaint);
+ paint.setStartHyphenEdit(getStartHyphenEdit(line));
+ paint.setEndHyphenEdit(getEndHyphenEdit(line));
+ tl.set(paint, mText, start, end, dir, directions,
+ false, null, // tab width is not used for cluster counting.
+ getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
+ isFallbackLineSpacingEnabled());
+ if (mLineInfo == null) {
+ mLineInfo = new TextLine.LineInfo();
+ }
+ mLineInfo.setClusterCount(0);
+ tl.metrics(null, null, mUseBoundsForWidth, mLineInfo);
+ TextLine.recycle(tl);
+ return mLineInfo.getClusterCount();
+ }
+
+ private float getLineExtent(int line, boolean full) {
+ // TODO: should compute TabStops
+ return getLineExtent(line, null, full);
+ }
+
+ private float getLineExtent(int line, TabStops tabStops, boolean full) {
+ final int start = getLineStart(line);
+ final int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
+ final boolean hasTabs = getLineContainsTab(line);
+ final Directions directions = getLineDirections(line);
+ final int dir = getParagraphDirection(line);
+
+ final TextLine tl = TextLine.obtain();
+ final TextPaint paint = mWorkPaint;
+ paint.set(mPaint);
+ // TODO: not supported
+ // paint.setStartHyphenEdit(getStartHyphenEdit(line));
+ // paint.setEndHyphenEdit(getEndHyphenEdit(line));
+ tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops,
+ getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
+ isFallbackLineSpacingEnabled());
+ if (isJustificationRequired(line)) {
+ tl.justify(mJustificationMode, getJustifyWidth(line));
+ }
+ final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
+ TextLine.recycle(tl);
+ return width;
+ }
+
+ // FIXME: It may be faster to do a linear search for layouts without many lines.
+ public int getLineForVertical(int vertical) {
+ int high = getLineCount(), low = -1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (getLineTop(guess) > vertical)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < 0)
+ return 0;
+ else
+ return low;
+ }
+
+ public int getLineForOffset(int offset) {
+ int high = getLineCount(), low = -1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (getLineStart(guess) > offset)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < 0) {
+ return 0;
+ } else {
+ return low;
+ }
+ }
+
+ public int getOffsetForHorizontal(int line, float horiz) {
+ return getOffsetForHorizontal(line, horiz, true);
+ }
+
+ public int getOffsetForHorizontal(int line, float horiz, boolean primary) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final int getLineEnd(int line) {
+ return getLineStart(line + 1);
+ }
+
+ public int getLineVisibleEnd(int line) {
+ return getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
+ true /* trailingSpaceAtLastLineIsVisible */);
+ }
+
+ private int getLineVisibleEnd(int line, int start, int end,
+ boolean trailingSpaceAtLastLineIsVisible) {
+ CharSequence text = mText;
+ char ch;
+
+ // Historically, trailing spaces at the last line is counted as visible. However, this
+ // doesn't work well for justification.
+ if (trailingSpaceAtLastLineIsVisible) {
+ if (line == getLineCount() - 1) {
+ return end;
+ }
+ }
+
+ for (; end > start; end--) {
+ ch = text.charAt(end - 1);
+
+ if (ch == '\n') {
+ return end - 1;
+ }
+
+ if (!TextLine.isLineEndSpace(ch)) {
+ break;
+ }
+
+ }
+
+ return end;
+ }
+
+ public final int getLineBottom(int line) {
+ return getLineBottom(line, /* includeLineSpacing= */ true);
+ }
+
+ public int getLineBottom(int line, boolean includeLineSpacing) {
+ if (includeLineSpacing) {
+ return getLineTop(line + 1);
+ } else {
+ return getLineTop(line + 1) - getLineExtra(line);
+ }
+ }
+
+ public final int getLineBaseline(int line) {
+ // getLineTop(line+1) == getLineBottom(line)
+ return getLineTop(line+1) - getLineDescent(line);
+ }
+
+ public final int getLineAscent(int line) {
+ // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
+ return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
+ }
+
+ public int getLineExtra(@IntRange(from = 0) int line) {
+ return 0;
+ }
+
+ public int getOffsetToLeftOf(int offset) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public int getOffsetToRightOf(int offset) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public boolean shouldClampCursor(int line) {
+ // Only clamp cursor position in left-aligned displays.
+ switch (getParagraphAlignment(line)) {
+ case ALIGN_LEFT:
+ return true;
+ case ALIGN_NORMAL:
+ return getParagraphDirection(line) > 0;
+ default:
+ return false;
+ }
+
+ }
+
+ public void getCursorPath(final int point, final Path dest, final CharSequence editingBuffer) {
+ dest.reset();
+
+ int line = getLineForOffset(point);
+ int top = getLineTop(line);
+ int bottom = getLineBottom(line, /* includeLineSpacing= */ false);
+
+ boolean clamped = shouldClampCursor(line);
+ float h1 = getPrimaryHorizontal(point, clamped) - 0.5f;
+
+ int caps = 0;
+ int fn = 0;
+ int dist = 0;
+
+ if (caps != 0 || fn != 0) {
+ dist = (bottom - top) >> 2;
+
+ if (fn != 0)
+ top += dist;
+ if (caps != 0)
+ bottom -= dist;
+ }
+
+ if (h1 < 0.5f)
+ h1 = 0.5f;
+
+ dest.moveTo(h1, top);
+ dest.lineTo(h1, bottom);
+
+ if (caps == 2) {
+ dest.moveTo(h1, bottom);
+ dest.lineTo(h1 - dist, bottom + dist);
+ dest.lineTo(h1, bottom);
+ dest.lineTo(h1 + dist, bottom + dist);
+ } else if (caps == 1) {
+ dest.moveTo(h1, bottom);
+ dest.lineTo(h1 - dist, bottom + dist);
+
+ dest.moveTo(h1 - dist, bottom + dist - 0.5f);
+ dest.lineTo(h1 + dist, bottom + dist - 0.5f);
+
+ dest.moveTo(h1 + dist, bottom + dist);
+ dest.lineTo(h1, bottom);
+ }
+
+ if (fn == 2) {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1 - dist, top - dist);
+ dest.lineTo(h1, top);
+ dest.lineTo(h1 + dist, top - dist);
+ } else if (fn == 1) {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1 - dist, top - dist);
+
+ dest.moveTo(h1 - dist, top - dist + 0.5f);
+ dest.lineTo(h1 + dist, top - dist + 0.5f);
+
+ dest.moveTo(h1 + dist, top - dist);
+ dest.lineTo(h1, top);
+ }
+ }
+
+ public void getSelectionPath(int start, int end, Path dest) {
+ dest.reset();
+ getSelection(start, end, (left, top, right, bottom, textSelectionLayout) ->
+ dest.addRect(left, top, right, bottom, Path.Direction.CW));
+ }
+
+ public final void getSelection(int start, int end, final SelectionRectangleConsumer consumer) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final Alignment getParagraphAlignment(int line) {
+ Alignment align = mAlignment;
+
+ if (mSpannedText) {
+ Spanned sp = (Spanned) mText;
+ AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line),
+ getLineEnd(line),
+ AlignmentSpan.class);
+
+ int spanLength = spans.length;
+ if (spanLength > 0) {
+ align = spans[spanLength-1].getAlignment();
+ }
+ }
+
+ return align;
+ }
+
+ public final int getParagraphLeft(int line) {
+ int left = 0;
+ int dir = getParagraphDirection(line);
+ if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) {
+ return left; // leading margin has no impact, or no styles
+ }
+ return getParagraphLeadingMargin(line);
+ }
+
+ public final int getParagraphRight(int line) {
+ int right = mWidth;
+ int dir = getParagraphDirection(line);
+ if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) {
+ return right; // leading margin has no impact, or no styles
+ }
+ return right - getParagraphLeadingMargin(line);
+ }
+
+ private int getParagraphLeadingMargin(int line) {
+ if (!mSpannedText) {
+ return 0;
+ }
+ Spanned spanned = (Spanned) mText;
+
+ int lineStart = getLineStart(line);
+ int lineEnd = getLineEnd(line);
+ int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
+ LeadingMarginSpan.class);
+ LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd,
+ LeadingMarginSpan.class);
+ if (spans.length == 0) {
+ return 0; // no leading margin span;
+ }
+
+ int margin = 0;
+
+ boolean useFirstLineMargin = lineStart == 0 || spanned.charAt(lineStart - 1) == '\n';
+ for (int i = 0; i < spans.length; i++) {
+ if (spans[i] instanceof LeadingMarginSpan2) {
+ int spStart = spanned.getSpanStart(spans[i]);
+ int spanLine = getLineForOffset(spStart);
+ int count = ((LeadingMarginSpan2) spans[i]).getLeadingMarginLineCount();
+ // if there is more than one LeadingMarginSpan2, use the count that is greatest
+ useFirstLineMargin |= line < spanLine + count;
+ }
+ }
+ for (int i = 0; i < spans.length; i++) {
+ LeadingMarginSpan span = spans[i];
+ margin += span.getLeadingMargin(useFirstLineMargin);
+ }
+
+ return margin;
+ }
+
+ public static class TabStops {
+ private float[] mStops;
+ private int mNumStops;
+ private float mIncrement;
+
+ public TabStops(float increment, Object[] spans) {
+ reset(increment, spans);
+ }
+
+ void reset(float increment, Object[] spans) {
+ this.mIncrement = increment;
+
+ int ns = 0;
+ if (spans != null) {
+ float[] stops = this.mStops;
+ if (ns > 1) {
+ Arrays.sort(stops, 0, ns);
+ }
+ if (stops != this.mStops) {
+ this.mStops = stops;
+ }
+ }
+ this.mNumStops = ns;
+ }
+
+ float nextTab(float h) {
+ int ns = this.mNumStops;
+ if (ns > 0) {
+ float[] stops = this.mStops;
+ for (int i = 0; i < ns; ++i) {
+ float stop = stops[i];
+ if (stop > h) {
+ return stop;
+ }
+ }
+ }
+ return nextDefaultStop(h, mIncrement);
+ }
+
+ public static float nextDefaultStop(float h, float inc) {
+ return ((int) ((h + inc) / inc)) * inc;
+ }
+ }
+
+ protected final boolean isSpanned() {
+ return mSpannedText;
+ }
+
+ /* package */static T[] getParagraphSpans(Spanned text, int start, int end, Class type) {
+ if (start == end && start > 0) {
+ return ArrayUtils.emptyArray(type);
+ }
+
+ return text.getSpans(start, end, type);
+ }
+
+ private void ellipsize(int start, int end, int line,
+ char[] dest, int destoff, TextUtils.TruncateAt method) {
+ final int ellipsisCount = getEllipsisCount(line);
+ if (ellipsisCount == 0) {
+ return;
+ }
+ final int ellipsisStart = getEllipsisStart(line);
+ final int lineStart = getLineStart(line);
+
+ final String ellipsisString = "...";
+ final int ellipsisStringLen = ellipsisString.length();
+ // Use the ellipsis string only if there are that at least as many characters to replace.
+ final boolean useEllipsisString = ellipsisCount >= ellipsisStringLen;
+ final int min = Math.max(0, start - ellipsisStart - lineStart);
+ final int max = Math.min(ellipsisCount, end - ellipsisStart - lineStart);
+
+ for (int i = min; i < max; i++) {
+ final char c;
+ if (useEllipsisString && i < ellipsisStringLen) {
+ c = ellipsisString.charAt(i);
+ } else {
+ c = ' ';
+ }
+
+ final int a = i + ellipsisStart + lineStart;
+ dest[destoff + a - start] = c;
+ }
+ }
+
+ public static class Directions {
+ public int[] mDirections;
+
+ public Directions(int[] dirs) {
+ mDirections = dirs;
+ }
+
+ public @IntRange(from = 0) int getRunCount() {
+ return mDirections.length / 2;
+ }
+
+ public @IntRange(from = 0) int getRunStart(@IntRange(from = 0) int runIndex) {
+ return mDirections[runIndex * 2];
+ }
+
+ public @IntRange(from = 0) int getRunLength(@IntRange(from = 0) int runIndex) {
+ return mDirections[runIndex * 2 + 1] & RUN_LENGTH_MASK;
+ }
+
+ @IntRange(from = 0)
+ public int getRunLevel(int runIndex) {
+ return (mDirections[runIndex * 2 + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
+ }
+
+ public boolean isRunRtl(int runIndex) {
+ return (mDirections[runIndex * 2 + 1] & RUN_RTL_FLAG) != 0;
+ }
+ }
+
+ public abstract int getEllipsisStart(int line);
+
+ public abstract int getEllipsisCount(int line);
+
+ /* package */ static class Ellipsizer implements CharSequence, GetChars {
+ /* package */ CharSequence mText;
+ /* package */ Layout mLayout;
+ /* package */ int mWidth;
+ /* package */ TextUtils.TruncateAt mMethod;
+
+ public Ellipsizer(CharSequence s) {
+ mText = s;
+ }
+
+ @Override
+ public char charAt(int off) {
+ char[] buf = TextUtils.obtain(1);
+ getChars(off, off + 1, buf, 0);
+ char ret = buf[0];
+
+ TextUtils.recycle(buf);
+ return ret;
+ }
+
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ int line1 = mLayout.getLineForOffset(start);
+ int line2 = mLayout.getLineForOffset(end);
+
+ TextUtils.getChars(mText, start, end, dest, destoff);
+
+ for (int i = line1; i <= line2; i++) {
+ mLayout.ellipsize(start, end, i, dest, destoff, mMethod);
+ }
+ }
+
+ @Override
+ public int length() {
+ return mText.length();
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ char[] s = new char[end - start];
+ getChars(start, end, s, 0);
+ return new String(s);
+ }
+
+ @Override
+ public String toString() {
+ char[] s = new char[length()];
+ getChars(0, length(), s, 0);
+ return new String(s);
+ }
+
+ }
+
+ /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned {
+ private Spanned mSpanned;
+
+ public SpannedEllipsizer(CharSequence display) {
+ super(display);
+ mSpanned = (Spanned) display;
+ }
+
+ public T[] getSpans(int start, int end, Class type) {
+ return mSpanned.getSpans(start, end, type);
+ }
+
+ public int getSpanStart(Object tag) {
+ return mSpanned.getSpanStart(tag);
+ }
+
+ public int getSpanEnd(Object tag) {
+ return mSpanned.getSpanEnd(tag);
+ }
+
+ public int getSpanFlags(Object tag) {
+ return mSpanned.getSpanFlags(tag);
+ }
+
+ @SuppressWarnings("rawtypes")
+ public int nextSpanTransition(int start, int limit, Class type) {
+ return mSpanned.nextSpanTransition(start, limit, type);
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ char[] s = new char[end - start];
+ getChars(start, end, s, 0);
+
+ SpannableString ss = new SpannableString(new String(s));
+ TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
+ return ss;
+ }
+ }
+
+ private CharSequence mText;
+ private TextPaint mPaint;
+ private final TextPaint mWorkPaint = new TextPaint();
+ private final Paint mWorkPlainPaint = new Paint();
+ private int mWidth;
+ private Alignment mAlignment = Alignment.ALIGN_NORMAL;
+ private float mSpacingMult;
+ private float mSpacingAdd;
+ private static final Rect sTempRect = new Rect();
+ private boolean mSpannedText;
+ private TextDirectionHeuristic mTextDir;
+ private boolean mIncludePad;
+ private boolean mFallbackLineSpacing;
+ private int mEllipsizedWidth;
+ private TextUtils.TruncateAt mEllipsize;
+ private int mMaxLines;
+ private int mBreakStrategy;
+ private int mHyphenationFrequency;
+ private int[] mLeftIndents;
+ private int[] mRightIndents;
+ private int mJustificationMode;
+ private boolean mUseBoundsForWidth;
+ private boolean mShiftDrawingOffsetForStartOverhang;
+ private @Nullable Paint.FontMetrics mMinimumFontMetrics;
+
+ private TextLine.LineInfo mLineInfo = null;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Direction {}
+
+ public static final int DIR_LEFT_TO_RIGHT = 1;
+ public static final int DIR_RIGHT_TO_LEFT = -1;
+
+ /* package */ static final int DIR_REQUEST_LTR = 1;
+ /* package */ static final int DIR_REQUEST_RTL = -1;
+ /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
+ /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
+
+ /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
+ /* package */ static final int RUN_LEVEL_SHIFT = 26;
+ /* package */ static final int RUN_LEVEL_MASK = 0x3f;
+ /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
+
+ public enum Alignment {
+ ALIGN_NORMAL,
+ ALIGN_OPPOSITE,
+ ALIGN_CENTER,
+ /** @hide */
+ ALIGN_LEFT,
+ /** @hide */
+ ALIGN_RIGHT,
+ }
+
+ private static final float TAB_INCREMENT = 20;
+
+ /** @hide */
+ public static final Directions DIRS_ALL_LEFT_TO_RIGHT =
+ new Directions(new int[] { 0, RUN_LENGTH_MASK });
+
+ /** @hide */
+ public static final Directions DIRS_ALL_RIGHT_TO_LEFT =
+ new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TextSelectionLayout {}
+
+ /** @hide */
+ public static final int TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT = 0;
+ /** @hide */
+ public static final int TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT = 1;
+
+ /** @hide */
+ @FunctionalInterface
+ public interface SelectionRectangleConsumer {
+ void accept(float left, float top, float right, float bottom,
+ @TextSelectionLayout int textSelectionLayout);
+ }
+
+ @FunctionalInterface
+ public interface TextInclusionStrategy {
+ boolean isSegmentInside(@NonNull RectF segmentBounds, @NonNull RectF area);
+ }
+
+ public static final class Builder {
+ public Builder(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextPaint paint,
+ @IntRange(from = 0) int width) {
+ mText = text;
+ mStart = start;
+ mEnd = end;
+ mPaint = paint;
+ mWidth = width;
+ mEllipsizedWidth = width;
+ }
+
+ @NonNull
+ public Builder setAlignment(@NonNull Alignment alignment) {
+ mAlignment = alignment;
+ return this;
+ }
+
+ @NonNull
+ public Builder setTextDirectionHeuristic(@NonNull TextDirectionHeuristic textDirection) {
+ mTextDir = textDirection;
+ return this;
+ }
+
+ @NonNull
+ public Builder setLineSpacingAmount(float amount) {
+ mSpacingAdd = amount;
+ return this;
+ }
+
+ @NonNull
+ public Builder setLineSpacingMultiplier(@FloatRange(from = 0) float multiplier) {
+ mSpacingMult = multiplier;
+ return this;
+ }
+
+ @NonNull
+ public Builder setFontPaddingIncluded(boolean includeFontPadding) {
+ mIncludePad = includeFontPadding;
+ return this;
+ }
+
+ @NonNull
+ public Builder setFallbackLineSpacingEnabled(boolean fallbackLineSpacing) {
+ mFallbackLineSpacing = fallbackLineSpacing;
+ return this;
+ }
+
+ @NonNull
+ public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizeWidth) {
+ mEllipsizedWidth = ellipsizeWidth;
+ return this;
+ }
+
+ @NonNull
+ public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
+ mEllipsize = ellipsize;
+ return this;
+ }
+
+ @NonNull
+ public Builder setMaxLines(@IntRange(from = 1) int maxLines) {
+ mMaxLines = maxLines;
+ return this;
+ }
+
+ @NonNull
+ public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
+ mBreakStrategy = breakStrategy;
+ return this;
+ }
+
+ @NonNull
+ public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
+ mHyphenationFrequency = hyphenationFrequency;
+ return this;
+ }
+
+ @NonNull
+ public Builder setLeftIndents(@Nullable int[] leftIndents) {
+ mLeftIndents = leftIndents;
+ return this;
+ }
+
+ @NonNull
+ public Builder setRightIndents(@Nullable int[] rightIndents) {
+ mRightIndents = rightIndents;
+ return this;
+ }
+
+ @NonNull
+ public Builder setJustificationMode(@JustificationMode int justificationMode) {
+ mJustificationMode = justificationMode;
+ return this;
+ }
+
+ // The corresponding getter is getUseBoundsForWidth
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
+ mUseBoundsForWidth = useBoundsForWidth;
+ return this;
+ }
+
+ @NonNull
+ // The corresponding getter is getShiftDrawingOffsetForStartOverhang()
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setShiftDrawingOffsetForStartOverhang(
+ boolean shiftDrawingOffsetForStartOverhang) {
+ mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
+ return this;
+ }
+
+ @NonNull
+ public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) {
+ mMinimumFontMetrics = minimumFontMetrics;
+ return this;
+ }
+
+ @NonNull
+ public Layout build() {
+ return StaticLayout.Builder.obtain(mText, mStart, mEnd, mPaint, mWidth)
+ .setAlignment(mAlignment)
+ .setLineSpacing(mSpacingAdd, mSpacingMult)
+ .setTextDirection(mTextDir)
+ .setIncludePad(mIncludePad)
+ .setUseLineSpacingFromFallbacks(mFallbackLineSpacing)
+ .setEllipsizedWidth(mEllipsizedWidth)
+ .setEllipsize(mEllipsize)
+ .setMaxLines(mMaxLines)
+ .setBreakStrategy(mBreakStrategy)
+ .setHyphenationFrequency(mHyphenationFrequency)
+ .setIndents(mLeftIndents, mRightIndents)
+ .setJustificationMode(mJustificationMode)
+ .setUseBoundsForWidth(mUseBoundsForWidth)
+ .setShiftDrawingOffsetForStartOverhang(mShiftDrawingOffsetForStartOverhang)
+ .build();
+ }
+
+ private final CharSequence mText;
+ private final int mStart;
+ private final int mEnd;
+ private final TextPaint mPaint;
+ private final int mWidth;
+ private Alignment mAlignment = Alignment.ALIGN_NORMAL;
+ private float mSpacingMult = 1.0f;
+ private float mSpacingAdd = 0.0f;
+ private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
+ private boolean mIncludePad = true;
+ private boolean mFallbackLineSpacing = false;
+ private int mEllipsizedWidth;
+ private TextUtils.TruncateAt mEllipsize = null;
+ private int mMaxLines = Integer.MAX_VALUE;
+ private int mBreakStrategy = BREAK_STRATEGY_SIMPLE;
+ private int mHyphenationFrequency = HYPHENATION_FREQUENCY_NONE;
+ private int[] mLeftIndents = null;
+ private int[] mRightIndents = null;
+ private int mJustificationMode = JUSTIFICATION_MODE_NONE;
+ private boolean mUseBoundsForWidth;
+ private boolean mShiftDrawingOffsetForStartOverhang;
+ private Paint.FontMetrics mMinimumFontMetrics;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Getters of parameters that is used for building Layout instance
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ // TODO(316208691): Revive following removed API docs.
+ // @see Layout.Builder
+ @NonNull
+ public final CharSequence getText() {
+ return mText;
+ }
+
+ // TODO(316208691): Revive following removed API docs.
+ // @see Layout.Builder
+ @NonNull
+ public final TextPaint getPaint() {
+ return mPaint;
+ }
+
+ // TODO(316208691): Revive following removed API docs.
+ // @see Layout.Builder
+ @IntRange(from = 0)
+ public final int getWidth() {
+ return mWidth;
+ }
+
+ // TODO(316208691): Revive following removed API docs.
+ // @see Layout.Builder#setAlignment(Alignment)
+ @NonNull
+ public final Alignment getAlignment() {
+ return mAlignment;
+ }
+
+ @NonNull
+ public final TextDirectionHeuristic getTextDirectionHeuristic() {
+ return mTextDir;
+ }
+
+ // TODO(316208691): Revive following removed API docs.
+ // This is an alias of {@link #getLineSpacingMultiplier}.
+ // @see Layout.Builder#setLineSpacingMultiplier(float)
+ // @see Layout#getLineSpacingMultiplier()
+ public final float getSpacingMultiplier() {
+ return getLineSpacingMultiplier();
+ }
+
+ public final float getLineSpacingMultiplier() {
+ return mSpacingMult;
+ }
+
+ // TODO(316208691): Revive following removed API docs.
+ // This is an alias of {@link #getLineSpacingAmount()}.
+ // @see Layout.Builder#setLineSpacingAmount(float)
+ // @see Layout#getLineSpacingAmount()
+ public final float getSpacingAdd() {
+ return getLineSpacingAmount();
+ }
+
+ public final float getLineSpacingAmount() {
+ return mSpacingAdd;
+ }
+
+ public final boolean isFontPaddingIncluded() {
+ return mIncludePad;
+ }
+
+ // TODO(316208691): Revive following removed API docs.
+ // @see Layout.Builder#setFallbackLineSpacingEnabled(boolean)
+ // not being final because of already published API.
+ public boolean isFallbackLineSpacingEnabled() {
+ return mFallbackLineSpacing;
+ }
+
+ // TODO(316208691): Revive following removed API docs.
+ // @see Layout.Builder#setEllipsizedWidth(int)
+ // @see Layout.Builder#setEllipsize(TextUtils.TruncateAt)
+ // @see Layout#getEllipsize()
+ @IntRange(from = 0)
+ public int getEllipsizedWidth() { // not being final because of already published API.
+ return mEllipsizedWidth;
+ }
+
+ @Nullable
+ public final TextUtils.TruncateAt getEllipsize() {
+ return mEllipsize;
+ }
+
+ @IntRange(from = 1)
+ public final int getMaxLines() {
+ return mMaxLines;
+ }
+
+ @BreakStrategy
+ public final int getBreakStrategy() {
+ return mBreakStrategy;
+ }
+
+ @HyphenationFrequency
+ public final int getHyphenationFrequency() {
+ return mHyphenationFrequency;
+ }
+
+ @Nullable
+ public final int[] getLeftIndents() {
+ if (mLeftIndents == null) {
+ return null;
+ }
+ int[] newArray = new int[mLeftIndents.length];
+ System.arraycopy(mLeftIndents, 0, newArray, 0, newArray.length);
+ return newArray;
+ }
+
+ @Nullable
+ public final int[] getRightIndents() {
+ if (mRightIndents == null) {
+ return null;
+ }
+ int[] newArray = new int[mRightIndents.length];
+ System.arraycopy(mRightIndents, 0, newArray, 0, newArray.length);
+ return newArray;
+ }
+
+ @JustificationMode
+ public final int getJustificationMode() {
+ return mJustificationMode;
+ }
+
+ public boolean getUseBoundsForWidth() {
+ return mUseBoundsForWidth;
+ }
+
+ public boolean getShiftDrawingOffsetForStartOverhang() {
+ return mShiftDrawingOffsetForStartOverhang;
+ }
+
+ @Nullable
+ public Paint.FontMetrics getMinimumFontMetrics() {
+ return mMinimumFontMetrics;
+ }
+
+ private interface CharacterBoundsListener {
+ void onCharacterBounds(int index, int lineNum, float left, float top, float right,
+ float bottom);
+
+ /** Called after the last character has been sent to {@link #onCharacterBounds}. */
+ default void onEnd() {}
+ }
+}
+
diff --git a/AndroidCompat/src/main/java/android/text/StaticLayout.java b/AndroidCompat/src/main/java/android/text/StaticLayout.java
new file mode 100644
index 00000000..e5a01438
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/text/StaticLayout.java
@@ -0,0 +1,631 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed 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 android.text;
+
+import android.annotation.ColorInt;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.Log;
+import java.awt.RenderingHints;
+import java.awt.font.LineBreakMeasurer;
+import java.awt.font.FontRenderContext;
+import java.awt.font.TextAttribute;
+import java.awt.font.TextLayout;
+import java.text.AttributedString;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+
+
+public class StaticLayout extends Layout {
+ /*
+ * The break iteration is done in native code. The protocol for using the native code is as
+ * follows.
+ *
+ * First, call nInit to setup native line breaker object. Then, for each paragraph, do the
+ * following:
+ *
+ * - Create MeasuredParagraph by MeasuredParagraph.buildForStaticLayout which measures in
+ * native.
+ * - Run LineBreaker.computeLineBreaks() to obtain line breaks for the paragraph.
+ *
+ * After all paragraphs, call finish() to release expensive buffers.
+ */
+
+ static final String TAG = "StaticLayout";
+
+ public final static class Builder {
+ private Builder() {}
+
+ @NonNull
+ public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end, @NonNull TextPaint paint,
+ @IntRange(from = 0) int width) {
+ Builder b = new Builder();
+
+ // set default initial values
+ b.mText = source;
+ b.mStart = start;
+ b.mEnd = end;
+ b.mPaint = paint;
+ b.mWidth = width;
+ b.mAlignment = Alignment.ALIGN_NORMAL;
+ b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
+ b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
+ b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
+ b.mIncludePad = true;
+ b.mFallbackLineSpacing = false;
+ b.mEllipsizedWidth = width;
+ b.mEllipsize = null;
+ b.mMaxLines = Integer.MAX_VALUE;
+ b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
+ b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
+ b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
+ b.mMinimumFontMetrics = null;
+ return b;
+ }
+
+ // release any expensive state
+ /* package */ void finish() {
+ mText = null;
+ mPaint = null;
+ mLeftIndents = null;
+ mRightIndents = null;
+ mMinimumFontMetrics = null;
+ }
+
+ public Builder setText(CharSequence source) {
+ return setText(source, 0, source.length());
+ }
+
+ @NonNull
+ public Builder setText(@NonNull CharSequence source, int start, int end) {
+ mText = source;
+ mStart = start;
+ mEnd = end;
+ return this;
+ }
+
+ @NonNull
+ public Builder setPaint(@NonNull TextPaint paint) {
+ mPaint = paint;
+ return this;
+ }
+
+ @NonNull
+ public Builder setWidth(@IntRange(from = 0) int width) {
+ mWidth = width;
+ if (mEllipsize == null) {
+ mEllipsizedWidth = width;
+ }
+ return this;
+ }
+
+ @NonNull
+ public Builder setAlignment(@NonNull Alignment alignment) {
+ mAlignment = alignment;
+ return this;
+ }
+
+ @NonNull
+ public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
+ mTextDir = textDir;
+ return this;
+ }
+
+ @NonNull
+ public Builder setLineSpacing(float spacingAdd, float spacingMult) {
+ mSpacingAdd = spacingAdd;
+ mSpacingMult = spacingMult;
+ return this;
+ }
+
+ @NonNull
+ public Builder setIncludePad(boolean includePad) {
+ mIncludePad = includePad;
+ return this;
+ }
+
+ @NonNull
+ public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
+ mFallbackLineSpacing = useLineSpacingFromFallbacks;
+ return this;
+ }
+
+ @NonNull
+ public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) {
+ mEllipsizedWidth = ellipsizedWidth;
+ return this;
+ }
+
+ @NonNull
+ public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
+ mEllipsize = ellipsize;
+ return this;
+ }
+
+ @NonNull
+ public Builder setMaxLines(@IntRange(from = 0) int maxLines) {
+ mMaxLines = maxLines;
+ return this;
+ }
+
+ @NonNull
+ public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
+ mBreakStrategy = breakStrategy;
+ return this;
+ }
+
+ @NonNull
+ public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
+ mHyphenationFrequency = hyphenationFrequency;
+ return this;
+ }
+
+ @NonNull
+ public Builder setIndents(@Nullable int[] leftIndents, @Nullable int[] rightIndents) {
+ mLeftIndents = leftIndents;
+ mRightIndents = rightIndents;
+ return this;
+ }
+
+ @NonNull
+ public Builder setJustificationMode(@JustificationMode int justificationMode) {
+ mJustificationMode = justificationMode;
+ return this;
+ }
+
+ @NonNull
+ /* package */ Builder setAddLastLineLineSpacing(boolean value) {
+ mAddLastLineLineSpacing = value;
+ return this;
+ }
+
+ @SuppressLint("MissingGetterMatchingBuilder") // The base class `Layout` has a getter.
+ @NonNull
+ public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
+ mUseBoundsForWidth = useBoundsForWidth;
+ return this;
+ }
+
+ @NonNull
+ // The corresponding getter is getShiftDrawingOffsetForStartOverhang()
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setShiftDrawingOffsetForStartOverhang(
+ boolean shiftDrawingOffsetForStartOverhang) {
+ mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
+ return this;
+ }
+
+ public Builder setCalculateBounds(boolean value) {
+ mCalculateBounds = value;
+ return this;
+ }
+
+ @NonNull
+ public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) {
+ mMinimumFontMetrics = minimumFontMetrics;
+ return this;
+ }
+
+ @NonNull
+ public StaticLayout build() {
+ StaticLayout result = new StaticLayout(this, mIncludePad, mEllipsize != null
+ ? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL);
+ return result;
+ }
+
+ private CharSequence mText;
+ private int mStart;
+ private int mEnd;
+ private TextPaint mPaint;
+ private int mWidth;
+ private Alignment mAlignment;
+ private TextDirectionHeuristic mTextDir;
+ private float mSpacingMult;
+ private float mSpacingAdd;
+ private boolean mIncludePad;
+ private boolean mFallbackLineSpacing;
+ private int mEllipsizedWidth;
+ private TextUtils.TruncateAt mEllipsize;
+ private int mMaxLines;
+ private int mBreakStrategy;
+ private int mHyphenationFrequency;
+ @Nullable private int[] mLeftIndents;
+ @Nullable private int[] mRightIndents;
+ private int mJustificationMode;
+ private boolean mAddLastLineLineSpacing;
+ private boolean mUseBoundsForWidth;
+ private boolean mShiftDrawingOffsetForStartOverhang;
+ private boolean mCalculateBounds;
+ @Nullable private Paint.FontMetrics mMinimumFontMetrics;
+
+ private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
+ }
+
+ private StaticLayout() {
+ super(
+ null, // text
+ null, // paint
+ 0, // width
+ null, // alignment
+ null, // textDir
+ 1, // spacing multiplier
+ 0, // spacing amount
+ false, // include font padding
+ false, // fallback line spacing
+ 0, // ellipsized width
+ null, // ellipsize
+ 1, // maxLines
+ BREAK_STRATEGY_SIMPLE,
+ HYPHENATION_FREQUENCY_NONE,
+ null, // leftIndents
+ null, // rightIndents
+ JUSTIFICATION_MODE_NONE,
+ false, // useBoundsForWidth
+ false, // shiftDrawingOffsetForStartOverhang
+ null // minimumFontMetrics
+ );
+
+ mColumns = COLUMNS_ELLIPSIZE;
+ mLineDirections = new Directions[2];
+ mLines = new int[2 * mColumns];
+ }
+
+ @Deprecated
+ public StaticLayout(CharSequence source, TextPaint paint,
+ int width,
+ Alignment align, float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(source, 0, source.length(), paint, width, align,
+ spacingmult, spacingadd, includepad);
+ }
+
+ @Deprecated
+ public StaticLayout(CharSequence source, int bufstart, int bufend,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(source, bufstart, bufend, paint, outerwidth, align,
+ spacingmult, spacingadd, includepad, null, 0);
+ }
+
+ @Deprecated
+ public StaticLayout(CharSequence source, int bufstart, int bufend,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
+ this(source, bufstart, bufend, paint, outerwidth, align,
+ TextDirectionHeuristics.FIRSTSTRONG_LTR,
+ spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
+ }
+
+ @Deprecated
+ public StaticLayout(CharSequence source, int bufstart, int bufend,
+ TextPaint paint, int outerwidth,
+ Alignment align, TextDirectionHeuristic textDir,
+ float spacingmult, float spacingadd,
+ boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
+ this(Builder.obtain(source, bufstart, bufend, paint, outerwidth)
+ .setAlignment(align)
+ .setTextDirection(textDir)
+ .setLineSpacing(spacingadd, spacingmult)
+ .setIncludePad(includepad)
+ .setEllipsize(ellipsize)
+ .setEllipsizedWidth(ellipsizedWidth)
+ .setMaxLines(maxLines), includepad,
+ ellipsize != null ? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL);
+ }
+
+ private StaticLayout(Builder b, boolean trackPadding, int columnSize) {
+ super((b.mEllipsize == null) ? b.mText : (b.mText instanceof Spanned)
+ ? new SpannedEllipsizer(b.mText) : new Ellipsizer(b.mText),
+ b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd,
+ b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize,
+ b.mMaxLines, b.mBreakStrategy, b.mHyphenationFrequency, b.mLeftIndents,
+ b.mRightIndents, b.mJustificationMode, b.mUseBoundsForWidth,
+ b.mShiftDrawingOffsetForStartOverhang, b.mMinimumFontMetrics);
+
+ mColumns = columnSize;
+ if (b.mEllipsize != null) {
+ Ellipsizer e = (Ellipsizer) getText();
+
+ e.mLayout = this;
+ e.mWidth = b.mEllipsizedWidth;
+ e.mMethod = b.mEllipsize;
+ throw new UnsupportedOperationException("Ellipsis not supported");
+ }
+
+ mLineDirections = new Directions[2];
+ mLines = new int[2 * mColumns];
+ mMaximumVisibleLineCount = b.mMaxLines;
+
+ mLeftIndents = b.mLeftIndents;
+ mRightIndents = b.mRightIndents;
+
+ String str = b.mText.subSequence(b.mStart, b.mEnd).toString();
+ AttributedString text = new AttributedString(str);
+ text.addAttribute(TextAttribute.FONT, getPaint().getFont());
+ FontRenderContext frc = new FontRenderContext(getPaint().getFont().getTransform(), RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT, RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
+ LineBreakMeasurer measurer = new LineBreakMeasurer(text.getIterator(), frc);
+ // TODO: directions
+
+ float y = 0;
+ while (measurer.getPosition() < str.length()) {
+ int off = mLineCount * mColumns;
+ int want = off + mColumns + TOP;
+ if (want >= mLines.length) {
+ final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
+ System.arraycopy(mLines, 0, grow, 0, mLines.length);
+ mLines = grow;
+ }
+
+ int pos = measurer.getPosition();
+ TextLayout l = measurer.nextLayout(getWidth());
+ mLines[off + START] = pos;
+ mLines[off + TOP] = (int) y;
+ mLines[off + DESCENT] = (int) (l.getDescent() + l.getLeading());
+ mLines[off + EXTRA] = (int) l.getLeading();
+ mLines[off + DIR] |= Layout.DIR_LEFT_TO_RIGHT << DIR_SHIFT;
+
+ y += l.getAscent();
+ y += l.getDescent() + l.getLeading();
+
+ mLines[off + mColumns + START] = measurer.getPosition();
+ mLines[off + mColumns + TOP] = (int) y;
+
+ mLineCount += 1;
+ }
+ }
+
+ // Override the base class so we can directly access our members,
+ // rather than relying on member functions.
+ // The logic mirrors that of Layout.getLineForVertical
+ // FIXME: It may be faster to do a linear search for layouts without many lines.
+ @Override
+ public int getLineForVertical(int vertical) {
+ int high = mLineCount;
+ int low = -1;
+ int guess;
+ int[] lines = mLines;
+ while (high - low > 1) {
+ guess = (high + low) >> 1;
+ if (lines[mColumns * guess + TOP] > vertical){
+ high = guess;
+ } else {
+ low = guess;
+ }
+ }
+ if (low < 0) {
+ return 0;
+ } else {
+ return low;
+ }
+ }
+
+ @Override
+ public int getLineCount() {
+ return mLineCount;
+ }
+
+ @Override
+ public int getLineTop(int line) {
+ return mLines[mColumns * line + TOP];
+ }
+
+ @Override
+ public int getLineExtra(int line) {
+ return mLines[mColumns * line + EXTRA];
+ }
+
+ @Override
+ public int getLineDescent(int line) {
+ return mLines[mColumns * line + DESCENT];
+ }
+
+ @Override
+ public int getLineStart(int line) {
+ return mLines[mColumns * line + START] & START_MASK;
+ }
+
+ @Override
+ public int getParagraphDirection(int line) {
+ return mLines[mColumns * line + DIR] >> DIR_SHIFT;
+ }
+
+ @Override
+ public boolean getLineContainsTab(int line) {
+ return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
+ }
+
+ @Override
+ public final Directions getLineDirections(int line) {
+ if (line > getLineCount()) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ return new Directions(null);
+ // return mLineDirections[line];
+ }
+
+ @Override
+ public int getTopPadding() {
+ return mTopPadding;
+ }
+
+ @Override
+ public int getBottomPadding() {
+ return mBottomPadding;
+ }
+
+ // To store into single int field, pack the pair of start and end hyphen edit.
+ static int packHyphenEdit(
+ @Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) {
+ return start << START_HYPHEN_BITS_SHIFT | end;
+ }
+
+ static int unpackStartHyphenEdit(int packedHyphenEdit) {
+ return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT;
+ }
+
+ static int unpackEndHyphenEdit(int packedHyphenEdit) {
+ return packedHyphenEdit & END_HYPHEN_MASK;
+ }
+
+ @Override
+ public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) {
+ return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
+ }
+
+ @Override
+ public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) {
+ return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
+ }
+
+ @Override
+ public int getIndentAdjust(int line, Alignment align) {
+ if (align == Alignment.ALIGN_LEFT) {
+ if (mLeftIndents == null) {
+ return 0;
+ } else {
+ return mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
+ }
+ } else if (align == Alignment.ALIGN_RIGHT) {
+ if (mRightIndents == null) {
+ return 0;
+ } else {
+ return -mRightIndents[Math.min(line, mRightIndents.length - 1)];
+ }
+ } else if (align == Alignment.ALIGN_CENTER) {
+ int left = 0;
+ if (mLeftIndents != null) {
+ left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
+ }
+ int right = 0;
+ if (mRightIndents != null) {
+ right = mRightIndents[Math.min(line, mRightIndents.length - 1)];
+ }
+ return (left - right) >> 1;
+ } else {
+ throw new AssertionError("unhandled alignment " + align);
+ }
+ }
+
+ @Override
+ public int getEllipsisCount(int line) {
+ if (mColumns < COLUMNS_ELLIPSIZE) {
+ return 0;
+ }
+
+ return mLines[mColumns * line + ELLIPSIS_COUNT];
+ }
+
+ @Override
+ public int getEllipsisStart(int line) {
+ if (mColumns < COLUMNS_ELLIPSIZE) {
+ return 0;
+ }
+
+ return mLines[mColumns * line + ELLIPSIS_START];
+ }
+
+ @Override
+ @NonNull
+ public RectF computeDrawingBoundingBox() {
+ // Cache the drawing bounds result because it does not change after created.
+ if (mDrawingBounds == null) {
+ mDrawingBounds = super.computeDrawingBoundingBox();
+ }
+ return mDrawingBounds;
+ }
+
+ @Override
+ public int getHeight(boolean cap) {
+ if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1
+ && Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "maxLineHeight should not be -1. "
+ + " maxLines:" + mMaximumVisibleLineCount
+ + " lineCount:" + mLineCount);
+ }
+
+ return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1
+ ? mMaxLineHeight : super.getHeight();
+ }
+
+ private int mLineCount;
+ private int mTopPadding, mBottomPadding;
+ private int mColumns;
+ private RectF mDrawingBounds = null; // lazy calculation.
+
+ private boolean mEllipsized;
+
+ private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
+
+ private static final int COLUMNS_NORMAL = 5;
+ private static final int COLUMNS_ELLIPSIZE = 7;
+ private static final int START = 0;
+ private static final int DIR = START;
+ private static final int TAB = START;
+ private static final int TOP = 1;
+ private static final int DESCENT = 2;
+ private static final int EXTRA = 3;
+ private static final int HYPHEN = 4;
+ private static final int ELLIPSIS_START = 5;
+ private static final int ELLIPSIS_COUNT = 6;
+
+ private int[] mLines;
+ private Directions[] mLineDirections;
+ private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
+
+ private static final int START_MASK = 0x1FFFFFFF;
+ private static final int DIR_SHIFT = 30;
+ private static final int TAB_MASK = 0x20000000;
+ private static final int HYPHEN_MASK = 0xFF;
+ private static final int START_HYPHEN_BITS_SHIFT = 3;
+ private static final int START_HYPHEN_MASK = 0x18; // 0b11000
+ private static final int END_HYPHEN_MASK = 0x7; // 0b00111
+
+ private static final float TAB_INCREMENT = 20; // same as Layout, but that's private
+
+ private static final char CHAR_NEW_LINE = '\n';
+
+ private static final double EXTRA_ROUNDING = 0.5;
+
+ private static final int DEFAULT_MAX_LINE_HEIGHT = -1;
+
+ // Unused, here because of gray list private API accesses.
+ /*package*/ static class LineBreaks {
+ private static final int INITIAL_SIZE = 16;
+ public int[] breaks = new int[INITIAL_SIZE];
+ public float[] widths = new float[INITIAL_SIZE];
+ public float[] ascents = new float[INITIAL_SIZE];
+ public float[] descents = new float[INITIAL_SIZE];
+ public int[] flags = new int[INITIAL_SIZE]; // hasTab
+ // breaks, widths, and flags should all have the same length
+ }
+
+ @Nullable private int[] mLeftIndents;
+ @Nullable private int[] mRightIndents;
+}
+
diff --git a/AndroidCompat/src/main/java/android/text/TextLine.java b/AndroidCompat/src/main/java/android/text/TextLine.java
new file mode 100644
index 00000000..8c9f972d
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/text/TextLine.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed 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 android.text;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.text.Layout.Directions;
+import android.text.Layout.TabStops;
+import java.awt.RenderingHints;
+import java.awt.font.FontRenderContext;
+
+
+public class TextLine {
+ private TextPaint mPaint;
+ private CharSequence mText;
+ private int mStart;
+ private int mLen;
+ private int mDir;
+ private Directions mDirections;
+ private boolean mHasTabs;
+ private TabStops mTabs;
+ private char[] mChars;
+ private boolean mCharsValid;
+ private Spanned mSpanned;
+ private PrecomputedText mComputed;
+ private RectF mTmpRectForMeasure;
+ private RectF mTmpRectForPaintAPI;
+ private Rect mTmpRectForPrecompute;
+
+
+ public static final class LineInfo {
+ private int mClusterCount;
+
+ public int getClusterCount() {
+ return mClusterCount;
+ }
+
+ public void setClusterCount(int clusterCount) {
+ mClusterCount = clusterCount;
+ }
+ };
+
+ public float getAddedWordSpacingInPx() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float getAddedLetterSpacingInPx() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public boolean isJustifying() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Not allowed to access. If it's for memory leak workaround, it was already fixed M. */
+ private static final TextLine[] sCached = new TextLine[3];
+
+ public static TextLine obtain() {
+ TextLine tl;
+ synchronized (sCached) {
+ for (int i = sCached.length; --i >= 0;) {
+ if (sCached[i] != null) {
+ tl = sCached[i];
+ sCached[i] = null;
+ return tl;
+ }
+ }
+ }
+ tl = new TextLine();
+ return tl;
+ }
+
+ public static TextLine recycle(TextLine tl) {
+ synchronized(sCached) {
+ for (int i = 0; i < sCached.length; ++i) {
+ if (sCached[i] == null) {
+ sCached[i] = tl;
+ break;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
+ Directions directions, boolean hasTabs, TabStops tabStops,
+ int ellipsisStart, int ellipsisEnd, boolean useFallbackLineSpacing) {
+ mPaint = paint;
+ mText = text;
+ mStart = start;
+ mLen = limit - start;
+ mDir = dir;
+ mDirections = directions;
+ if (mDirections == null) {
+ throw new IllegalArgumentException("Directions cannot be null");
+ }
+ mHasTabs = hasTabs;
+ mSpanned = null;
+
+ if (text instanceof Spanned) {
+ mSpanned = (Spanned) text;
+ }
+
+ mComputed = null;
+ if (text instanceof PrecomputedText) {
+ // Here, no need to check line break strategy or hyphenation frequency since there is no
+ // line break concept here.
+ mComputed = (PrecomputedText) text;
+ if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
+ mComputed = null;
+ }
+ }
+
+ mTabs = tabStops;
+ }
+
+ public void justify(@Layout.JustificationMode int justificationMode, float justifyWidth) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public static int calculateRunFlag(int bidiRunIndex, int bidiRunCount, int lineDirection) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public static int resolveRunFlagForSubSequence(int runFlag, boolean isRtlRun, int runStart,
+ int runEnd, int spanStart, int spanEnd) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth,
+ @Nullable LineInfo lineInfo) {
+ FontRenderContext frc = new FontRenderContext(null, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT, RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
+ return (float) mPaint.getFont().getStringBounds(mText.toString(), mStart, mStart + mLen, frc).getWidth();
+ }
+
+ public float measure(@IntRange(from = 0) int offset, boolean trailing,
+ @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void measureAllBounds(@NonNull float[] bounds, @Nullable float[] advances) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
+ throw new RuntimeException("Stub!");
+ }
+
+ // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
+ public static boolean isLineEndSpace(char ch) {
+ return ch == ' ' || ch == '\t' || ch == 0x1680
+ || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
+ || ch == 0x205F || ch == 0x3000;
+ }
+
+ void draw(Canvas c, float x, int top, int y, int bottom) {
+ c.drawText(mText, mStart, mStart + mLen, x, y, mPaint);
+ }
+}
diff --git a/AndroidCompat/src/main/java/android/text/TextPaint.java b/AndroidCompat/src/main/java/android/text/TextPaint.java
new file mode 100644
index 00000000..c8c86373
--- /dev/null
+++ b/AndroidCompat/src/main/java/android/text/TextPaint.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed 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 android.text;
+
+import android.annotation.ColorInt;
+import android.graphics.Paint;
+
+public class TextPaint extends Paint {
+
+ // Special value 0 means no background paint
+ @ColorInt
+ public int bgColor;
+ public int baselineShift;
+ @ColorInt
+ public int linkColor;
+ public int[] drawableState;
+ public float density = 1.0f;
+ @ColorInt
+ public int underlineColor = 0;
+
+ public float underlineThickness;
+
+ public TextPaint() {
+ super();
+ }
+
+ public TextPaint(int flags) {
+ super(flags);
+ }
+
+ public TextPaint(Paint p) {
+ super(p);
+ }
+
+ public void set(TextPaint tp) {
+ super.set(tp);
+
+ bgColor = tp.bgColor;
+ baselineShift = tp.baselineShift;
+ linkColor = tp.linkColor;
+ drawableState = tp.drawableState;
+ density = tp.density;
+ underlineColor = tp.underlineColor;
+ underlineThickness = tp.underlineThickness;
+ }
+
+ public void setUnderlineText(int color, float thickness) {
+ underlineColor = color;
+ underlineThickness = thickness;
+ }
+
+ @Override
+ public float getUnderlineThickness() {
+ if (underlineColor != 0) { // Return custom thickness only if underline color is set.
+ return underlineThickness;
+ } else {
+ return super.getUnderlineThickness();
+ }
+ }
+}
diff --git a/AndroidCompat/src/main/java/com/android/internal/util/ArrayUtils.java b/AndroidCompat/src/main/java/com/android/internal/util/ArrayUtils.java
index 3e4e8862..822513e2 100644
--- a/AndroidCompat/src/main/java/com/android/internal/util/ArrayUtils.java
+++ b/AndroidCompat/src/main/java/com/android/internal/util/ArrayUtils.java
@@ -14,13 +14,13 @@
* limitations under the License.
*/
package com.android.internal.util;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.ArraySet;
-import libcore.util.EmptyArray;
-
import java.lang.reflect.Array;
import java.util.*;
+import libcore.util.EmptyArray;
+import android.annotation.NonNull;
+
/**
* ArrayUtils contains some methods that you can call to find out
* the most efficient increments by which to grow arrays.
@@ -50,6 +50,10 @@ public class ArrayUtils {
public static Object[] newUnpaddedObjectArray(int minLen) {
return new Object[minLen];
}
+ @SuppressWarnings("unchecked")
+ public static T[] newUnpaddedArray(Class clazz, int minLen) {
+ return (T[])Array.newInstance(clazz, minLen);
+ }
/**
* Checks if the beginnings of two byte arrays are equal.
*
@@ -468,4 +472,4 @@ public class ArrayUtils {
}
return size - leftIdx;
}
-}
\ No newline at end of file
+}
diff --git a/AndroidCompat/src/main/java/com/android/internal/util/GrowingArrayUtils.java b/AndroidCompat/src/main/java/com/android/internal/util/GrowingArrayUtils.java
new file mode 100644
index 00000000..f4b1cab6
--- /dev/null
+++ b/AndroidCompat/src/main/java/com/android/internal/util/GrowingArrayUtils.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed 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 com.android.internal.util;
+
+public final class GrowingArrayUtils {
+
+ public static T[] append(T[] array, int currentSize, T element) {
+ assert currentSize <= array.length;
+
+ if (currentSize + 1 > array.length) {
+ @SuppressWarnings("unchecked")
+ T[] newArray = ArrayUtils.newUnpaddedArray(
+ (Class) array.getClass().getComponentType(), growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+
+ public static int[] append(int[] array, int currentSize, int element) {
+ assert currentSize <= array.length;
+
+ if (currentSize + 1 > array.length) {
+ int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+
+ public static long[] append(long[] array, int currentSize, long element) {
+ assert currentSize <= array.length;
+
+ if (currentSize + 1 > array.length) {
+ long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+
+ public static boolean[] append(boolean[] array, int currentSize, boolean element) {
+ assert currentSize <= array.length;
+
+ if (currentSize + 1 > array.length) {
+ boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+
+ public static float[] append(float[] array, int currentSize, float element) {
+ assert currentSize <= array.length;
+
+ if (currentSize + 1 > array.length) {
+ float[] newArray = ArrayUtils.newUnpaddedFloatArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+
+ public static T[] insert(T[] array, int currentSize, int index, T element) {
+ assert currentSize <= array.length;
+
+ if (currentSize + 1 <= array.length) {
+ System.arraycopy(array, index, array, index + 1, currentSize - index);
+ array[index] = element;
+ return array;
+ }
+
+ @SuppressWarnings("unchecked")
+ T[] newArray = ArrayUtils.newUnpaddedArray((Class)array.getClass().getComponentType(),
+ growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, index);
+ newArray[index] = element;
+ System.arraycopy(array, index, newArray, index + 1, array.length - index);
+ return newArray;
+ }
+
+ public static int[] insert(int[] array, int currentSize, int index, int element) {
+ assert currentSize <= array.length;
+
+ if (currentSize + 1 <= array.length) {
+ System.arraycopy(array, index, array, index + 1, currentSize - index);
+ array[index] = element;
+ return array;
+ }
+
+ int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, index);
+ newArray[index] = element;
+ System.arraycopy(array, index, newArray, index + 1, array.length - index);
+ return newArray;
+ }
+
+ public static long[] insert(long[] array, int currentSize, int index, long element) {
+ assert currentSize <= array.length;
+
+ if (currentSize + 1 <= array.length) {
+ System.arraycopy(array, index, array, index + 1, currentSize - index);
+ array[index] = element;
+ return array;
+ }
+
+ long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, index);
+ newArray[index] = element;
+ System.arraycopy(array, index, newArray, index + 1, array.length - index);
+ return newArray;
+ }
+
+ public static boolean[] insert(boolean[] array, int currentSize, int index, boolean element) {
+ assert currentSize <= array.length;
+
+ if (currentSize + 1 <= array.length) {
+ System.arraycopy(array, index, array, index + 1, currentSize - index);
+ array[index] = element;
+ return array;
+ }
+
+ boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, index);
+ newArray[index] = element;
+ System.arraycopy(array, index, newArray, index + 1, array.length - index);
+ return newArray;
+ }
+
+ public static int growSize(int currentSize) {
+ return currentSize <= 4 ? 8 : currentSize * 2;
+ }
+
+ // Uninstantiable
+ private GrowingArrayUtils() {}
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 16af2491..687d5f5e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -136,6 +136,8 @@ twelvemonkeys-imageio-metadata = { module = "com.twelvemonkeys.imageio:imageio-m
twelvemonkeys-imageio-jpeg = { module = "com.twelvemonkeys.imageio:imageio-jpeg", version.ref = "twelvemonkeys" }
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
+sejda-webp = "com.github.usefulness:webp-imageio:0.10.0"
+
# Testing
mockk = "io.mockk:mockk:1.14.2"