• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ninepatch;
18 
19 import java.awt.Graphics2D;
20 import java.awt.Rectangle;
21 import java.awt.RenderingHints;
22 import java.awt.image.BufferedImage;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.util.ArrayList;
28 import java.util.List;
29 
30 /**
31  * Represents a 9-Patch bitmap.
32  */
33 public class NinePatch {
34     public static final String EXTENSION_9PATCH = ".9.png";
35 
36     private BufferedImage mImage;
37 
38     private int mMinWidth;
39     private int mMinHeight;
40 
41     private int[] row;
42     private int[] column;
43 
44     private boolean mVerticalStartWithPatch;
45     private boolean mHorizontalStartWithPatch;
46 
47     private List<Rectangle> mFixed;
48     private List<Rectangle> mPatches;
49     private List<Rectangle> mHorizontalPatches;
50     private List<Rectangle> mVerticalPatches;
51 
52     private Pair<Integer> mHorizontalPadding;
53     private Pair<Integer> mVerticalPadding;
54 
55     private float mHorizontalPatchesSum;
56     private float mVerticalPatchesSum;
57 
58     private int mRemainderHorizontal;
59 
60     private int mRemainderVertical;
61 
62     /**
63      * Loads a 9 patch or regular bitmap.
64      * @param fileUrl the URL of the file to load.
65      * @param convert if <code>true</code>, non 9-patch bitmap will be converted into a 9 patch.
66      * If <code>false</code> and the bitmap is not a 9 patch, the method will return
67      * <code>null</code>.
68      * @return a {@link NinePatch} or <code>null</code>.
69      * @throws IOException
70      */
load(URL fileUrl, boolean convert)71     public static NinePatch load(URL fileUrl, boolean convert) throws IOException {
72         BufferedImage image = null;
73         try {
74             image  = GraphicsUtilities.loadCompatibleImage(fileUrl);
75         } catch (MalformedURLException e) {
76             // really this shouldn't be happening since we're not creating the URL manually.
77             return null;
78         }
79 
80         boolean is9Patch = fileUrl.getPath().toLowerCase().endsWith(EXTENSION_9PATCH);
81 
82         return load(image, is9Patch, convert);
83     }
84 
85     /**
86      * Loads a 9 patch or regular bitmap.
87      * @param stream the {@link InputStream} of the file to load.
88      * @param is9Patch whether the file represents a 9-patch
89      * @param convert if <code>true</code>, non 9-patch bitmap will be converted into a 9 patch.
90      * If <code>false</code> and the bitmap is not a 9 patch, the method will return
91      * <code>null</code>.
92      * @return a {@link NinePatch} or <code>null</code>.
93      * @throws IOException
94      */
load(InputStream stream, boolean is9Patch, boolean convert)95     public static NinePatch load(InputStream stream, boolean is9Patch, boolean convert)
96             throws IOException {
97         BufferedImage image = null;
98         try {
99             image  = GraphicsUtilities.loadCompatibleImage(stream);
100         } catch (MalformedURLException e) {
101             // really this shouldn't be happening since we're not creating the URL manually.
102             return null;
103         }
104 
105         return load(image, is9Patch, convert);
106     }
107 
108     /**
109      * Loads a 9 patch or regular bitmap.
110      * @param image the source {@link BufferedImage}.
111      * @param is9Patch whether the file represents a 9-patch
112      * @param convert if <code>true</code>, non 9-patch bitmap will be converted into a 9 patch.
113      * If <code>false</code> and the bitmap is not a 9 patch, the method will return
114      * <code>null</code>.
115      * @return a {@link NinePatch} or <code>null</code>.
116      * @throws IOException
117      */
load(BufferedImage image, boolean is9Patch, boolean convert)118     public static NinePatch load(BufferedImage image, boolean is9Patch, boolean convert) {
119         if (is9Patch == false) {
120             if (convert) {
121                 image = convertTo9Patch(image);
122             } else {
123                 return null;
124             }
125         } else {
126             ensure9Patch(image);
127         }
128 
129         return new NinePatch(image);
130     }
131 
getWidth()132     public int getWidth() {
133         return mImage.getWidth() - 2;
134     }
135 
getHeight()136     public int getHeight() {
137         return mImage.getHeight() - 2;
138     }
139 
140     /**
141      *
142      * @param padding array of left, top, right, bottom padding
143      * @return
144      */
getPadding(int[] padding)145     public boolean getPadding(int[] padding) {
146         padding[0] = mHorizontalPadding.mFirst; // left
147         padding[2] = mHorizontalPadding.mSecond; // right
148         padding[1] = mVerticalPadding.mFirst; // top
149         padding[3] = mVerticalPadding.mSecond; // bottom
150         return true;
151     }
152 
153 
draw(Graphics2D graphics2D, int x, int y, int scaledWidth, int scaledHeight)154     public void draw(Graphics2D graphics2D, int x, int y, int scaledWidth, int scaledHeight) {
155         if (scaledWidth <= 1 || scaledHeight <= 1) {
156             return;
157         }
158 
159         Graphics2D g = (Graphics2D)graphics2D.create();
160         g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
161                 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
162 
163 
164         try {
165             if (mPatches.size() == 0) {
166                 g.drawImage(mImage, x, y, scaledWidth, scaledHeight, null);
167                 return;
168             }
169 
170             g.translate(x, y);
171             x = y = 0;
172 
173             computePatches(scaledWidth, scaledHeight);
174 
175             int fixedIndex = 0;
176             int horizontalIndex = 0;
177             int verticalIndex = 0;
178             int patchIndex = 0;
179 
180             boolean hStretch;
181             boolean vStretch;
182 
183             float vWeightSum = 1.0f;
184             float vRemainder = mRemainderVertical;
185 
186             vStretch = mVerticalStartWithPatch;
187             while (y < scaledHeight - 1) {
188                 hStretch = mHorizontalStartWithPatch;
189 
190                 int height = 0;
191                 float vExtra = 0.0f;
192 
193                 float hWeightSum = 1.0f;
194                 float hRemainder = mRemainderHorizontal;
195 
196                 while (x < scaledWidth - 1) {
197                     Rectangle r;
198                     if (!vStretch) {
199                         if (hStretch) {
200                             r = mHorizontalPatches.get(horizontalIndex++);
201                             float extra = r.width / mHorizontalPatchesSum;
202                             int width = (int) (extra * hRemainder / hWeightSum);
203                             hWeightSum -= extra;
204                             hRemainder -= width;
205                             g.drawImage(mImage, x, y, x + width, y + r.height, r.x, r.y,
206                                     r.x + r.width, r.y + r.height, null);
207                             x += width;
208                         } else {
209                             r = mFixed.get(fixedIndex++);
210                             g.drawImage(mImage, x, y, x + r.width, y + r.height, r.x, r.y,
211                                     r.x + r.width, r.y + r.height, null);
212                             x += r.width;
213                         }
214                         height = r.height;
215                     } else {
216                         if (hStretch) {
217                             r = mPatches.get(patchIndex++);
218                             vExtra = r.height / mVerticalPatchesSum;
219                             height = (int) (vExtra * vRemainder / vWeightSum);
220                             float extra = r.width / mHorizontalPatchesSum;
221                             int width = (int) (extra * hRemainder / hWeightSum);
222                             hWeightSum -= extra;
223                             hRemainder -= width;
224                             g.drawImage(mImage, x, y, x + width, y + height, r.x, r.y,
225                                     r.x + r.width, r.y + r.height, null);
226                             x += width;
227                         } else {
228                             r = mVerticalPatches.get(verticalIndex++);
229                             vExtra = r.height / mVerticalPatchesSum;
230                             height = (int) (vExtra * vRemainder / vWeightSum);
231                             g.drawImage(mImage, x, y, x + r.width, y + height, r.x, r.y,
232                                     r.x + r.width, r.y + r.height, null);
233                             x += r.width;
234                         }
235 
236                     }
237                     hStretch = !hStretch;
238                 }
239                 x = 0;
240                 y += height;
241                 if (vStretch) {
242                     vWeightSum -= vExtra;
243                     vRemainder -= height;
244                 }
245                 vStretch = !vStretch;
246             }
247 
248         } finally {
249             g.dispose();
250         }
251     }
252 
computePatches(int scaledWidth, int scaledHeight)253     void computePatches(int scaledWidth, int scaledHeight) {
254         boolean measuredWidth = false;
255         boolean endRow = true;
256 
257         int remainderHorizontal = 0;
258         int remainderVertical = 0;
259 
260         if (mFixed.size() > 0) {
261             int start = mFixed.get(0).y;
262             for (Rectangle rect : mFixed) {
263                 if (rect.y > start) {
264                     endRow = true;
265                     measuredWidth = true;
266                 }
267                 if (!measuredWidth) {
268                     remainderHorizontal += rect.width;
269                 }
270                 if (endRow) {
271                     remainderVertical += rect.height;
272                     endRow = false;
273                     start = rect.y;
274                 }
275             }
276         }
277 
278         mRemainderHorizontal = scaledWidth - remainderHorizontal;
279 
280         mRemainderVertical = scaledHeight - remainderVertical;
281 
282         mHorizontalPatchesSum = 0;
283         if (mHorizontalPatches.size() > 0) {
284             int start = -1;
285             for (Rectangle rect : mHorizontalPatches) {
286                 if (rect.x > start) {
287                     mHorizontalPatchesSum += rect.width;
288                     start = rect.x;
289                 }
290             }
291         } else {
292             int start = -1;
293             for (Rectangle rect : mPatches) {
294                 if (rect.x > start) {
295                     mHorizontalPatchesSum += rect.width;
296                     start = rect.x;
297                 }
298             }
299         }
300 
301         mVerticalPatchesSum = 0;
302         if (mVerticalPatches.size() > 0) {
303             int start = -1;
304             for (Rectangle rect : mVerticalPatches) {
305                 if (rect.y > start) {
306                     mVerticalPatchesSum += rect.height;
307                     start = rect.y;
308                 }
309             }
310         } else {
311             int start = -1;
312             for (Rectangle rect : mPatches) {
313                 if (rect.y > start) {
314                     mVerticalPatchesSum += rect.height;
315                     start = rect.y;
316                 }
317             }
318         }
319     }
320 
321 
NinePatch(BufferedImage image)322     private NinePatch(BufferedImage image) {
323         mImage = image;
324 
325         findPatches();
326     }
327 
findPatches()328     private void findPatches() {
329         int width = mImage.getWidth();
330         int height = mImage.getHeight();
331 
332         row = GraphicsUtilities.getPixels(mImage, 0, 0, width, 1, row);
333         column = GraphicsUtilities.getPixels(mImage, 0, 0, 1, height, column);
334 
335         boolean[] result = new boolean[1];
336         Pair<List<Pair<Integer>>> left = getPatches(column, result);
337         mVerticalStartWithPatch = result[0];
338 
339         result = new boolean[1];
340         Pair<List<Pair<Integer>>> top = getPatches(row, result);
341         mHorizontalStartWithPatch = result[0];
342 
343         mFixed = getRectangles(left.mFirst, top.mFirst);
344         mPatches = getRectangles(left.mSecond, top.mSecond);
345 
346         if (mFixed.size() > 0) {
347             mHorizontalPatches = getRectangles(left.mFirst, top.mSecond);
348             mVerticalPatches = getRectangles(left.mSecond, top.mFirst);
349         } else {
350             if (top.mFirst.size() > 0) {
351                 mHorizontalPatches = new ArrayList<Rectangle>(0);
352                 mVerticalPatches = getVerticalRectangles(top.mFirst);
353             } else if (left.mFirst.size() > 0) {
354                 mHorizontalPatches = getHorizontalRectangles(left.mFirst);
355                 mVerticalPatches = new ArrayList<Rectangle>(0);
356             } else {
357                 mHorizontalPatches = mVerticalPatches = new ArrayList<Rectangle>(0);
358             }
359         }
360 
361         row = GraphicsUtilities.getPixels(mImage, 0, height - 1, width, 1, row);
362         column = GraphicsUtilities.getPixels(mImage, width - 1, 0, 1, height, column);
363 
364         top = getPatches(row, result);
365         mHorizontalPadding = getPadding(top.mFirst);
366 
367         left = getPatches(column, result);
368         mVerticalPadding = getPadding(left.mFirst);
369     }
370 
getVerticalRectangles(List<Pair<Integer>> topPairs)371     private List<Rectangle> getVerticalRectangles(List<Pair<Integer>> topPairs) {
372         List<Rectangle> rectangles = new ArrayList<Rectangle>();
373         for (Pair<Integer> top : topPairs) {
374             int x = top.mFirst;
375             int width = top.mSecond - top.mFirst;
376 
377             rectangles.add(new Rectangle(x, 1, width, mImage.getHeight() - 2));
378         }
379         return rectangles;
380     }
381 
getHorizontalRectangles(List<Pair<Integer>> leftPairs)382     private List<Rectangle> getHorizontalRectangles(List<Pair<Integer>> leftPairs) {
383         List<Rectangle> rectangles = new ArrayList<Rectangle>();
384         for (Pair<Integer> left : leftPairs) {
385             int y = left.mFirst;
386             int height = left.mSecond - left.mFirst;
387 
388             rectangles.add(new Rectangle(1, y, mImage.getWidth() - 2, height));
389         }
390         return rectangles;
391     }
392 
getPadding(List<Pair<Integer>> pairs)393     private Pair<Integer> getPadding(List<Pair<Integer>> pairs) {
394         if (pairs.size() == 0) {
395             return new Pair<Integer>(0, 0);
396         } else if (pairs.size() == 1) {
397             if (pairs.get(0).mFirst == 1) {
398                 return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst, 0);
399             } else {
400                 return new Pair<Integer>(0, pairs.get(0).mSecond - pairs.get(0).mFirst);
401             }
402         } else {
403             int index = pairs.size() - 1;
404             return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst,
405                     pairs.get(index).mSecond - pairs.get(index).mFirst);
406         }
407     }
408 
getRectangles(List<Pair<Integer>> leftPairs, List<Pair<Integer>> topPairs)409     private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs,
410             List<Pair<Integer>> topPairs) {
411         List<Rectangle> rectangles = new ArrayList<Rectangle>();
412         for (Pair<Integer> left : leftPairs) {
413             int y = left.mFirst;
414             int height = left.mSecond - left.mFirst;
415             for (Pair<Integer> top : topPairs) {
416                 int x = top.mFirst;
417                 int width = top.mSecond - top.mFirst;
418 
419                 rectangles.add(new Rectangle(x, y, width, height));
420             }
421         }
422         return rectangles;
423     }
424 
getPatches(int[] pixels, boolean[] startWithPatch)425     private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) {
426         int lastIndex = 1;
427         int lastPixel = pixels[1];
428         boolean first = true;
429 
430         List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>();
431         List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>();
432 
433         for (int i = 1; i < pixels.length - 1; i++) {
434             int pixel = pixels[i];
435             if (pixel != lastPixel) {
436                 if (lastPixel == 0xFF000000) {
437                     if (first) startWithPatch[0] = true;
438                     patches.add(new Pair<Integer>(lastIndex, i));
439                 } else {
440                     fixed.add(new Pair<Integer>(lastIndex, i));
441                 }
442                 first = false;
443 
444                 lastIndex = i;
445                 lastPixel = pixel;
446             }
447         }
448         if (lastPixel == 0xFF000000) {
449             if (first) startWithPatch[0] = true;
450             patches.add(new Pair<Integer>(lastIndex, pixels.length - 1));
451         } else {
452             fixed.add(new Pair<Integer>(lastIndex, pixels.length - 1));
453         }
454 
455         if (patches.size() == 0) {
456             patches.add(new Pair<Integer>(1, pixels.length - 1));
457             startWithPatch[0] = true;
458             fixed.clear();
459         }
460 
461         return new Pair<List<Pair<Integer>>>(fixed, patches);
462     }
463 
ensure9Patch(BufferedImage image)464     private static void ensure9Patch(BufferedImage image) {
465         int width = image.getWidth();
466         int height = image.getHeight();
467         for (int i = 0; i < width; i++) {
468             int pixel = image.getRGB(i, 0);
469             if (pixel != 0 && pixel != 0xFF000000) {
470                 image.setRGB(i, 0, 0);
471             }
472             pixel = image.getRGB(i, height - 1);
473             if (pixel != 0 && pixel != 0xFF000000) {
474                 image.setRGB(i, height - 1, 0);
475             }
476         }
477         for (int i = 0; i < height; i++) {
478             int pixel = image.getRGB(0, i);
479             if (pixel != 0 && pixel != 0xFF000000) {
480                 image.setRGB(0, i, 0);
481             }
482             pixel = image.getRGB(width - 1, i);
483             if (pixel != 0 && pixel != 0xFF000000) {
484                 image.setRGB(width - 1, i, 0);
485             }
486         }
487     }
488 
convertTo9Patch(BufferedImage image)489     private static BufferedImage convertTo9Patch(BufferedImage image) {
490         BufferedImage buffer = GraphicsUtilities.createTranslucentCompatibleImage(
491                 image.getWidth() + 2, image.getHeight() + 2);
492 
493         Graphics2D g2 = buffer.createGraphics();
494         g2.drawImage(image, 1, 1, null);
495         g2.dispose();
496 
497         return buffer;
498     }
499 
500     static class Pair<E> {
501         E mFirst;
502         E mSecond;
503 
Pair(E first, E second)504         Pair(E first, E second) {
505             mFirst = first;
506             mSecond = second;
507         }
508 
509         @Override
toString()510         public String toString() {
511             return "Pair[" + mFirst + ", " + mSecond + "]";
512         }
513     }
514 }
515