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"