/* * Copyright (C) 2016-2018 Muhammad Tayyab Akram * * 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.mta.tehreer.graphics; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.util.Log; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import com.mta.tehreer.collections.FloatList; import com.mta.tehreer.collections.IntList; import com.mta.tehreer.collections.PointList; import com.mta.tehreer.sfnt.WritingDirection; import static com.mta.tehreer.internal.util.Preconditions.checkArgument; import static com.mta.tehreer.internal.util.Preconditions.checkNotNull; /** * The <code>Renderer</code> class represents a generic glyph renderer. It can be used to generate * glyph paths, measure their bounding boxes and draw them on a <code>Canvas</code> object. */ public class Renderer { private static final String TAG = Renderer.class.getSimpleName(); private @NonNull GlyphStrike mGlyphStrike = new GlyphStrike(); private int mGlyphLineRadius; private int mGlyphLineCap; private int mGlyphLineJoin; private int mGlyphMiterLimit; private @NonNull Paint mPaint = new Paint(); private boolean mShouldRender = false; private boolean mShadowLayerSynced = true; private @ColorInt int mFillColor = Color.BLACK; private @NonNull RenderingStyle mRenderingStyle = RenderingStyle.FILL; private @NonNull WritingDirection mWritingDirection = WritingDirection.LEFT_TO_RIGHT; private Typeface mTypeface = null; private float mTypeSize = 16.0f; private float mSlantAngle = 0.0f; private float mScaleX = 1.0f; private float mScaleY = 1.0f; private @ColorInt int mStrokeColor = Color.BLACK; private float mStrokeWidth; private StrokeCap mStrokeCap; private StrokeJoin mStrokeJoin; private float mStrokeMiter; private float mShadowRadius = 0.0f; private float mShadowDx = 0.0f; private float mShadowDy = 0.0f; private @ColorInt int mShadowColor = Color.TRANSPARENT; /** * Constructs a renderer object. */ public Renderer() { updatePixelSizes(); updateTransform(); setStrokeWidth(1.0f); setStrokeCap(StrokeCap.BUTT); setStrokeJoin(StrokeJoin.ROUND); setStrokeMiter(1.0f); } private void updatePixelSizes() { int pixelWidth = (int) ((mTypeSize * mScaleX * 64.0f) + 0.5f); int pixelHeight = (int) ((mTypeSize * mScaleY * 64.0f) + 0.5f); // Minimum size supported by Freetype is 64x64. mShouldRender = (pixelWidth >= 64 && pixelHeight >= 64); mGlyphStrike.pixelWidth = pixelWidth; mGlyphStrike.pixelHeight = pixelHeight; } private void updateTransform() { mGlyphStrike.skewX = (int) ((mSlantAngle * 0x10000) + 0.5f); } private void syncShadowLayer() { if (!mShadowLayerSynced) { mShadowLayerSynced = true; mPaint.setShadowLayer(mShadowRadius, mShadowDx, mShadowDy, mShadowColor); } } /** * Returns this renderer's fill color for glyphs. The default value is <code>Color.BLACK</code>. * * @return The fill color of this renderer expressed as ARGB integer. */ public @ColorInt int getFillColor() { return mFillColor; } /** * Sets this renderer's fill color for glyphs. The default value is <code>Color.BLACK</code>. * * @param fillColor The 32-bit value of color expressed as ARGB. */ public void setFillColor(@ColorInt int fillColor) { mFillColor = fillColor; } /** * Returns this renderer's style, used for controlling how glyphs should appear while drawing. * The default value is {@link RenderingStyle#FILL}. * * @return The style setting of this renderer. */ public @NonNull RenderingStyle getRenderingStyle() { return mRenderingStyle; } /** * Sets this renderer's style, used for controlling how glyphs should appear while drawing. The * default value is {@link RenderingStyle#FILL}. * * @param renderingStyle The new style setting for the renderer. */ public void setRenderingStyle(@NonNull RenderingStyle renderingStyle) { checkNotNull(renderingStyle); mRenderingStyle = renderingStyle; } /** * Returns the direction in which the pen will advance after drawing a glyph. The default value * is {@link WritingDirection#LEFT_TO_RIGHT}. * * @return The current writing direction. */ public @NonNull WritingDirection getWritingDirection() { return mWritingDirection; } /** * Sets the direction in which the pen will advance after drawing a glyph. The default value is * {@link WritingDirection#LEFT_TO_RIGHT}. * * @param writingDirection The new writing direction. */ public void setWritingDirection(@NonNull WritingDirection writingDirection) { checkNotNull(writingDirection); mWritingDirection = writingDirection; } /** * Returns this renderer's typeface, used for drawing glyphs. * * @return The typeface of this renderer. */ public Typeface getTypeface() { return mTypeface; } /** * Sets this renderer's typeface, used for drawing glyphs. * * @param typeface The typeface to use for drawing glyphs. */ public void setTypeface(Typeface typeface) { mTypeface = typeface; mGlyphStrike.typeface = typeface; } /** * Returns this renderer's type size, applied on glyphs while drawing. * * @return The type size of this renderer in pixels. */ public float getTypeSize() { return mTypeSize; } /** * Sets this renderer's type size, applied on glyphs while drawing. * * @param typeSize The new type size in pixels. * * @throws IllegalArgumentException if <code>typeSize</code> is negative. */ public void setTypeSize(float typeSize) { checkArgument(typeSize >= 0.0f, "The value of type size is negative"); mTypeSize = typeSize; updatePixelSizes(); } /** * Returns this renderer's slant angle for glyphs. The default value is 0. * * @return The slant angle of this renderer for drawing glyphs. */ public float getSlantAngle() { return mSlantAngle; } /** * Sets this renderer's slant angle for glyphs. The default value is 0. * * @param slantAngle The slant angle for drawing glyphs. */ public void setSlantAngle(float slantAngle) { mSlantAngle = slantAngle; updateTransform(); } /** * Returns this renderer's horizontal scale factor for glyphs. The default value is 1.0. * * @return The horizontal scale factor of this renderer for drawing/measuring glyphs. */ public float getScaleX() { return mScaleX; } /** * Sets this renderer's horizontal scale factor for glyphs. The default value is 1.0. Values * greater than 1.0 will stretch the glyphs wider. Values less than 1.0 will stretch the glyphs * narrower. * * @param scaleX The horizontal scale factor for drawing/measuring glyphs. */ public void setScaleX(float scaleX) { checkArgument(scaleX >= 0.0, "Scale value is negative"); mScaleX = scaleX; updatePixelSizes(); } /** * Returns this renderer's vertical scale factor for glyphs. The default value is 1.0. * * @return The vertical scale factor of this renderer for drawing/measuring glyphs. */ public float getScaleY() { return mScaleY; } /** * Sets this renderer's vertical scale factor for glyphs. The default value is 1.0. Values * greater than 1.0 will stretch the glyphs wider. Values less than 1.0 will stretch the glyphs * narrower. * * @param scaleY The vertical scale factor for drawing/measuring glyphs. */ public void setScaleY(float scaleY) { checkArgument(scaleY >= 0.0, "Scale value is negative"); mScaleY = scaleY; updatePixelSizes(); } /** * Returns this renderer's stroke color for glyphs. The default value is * <code>Color.BLACK</code>. * * @return The stroke color of this renderer expressed as ARGB integer. */ public @ColorInt int getStrokeColor() { return mStrokeColor; } /** * Sets this renderer's stroke color for glyphs. The default value is <code>Color.BLACK</code>. * * @param strokeColor The 32-bit value of color expressed as ARGB. */ public void setStrokeColor(@ColorInt int strokeColor) { mStrokeColor = strokeColor; } /** * Returns this renderer's width for stroking glyphs. * * @return The stroke width of this renderer in pixels. */ public float getStrokeWidth() { return mStrokeWidth; } /** * Sets this renderer's width for stroking glyphs. * * @param strokeWidth The stroke width in pixels. */ public void setStrokeWidth(float strokeWidth) { checkArgument(strokeWidth >= 0.0f, "Stroke width is negative"); mStrokeWidth = strokeWidth; mGlyphLineRadius = (int) ((strokeWidth * 64.0f / 2.0f) + 0.5f); } /** * Returns this renderer's cap, controlling how the start and end of stroked lines and paths are * treated. The default value is {@link StrokeCap#BUTT}. * * @return The stroke cap style of this renderer. */ public @NonNull StrokeCap getStrokeCap() { return mStrokeCap; } /** * Sets this renderer's cap, controlling how the start and end of stroked lines and paths are * treated. The default value is {@link StrokeCap#BUTT}. * * @param strokeCap The new stroke cap style. */ public void setStrokeCap(@NonNull StrokeCap strokeCap) { checkNotNull(strokeCap); mStrokeCap = strokeCap; mGlyphLineCap = strokeCap.value; } /** * Returns this renderer's stroke join type. The default value is {@link StrokeJoin#ROUND}. * * @return The stroke join type of this renderer. */ public @NonNull StrokeJoin getStrokeJoin() { return mStrokeJoin; } /** * Sets this renderer's stroke join type. The default value is {@link StrokeJoin#ROUND}. * * @param strokeJoin The new stroke join type. */ public void setStrokeJoin(@NonNull StrokeJoin strokeJoin) { checkNotNull(strokeJoin); mStrokeJoin = strokeJoin; mGlyphLineJoin = strokeJoin.value; } /** * Returns this renderer's stroke miter value. Used to control the behavior of miter joins when * the joins angle is sharp. * * @return The miter limit of this renderer in pixels. */ public float getStrokeMiter() { return mStrokeMiter; } /** * Sets this renderer's stroke miter value. This is used to control the behavior of miter joins * when the joins angle is sharp. * * @param strokeMiter The value of miter limit in pixels. * * @throws IllegalArgumentException if <code>strokeMiter</code> is less than one. */ public void setStrokeMiter(float strokeMiter) { checkArgument(strokeMiter >= 1.0f, "Stroke miter is less than one"); mStrokeMiter = strokeMiter; mGlyphMiterLimit = (int) ((strokeMiter * 0x10000) + 0.5f); } /** * Returns this renderer's shadow radius, used when drawing glyphs. The default value is zero. * * @return The shadow radius of this renderer in pixels. */ public float getShadowRadius() { return mShadowRadius; } /** * Sets this renderer's shadow radius. The default value is zero. The shadow is disabled if the * radius is set to zero. * * @param shadowRadius The value of shadow radius in pixels. * * @throws IllegalArgumentException if <code>shadowRadius</code> is negative. */ public void setShadowRadius(float shadowRadius) { checkArgument(shadowRadius >= 0.0f, "Shadow radius is negative"); mShadowRadius = shadowRadius; mShadowLayerSynced = false; } /** * Returns this renderer's horizontal shadow offset. * * @return The horizontal shadow offset of this renderer in pixels. */ public float getShadowDx() { return mShadowDx; } /** * Sets this renderer's horizontal shadow offset. * * @param shadowDx The value of horizontal shadow offset in pixels. */ public void setShadowDx(float shadowDx) { mShadowDx = shadowDx; mShadowLayerSynced = false; } /** * Returns this renderer's vertical shadow offset. * * @return The vertical shadow offset of this renderer in pixels. */ public float getShadowDy() { return mShadowDy; } /** * Sets this renderer's vertical shadow offset. * * @param shadowDy The value of vertical shadow offset in pixels. */ public void setShadowDy(float shadowDy) { mShadowDy = shadowDy; mShadowLayerSynced = false; } /** * Returns this renderer's shadow color. * * @return The shadow color of this renderer expressed as ARGB integer. */ public @ColorInt int getShadowColor() { return mShadowColor; } /** * Sets this renderer's shadow color. * * @param shadowColor The 32-bit value of color expressed as ARGB. */ public void setShadowColor(@ColorInt int shadowColor) { mShadowColor = shadowColor; mShadowLayerSynced = false; } private @NonNull Path getGlyphPath(int glyphId) { return GlyphCache.getInstance().getGlyphPath(mGlyphStrike, glyphId); } /** * Generates the path of the specified glyph. * * @param glyphId The ID of glyph whose path is generated. * @return The path of the glyph specified by <code>glyphId</code>. */ public @NonNull Path generatePath(int glyphId) { Path glyphPath = new Path(); glyphPath.addPath(getGlyphPath(glyphId)); return glyphPath; } /** * Generates a cumulative path of specified glyphs. * * @param glyphIds The list containing the glyph IDs. * @param offsets The list containing the glyph offsets. * @param advances The list containing the glyph advances. * @return The cumulative path of specified glyphs. */ public @NonNull Path generatePath(@NonNull IntList glyphIds, @NonNull PointList offsets, @NonNull FloatList advances) { Path cumulativePath = new Path(); float penX = 0.0f; int size = glyphIds.size(); for (int i = 0; i < size; i++) { int glyphId = glyphIds.get(i); float xOffset = offsets.getX(i); float yOffset = offsets.getY(i); float advance = advances.get(i); Path glyphPath = getGlyphPath(glyphId); cumulativePath.addPath(glyphPath, penX + xOffset, yOffset); penX += advance; } return cumulativePath; } private void getBoundingBox(int glyphId, @NonNull RectF boundingBox) { Glyph glyph = GlyphCache.getInstance().getMaskGlyph(mGlyphStrike, glyphId); boundingBox.set(glyph.leftSideBearing(), glyph.topSideBearing(), glyph.rightSideBearing(), glyph.bottomSideBearing()); } /** * Calculates the bounding box of specified glyph. * * @param glyphId The ID of glyph whose bounding box is calculated. * @return A rectangle that tightly encloses the path of the specified glyph. */ public @NonNull RectF computeBoundingBox(int glyphId) { RectF boundingBox = new RectF(); getBoundingBox(glyphId, boundingBox); return boundingBox; } /** * Calculates the bounding box of specified glyphs. * * @param glyphIds The list containing the glyph IDs. * @param offsets The list containing the glyph offsets. * @param advances The list containing the glyph advances. * @return A rectangle that tightly encloses the paths of specified glyphs. */ public @NonNull RectF computeBoundingBox(@NonNull IntList glyphIds, @NonNull PointList offsets, @NonNull FloatList advances) { RectF glyphBBox = new RectF(); RectF cumulativeBBox = new RectF(); float penX = 0.0f; int size = glyphIds.size(); for (int i = 0; i < size; i++) { int glyphId = glyphIds.get(i); float xOffset = offsets.getX(i); float yOffset = offsets.getY(i); float advance = advances.get(i); getBoundingBox(glyphId, glyphBBox); glyphBBox.offset(penX + xOffset, yOffset); cumulativeBBox.union(cumulativeBBox); penX += advance; } return cumulativeBBox; } private void drawGlyphs(@NonNull Canvas canvas, @NonNull IntList glyphIds, @NonNull PointList offsets, @NonNull FloatList advances, boolean strokeMode) { GlyphCache cache = GlyphCache.getInstance(); boolean reverseMode = (mWritingDirection == WritingDirection.RIGHT_TO_LEFT); float penX = 0.0f; int size = glyphIds.size(); for (int i = 0; i < size; i++) { int glyphId = glyphIds.get(i); float xOffset = offsets.getX(i); float yOffset = offsets.getY(i); float advance = advances.get(i); if (reverseMode) { penX -= advance; } Glyph maskGlyph = (!strokeMode ? cache.getMaskGlyph(mGlyphStrike, glyphId) : cache.getMaskGlyph(mGlyphStrike, glyphId, mGlyphLineRadius, mGlyphLineCap, mGlyphLineJoin, mGlyphMiterLimit)); Bitmap maskBitmap = maskGlyph.bitmap(); if (maskBitmap != null) { int left = (int) (penX + xOffset + maskGlyph.leftSideBearing() + 0.5f); int top = (int) (-yOffset - maskGlyph.topSideBearing() + 0.5f); canvas.drawBitmap(maskBitmap, left, top, mPaint); } if (!reverseMode) { penX += advance; } } } /** * Draws specified glyphs onto the given canvas. The shadow will not be drawn if the canvas is * hardware accelerated. * * @param canvas The canvas onto which to draw the glyphs. * @param glyphIds The list containing the glyph IDs. * @param offsets The list containing the glyph offsets. * @param advances The list containing the glyph advances. */ public void drawGlyphs(@NonNull Canvas canvas, @NonNull IntList glyphIds, @NonNull PointList offsets, @NonNull FloatList advances) { if (mShouldRender) { syncShadowLayer(); if (mShadowRadius > 0.0f && canvas.isHardwareAccelerated()) { Log.e(TAG, "Canvas is hardware accelerated, shadow will not be rendered"); } if (mRenderingStyle == RenderingStyle.FILL || mRenderingStyle == RenderingStyle.FILL_STROKE) { mPaint.setColor(mFillColor); drawGlyphs(canvas, glyphIds, offsets, advances, false); } if (mRenderingStyle == RenderingStyle.STROKE || mRenderingStyle == RenderingStyle.FILL_STROKE) { mPaint.setColor(mStrokeColor); drawGlyphs(canvas, glyphIds, offsets, advances, true); } } } }