diff options
author | 2014-07-08 17:13:08 -0700 | |
---|---|---|
committer | 2014-07-14 15:01:27 -0700 | |
commit | 47cd8e921db73e894f94ec4729ade90da50996f5 (patch) | |
tree | dc087c5a19fec4c564ef73f6d487f1d72cf1cdb4 | |
parent | 5028fb035794c207698e52b276c54de109dd5022 (diff) |
Implement outline support for nine patches
b/15856895
Nine patches now have outline round rect metadata stored as optional
png tags. aapt generates these automatically by inspecting the bitmap
pixels to estimate outline bounds and round rect radius, based on
opacity.
Change-Id: I226e328a97873010d9e1adb797ac48f93a31183c
-rw-r--r-- | core/jni/android/graphics/BitmapFactory.cpp | 62 | ||||
-rw-r--r-- | core/jni/android/graphics/Graphics.cpp | 12 | ||||
-rw-r--r-- | core/jni/android/graphics/GraphicsJNI.h | 6 | ||||
-rw-r--r-- | core/jni/android/graphics/NinePatchPeeker.cpp | 24 | ||||
-rw-r--r-- | core/jni/android/graphics/NinePatchPeeker.h | 34 | ||||
-rw-r--r-- | graphics/java/android/graphics/Bitmap.java | 20 | ||||
-rw-r--r-- | graphics/java/android/graphics/NinePatch.java | 34 | ||||
-rw-r--r-- | graphics/java/android/graphics/Rect.java | 13 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/NinePatchDrawable.java | 29 | ||||
-rw-r--r-- | tools/aapt/Images.cpp | 134 |
10 files changed, 279 insertions, 89 deletions
diff --git a/core/jni/android/graphics/BitmapFactory.cpp b/core/jni/android/graphics/BitmapFactory.cpp index b8fcff672447..a890eb4b5bf9 100644 --- a/core/jni/android/graphics/BitmapFactory.cpp +++ b/core/jni/android/graphics/BitmapFactory.cpp @@ -39,14 +39,12 @@ jfieldID gOptions_heightFieldID; jfieldID gOptions_mimeFieldID; jfieldID gOptions_mCancelID; jfieldID gOptions_bitmapFieldID; + jfieldID gBitmap_nativeBitmapFieldID; -jfieldID gBitmap_layoutBoundsFieldID; +jfieldID gBitmap_ninePatchInsetsFieldID; -#if 0 - #define TRACE_BITMAP(code) code -#else - #define TRACE_BITMAP(code) -#endif +jclass gInsetStruct_class; +jmethodID gInsetStruct_constructorMethodID; using namespace android; @@ -195,8 +193,7 @@ private: const unsigned int mSize; }; -static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, - jobject options) { +static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) { int sampleSize = 1; @@ -331,12 +328,12 @@ static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding } jbyteArray ninePatchChunk = NULL; - if (peeker.fPatch != NULL) { + if (peeker.mPatch != NULL) { if (willScale) { - scaleNinePatchChunk(peeker.fPatch, scale); + scaleNinePatchChunk(peeker.mPatch, scale); } - size_t ninePatchArraySize = peeker.fPatch->serializedSize(); + size_t ninePatchArraySize = peeker.mPatch->serializedSize(); ninePatchChunk = env->NewByteArray(ninePatchArraySize); if (ninePatchChunk == NULL) { return nullObjectReturn("ninePatchChunk == null"); @@ -347,28 +344,18 @@ static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding return nullObjectReturn("primitive array == null"); } - memcpy(array, peeker.fPatch, peeker.fPatchSize); + memcpy(array, peeker.mPatch, peeker.mPatchSize); env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0); } - jintArray layoutBounds = NULL; - if (peeker.fLayoutBounds != NULL) { - layoutBounds = env->NewIntArray(4); - if (layoutBounds == NULL) { - return nullObjectReturn("layoutBounds == null"); - } - - jint scaledBounds[4]; - if (willScale) { - for (int i=0; i<4; i++) { - scaledBounds[i] = (jint)((((jint*)peeker.fLayoutBounds)[i]*scale) + .5f); - } - } else { - memcpy(scaledBounds, (jint*)peeker.fLayoutBounds, sizeof(scaledBounds)); - } - env->SetIntArrayRegion(layoutBounds, 0, 4, scaledBounds); + jobject ninePatchInsets = NULL; + if (peeker.mHasInsets) { + ninePatchInsets = env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID, + peeker.mOpticalInsets[0], peeker.mOpticalInsets[1], peeker.mOpticalInsets[2], peeker.mOpticalInsets[3], + peeker.mOutlineInsets[0], peeker.mOutlineInsets[1], peeker.mOutlineInsets[2], peeker.mOutlineInsets[3], + peeker.mOutlineRadius, peeker.mOutlineFilled, scale); if (javaBitmap != NULL) { - env->SetObjectField(javaBitmap, gBitmap_layoutBoundsFieldID, layoutBounds); + env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID, ninePatchInsets); } } @@ -409,10 +396,10 @@ static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding } if (padding) { - if (peeker.fPatch != NULL) { + if (peeker.mPatch != NULL) { GraphicsJNI::set_jrect(env, padding, - peeker.fPatch->paddingLeft, peeker.fPatch->paddingTop, - peeker.fPatch->paddingRight, peeker.fPatch->paddingBottom); + peeker.mPatch->paddingLeft, peeker.mPatch->paddingTop, + peeker.mPatch->paddingRight, peeker.mPatch->paddingBottom); } else { GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1); } @@ -446,7 +433,7 @@ static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding // now create the java bitmap return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(), - bitmapCreateFlags, ninePatchChunk, layoutBounds, -1); + bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } // Need to buffer enough input to be able to rewind as much as might be read by a decoder @@ -576,7 +563,7 @@ int register_android_graphics_BitmapFactory(JNIEnv* env) { jclass options_class = env->FindClass("android/graphics/BitmapFactory$Options"); SkASSERT(options_class); gOptions_bitmapFieldID = getFieldIDCheck(env, options_class, "inBitmap", - "Landroid/graphics/Bitmap;"); + "Landroid/graphics/Bitmap;"); gOptions_justBoundsFieldID = getFieldIDCheck(env, options_class, "inJustDecodeBounds", "Z"); gOptions_sampleSizeFieldID = getFieldIDCheck(env, options_class, "inSampleSize", "I"); gOptions_configFieldID = getFieldIDCheck(env, options_class, "inPreferredConfig", @@ -598,7 +585,12 @@ int register_android_graphics_BitmapFactory(JNIEnv* env) { jclass bitmap_class = env->FindClass("android/graphics/Bitmap"); SkASSERT(bitmap_class); gBitmap_nativeBitmapFieldID = getFieldIDCheck(env, bitmap_class, "mNativeBitmap", "J"); - gBitmap_layoutBoundsFieldID = getFieldIDCheck(env, bitmap_class, "mOpticalInsets", "[I"); + gBitmap_ninePatchInsetsFieldID = getFieldIDCheck(env, bitmap_class, "mNinePatchInsets", + "Landroid/graphics/NinePatch$InsetStruct;"); + + gInsetStruct_class = (jclass) env->NewGlobalRef(env->FindClass("android/graphics/NinePatch$InsetStruct")); + gInsetStruct_constructorMethodID = env->GetMethodID(gInsetStruct_class, "<init>", "(IIIIIIIIFZF)V"); + int ret = AndroidRuntime::registerNativeMethods(env, "android/graphics/BitmapFactory$Options", gOptionsMethods, diff --git a/core/jni/android/graphics/Graphics.cpp b/core/jni/android/graphics/Graphics.cpp index 9177696957b4..5cc2b9548f18 100644 --- a/core/jni/android/graphics/Graphics.cpp +++ b/core/jni/android/graphics/Graphics.cpp @@ -415,7 +415,7 @@ static void assert_premultiplied(const SkBitmap& bitmap, bool isPremultiplied) { } jobject GraphicsJNI::createBitmap(JNIEnv* env, SkBitmap* bitmap, jbyteArray buffer, - int bitmapCreateFlags, jbyteArray ninepatch, jintArray layoutbounds, int density) + int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, int density) { SkASSERT(bitmap); SkASSERT(bitmap->pixelRef()); @@ -429,17 +429,11 @@ jobject GraphicsJNI::createBitmap(JNIEnv* env, SkBitmap* bitmap, jbyteArray buff jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, reinterpret_cast<jlong>(bitmap), buffer, bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied, - ninepatch, layoutbounds); + ninePatchChunk, ninePatchInsets); hasException(env); // For the side effect of logging. return obj; } -jobject GraphicsJNI::createBitmap(JNIEnv* env, SkBitmap* bitmap, int bitmapCreateFlags, - jbyteArray ninepatch, int density) -{ - return createBitmap(env, bitmap, NULL, bitmapCreateFlags, ninepatch, NULL, density); -} - void GraphicsJNI::reinitBitmap(JNIEnv* env, jobject javaBitmap, SkBitmap* bitmap, bool isPremultiplied) { @@ -720,7 +714,7 @@ int register_android_graphics_Graphics(JNIEnv* env) gBitmap_class = make_globalref(env, "android/graphics/Bitmap"); gBitmap_nativeInstanceID = getFieldIDCheck(env, gBitmap_class, "mNativeBitmap", "J"); - gBitmap_constructorMethodID = env->GetMethodID(gBitmap_class, "<init>", "(J[BIIIZZ[B[I)V"); + gBitmap_constructorMethodID = env->GetMethodID(gBitmap_class, "<init>", "(J[BIIIZZ[BLandroid/graphics/NinePatch$InsetStruct;)V"); gBitmap_reinitMethodID = env->GetMethodID(gBitmap_class, "reinit", "(IIZ)V"); gBitmap_getAllocationByteCountMethodID = env->GetMethodID(gBitmap_class, "getAllocationByteCount", "()I"); gBitmapRegionDecoder_class = make_globalref(env, "android/graphics/BitmapRegionDecoder"); diff --git a/core/jni/android/graphics/GraphicsJNI.h b/core/jni/android/graphics/GraphicsJNI.h index a03391d5818f..8150edfb16ff 100644 --- a/core/jni/android/graphics/GraphicsJNI.h +++ b/core/jni/android/graphics/GraphicsJNI.h @@ -76,10 +76,12 @@ public: bitmap's SkAlphaType must already be in sync with bitmapCreateFlags. */ static jobject createBitmap(JNIEnv* env, SkBitmap* bitmap, jbyteArray buffer, - int bitmapCreateFlags, jbyteArray ninepatch, jintArray layoutbounds, int density = -1); + int bitmapCreateFlags, jbyteArray ninePatch, jobject ninePatchInsets, int density = -1); static jobject createBitmap(JNIEnv* env, SkBitmap* bitmap, int bitmapCreateFlags, - jbyteArray ninepatch, int density = -1); + jbyteArray ninePatch, int density = -1) { + return createBitmap(env, bitmap, NULL, bitmapCreateFlags, ninePatch, NULL, density); + } /** Reinitialize a bitmap. bitmap must already have its SkAlphaType set in sync with isPremultiplied diff --git a/core/jni/android/graphics/NinePatchPeeker.cpp b/core/jni/android/graphics/NinePatchPeeker.cpp index 5daa1ad6e2ef..ea5193bde0ed 100644 --- a/core/jni/android/graphics/NinePatchPeeker.cpp +++ b/core/jni/android/graphics/NinePatchPeeker.cpp @@ -21,7 +21,7 @@ using namespace android; bool NinePatchPeeker::peek(const char tag[], const void* data, size_t length) { - if (strcmp("npTc", tag) == 0 && length >= sizeof(Res_png_9patch)) { + if (!strcmp("npTc", tag) && length >= sizeof(Res_png_9patch)) { Res_png_9patch* patch = (Res_png_9patch*) data; size_t patchSize = patch->serializedSize(); assert(length == patchSize); @@ -30,12 +30,9 @@ bool NinePatchPeeker::peek(const char tag[], const void* data, size_t length) { memcpy(patchNew, patch, patchSize); Res_png_9patch::deserialize(patchNew); patchNew->fileToDevice(); - free(fPatch); - fPatch = patchNew; - fPatchSize = patchSize; - //printf("9patch: (%d,%d)-(%d,%d)\n", - // fPatch.sizeLeft, fPatch.sizeTop, - // fPatch.sizeRight, fPatch.sizeBottom); + free(mPatch); + mPatch = patchNew; + mPatchSize = patchSize; // now update our host to force index or 32bit config // 'cause we don't want 565 predithered, since as a 9patch, we know @@ -47,10 +44,15 @@ bool NinePatchPeeker::peek(const char tag[], const void* data, size_t length) { table.fPrefFor_8bpc_NoAlpha_src = SkBitmap::kARGB_8888_Config; table.fPrefFor_8bpc_YesAlpha_src = SkBitmap::kARGB_8888_Config; - fHost->setPrefConfigTable(table); - } else if (strcmp("npLb", tag) == 0 && length == sizeof(int) * 4) { - fLayoutBounds = new int[4]; - memcpy(fLayoutBounds, data, sizeof(int) * 4); + mHost->setPrefConfigTable(table); + } else if (!strcmp("npLb", tag) && length == sizeof(int32_t) * 4) { + mHasInsets = true; + memcpy(&mOpticalInsets, data, sizeof(int32_t) * 4); + } else if (!strcmp("npOl", tag) && length == 24) { // 4 int32_ts, 1 float, 1 int32_t sized bool + mHasInsets = true; + memcpy(&mOutlineInsets, data, sizeof(int32_t) * 4); + mOutlineRadius = ((const float*)data)[4]; + mOutlineFilled = ((const int32_t*)data)[5] & 0x01; } return true; // keep on decoding } diff --git a/core/jni/android/graphics/NinePatchPeeker.h b/core/jni/android/graphics/NinePatchPeeker.h index 204386273a5f..8d3e6cf57884 100644 --- a/core/jni/android/graphics/NinePatchPeeker.h +++ b/core/jni/android/graphics/NinePatchPeeker.h @@ -23,26 +23,34 @@ using namespace android; class NinePatchPeeker : public SkImageDecoder::Peeker { - SkImageDecoder* fHost; +private: + // the host lives longer than we do, so a raw ptr is safe + SkImageDecoder* mHost; public: - NinePatchPeeker(SkImageDecoder* host) { - // the host lives longer than we do, so a raw ptr is safe - fHost = host; - fPatch = NULL; - fPatchSize = 0; - fLayoutBounds = NULL; + NinePatchPeeker(SkImageDecoder* host) + : mHost(host) + , mPatch(NULL) + , mPatchSize(0) + , mHasInsets(false) + , mOutlineRadius(0) + , mOutlineFilled(false) { + memset(mOpticalInsets, 0, 4 * sizeof(int32_t)); + memset(mOutlineInsets, 0, 4 * sizeof(int32_t)); } ~NinePatchPeeker() { - free(fPatch); - delete fLayoutBounds; + free(mPatch); } - Res_png_9patch* fPatch; - size_t fPatchSize; - int *fLayoutBounds; - virtual bool peek(const char tag[], const void* data, size_t length); + + Res_png_9patch* mPatch; + size_t mPatchSize; + bool mHasInsets; + int32_t mOpticalInsets[4]; + int32_t mOutlineInsets[4]; + float mOutlineRadius; + bool mOutlineFilled; }; #endif // NinePatchPeeker_h diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 9561ac494c25..4b00e22252b4 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -73,8 +73,8 @@ public final class Bitmap implements Parcelable { */ private boolean mRequestPremultiplied; - private byte[] mNinePatchChunk; // may be null - private int[] mOpticalInsets; // may be null + private byte[] mNinePatchChunk; // may be null + private NinePatch.InsetStruct mNinePatchInsets; // may be null private int mWidth; private int mHeight; private boolean mRecycled; @@ -111,7 +111,7 @@ public final class Bitmap implements Parcelable { @SuppressWarnings({"UnusedDeclaration"}) // called from JNI Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, - byte[] ninePatchChunk, int[] opticalInsets) { + byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) { if (nativeBitmap == 0) { throw new RuntimeException("internal error: native bitmap is 0"); } @@ -126,7 +126,7 @@ public final class Bitmap implements Parcelable { mFinalizer = new BitmapFinalizer(nativeBitmap); mNinePatchChunk = ninePatchChunk; - mOpticalInsets = opticalInsets; + mNinePatchInsets = ninePatchInsets; if (density >= 0) { mDensity = density; } @@ -946,16 +946,18 @@ public final class Bitmap implements Parcelable { * @hide */ public void getOpticalInsets(@NonNull Rect outInsets) { - if (mOpticalInsets == null) { + if (mNinePatchInsets == null) { outInsets.setEmpty(); } else { - outInsets.left = mOpticalInsets[0]; - outInsets.top = mOpticalInsets[1]; - outInsets.right = mOpticalInsets[2]; - outInsets.bottom = mOpticalInsets[3]; + outInsets.set(mNinePatchInsets.opticalRect); } } + /** @hide */ + public NinePatch.InsetStruct getNinePatchInsets() { + return mNinePatchInsets; + } + /** * Specifies the known formats a bitmap can be compressed into */ diff --git a/graphics/java/android/graphics/NinePatch.java b/graphics/java/android/graphics/NinePatch.java index befac924df90..335bce07c8c1 100644 --- a/graphics/java/android/graphics/NinePatch.java +++ b/graphics/java/android/graphics/NinePatch.java @@ -16,7 +16,6 @@ package android.graphics; - /** * The NinePatch class permits drawing a bitmap in nine or more sections. * Essentially, it allows the creation of custom graphics that will scale the @@ -32,6 +31,39 @@ package android.graphics; * </p> */ public class NinePatch { + /** + * Struct of inset information attached to a 9 patch bitmap. + * + * Present on a 9 patch bitmap if it optical insets were manually included, + * or if outline insets were automatically included by aapt. + * + * @hide + */ + public static class InsetStruct { + @SuppressWarnings({"UnusedDeclaration"}) // called from JNI + InsetStruct(int opticalLeft, int opticalTop, int opticalRight, int opticalBottom, + int outlineLeft, int outlineTop, int outlineRight, int outlineBottom, + float outlineRadius, boolean outlineFilled, float decodeScale) { + opticalRect = new Rect(opticalLeft, opticalTop, opticalRight, opticalBottom); + outlineRect = new Rect(outlineLeft, outlineTop, outlineRight, outlineBottom); + + if (decodeScale != 1.0f) { + // if bitmap was scaled when decoded, scale the insets from the metadata values + opticalRect.scale(decodeScale); + + // round inward while scaling outline, as the outline should always be conservative + outlineRect.scaleRoundIn(decodeScale); + } + this.outlineRadius = outlineRadius * decodeScale; + this.outlineFilled = outlineFilled; + } + + public final Rect opticalRect; + public final Rect outlineRect; + public final float outlineRadius; + public final boolean outlineFilled; + } + private final Bitmap mBitmap; /** diff --git a/graphics/java/android/graphics/Rect.java b/graphics/java/android/graphics/Rect.java index 437d2f485d44..a9a8f37c6dae 100644 --- a/graphics/java/android/graphics/Rect.java +++ b/graphics/java/android/graphics/Rect.java @@ -609,4 +609,17 @@ public final class Rect implements Parcelable { bottom = (int) (bottom * scale + 0.5f); } } + + /** + * Scales up the rect by the given scale, rounding values toward the inside. + * @hide + */ + public void scaleRoundIn(float scale) { + if (scale != 1.0f) { + left = (int) Math.ceil(left * scale); + top = (int) Math.ceil(top * scale); + right = (int) Math.floor(right * scale); + bottom = (int) Math.floor(bottom * scale); + } + } } diff --git a/graphics/java/android/graphics/drawable/NinePatchDrawable.java b/graphics/java/android/graphics/drawable/NinePatchDrawable.java index 24bbf7c10e8f..c0110c98ba7d 100644 --- a/graphics/java/android/graphics/drawable/NinePatchDrawable.java +++ b/graphics/java/android/graphics/drawable/NinePatchDrawable.java @@ -16,6 +16,7 @@ package android.graphics.drawable; +import android.annotation.NonNull; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.Resources.Theme; @@ -26,6 +27,7 @@ import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Insets; import android.graphics.NinePatch; +import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; @@ -281,14 +283,35 @@ public class NinePatchDrawable extends Drawable { return false; } + @Override + public boolean getOutline(@NonNull Outline outline) { + final Rect bounds = getBounds(); + if (bounds.isEmpty()) return false; + + if (mNinePatchState != null) { + NinePatch.InsetStruct insets = mNinePatchState.getBitmap().getNinePatchInsets(); + if (insets != null) { + final Rect outlineInsets = insets.outlineRect; + outline.setRoundRect(bounds.left + outlineInsets.left, + bounds.top + outlineInsets.top, + bounds.right - outlineInsets.right, + bounds.bottom - outlineInsets.bottom, + insets.outlineRadius); + outline.setFilled(insets.outlineFilled); + return true; + } + } + return super.getOutline(outline); + } + /** * @hide */ @Override public Insets getOpticalInsets() { if (needsMirroring()) { - return Insets.of(mOpticalInsets.right, mOpticalInsets.top, mOpticalInsets.right, - mOpticalInsets.bottom); + return Insets.of(mOpticalInsets.right, mOpticalInsets.top, + mOpticalInsets.left, mOpticalInsets.bottom); } else { return mOpticalInsets; } @@ -574,7 +597,7 @@ public class NinePatchDrawable extends Drawable { } NinePatchState(NinePatch ninePatch, Rect padding) { - this(ninePatch, padding, new Rect(), DEFAULT_DITHER, false); + this(ninePatch, padding, null, DEFAULT_DITHER, false); } NinePatchState(NinePatch ninePatch, Rect padding, Rect opticalInsets) { diff --git a/tools/aapt/Images.cpp b/tools/aapt/Images.cpp index 12f5b92ab3fa..28de933a315e 100644 --- a/tools/aapt/Images.cpp +++ b/tools/aapt/Images.cpp @@ -77,6 +77,14 @@ struct image_info int32_t layoutBoundsRight; int32_t layoutBoundsBottom; + // Round rect outline description + int32_t outlineInsetsLeft; + int32_t outlineInsetsTop; + int32_t outlineInsetsRight; + int32_t outlineInsetsBottom; + float outlineRadius; + bool outlineFilled; + png_uint_32 allocHeight; png_bytepp allocRows; }; @@ -397,6 +405,98 @@ static status_t get_vertical_layout_bounds_ticks( return NO_ERROR; } +static void find_max_opacity(png_byte** rows, + int startX, int startY, int endX, int endY, int dX, int dY, + int* out_inset) +{ + bool opaque_within_inset = true; + unsigned char max_opacity = 0; + int inset = 0; + *out_inset = 0; + for (int x = startX, y = startY; x != endX && y != endY; x += dX, y += dY, inset++) { + png_byte* color = rows[y] + x * 4; + unsigned char opacity = color[3]; + if (opacity > max_opacity) { + max_opacity = opacity; + *out_inset = inset; + } + if (opacity == 0xff) return; + } +} + +static bool is_opaque_over_row(png_byte* row, int startX, int endX) +{ + for (int x = startX; x < endX; x++) { + png_byte* color = row + x * 4; + if (color[3] != 0xff) return false; + } + return true; +} + +static bool is_opaque_over_col(png_byte** rows, int offsetX, int startY, int endY) +{ + for (int y = startY; y < endY; y++) { + png_byte* color = rows[y] + offsetX * 4; + if (color[3] != 0xff) return false; + } + return true; +} + +static void get_outline(image_info* image) +{ + int midX = image->width / 2; + int midY = image->height / 2; + int endX = image->width - 2; + int endY = image->height - 2; + + // find left and right extent of nine patch content on center row + if (image->width > 4) { + find_max_opacity(image->rows, 1, midY, midX, -1, 1, 0, &image->outlineInsetsLeft); + find_max_opacity(image->rows, endX, midY, midX, -1, -1, 0, &image->outlineInsetsRight); + } else { + image->outlineInsetsLeft = 0; + image->outlineInsetsRight = 0; + } + + // find top and bottom extent of nine patch content on center column + if (image->height > 4) { + find_max_opacity(image->rows, midX, 1, -1, midY, 0, 1, &image->outlineInsetsTop); + find_max_opacity(image->rows, midX, endY, -1, midY, 0, -1, &image->outlineInsetsBottom); + } else { + image->outlineInsetsTop = 0; + image->outlineInsetsBottom = 0; + } + + int innerStartX = 1 + image->outlineInsetsLeft; + int innerStartY = 1 + image->outlineInsetsTop; + int innerEndX = endX - image->outlineInsetsRight; + int innerEndY = endY - image->outlineInsetsBottom; + int innerMidX = (innerEndX + innerStartX) / 2; + int innerMidY = (innerEndY + innerStartY) / 2; + + // assuming the image is a round rect, compute the radius by marching + // diagonally from the top left corner towards the center + image->outlineFilled = is_opaque_over_row(image->rows[innerMidY], innerStartX, innerEndX) + && is_opaque_over_col(image->rows, innerMidX, innerStartY, innerStartY); + + int diagonalInset = 0; + find_max_opacity(image->rows, innerStartX, innerStartY, innerMidX, innerMidY, 1, 1, + &diagonalInset); + + // Determine source radius based upon inset + // radius = 1 / (sqrt(2) - 1) * inset + image->outlineRadius = 2.4142f * diagonalInset; + + NOISY(printf("outline insets %d %d %d %d, rad %f, filled %d\n", + image->outlineFilled, + image->outlineInsetsLeft, + image->outlineInsetsTop, + image->outlineInsetsRight, + image->outlineInsetsBottom, + image->outlineRadius, + image->outlineFilled)); +} + static uint32_t get_color( png_bytepp rows, int left, int top, int right, int bottom) @@ -571,6 +671,9 @@ static status_t do_9patch(const char* imageName, image_info* image) image->layoutBoundsRight, image->layoutBoundsBottom)); } + // use opacity of pixels to estimate the round rect outline + get_outline(image); + // If padding is not yet specified, take values from size. if (image->info9Patch.paddingLeft < 0) { image->info9Patch.paddingLeft = xDivs[0]; @@ -966,9 +1069,10 @@ static void write_png(const char* imageName, int bit_depth, interlace_type, compression_type; int i; - png_unknown_chunk unknowns[2]; + png_unknown_chunk unknowns[3]; unknowns[0].data = NULL; unknowns[1].data = NULL; + unknowns[2].data = NULL; png_bytepp outRows = (png_bytepp) malloc((int) imageInfo.height * sizeof(png_bytep)); if (outRows == (png_bytepp) 0) { @@ -1038,12 +1142,17 @@ static void write_png(const char* imageName, } if (imageInfo.is9Patch) { - int chunk_count = 1 + (imageInfo.haveLayoutBounds ? 1 : 0); - int p_index = imageInfo.haveLayoutBounds ? 1 : 0; - int b_index = 0; + int chunk_count = 2 + (imageInfo.haveLayoutBounds ? 1 : 0); + int p_index = imageInfo.haveLayoutBounds ? 2 : 1; + int b_index = 1; + int o_index = 0; + + // Chunks ordered thusly because older platforms depend on the base 9 patch data being last png_byte *chunk_names = imageInfo.haveLayoutBounds - ? (png_byte*)"npLb\0npTc\0" - : (png_byte*)"npTc"; + ? (png_byte*)"npOl\0npLb\0npTc\0" + : (png_byte*)"npOl\0npTc"; + + // base 9 patch data NOISY(printf("Adding 9-patch info...\n")); strcpy((char*)unknowns[p_index].name, "npTc"); unknowns[p_index].data = (png_byte*)imageInfo.serialize9patch(); @@ -1051,6 +1160,18 @@ static void write_png(const char* imageName, // TODO: remove the check below when everything works checkNinePatchSerialization(&imageInfo.info9Patch, unknowns[p_index].data); + // automatically generated 9 patch outline data + int chunk_size = sizeof(png_uint_32) * 6; + strcpy((char*)unknowns[o_index].name, "npOl"); + unknowns[o_index].data = (png_byte*) calloc(chunk_size, 1); + png_byte outputData[chunk_size]; + memcpy(&outputData, &imageInfo.outlineInsetsLeft, 4 * sizeof(png_uint_32)); + ((float*) outputData)[4] = imageInfo.outlineRadius; + ((png_uint_32*) outputData)[5] = imageInfo.outlineFilled ? 1 : 0; + memcpy(unknowns[o_index].data, &outputData, chunk_size); + unknowns[o_index].size = chunk_size; + + // optional optical inset / layout bounds data if (imageInfo.haveLayoutBounds) { int chunk_size = sizeof(png_uint_32) * 4; strcpy((char*)unknowns[b_index].name, "npLb"); @@ -1099,6 +1220,7 @@ static void write_png(const char* imageName, free(outRows); free(unknowns[0].data); free(unknowns[1].data); + free(unknowns[2].data); png_get_IHDR(write_ptr, write_info, &width, &height, &bit_depth, &color_type, &interlace_type, |