File Coverage

blib/lib/Algorithm/BinPack/2D.pm
Criterion Covered Total %
statement 78 78 100.0
branch 35 36 97.2
condition 31 33 93.9
subroutine 10 10 100.0
pod 3 7 42.8
total 157 164 95.7


line stmt bran cond sub pod time code
1             # vim: set expandtab ts=4 sw=4 nowrap ft=perl ff=unix :
2             package Algorithm::BinPack::2D;
3              
4 3     3   58340 use strict;
  3         5  
  3         99  
5 3     3   16 use warnings;
  3         9  
  3         85  
6 3     3   32 use Carp;
  3         5  
  3         3537  
7              
8             our $VERSION = 0.03;
9              
10             =head1 NAME
11              
12             Algorithm::BinPack::2D - efficiently pack items into rectangles
13              
14             =head1 SYNOPSIS
15              
16             C efficiently packs items into bins.
17             The bins are given a maximum width and height,
18             and items are packed in with as little empty space as possible.
19             An example use would be backing up small images to concatenated images,
20             while minimizing the number of images required.
21              
22             my $bp = Algorithm::BinPack::2D->new(binwidth => 512, binheight => 512);
23              
24             $bp->add_item(label => "one.png", width => 30, height => 10);
25             $bp->add_item(label => "two.png", width => 200, height => 40);
26             $bp->add_item(label => "three.png", width => 30, height => 300);
27             $bp->add_item(label => "four.png", width => 400, height => 100);
28              
29             for ($bp->pack_bins) {
30             print "Bin width: ", $_->{width}, " x ", $_->{height}, "\n";
31             print " Item: ", $_->{label}, "\n" for @{ $_->{items} };
32             }
33             =cut
34              
35             =head1 METHODS
36              
37             =over 8
38              
39             =item new
40              
41             Creates a new C object.
42             The maximum bin width and height is specified as a named argument 'binwidth' and 'binheight',
43             and is required.
44              
45             my $bp = Algorithm::BinPack::2D->new(binwidth => 512, binheight => 512);
46              
47             =cut
48              
49             sub new {
50 2     2 1 1371 my $name = shift;
51 2         11 my $self = {@_};
52              
53 2         8 bless $self, $name;
54             }
55              
56             =item add_item
57              
58             Adds an item to be packed into a bin.
59             Required named arguments are 'label', 'width' and 'height',
60             but any others can be specified, and will be saved.
61              
62             $bp->add_item(label => 'one', width => 1, height => 1);
63              
64             =cut
65              
66             sub add_item {
67 18     18 1 4978 my $self = shift;
68 18         70 my $item = {@_};
69              
70 18 100 100     213 unless ($item->{label}
      100        
      100        
      66        
71             && $item->{width}
72             && $item->{height}
73             && $item->{width} > 0
74             && $item->{height} > 0) {
75 4         48 croak 'Item must have label, width and height.';
76             }
77              
78 14 100 66     79 if ( $self->{binwidth} < $item->{width}
79             || $self->{binheight} < $item->{height}) {
80 1         28 croak
81             "Item size is too big. Max size is $self->{binwidth}x$self->{binheight}.";
82             }
83              
84 13         17 push @{ $self->{items} }, $item;
  13         49  
85             }
86              
87             =item pack_bins
88              
89             Packs the items into bins.
90             This method tries to leave as little empty space in each bin as possible.
91             It returns a list of hashrefs with the key
92             'width' containing the total bin width,
93             'height' containing the total bin height,
94             and 'items' containing an arrayref holding the items in the bin.
95             Each item is in turn a hashref containing the keys 'label', 'x', 'y', 'width' and 'height'.
96              
97             for my $bin ($bp->pack_bins) {
98             print "Bin width: ", $bin->{width}, " x ", $bin->{height}, "\n";
99              
100             for my $item (@{ $bin->{items} }) {
101             printf " %-6s %-20s\n", $_, $item->{$_} for keys %{ $item };
102             print " ---\n";
103             }
104             }
105              
106             =cut
107              
108             sub pack_bins {
109 1     1 1 6 my $self = shift;
110 1         3 my $bin_width = $self->{binwidth};
111 1         2 my $bin_height = $self->{binheight};
112              
113 1         3 my @bins;
114 1         5 push @bins, make_new_bin($bin_width, $bin_height);
115              
116 1         7 for my $item (sort_items($self->{items})) {
117 8         12 my ($width, $height, $label) = @{$item}{qw(width height label)};
  8         21  
118 8         10 my $rect;
119 8         13 for my $bin (@bins) {
120 15         28 $rect = pack_in_a_bin($bin, $width, $height, $label);
121 15 100       40 last if $rect;
122             }
123 8 100       23 unless ($rect) {
124 2         6 my $new_bin = make_new_bin($bin_width, $bin_height);
125 2         5 push @bins, $new_bin;
126 2         5 $rect = pack_in_a_bin($new_bin, $width, $height, $label);
127             }
128             }
129              
130             # filter filled nodes
131             map {
132 1         3 my $bin = $_;
  3         5  
133 3         5 my $items = [];
134 3         9 my ($max_width, $max_height) = filter_filled_node($bin, $items, 0, 0);
135             +{
136 3         28 width => $max_width,
137             height => $max_height,
138             items => $items,
139             }
140             } @bins;
141             } ## end sub pack_bins
142              
143             sub filter_filled_node {
144 21     21 0 28 my ($bin, $filtered_nodes, $max_width, $max_height) = @_;
145              
146 21 100       72 ($max_width, $max_height) =
147             filter_filled_node($bin->{left}, $filtered_nodes, $max_width, $max_height)
148             if $bin->{left};
149 21 100       61 ($max_width, $max_height) = filter_filled_node(
150             $bin->{right}, $filtered_nodes, $max_width,
151             $max_height
152             ) if $bin->{right};
153              
154 21 100       52 if ($bin->{filled}) {
155 8         43 push @$filtered_nodes,
156             +{
157             x => $bin->{x},
158             y => $bin->{y},
159             label => $bin->{label},
160             width => $bin->{width},
161             height => $bin->{height},
162             };
163 8         17 my $max_x = $bin->{x} + $bin->{width};
164 8         12 my $max_y = $bin->{y} + $bin->{height};
165 8 100       20 $max_width = $max_x if $max_width < $max_x;
166 8 100       19 $max_height = $max_y if $max_height < $max_y;
167             }
168 21         53 ($max_width, $max_height);
169             }
170              
171             sub make_new_bin {
172 3     3 0 6 my ($bin_width, $bin_height) = @_;
173              
174             return +{
175 3         14 x => 0,
176             y => 0,
177             filled => 0,
178             width => $bin_width,
179             height => $bin_height,
180             };
181             }
182              
183             sub pack_in_a_bin {
184 98     98 0 181 my ($bin, $width, $height, $label) = @_;
185              
186 98 100       203 if ($bin->{left}) {
187 37   100     89 return pack_in_a_bin($bin->{left}, $width, $height, $label)
188             || pack_in_a_bin($bin->{right}, $width, $height, $label);
189             }
190              
191 61 100 100     278 if ( $bin->{filled}
      100        
192             || $bin->{width} < $width
193             || $bin->{height} < $height) {
194 44         186 return;
195             }
196              
197 17 100 100     78 if ($bin->{width} == $width && $bin->{height} == $height) {
198 8         11 $bin->{filled} = 1;
199 8         14 $bin->{label} = $label;
200 8         39 return $bin;
201             }
202              
203 9         16 my $width_diff = $bin->{width} - $width;
204 9         12 my $height_diff = $bin->{height} - $height;
205              
206 9 100       18 if ($width_diff > $height_diff) {
207 3         31 $bin->{left} = +{
208             x => $bin->{x},
209             y => $bin->{y},
210             filled => 0,
211             width => $width,
212             height => $bin->{height},
213             };
214 3         15 $bin->{right} = +{
215             x => $bin->{x} + $width,
216             y => $bin->{y},
217             filled => 0,
218             width => $width_diff,
219             height => $bin->{height},
220             };
221             } else {
222 6         36 $bin->{left} = +{
223             x => $bin->{x},
224             y => $bin->{y},
225             filled => 0,
226             width => $bin->{width},
227             height => $height,
228             };
229 6         30 $bin->{right} = +{
230             x => $bin->{x},
231             y => $bin->{y} + $height,
232             filled => 0,
233             width => $bin->{width},
234             height => $height_diff,
235             };
236             }
237              
238 9         41 return pack_in_a_bin($bin->{left}, $width, $height, $label);
239             } ## end sub pack_in_a_bin
240              
241             sub sort_items {
242 1     1 0 2 my $items = shift;
243              
244 14 100       38 sort {
245             # Sorting by max(width, height) is the best heuristic.
246 1         6 my $abigger = $a->{width} > $a->{height} ? $a->{width} : $a->{height};
247 14 100       28 my $bbigger = $b->{width} > $b->{height} ? $b->{width} : $b->{height};
248              
249 14 100       30 my $asmaller = $a->{width} <= $a->{height} ? $a->{width} : $a->{height};
250 14 100       36 my $bsmaller = $b->{width} <= $b->{height} ? $b->{width} : $b->{height};
251              
252 14 50 100     78 $bbigger <=> $abigger
      100        
253             || $bsmaller <=> $asmaller
254             || $b->{width} <=> $a->{width}
255             || $a->{label} cmp $b->{label}
256 1         2 } @{$items};
257             }
258              
259             1;
260              
261             =back
262              
263             =head1 SEE ALSO
264              
265             C
266              
267             =head1 AUTHOR
268              
269             Tasuku SUENAGA a.k.a. gunyarakun Etasuku-s-cpan ATAT titech.acE
270              
271             =head1 LICENSE
272              
273             Copyright (C) Tasuku SUENAGA a.k.a. gunyarakun
274              
275             This library is free software; you can redistribute it and/or modify
276             it under the same terms as Perl itself.
277             =cut