1 /*
2  * Copyright (C) 2013 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 android.graphics;
18 
19 /**
20  * @hide
21  */
22 public class Atlas {
23     /**
24      * WARNING: These flag values are part of the on-disk configuration information,
25      * do not change their values.
26      */
27 
28     /** DELETED: FLAG_ROTATION = 0x01 */
29 
30     /**
31      * This flag indicates whether the packing algorithm should leave
32      * an empty 1 pixel wide border around each bitmap. This border can
33      * be useful if the content of the atlas will be used in OpenGL using
34      * bilinear filtering.
35      */
36     public static final int FLAG_ADD_PADDING = 0x2;
37     /**
38      * Default flags: allow rotations and add padding.
39      */
40     public static final int FLAG_DEFAULTS = FLAG_ADD_PADDING;
41 
42     /**
43      * Each type defines a different packing algorithm that can
44      * be used by an {@link Atlas}. The best algorithm to use
45      * will depend on the source dataset and the dimensions of
46      * the atlas.
47      */
48     public enum Type {
49         SliceMinArea,
50         SliceMaxArea,
51         SliceShortAxis,
52         SliceLongAxis
53     }
54 
55     /**
56      * Represents a bitmap packed in the atlas. Each entry has a location in
57      * pixels in the atlas and a rotation flag.
58      */
59     public static class Entry {
60         /**
61          * Location, in pixels, of the bitmap on the X axis in the atlas.
62          */
63         public int x;
64         /**
65          * Location, in pixels, of the bitmap on the Y axis in the atlas.
66          */
67         public int y;
68     }
69 
70     private final Policy mPolicy;
71 
72     /**
73      * Creates a new atlas with the specified algorithm and dimensions
74      * in pixels. Calling this constructor is equivalent to calling
75      * {@link #Atlas(Atlas.Type, int, int, int)} with {@link #FLAG_DEFAULTS}.
76      *
77      * @param type The algorithm to use to pack rectangles in the atlas
78      * @param width The width of the atlas in pixels
79      * @param height The height of the atlas in pixels
80      *
81      * @see #Atlas(Atlas.Type, int, int, int)
82      */
Atlas(Type type, int width, int height)83     public Atlas(Type type, int width, int height) {
84         this(type, width, height, FLAG_DEFAULTS);
85     }
86 
87     /**
88      * Creates a new atlas with the specified algorithm and dimensions
89      * in pixels. A set of flags can also be specified to control the
90      * behavior of the atlas.
91      *
92      * @param type The algorithm to use to pack rectangles in the atlas
93      * @param width The width of the atlas in pixels
94      * @param height The height of the atlas in pixels
95      * @param flags Optional flags to control the behavior of the atlas:
96      *              {@link #FLAG_ADD_PADDING}, {@link #FLAG_ALLOW_ROTATIONS}
97      *
98      * @see #Atlas(Atlas.Type, int, int)
99      */
Atlas(Type type, int width, int height, int flags)100     public Atlas(Type type, int width, int height, int flags) {
101         mPolicy = findPolicy(type, width, height, flags);
102     }
103 
104     /**
105      * Packs a rectangle of the specified dimensions in this atlas.
106      *
107      * @param width The width of the rectangle to pack in the atlas
108      * @param height The height of the rectangle to pack in the atlas
109      *
110      * @return An {@link Entry} instance if the rectangle was packed in
111      *         the atlas, or null if the rectangle could not fit
112      *
113      * @see #pack(int, int, Atlas.Entry)
114      */
pack(int width, int height)115     public Entry pack(int width, int height) {
116         return pack(width, height, null);
117     }
118 
119     /**
120      * Packs a rectangle of the specified dimensions in this atlas.
121      *
122      * @param width The width of the rectangle to pack in the atlas
123      * @param height The height of the rectangle to pack in the atlas
124      * @param entry Out parameter that will be filled in with the location
125      *              and attributes of the packed rectangle, can be null
126      *
127      * @return An {@link Entry} instance if the rectangle was packed in
128      *         the atlas, or null if the rectangle could not fit
129      *
130      * @see #pack(int, int)
131      */
pack(int width, int height, Entry entry)132     public Entry pack(int width, int height, Entry entry) {
133         if (entry == null) entry = new Entry();
134         return mPolicy.pack(width, height, entry);
135     }
136 
findPolicy(Type type, int width, int height, int flags)137     private static Policy findPolicy(Type type, int width, int height, int flags) {
138         switch (type) {
139             case SliceMinArea:
140                 return new SlicePolicy(width, height, flags,
141                         new SlicePolicy.MinAreaSplitDecision());
142             case SliceMaxArea:
143                 return new SlicePolicy(width, height, flags,
144                         new SlicePolicy.MaxAreaSplitDecision());
145             case SliceShortAxis:
146                 return new SlicePolicy(width, height, flags,
147                         new SlicePolicy.ShorterFreeAxisSplitDecision());
148             case SliceLongAxis:
149                 return new SlicePolicy(width, height, flags,
150                         new SlicePolicy.LongerFreeAxisSplitDecision());
151         }
152         return null;
153     }
154 
155     /**
156      * A policy defines how the atlas performs the packing operation.
157      */
158     private static abstract class Policy {
pack(int width, int height, Entry entry)159         abstract Entry pack(int width, int height, Entry entry);
160     }
161 
162     /**
163      * The Slice algorightm divides the remaining empty space either
164      * horizontally or vertically after a bitmap is placed in the atlas.
165      *
166      * NOTE: the algorithm is explained below using a tree but is
167      * implemented using a linked list instead for performance reasons.
168      *
169      * The algorithm starts with a single empty cell covering the entire
170      * atlas:
171      *
172      *  -----------------------
173      * |                       |
174      * |                       |
175      * |                       |
176      * |      Empty space      |
177      * |          (C0)         |
178      * |                       |
179      * |                       |
180      * |                       |
181      *  -----------------------
182      *
183      * The tree of cells looks like this:
184      *
185      * N0(free)
186      *
187      * The algorithm then places a bitmap B1, if possible:
188      *
189      *  -----------------------
190      * |        |              |
191      * |   B1   |              |
192      * |        |              |
193      * |--------               |
194      * |                       |
195      * |                       |
196      * |                       |
197      * |                       |
198      *  -----------------------
199      *
200      *  After placing a bitmap in an empty cell, the algorithm splits
201      *  the remaining space in two new empty cells. The split can occur
202      *  vertically or horizontally (this is controlled by the "split
203      *  decision" parameter of the algorithm.)
204      *
205      *  Here is for the instance the result of a vertical split:
206      *
207      *  -----------------------
208      * |        |              |
209      * |   B1   |              |
210      * |        |              |
211      * |--------|      C2      |
212      * |        |              |
213      * |        |              |
214      * |   C1   |              |
215      * |        |              |
216      *  -----------------------
217      *
218      * The cells tree now looks like this:
219      *
220      *       C0(occupied)
221      *           / \
222      *          /   \
223      *         /     \
224      *        /       \
225      *    C1(free)  C2(free)
226      *
227      * For each bitmap to place in the atlas, the Slice algorithm
228      * will visit the free cells until it finds one where a bitmap can
229      * fit. It will then split the now occupied cell and proceed onto
230      * the next bitmap.
231      */
232     private static class SlicePolicy extends Policy {
233         private final Cell mRoot = new Cell();
234 
235         private final SplitDecision mSplitDecision;
236 
237         private final int mPadding;
238 
239         /**
240          * A cell represents a sub-rectangle of the atlas. A cell is
241          * a node in a linked list representing the available free
242          * space in the atlas.
243          */
244         private static class Cell {
245             int x;
246             int y;
247 
248             int width;
249             int height;
250 
251             Cell next;
252 
253             @Override
toString()254             public String toString() {
255                 return String.format("cell[x=%d y=%d width=%d height=%d", x, y, width, height);
256             }
257         }
258 
SlicePolicy(int width, int height, int flags, SplitDecision splitDecision)259         SlicePolicy(int width, int height, int flags, SplitDecision splitDecision) {
260             mPadding = (flags & FLAG_ADD_PADDING) != 0 ? 1 : 0;
261 
262             // The entire atlas is empty at first, minus padding
263             Cell first = new Cell();
264             first.x = first.y = mPadding;
265             first.width = width - 2 * mPadding;
266             first.height = height - 2 * mPadding;
267 
268             mRoot.next = first;
269             mSplitDecision = splitDecision;
270         }
271 
272         @Override
pack(int width, int height, Entry entry)273         Entry pack(int width, int height, Entry entry) {
274             Cell cell = mRoot.next;
275             Cell prev = mRoot;
276 
277             while (cell != null) {
278                 if (insert(cell, prev, width, height, entry)) {
279                     return entry;
280                 }
281 
282                 prev = cell;
283                 cell = cell.next;
284             }
285 
286             return null;
287         }
288 
289         /**
290          * Defines how the remaining empty space should be split up:
291          * vertically or horizontally.
292          */
293         private static interface SplitDecision {
294             /**
295              * Returns true if the remaining space defined by
296              * <code>freeWidth</code> and <code>freeHeight</code>
297              * should be split horizontally.
298              *
299              * @param freeWidth The rectWidth of the free space after packing a rectangle
300              * @param freeHeight The rectHeight of the free space after packing a rectangle
301              * @param rectWidth The rectWidth of the rectangle that was packed in a cell
302              * @param rectHeight The rectHeight of the rectangle that was packed in a cell
303              */
splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)304             boolean splitHorizontal(int freeWidth, int freeHeight,
305                     int rectWidth, int rectHeight);
306         }
307 
308         // Splits the free area horizontally to minimize the horizontal section area
309         private static class MinAreaSplitDecision implements SplitDecision {
310             @Override
splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)311             public boolean splitHorizontal(int freeWidth, int freeHeight,
312                     int rectWidth, int rectHeight) {
313                 return rectWidth * freeHeight > freeWidth * rectHeight;
314             }
315         }
316 
317         // Splits the free area horizontally to maximize the horizontal section area
318         private static class MaxAreaSplitDecision implements SplitDecision {
319             @Override
splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)320             public boolean splitHorizontal(int freeWidth, int freeHeight,
321                     int rectWidth, int rectHeight) {
322                 return rectWidth * freeHeight <= freeWidth * rectHeight;
323             }
324         }
325 
326         // Splits the free area horizontally if the horizontal axis is shorter
327         private static class ShorterFreeAxisSplitDecision implements SplitDecision {
328             @Override
splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)329             public boolean splitHorizontal(int freeWidth, int freeHeight,
330                     int rectWidth, int rectHeight) {
331                 return freeWidth <= freeHeight;
332             }
333         }
334 
335         // Splits the free area horizontally if the vertical axis is shorter
336         private static class LongerFreeAxisSplitDecision implements SplitDecision {
337             @Override
splitHorizontal(int freeWidth, int freeHeight, int rectWidth, int rectHeight)338             public boolean splitHorizontal(int freeWidth, int freeHeight,
339                     int rectWidth, int rectHeight) {
340                 return freeWidth > freeHeight;
341             }
342         }
343 
344         /**
345          * Attempts to pack a rectangle of specified dimensions in the available
346          * empty space.
347          *
348          * @param cell The cell representing free space in which to pack the rectangle
349          * @param prev The previous cell in the free space linked list
350          * @param width The width of the rectangle to pack
351          * @param height The height of the rectangle to pack
352          * @param entry Stores the location of the packged rectangle, if it fits
353          *
354          * @return True if the rectangle was packed in the atlas, false otherwise
355          */
insert(Cell cell, Cell prev, int width, int height, Entry entry)356         private boolean insert(Cell cell, Cell prev, int width, int height, Entry entry) {
357             if (cell.width < width || cell.height < height) {
358                 return false;
359             }
360 
361             // Remaining free space after packing the rectangle
362             int deltaWidth = cell.width - width;
363             int deltaHeight = cell.height - height;
364 
365             // Split the remaining free space into two new cells
366             Cell first = new Cell();
367             Cell second = new Cell();
368 
369             first.x = cell.x + width + mPadding;
370             first.y = cell.y;
371             first.width = deltaWidth - mPadding;
372 
373             second.x = cell.x;
374             second.y = cell.y + height + mPadding;
375             second.height = deltaHeight - mPadding;
376 
377             if (mSplitDecision.splitHorizontal(deltaWidth, deltaHeight,
378                     width, height)) {
379                 first.height = height;
380                 second.width = cell.width;
381             } else {
382                 first.height = cell.height;
383                 second.width = width;
384 
385                 // The order of the cells matters for efficient packing
386                 // We want to give priority to the cell chosen by the
387                 // split decision heuristic
388                 Cell temp = first;
389                 first = second;
390                 second = temp;
391             }
392 
393             // Remove degenerate cases to keep the free list as small as possible
394             if (first.width > 0 && first.height > 0) {
395                 prev.next = first;
396                 prev = first;
397             }
398 
399             if (second.width > 0 && second.height > 0) {
400                 prev.next = second;
401                 second.next = cell.next;
402             } else {
403                 prev.next = cell.next;
404             }
405 
406             // The cell is now completely removed from the free list
407             cell.next = null;
408 
409             // Return the location and rotation of the packed rectangle
410             entry.x = cell.x;
411             entry.y = cell.y;
412 
413             return true;
414         }
415     }
416 }
417