diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java | 882 |
1 files changed, 882 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java new file mode 100644 index 000000000..f1022c3a2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java @@ -0,0 +1,882 @@ +/* + * Copyright (C) 2013 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.ide.eclipse.adt.internal.editors.draw9patch.graphics; + +import static com.android.SdkConstants.DOT_9PNG; +import static com.android.SdkConstants.DOT_PNG; +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Rectangle; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The model of 9-patched image. + */ +public class NinePatchedImage { + private static final boolean DEBUG = false; + + /** + * Get 9-patched filename as like image.9.png . + */ + public static String getNinePatchedFileName(String fileName) { + if (fileName.endsWith(DOT_9PNG)) { + return fileName; + } + return fileName.substring(0, fileName.lastIndexOf(DOT_PNG)) + DOT_9PNG; + } + + // For stretch regions and padding + public static final int BLACK_TICK = 0xFF000000; + // For Layout Bounds + public static final int RED_TICK = 0xFFFF0000; + // Blank + public static final int TRANSPARENT_TICK = 0x00000000; + + private ImageData mBaseImageData; + + private Image mBaseImage = null; + + private boolean mHasNinePatchExtension = false; + + private boolean mDirtyFlag = false; + + private int[] mHorizontalPatchPixels = null; + private int[] mVerticalPatchPixels = null; + + private int[] mHorizontalContentPixels = null; + private int[] mVerticalContentPixels = null; + + // for Prevent unexpected stretch in StretchsView + private boolean mRedTickOnlyInHorizontalFlag = false; + private boolean mRedTickOnlyInVerticalFlag = false; + + private final List<Tick> mHorizontalPatches = new ArrayList<Tick>(); + private final List<Tick> mVerticalPatches = new ArrayList<Tick>(); + + private final List<Tick> mHorizontalContents = new ArrayList<Tick>(); + private final List<Tick> mVerticalContents = new ArrayList<Tick>(); + + + private static final int CHUNK_BIN_SIZE = 100; + private final List<Chunk> mChunkBin = new ArrayList<Chunk>(CHUNK_BIN_SIZE); + + private int mHorizontalFixedPatchSum = 0; + private int mVerticalFixedPatchSum = 0; + + private static final int PROJECTION_BIN_SIZE = 100; + private final List<Projection> mProjectionBin = new ArrayList<Projection>(PROJECTION_BIN_SIZE); + + private Chunk[][] mPatchChunks = null; + + public ImageData getImageData() { + return mBaseImageData; + } + + public int getWidth() { + return mBaseImageData.width; + } + + public int getHeight() { + return mBaseImageData.height; + } + + public Image getImage() { + if (mBaseImage == null) { + mBaseImage = new Image(AdtPlugin.getDisplay(), mBaseImageData); + } + return mBaseImage; + } + + public boolean hasNinePatchExtension() { + return mHasNinePatchExtension; + } + + /** + * Get the image has/hasn't been edited flag. + * @return If has been edited, return true + */ + public boolean isDirty() { + return mDirtyFlag; + } + + /** + * Clear dirty(edited) flag. + */ + public void clearDirtyFlag() { + mDirtyFlag = false; + } + + public NinePatchedImage(String fileName) { + boolean hasNinePatchExtension = fileName.endsWith(DOT_9PNG); + ImageData data = new ImageData(fileName); + + initNinePatchedImage(data, hasNinePatchExtension); + } + + public NinePatchedImage(InputStream inputStream, String fileName) { + boolean hasNinePatchExtension = fileName.endsWith(DOT_9PNG); + ImageData data = new ImageData(inputStream); + + initNinePatchedImage(data, hasNinePatchExtension); + } + + private Chunk getChunk() { + if (mChunkBin.size() > 0) { + Chunk chunk = mChunkBin.remove(0); + chunk.init(); + return chunk; + } + return new Chunk(); + } + + private static final void recycleChunks(Chunk[][] patchChunks, List<Chunk> bin) { + int yLen = patchChunks.length; + int xLen = patchChunks[0].length; + + for (int y = 0; y < yLen; y++) { + for (int x = 0; x < xLen; x++) { + if (bin.size() < CHUNK_BIN_SIZE) { + bin.add(patchChunks[y][x]); + } + patchChunks[y][x] = null; + } + } + } + + private Projection getProjection() { + if (mProjectionBin.size() > 0) { + Projection projection = mProjectionBin.remove(0); + return projection; + } + return new Projection(); + } + + private static final void recycleProjections(Projection[][] projections, List<Projection> bin) { + int yLen = projections.length; + int xLen = 0; + if (yLen > 0) { + xLen = projections[0].length; + } + + for (int y = 0; y < yLen; y++) { + for (int x = 0; x < xLen; x++) { + if (bin.size() < CHUNK_BIN_SIZE) { + bin.add(projections[y][x]); + } + projections[y][x] = null; + } + } + } + + private static final int[] initArray(int[] array) { + int len = array.length; + for (int i = 0; i < len; i++) { + array[i] = TRANSPARENT_TICK; + } + return array; + } + + /** + * Get one pixel with alpha from the image. + * @return packed integer value as ARGB8888 + */ + private static final int getPixel(ImageData image, int x, int y) { + return (image.getAlpha(x, y) << 24) + image.getPixel(x, y); + } + + private static final boolean isTransparentPixel(ImageData image, int x, int y) { + return image.getAlpha(x, y) == 0x0; + } + + private static final boolean isValidTickColor(int pixel) { + return (pixel == BLACK_TICK || pixel == RED_TICK); + } + + private void initNinePatchedImage(ImageData imageData, boolean hasNinePatchExtension) { + mBaseImageData = imageData; + mHasNinePatchExtension = hasNinePatchExtension; + } + + private boolean ensurePixel(int x, int y, int[] pixels, int index) { + boolean isValid = true; + int pixel = getPixel(mBaseImageData, x, y); + if (!isTransparentPixel(mBaseImageData, x, y)) { + if (index == 0 || index == pixels.length - 1) { + isValid = false; + } + if (isValidTickColor(pixel)) { + pixels[index] = pixel; + } else { + isValid = false; + } + // clear pixel + mBaseImageData.setAlpha(x, y, 0x0); + } + return isValid; + } + + private boolean ensureHorizontalPixel(int x, int y, int[] pixels) { + return ensurePixel(x, y, pixels, x); + } + + private boolean ensureVerticalPixel(int x, int y, int[] pixels) { + return ensurePixel(x, y, pixels, y); + } + + /** + * Ensure that image data is 9-patch. + */ + public boolean ensure9Patch() { + boolean isValid = true; + + int width = mBaseImageData.width; + int height = mBaseImageData.height; + + createPatchArray(); + createContentArray(); + + // horizontal + for (int x = 0; x < width; x++) { + // top row + if (!ensureHorizontalPixel(x, 0, mHorizontalPatchPixels)) { + isValid = false; + } + // bottom row + if (!ensureHorizontalPixel(x, height - 1, mHorizontalContentPixels)) { + isValid = false; + } + } + // vertical + for (int y = 0; y < height; y++) { + // left column + if (!ensureVerticalPixel(0, y, mVerticalPatchPixels)) { + isValid = false; + } + // right column + if (!ensureVerticalPixel(width -1, y, mVerticalContentPixels)) { + isValid = false; + } + } + findPatches(); + findContentsArea(); + + return isValid; + } + + private void createPatchArray() { + mHorizontalPatchPixels = initArray(new int[mBaseImageData.width]); + mVerticalPatchPixels = initArray(new int[mBaseImageData.height]); + } + + private void createContentArray() { + mHorizontalContentPixels = initArray(new int[mBaseImageData.width]); + mVerticalContentPixels = initArray(new int[mBaseImageData.height]); + } + + /** + * Convert to 9-patch image. + * <p> + * This method doesn't consider that target image is already 9-patched or + * not. + * </p> + */ + public void convertToNinePatch() { + mBaseImageData = GraphicsUtilities.convertToNinePatch(mBaseImageData); + mHasNinePatchExtension = true; + + createPatchArray(); + createContentArray(); + + findPatches(); + findContentsArea(); + } + + public boolean isValid(int x, int y) { + return (x == 0) ^ (y == 0) + ^ (x == mBaseImageData.width - 1) ^ (y == mBaseImageData.height - 1); + } + + /** + * Set patch or content. + */ + public void setPatch(int x, int y, int color) { + if (isValid(x, y)) { + if (x == 0) { + mVerticalPatchPixels[y] = color; + } else if (y == 0) { + mHorizontalPatchPixels[x] = color; + } else if (x == mBaseImageData.width - 1) { + mVerticalContentPixels[y] = color; + } else if (y == mBaseImageData.height - 1) { + mHorizontalContentPixels[x] = color; + } + + // Mark as dirty + mDirtyFlag = true; + } + } + + /** + * Erase the pixel. + */ + public void erase(int x, int y) { + if (isValid(x, y)) { + int color = TRANSPARENT_TICK; + if (x == 0) { + mVerticalPatchPixels[y] = color; + } else if (y == 0) { + mHorizontalPatchPixels[x] = color; + } else if (x == mBaseImageData.width - 1) { + mVerticalContentPixels[y] = color; + } else if (y == mBaseImageData.height - 1) { + mHorizontalContentPixels[x] = color; + } + + // Mark as dirty + mDirtyFlag = true; + } + } + + public List<Tick> getHorizontalPatches() { + return mHorizontalPatches; + } + + public List<Tick> getVerticalPatches() { + return mVerticalPatches; + } + + /** + * Find patches from pixels array. + * @param pixels Target of seeking ticks. + * @param out Add the found ticks. + * @return If BlackTick is not found but only RedTick is found, returns true + */ + private static boolean findPatches(int[] pixels, List<Tick> out) { + boolean redTickOnly = true; + Tick patch = null; + int len = 0; + + // find patches + out.clear(); + len = pixels.length - 1; + for (int i = 1; i < len; i++) { + int pixel = pixels[i]; + + if (redTickOnly && pixel != TRANSPARENT_TICK && pixel != RED_TICK) { + redTickOnly = false; + } + + if (patch != null) { + if (patch.color != pixel) { + patch.end = i; + out.add(patch); + patch = null; + } + } + if (patch == null) { + patch = new Tick(pixel); + patch.start = i; + } + } + + if (patch != null) { + patch.end = len; + out.add(patch); + } + return redTickOnly; + } + + public void findPatches() { + + // find horizontal patches + mRedTickOnlyInHorizontalFlag = findPatches(mHorizontalPatchPixels, mHorizontalPatches); + + // find vertical patches + mRedTickOnlyInVerticalFlag = findPatches(mVerticalPatchPixels, mVerticalPatches); + } + + public Rectangle getContentArea() { + Tick horizontal = getContentArea(mHorizontalContents); + Tick vertical = getContentArea(mVerticalContents); + + Rectangle rect = new Rectangle(0, 0, 0, 0); + rect.x = 1; + rect.width = mBaseImageData.width - 1; + rect.y = 1; + rect.height = mBaseImageData.height - 1; + + if (horizontal != null) { + rect.x = horizontal.start; + rect.width = horizontal.getLength(); + } + if (vertical != null) { + rect.y = vertical.start; + rect.height = vertical.getLength(); + } + + return rect; + } + + private Tick getContentArea(List<Tick> list) { + int size = list.size(); + if (size == 0) { + return null; + } + if (size == 1) { + return list.get(0); + } + + Tick start = null; + Tick end = null; + + for (int i = 0; i < size; i++) { + Tick t = list.get(i); + if (t.color == BLACK_TICK) { + if (start == null) { + start = t; + end = t; + } else { + end = t; + } + } + } + + // red tick only + if (start == null) { + return null; + } + + Tick result = new Tick(start.color); + result.start = start.start; + result.end = end.end; + + return result; + } + + /** + * This is for unit test use only. + * @see com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImageTest + */ + public List<Tick> getHorizontalContents() { + return mHorizontalContents; + } + + /** + * This is for unit test use only. + * @see com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImageTest + */ + public List<Tick> getVerticalContents() { + return mVerticalContents; + } + + private static void findContentArea(int[] pixels, List<Tick> out) { + Tick contents = null; + int len = 0; + + // find horizontal contents area + out.clear(); + len = pixels.length - 1; + for (int x = 1; x < len; x++) { + if (contents != null) { + if (contents.color != pixels[x]) { + contents.end = x; + out.add(contents); + contents = null; + } + } + if (contents == null) { + contents = new Tick(pixels[x]); + contents.start = x; + } + } + + if (contents != null) { + contents.end = len; + out.add(contents); + } + } + + public void findContentsArea() { + + // find horizontal contents area + findContentArea(mHorizontalContentPixels, mHorizontalContents); + + // find vertical contents area + findContentArea(mVerticalContentPixels, mVerticalContents); + } + + /** + * Get raw image data. + * <p> + * The raw image data is applicable for save. + * </p> + */ + public ImageData getRawImageData() { + ImageData image = GraphicsUtilities.copy(mBaseImageData); + + final int width = image.width; + final int height = image.height; + int len = 0; + + len = mHorizontalPatchPixels.length; + for (int x = 0; x < len; x++) { + int pixel = mHorizontalPatchPixels[x]; + if (pixel != TRANSPARENT_TICK) { + image.setAlpha(x, 0, 0xFF); + image.setPixel(x, 0, pixel); + } + } + + len = mVerticalPatchPixels.length; + for (int y = 0; y < len; y++) { + int pixel = mVerticalPatchPixels[y]; + if (pixel != TRANSPARENT_TICK) { + image.setAlpha(0, y, 0xFF); + image.setPixel(0, y, pixel); + } + } + + len = mHorizontalContentPixels.length; + for (int x = 0; x < len; x++) { + int pixel = mHorizontalContentPixels[x]; + if (pixel != TRANSPARENT_TICK) { + image.setAlpha(x, height - 1, 0xFF); + image.setPixel(x, height - 1, pixel); + } + } + + len = mVerticalContentPixels.length; + for (int y = 0; y < len; y++) { + int pixel = mVerticalContentPixels[y]; + if (pixel != TRANSPARENT_TICK) { + image.setAlpha(width - 1, y, 0xFF); + image.setPixel(width - 1, y, pixel); + } + } + + return image; + } + + public Chunk[][] getChunks(Chunk[][] chunks) { + int lenY = mVerticalPatches.size(); + int lenX = mHorizontalPatches.size(); + + if (lenY == 0 || lenX == 0) { + return null; + } + + if (chunks == null) { + chunks = new Chunk[lenY][lenX]; + } else { + int y = chunks.length; + int x = chunks[0].length; + if (lenY != y || lenX != x) { + recycleChunks(chunks, mChunkBin); + chunks = new Chunk[lenY][lenX]; + } + } + + // for calculate weights + float horizontalPatchSum = 0; + float verticalPatchSum = 0; + + mVerticalFixedPatchSum = 0; + mHorizontalFixedPatchSum = 0; + + for (int y = 0; y < lenY; y++) { + Tick yTick = mVerticalPatches.get(y); + + for (int x = 0; x < lenX; x++) { + Tick xTick = mHorizontalPatches.get(x); + Chunk t = getChunk(); + chunks[y][x] = t; + + t.rect.x = xTick.start; + t.rect.width = xTick.getLength(); + t.rect.y = yTick.start; + t.rect.height = yTick.getLength(); + + if (mRedTickOnlyInHorizontalFlag + || xTick.color == BLACK_TICK || lenX == 1) { + t.type += Chunk.TYPE_HORIZONTAL; + if (y == 0) { + horizontalPatchSum += t.rect.width; + } + } + if (mRedTickOnlyInVerticalFlag + || yTick.color == BLACK_TICK || lenY == 1) { + t.type += Chunk.TYPE_VERTICAL; + if (x == 0) { + verticalPatchSum += t.rect.height; + } + } + + if ((t.type & Chunk.TYPE_HORIZONTAL) == 0 && lenX > 1 && y == 0) { + mHorizontalFixedPatchSum += t.rect.width; + } + if ((t.type & Chunk.TYPE_VERTICAL) == 0 && lenY > 1 && x == 0) { + mVerticalFixedPatchSum += t.rect.height; + } + + } + } + + // calc weights + for (int y = 0; y < lenY; y++) { + for (int x = 0; x < lenX; x++) { + Chunk chunk = chunks[y][x]; + if ((chunk.type & Chunk.TYPE_HORIZONTAL) != 0) { + chunk.horizontalWeight = chunk.rect.width / horizontalPatchSum; + } + if ((chunk.type & Chunk.TYPE_VERTICAL) != 0) { + chunk.verticalWeight = chunk.rect.height / verticalPatchSum; + + } + } + } + + return chunks; + } + + public Chunk[][] getCorruptedChunks(Chunk[][] chunks) { + chunks = getChunks(chunks); + + if (chunks != null) { + int yLen = chunks.length; + int xLen = chunks[0].length; + + for (int yPos = 0; yPos < yLen; yPos++) { + for (int xPos = 0; xPos < xLen; xPos++) { + Chunk c = chunks[yPos][xPos]; + Rectangle r = c.rect; + if ((c.type & Chunk.TYPE_HORIZONTAL) != 0 + && isHorizontalCorrupted(mBaseImageData, r)) { + c.type |= Chunk.TYPE_CORRUPT; + } + if ((c.type & Chunk.TYPE_VERTICAL) != 0 + && isVerticalCorrupted(mBaseImageData, r)) { + c.type |= Chunk.TYPE_CORRUPT; + } + } + } + } + return chunks; + } + + private static boolean isVerticalCorrupted(ImageData data, Rectangle r) { + int[] column = new int[r.width]; + int[] sample = new int[r.width]; + + GraphicsUtilities.getHorizontalPixels(data, r.x, r.y, r.width, column); + + int lenY = r.y + r.height; + for (int y = r.y; y < lenY; y++) { + GraphicsUtilities.getHorizontalPixels(data, r.x, y, r.width, sample); + if (!Arrays.equals(column, sample)) { + return true; + } + } + return false; + } + + private static boolean isHorizontalCorrupted(ImageData data, Rectangle r) { + int[] column = new int[r.height]; + int[] sample = new int[r.height]; + GraphicsUtilities.getVerticalPixels(data, r.x, r.y, r.height, column); + + int lenX = r.x + r.width; + for (int x = r.x; x < lenX; x++) { + GraphicsUtilities.getVerticalPixels(data, x, r.y, r.height, sample); + if (!Arrays.equals(column, sample)) { + return true; + } + } + return false; + } + + public Projection[][] getProjections(int width, int height, Projection[][] projections) { + mPatchChunks = getChunks(mPatchChunks); + if (mPatchChunks == null) { + return null; + } + + if (DEBUG) { + System.out.println(String.format("width:%d, height:%d", width, height)); + } + + int lenY = mPatchChunks.length; + int lenX = mPatchChunks[0].length; + + if (projections == null) { + projections = new Projection[lenY][lenX]; + } else { + int y = projections.length; + int x = projections[0].length; + if (lenY != y || lenX != x) { + recycleProjections(projections, mProjectionBin); + projections = new Projection[lenY][lenX]; + } + } + + float xZoom = ((float) width / mBaseImageData.width); + float yZoom = ((float) height / mBaseImageData.height); + + if (DEBUG) { + System.out.println(String.format("xZoom:%f, yZoom:%f", xZoom, yZoom)); + } + + int destX = 0; + int destY = 0; + int streatchableWidth = width - mHorizontalFixedPatchSum; + streatchableWidth = streatchableWidth > 0 ? streatchableWidth : 1; + + int streatchableHeight = height - mVerticalFixedPatchSum; + streatchableHeight = streatchableHeight > 0 ? streatchableHeight : 1; + + if (DEBUG) { + System.out.println(String.format("streatchable %d %d", streatchableWidth, + streatchableHeight)); + } + + for (int yPos = 0; yPos < lenY; yPos++) { + destX = 0; + Projection p = null; + for (int xPos = 0; xPos < lenX; xPos++) { + Chunk chunk = mPatchChunks[yPos][xPos]; + + if (DEBUG) { + System.out.println(String.format("Tile[%d, %d] = %s", + yPos, xPos, chunk.toString())); + } + + p = getProjection(); + projections[yPos][xPos] = p; + + p.chunk = chunk; + p.src = chunk.rect; + p.dest.x = destX; + p.dest.y = destY; + + // fixed size + p.dest.width = chunk.rect.width; + p.dest.height = chunk.rect.height; + + // horizontal stretch + if ((chunk.type & Chunk.TYPE_HORIZONTAL) != 0) { + p.dest.width = Math.round(streatchableWidth * chunk.horizontalWeight); + } + // vertical stretch + if ((chunk.type & Chunk.TYPE_VERTICAL) != 0) { + p.dest.height = Math.round(streatchableHeight * chunk.verticalWeight); + } + + destX += p.dest.width; + } + destY += p.dest.height; + } + return projections; + } + + /** + * Projection class for make relation between chunked image and resized image. + */ + public static class Projection { + public Chunk chunk = null; + public Rectangle src = null; + public final Rectangle dest = new Rectangle(0, 0, 0, 0); + + @Override + public String toString() { + return String.format("src[%d, %d, %d, %d] => dest[%d, %d, %d, %d]", + src.x, src.y, src.width, src.height, + dest.x, dest.y, dest.width, dest.height); + } + } + + public static class Chunk { + public static final int TYPE_FIXED = 0x0; + public static final int TYPE_HORIZONTAL = 0x1; + public static final int TYPE_VERTICAL = 0x2; + public static final int TYPE_CORRUPT = 0x80000000; + + public int type = TYPE_FIXED; + + public Rectangle rect = new Rectangle(0, 0, 0, 0); + + public float horizontalWeight = 0.0f; + public float verticalWeight = 0.0f; + + void init() { + type = Chunk.TYPE_FIXED; + horizontalWeight = 0.0f; + verticalWeight = 0.0f; + rect.x = 0; + rect.y = 0; + rect.width = 0; + rect.height = 0; + } + + private String typeToString() { + switch (type) { + case TYPE_FIXED: + return "FIXED"; + case TYPE_HORIZONTAL: + return "HORIZONTAL"; + case TYPE_VERTICAL: + return "VERTICAL"; + case TYPE_HORIZONTAL + TYPE_VERTICAL: + return "BOTH"; + default: + return "UNKNOWN"; + } + } + + @Override + public String toString() { + return String.format("%s %f/%f %s", typeToString(), horizontalWeight, verticalWeight, + rect.toString()); + } + } + + public static class Tick { + public int start; + public int end; + public int color; + + /** + * Get the tick length. + */ + public int getLength() { + return end - start; + } + + public Tick(int tickColor) { + color = tickColor; + } + + @Override + public String toString() { + return String.format("%d tick: %d to %d", color, start, end); + } + } +} |