LP#1787479: Custom label printing toolbox
authorAdam Bowling <abowling@emeralddata.net>
Wed, 17 Oct 2018 20:15:44 +0000 (16:15 -0400)
committerChris Sharp <csharp@georgialibraries.org>
Thu, 21 Feb 2019 17:46:28 +0000 (12:46 -0500)
Signed-off-by: Adam Bowling <abowling@emeralddata.net>
Signed-off-by: Chris Sharp <csharp@georgialibraries.org>

Conflicts:
Open-ILS/src/templates/staff/cat/printlabels/t_view.tt2

Open-ILS/src/templates/staff/cat/printlabels/t_view.tt2
Open-ILS/web/js/ui/default/staff/cat/printlabels/app.js

index fc5dfed..880eaed 100644 (file)
 <style>
   /* TODO: move me */
+
+  body {
+
+  }
+
+  table.page-break {
+  page-break-before: always;
+  }
+
+  div.print-label-toolbox input[type=number], div.print-label-toolbox input[type=text] {
+  border: 1px solid #999;
+  border-radius: 3px;
+  margin-right: 12px;
+  width: 56px;
+  }
+
+  div.print-label-toolbox div.eg-print-label-section {
+  border-bottom: 1px solid #DED;
+  display: block;
+  margin: 0 0 10px 0;
+  padding: 0 0 10px 0;
+  }
+
+  div.print-label-toolbox input.ng-invalid {
+  background-color: #FFFF00;
+  color: #FF0000;
+  }
+
+  div.print-label-toolbox label {
+  padding-right: 4px;
+  }
+
+  div.print-label-toolbox ul {
+  display: block;
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+  }
+
+  div.print-label-toolbox ul li {
+  display: inline-block;
+  padding: 0 0 0 6px;
+  }
+
+  div.print-label-toolbox ul li:first-child {
+  display: inline-block;
+  padding: 0 0 0 0;
+  }
+
+  table.custom-label-table td {
+  vertical-align: top;
+  }
+
   .print-template-text {
-    height: 36em;
-    width: 100%;
+  height: 36em;
+  width: 100%;
   }
+
   .cn-template-text {
-    height: 12em;
-    width: 100%;
+  height: 12em;
+  width: 100%;
   }
 </style>
 
-<div class="container-fluid" style="text-align:center">
-    <div class="alert alert-info alert-less-pad strong-text-2">
-        [% l('Print Item Labels') %]
-    </div>
-</div>
+<h2>[% l('Print Item Labels') %]</h2>
 
-<div class="row">
-    <div class="col-md-3">
-       <div class="input-group">
-            <span class="input-group-addon">[% l('Template') %]</span>
-            <eg-basic-combo-box list="template_name_list" selected="template_name"></eg-basic-combo-box>
-       </div>
-    </div>
-    <div class="col-md-1">
-        <button class="btn btn-default" ng-click="applyTemplate(template_name)">[% l('Apply') %]</button>
-    </div>
-    <div class="col-md-2">
-       <div class="input-group">
-            <span class="input-group-addon">[% l('Printer') %]</span>
-            <select class="form-control" ng-model="print.template_context">
-                <option value="default">[% l('Default') %]</option>
-                <option value="receipt">[% l('Receipt') %]</option>
-                <option value="label">[% l('Label') %]</option>
-                <option value="mail">[% l('Mail') %]</option>
-                <option value="offline">[% l('Offline') %]</option>
-            </select>
+<div class="row bg-info">
+    <div class="col-md-6">
+        <div class="row">
+            <div class="col-md-1">
+                <span class="h4">[% l('Template') %]</span>
+            </div>
+            <div class="col-md-5">
+                <eg-basic-combo-box list="template_name_list" selected="template_name"></eg-basic-combo-box>
+            </div>
+            <div class="col-md-1">
+                <button class="btn btn-default" ng-click="applyTemplate(template_name)">[% l('Apply') %]</button>
+            </div>
+            <div class="col-md-1">
+                <span class="h4">[% l('Printer') %]</span>
+            </div>
+            <div class="col-md-4">
+                <select class="form-control" ng-model="print.template_context">
+                    <option value="default">[% l('Default') %]</option>
+                    <option value="receipt">[% l('Receipt') %]</option>
+                    <option value="label">[% l('Label') %]</option>
+                    <option value="mail">[% l('Mail') %]</option>
+                    <option value="offline">[% l('Offline') %]</option>
+                </select>
+            </div>
         </div>
     </div>
-
     <div class="col-md-2">
         <div class="btn-group">
             <button class="btn btn-default" ng-click="saveTemplate(template_name)">[% l('Save') %]</button>
             <button class="btn btn-default" ng-click="deleteTemplate(template_name)">[% l('Delete') %]</button>
         </div>
     </div>
-
     <div class="col-md-3">
         <div class="btn-group">
             <span class="btn btn-default btn-file">
                 <input type="file" eg-file-reader container="imported_templates.data">
             </span>
             <label class="btn btn-default"
-               eg-json-exporter container="templates"
-               default-file-name="'[% l('exported_label_templates.json') %]'">
-               [% l('Export') %]
+                eg-json-exporter container="templates"
+                default-file-name="'[% l('exported_label_templates.json') %]'">
+                [% l('Export') %]
             </label>
-            <label class="btn btn-default" ng-click="reset_to_default()">[% l('Default') %]</label>
-         </div>
+            <label class="btn btn-default" ng-click="reset_to_default()">
+                [% l('Default') %]</button>
+            </div>
     </div>
-
     <div class="col-md-1 pull-right">
         <button class="btn btn-default" ng-click="print_labels()">[% l('Print') %]</button>
     </div>
 <hr/>
 
 <div class="row">
-  <div class="col-md-5">
-    <ul class="nav nav-tabs">
-        <li ng-class="{active : current_tab == 'cn_template'}">
-            <a ng-click="set_tab('cn_template')">
-                [% l('Call Number Template') %]
-            </a>
-        </li>
-        <li ng-class="{active : current_tab == 'call_numbers'}">
-            <a ng-click="set_tab('call_numbers')">
-                [% l('Call Numbers') %]
-            </a>
-        </li>
-        <li ng-class="{active : current_tab == 'settings'}">
-            <a ng-click="set_tab('settings')">
-                [% l('Settings') %]
-            </a>
-        </li>
-        <li ng-class="{active : current_tab == 'template'}">
-            <a ng-click="set_tab('template')">
-                [% l('Label Template') %]
-            </a>
-        </li>
-    </ul>
-    <div class="tab-content">
-        <div class="tab-pane active">
-            <div ng-show="current_tab == 'cn_template'">
-                <h4>
-                    [% l('Call Number Preview') %]
-                </h4>
-                <div eg-print-template-output ng-show="true"
-                    content="print.cn_template_content"
-                    context="{ copy : preview_scope.copies[0], get_cn_and_location_prefix : preview_scope.get_cn_and_location_prefix, get_cn_and_location_suffix : preview_scope.get_cn_and_location_suffix, settings : preview_scope.settings }"></div>
-                <h4>
+    <div class="col-md-5">
+        <ul class="nav nav-tabs">
+            <li ng-class="{active : current_tab == 'cn_template'}">
+                <a ng-click="set_tab('cn_template')">
                     [% l('Call Number Template') %]
-                </h4>
-                <div><span>[% l('Changes here will wipe out manual changes in the Call Numbers tab.') %]<br/></span></div>
-                <textarea ng-model="print.cn_template_content" class="print-template-text">
-                </textarea>
-                <div ng-repeat="copy in preview_scope.copies">
-                    <div id="cn_for_copy_{{copy.id}}" eg-print-template-output ng-show="false"
+                </a>
+            </li>
+            <li ng-class="{active : current_tab == 'call_numbers'}">
+                <a ng-click="set_tab('call_numbers')">
+                    [% l('Call Numbers') %]
+                </a>
+            </li>
+            <li ng-class="{active : current_tab == 'settings'}">
+                <a ng-click="set_tab('settings')">
+                    [% l('Settings') %]
+                </a>
+            </li>
+            <li ng-class="{active : current_tab == 'template'}">
+                <a ng-click="set_tab('template')">
+                    [% l('Label Template') %]
+                </a>
+            </li>
+        </ul>
+        <div class="tab-content">
+            <div class="tab-pane active">
+                <div ng-show="current_tab == 'cn_template'">
+                    <h4>
+                        [% l('Call Number Preview') %]
+                    </h4>
+                    <div eg-print-template-output ng-show="true"
                         content="print.cn_template_content"
-                        context="{ copy : copy, get_cn_and_location_prefix : preview_scope.get_cn_and_location_prefix, get_cn_and_location_suffix : preview_scope.get_cn_and_location_suffix, settings : preview_scope.settings }"></div>
-                </div>
-            </div>
-            <div ng-show="current_tab == 'call_numbers'">
-                <h4>
-                    [% l('Formatted Call Numbers') %]
-                </h4>
-                <div><span>[% l('Manual adjustments may be made here. These do not get saved with templates.') %]<br/></span></div>
-                <div ng-repeat="cn in rendered_call_number_set">
-                    <textarea ng-model="cn.value" class="cn-template-text">
+                        context="{ copy : preview_scope.copies[0], get_cn_and_location_prefix : preview_scope.get_cn_and_location_prefix, get_cn_and_location_suffix : preview_scope.get_cn_and_location_suffix, settings : preview_scope.settings }"></div>
+                    <h4>
+                        [% l('Call Number Template') %]
+                    </h4>
+                    <div>
+                        <span>
+                            [% l('Changes here will wipe out manual changes in the Call Numbers tab.') %]<br/>
+                        </span>
+                    </div>
+                    <textarea ng-model="print.cn_template_content" class="print-template-text">
                     </textarea>
+                    <div ng-repeat="copy in preview_scope.copies">
+                        <div id="cn_for_copy_{{copy.id}}" eg-print-template-output ng-show="false"
+                            content="print.cn_template_content"
+                            context="{ copy : copy, get_cn_and_location_prefix : preview_scope.get_cn_and_location_prefix, get_cn_and_location_suffix : preview_scope.get_cn_and_location_suffix, settings : preview_scope.settings }"></div>
+                    </div>
                 </div>
-            </div>
-            <div ng-show="current_tab == 'settings'">
-                <div><span>[% l('These settings do get saved with templates and will override corresponding Library Settings.') %]<br/></span></div>
-                <div ng-repeat="s in org_unit_settings">
-                    <div class="row" style="margin-top: 5mm;">
-                        <div class="col-md-6"><label>{{s.label}} <eg-help-popover help-text="{{s.description}}"></label></div>
-                        <div class="col-md-6"><input type="text" ng-model="preview_scope.settings[s.name]"></input></div>
+                <div ng-show="current_tab == 'call_numbers'">
+                    <h4>
+                        [% l('Formatted Call Numbers') %]
+                    </h4>
+                    <div>
+                        <span>
+                            [% l('Manual adjustments may be made here. These do not get saved with templates.') %]<br/>
+                        </span>
+                    </div>
+                    <div ng-repeat="cn in rendered_call_number_set">
+                        <textarea ng-model="cn.value" class="cn-template-text">
+                        </textarea>
                     </div>
                 </div>
-
-            </div>
-            <div ng-show="current_tab == 'template'">
-                <div ng-if="print.load_failed" class="alert alert-danger">
-                  [% l(
-                    "Unable to load template '[_1]'.  The web server returned an error.",
-                    '{{print.template_name}}')
-                  %]
+                <div ng-show="current_tab == 'settings'">
+                    <div>
+                        <span>
+                          <b>*All settings will be saved with templates</b>
+                        </span>
+                    </div>
+                    <form id="print_label_form" name="print_label_form">
+                        <div class="print-label-toolbox">
+                            <div class="eg-print-label-section">
+                                <h4>Print Feed Option</h4>
+                                <ul>
+                                    <li ng-repeat="x in preview_scope.toolbox_settings.feed_option.options">
+                                        <label>{{x.label}}:</label>
+                                        <input name="label-feed-option" ng-model="preview_scope.toolbox_settings.feed_option.selected" ng-change="redraw_label_table()" type="radio" value="{{x.value}}" />
+                                    </li>
+                                </ul>
+                            </div>
+                            <div class="eg-print-label-section">
+                                <h4>Print Option</h4>
+                                <ul>
+                                    <li ng-repeat="x in preview_scope.toolbox_settings.mode.options">
+                                        <label>{{x.label}}:</label>
+                                        <input name="label-output-mode" ng-model="preview_scope.toolbox_settings.mode.selected" type="radio" value="{{x.value}}" />
+                                    </li>
+                                </ul>
+                            </div>
+                            <h3>Page Settings</h3>
+                            <div class="eg-print-label-section">
+                                <h4>Page Margins</h4>
+                                <p>Must be in format "0.25in", "2.5cm", "1.5in", etc. (units of measurement must be valid CSS)</p>
+                                <ul>
+                                    <li ng-repeat="(x, y) in preview_scope.toolbox_settings.page.margins">
+                                        <label>{{y.label}}:</label>
+                                        <input type="text" eg-print-label-valid-css ng-model="preview_scope.toolbox_settings.page.margins[x].size" required />
+                                    </li>
+                                </ul>
+                            </div>
+                            <div class="eg-print-label-section">
+                                <h4>Print Grid Size</h4>
+                                <p>Please specify the number of columns and rows (if applicable) on the print medium. This setting is only used to control page layouts and breaks. It will not limit the number of rows printed, which is determined by the number of selected copies. The column count should include each label in a row. For example, if you have a row with two sets of labels that include one spine and one pocket, the count is four.</p>
+                                <ul>
+                                    <li ng-if="preview_scope.toolbox_settings.feed_option.selected === 'sheet'">
+                                        <label>Rows:</label>
+                                        <input type="number" ng-model="preview_scope.toolbox_settings.page.dimensions.rows" ng-min="1" required />
+                                    </li>
+                                    <li>
+                                        <label>Columns:</label>
+                                        <input type="number" ng-model="preview_scope.toolbox_settings.page.dimensions.columns" ng-min="1" required />
+                                    </li>
+                                </ul>
+                            </div>
+                            <div class="eg-print-label-section">
+                                <label>Gap Between Rows</label> <input type="text" ng-model="preview_scope.toolbox_settings.page.space_between_labels.vertical.size" ng-min="0" eg-print-label-valid-css required />
+                                <p>Must be in format "0.25in", "2.5cm", "1.5in", etc. (units of measurement must be valid CSS)</p><br />
+                                <label>Gap Between Columns of a Label Set</label>
+                              <input type="text" ng-model="preview_scope.toolbox_settings.page.label.gap.size" ng-min="0" eg-print-label-valid-css required />
+                                <p>Gap between a label “set” (i.e. gap between each spine/pocket combo). Use label 2 left margin to identify how much space to provide between labels within one set. Must be in format "0.25in", "2.5cm", "1.5in", etc. (units of measurement must be valid CSS)</p>
+                            </div>
+                            <div class="eg-print-label-section" ng-if="preview_scope.toolbox_settings.page.dimensions.rows > 1 || preview_scope.toolbox_settings.page.dimensions.columns > 1">
+                                <h4>Starting Position on Label Grid</h4>
+                                <p>Specify the starting row and column where labels should begin printing. Values must not exceed the respective row and column size specified in "Print Grid Size"</p>
+                                <ul>
+                                    <li ng-if="preview_scope.toolbox_settings.feed_option.selected === 'sheet'">
+                                        <label>Row:</label>
+                                        <input type="number" eg-print-label-valid-int eg-print-label-row-bounds ng-model="preview_scope.toolbox_settings.page.start_position.row" ng-min="1" required />
+                                    </li>
+                                    <li>
+                                        <label>Column:</label>
+                                        <input type="number" eg-print-label-valid-int="" eg-print-label-column-bounds ng-model="preview_scope.toolbox_settings.page.start_position.column" ng-min="1"  required />
+                                    </li>
+                                </ul>
+                            </div>
+                            <div class="eg-print-label-section">
+                                <h4>Label Set Configuration</h4>
+                                <label>Number of Labels in Set:</label>
+                              <input type="number" eg-print-label-valid-int="" ng-min="1" ng-model="preview_scope.toolbox_settings.page.label.set.size" />
+                              <br />
+                              <br />
+                                <ul ng-if="preview_scope.valid_print_label_start_column() && preview_scope.valid_print_label_start_row() && preview_scope.toolbox_settings.mode.selected === 'spine-pocket'">
+                                    <li ng-repeat="i in preview_scope.toolbox_settings.page.label.set.size | columnRowRange">
+                                        <label>Col. {{i + 1}}</label>
+                                        <select id="eg_print_label_column_spec_{{i}}" ng-model="preview_scope.toolbox_settings.page.column_class[i]" ng-change="redraw_label_table()">
+                                            <option value="spine">Label 1</option>
+                                            <option value="pocket">Label 2</option>
+                                        </select>
+                                    </li>
+                                </ul>
+                              <br />
+                            </div>                          
+                        </div>                
+                    </form>
+                    <div>
+                        <span>
+                            [% l('These settings do get saved with templates and will override corresponding Library Settings.') %]<br/>
+                        </span>
+                    </div>
+                    <div ng-repeat="s in org_unit_settings">
+                        <div class="row" style="margin-top: 5mm;">
+                        <div class="col-md-6"><label>{{s.label}} <eg-help-popover help-text="{{s.description}}"></label></div>
+                            <div class="col-md-6">
+                                <input type="text" ng-model="preview_scope.settings[s.name]"></input>
+                            </div>
+                        </div>
+                    </div>
                 </div>
-                <div>
-                  <textarea ng-model="print.template_content" class="print-template-text">
-                  </textarea>
+                <div ng-show="current_tab == 'template'">
+                    <div ng-if="print.load_failed" class="alert alert-danger">
+                        [% l(
+                        "Unable to load template '[_1]'.  The web server returned an error.",
+                        '{{print.template_name}}')
+                        %]
+                    </div>
+                    <div>
+                        <textarea ng-model="print.template_content" class="print-template-text">
+                        </textarea>
+                    </div>
                 </div>
             </div>
         </div>
     </div>
-  </div>
-  <div class="col-md-7">
-    <h3>
-        [% l('Label Preview') %]
-    </h3>
-    <div eg-print-template-output
-      content="print.template_content"
-      context="preview_scope"></div>
-  </div> <!-- col -->
+    <div class="col-md-7">
+        <h3>
+            [% l('Label Preview') %]
+        </h3>
+        <div eg-print-template-output
+          content="print.template_content"
+          context="preview_scope"></div>
+    </div>
+    <!-- col -->
 </div>
 
index 28b78d1..9d006b8 100644 (file)
@@ -5,69 +5,69 @@
 angular.module('egPrintLabels',
     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
 
-.config(function($routeProvider, $locationProvider, $compileProvider) {
+.config(function ($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
-       
+
     var resolver = {
-        delay : ['egStartup', function(egStartup) { return egStartup.go(); }]
+        delay: ['egStartup', function (egStartup) { return egStartup.go(); }]
     };
 
     $routeProvider.when('/cat/printlabels/:dataKey', {
         templateUrl: './cat/printlabels/t_view',
         controller: 'LabelCtrl',
-        resolve : resolver
+        resolve: resolver
     });
 
 })
 
-.factory('itemSvc', 
+.factory('itemSvc',
        ['egCore',
-function(egCore) {
+function (egCore) {
 
     var service = {
-        copies : [], // copy barcode search results
-        index : 0 // search grid index
+        copies: [], // copy barcode search results
+        index: 0 // search grid index
     };
 
-    service.flesh = {   
-        flesh : 3, 
-        flesh_fields : {
-            acp : ['call_number','location','status','location','floating','circ_modifier','age_protect','circ_lib'],
-            acn : ['record','prefix','suffix','owning_lib'],
-            bre : ['simple_record','creator','editor']
+    service.flesh = {
+        flesh: 3,
+        flesh_fields: {
+            acp: ['call_number', 'location', 'status', 'location', 'floating', 'circ_modifier', 'age_protect'],
+            acn: ['record', 'prefix', 'suffix'],
+            bre: ['simple_record', 'creator', 'editor']
         },
-        select : { 
+        select: {
             // avoid fleshing MARC on the bre
             // note: don't add simple_record.. not sure why
-            bre : ['id','tcn_value','creator','editor'],
-        } 
+            bre: ['id', 'tcn_value', 'creator', 'editor'],
+        }
     }
 
     // resolved with the last received copy
-    service.fetch = function(barcode, id, noListDupes) {
+    service.fetch = function (barcode, id, noListDupes) {
         var promise;
 
         if (barcode) {
-            promise = egCore.pcrud.search('acp', 
-                {barcode : barcode, deleted : 'f'}, service.flesh);
+            promise = egCore.pcrud.search('acp',
+                { barcode: barcode, deleted: 'f' }, service.flesh);
         } else {
             promise = egCore.pcrud.retrieve('acp', id, service.flesh);
         }
 
         var lastRes;
         return promise.then(
-            function() {return lastRes},
+            function () { return lastRes },
             null, // error
 
             // notify reads the stream of copies, one at a time.
-            function(copy) {
+            function (copy) {
 
                 var flatCopy;
                 if (noListDupes) {
                     // use the existing copy if possible
                     flatCopy = service.copies.filter(
-                        function(c) {return c.id == copy.id()})[0];
+                        function (c) { return c.id == copy.id() })[0];
                 }
 
                 if (!flatCopy) {
@@ -77,8 +77,8 @@ function(egCore) {
                 }
 
                 return lastRes = {
-                    copy : copy, 
-                    index : flatCopy.index
+                    copy: copy,
+                    index: flatCopy.index
                 }
             }
         );
@@ -90,19 +90,66 @@ function(egCore) {
 /**
  * Label controller!
  */
-.controller('LabelCtrl', 
-       ['$scope','$q','$window','$routeParams','$location','$timeout','egCore','egNet','ngToast','itemSvc',
-function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , ngToast , itemSvc ) {
+.controller('LabelCtrl',
+       ['$scope', '$q', '$window', '$routeParams', '$location', '$timeout', 'egCore', 'egNet', 'ngToast', 'itemSvc', 'labelOutputRowsFilter',
+function ($scope, $q, $window, $routeParams, $location, $timeout, egCore, egNet, ngToast, itemSvc, labelOutputRowsFilter) {
 
     var dataKey = $routeParams.dataKey;
     console.debug('dataKey: ' + dataKey);
 
     $scope.print = {
-        template_name : 'item_label',
-        template_output : '',
-        template_context : 'default'
+        template_name: 'item_label',
+        template_output: '',
+        template_context: 'default'
     };
 
+    var toolbox_settings = {
+        feed_option: {
+            options: [
+                { label: "Continuous", value: "continuous" },
+                { label: "Sheet", value: "sheet" },
+            ],
+            selected: "continuous"
+        },
+        label_set: {
+            margin_between: 0,
+            size: 1
+        },
+        mode: {
+            options: [
+                { label: "Label 1 Only", value: "spine-only" },
+                { label: "Labels 1 & 2", value: "spine-pocket" }
+            ],
+            selected: "spine-pocket"
+        },
+        page: {
+            column_class: ["spine"],
+            dimensions: {
+                columns: 2,
+                rows: 1
+            },
+            label: {
+                gap: {
+                    size: 0
+                },
+                set: {
+                    size: 2
+                }
+            },
+            margins: {
+                top: { size: 0, label: "Top" },
+                left: { size: 0, label: "Left" },
+            },
+            space_between_labels: {
+                horizontal: { size: 0, label: "Horizontal" },
+                vertical: { size: 0, label: "Vertical" }
+            },
+            start_position: {
+                column: 1,
+                row: 1
+            }
+        }
+    };
 
     if (dataKey && dataKey.length > 0) {
 
@@ -115,9 +162,10 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             if (data) {
 
                 $scope.preview_scope = {
-                     'copies' : []
-                    ,'settings' : {}
-                    ,'get_cn_for' : function(copy) {
+                    'copies': []
+                    , 'settings': {}
+                    , 'toolbox_settings': toolbox_settings
+                    , 'get_cn_for': function (copy) {
                         var key = $scope.rendered_cn_key_by_copy_id[copy.id];
                         if (key) {
                             var manual_cn = $scope.rendered_call_number_set[key];
@@ -130,22 +178,22 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                             return '...';
                         }
                     }
-                    ,'get_bib_for' : function(copy) {
+                    , 'get_bib_for': function (copy) {
                         return $scope.record_details[copy['call_number.record.id']];
                     }
-                    ,'get_cn_prefix' : function(copy) {
+                    , 'get_cn_prefix': function (copy) {
                         return copy['call_number.prefix.label'];
                     }
-                    ,'get_cn_suffix' : function(copy) {
+                    , 'get_cn_suffix': function (copy) {
                         return copy['call_number.suffix.label'];
                     }
-                    ,'get_location_prefix' : function(copy) {
+                    , 'get_location_prefix': function (copy) {
                         return copy['location.label_prefix'];
                     }
-                    ,'get_location_suffix' : function(copy) {
+                    , 'get_location_suffix': function (copy) {
                         return copy['location.label_suffix'];
                     }
-                    ,'get_cn_and_location_prefix' : function(copy,separator) {
+                    , 'get_cn_and_location_prefix': function (copy, separator) {
                         var acpl_prefix = copy['location.label_prefix'] || '';
                         var cn_prefix = copy['call_number.prefix.label'] || '';
                         var prefix = acpl_prefix + ' ' + cn_prefix;
@@ -153,7 +201,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                         if (separator && prefix != '') { prefix += separator; }
                         return prefix;
                     }
-                    ,'get_cn_and_location_suffix' : function(copy,separator) {
+                    , 'get_cn_and_location_suffix': function (copy, separator) {
                         var acpl_suffix = copy['location.label_suffix'] || '';
                         var cn_suffix = copy['call_number.suffix.label'] || '';
                         var suffix = cn_suffix + ' ' + acpl_suffix;
@@ -161,6 +209,12 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                         if (separator && suffix != '') { suffix = separator + suffix; }
                         return suffix;
                     }
+                    , 'valid_print_label_start_column': function () {
+                        return !angular.isNumber(toolbox_settings.page.dimensions.columns) || !angular.isNumber(toolbox_settings.page.start_position.column) ? false : (toolbox_settings.page.start_position.column <= toolbox_settings.page.dimensions.columns);
+                    }
+                    , 'valid_print_label_start_row': function () {
+                        return !angular.isNumber(toolbox_settings.page.dimensions.rows) || !angular.isNumber(toolbox_settings.page.start_position.row) ? false : (toolbox_settings.page.start_position.row <= toolbox_settings.page.dimensions.rows);
+                    }
                 };
                 $scope.record_details = {};
                 $scope.org_unit_settings = {};
@@ -168,33 +222,33 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                 var promises = [];
                 $scope.org_unit_setting_list = [
                      'webstaff.cat.label.font.family'
-                    ,'webstaff.cat.label.font.size'
-                    ,'webstaff.cat.label.font.weight'
-                    ,'webstaff.cat.label.inline_css'
-                    ,'webstaff.cat.label.left_label.height'
-                    ,'webstaff.cat.label.left_label.left_margin'
-                    ,'webstaff.cat.label.left_label.width'
-                    ,'webstaff.cat.label.right_label.height'
-                    ,'webstaff.cat.label.right_label.left_margin'
-                    ,'webstaff.cat.label.right_label.width'
-                    ,'webstaff.cat.label.call_number_wrap_filter_height'
-                    ,'webstaff.cat.label.call_number_wrap_filter_width'
+                    , 'webstaff.cat.label.font.size'
+                    , 'webstaff.cat.label.font.weight'
+                    , 'webstaff.cat.label.inline_css'
+                    , 'webstaff.cat.label.left_label.height'
+                    , 'webstaff.cat.label.left_label.left_margin'
+                    , 'webstaff.cat.label.left_label.width'
+                    , 'webstaff.cat.label.right_label.height'
+                    , 'webstaff.cat.label.right_label.left_margin'
+                    , 'webstaff.cat.label.right_label.width'
+                    , 'webstaff.cat.label.call_number_wrap_filter_height'
+                    , 'webstaff.cat.label.call_number_wrap_filter_width'
                 ];
 
                 promises.push(
-                    egCore.pcrud.search('coust',{name:$scope.org_unit_setting_list}).then(
+                    egCore.pcrud.search('coust', { name: $scope.org_unit_setting_list }).then(
                          null
-                        ,null
-                        ,function(yaous) {
+                        , null
+                        , function (yaous) {
                             $scope.org_unit_settings[yaous.name()] = egCore.idl.toHash(yaous, true);
                         }
                     )
                 );
 
                 promises.push(
-                    egCore.org.settings($scope.org_unit_setting_list).then(function(res) {
+                    egCore.org.settings($scope.org_unit_setting_list).then(function (res) {
                         $scope.preview_scope.settings = res;
-                        egCore.hatch.getItem('cat.printlabels.last_settings').then(function(last_settings) {
+                        egCore.hatch.getItem('cat.printlabels.last_settings').then(function (last_settings) {
                             if (last_settings) {
                                 for (s in last_settings) {
                                     $scope.preview_scope.settings[s] = last_settings[s];
@@ -204,20 +258,20 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                     })
                 );
 
-                angular.forEach(data.copies, function(copy) {
+                angular.forEach(data.copies, function (copy) {
                     promises.push(
-                        itemSvc.fetch(null,copy).then(function(res) {
+                        itemSvc.fetch(null, copy).then(function (res) {
                             var flat_copy = egCore.idl.toHash(res.copy, true);
                             $scope.preview_scope.copies.push(flat_copy);
-                            $scope.record_details[ flat_copy['call_number.record.id'] ] = 1;
+                            $scope.record_details[flat_copy['call_number.record.id']] = 1;
                         })
                     )
                 });
 
-                $q.all(promises).then(function() {
+                $q.all(promises).then(function () {
 
                     var promises2 = [];
-                    angular.forEach($scope.record_details, function(el,k,obj) {
+                    angular.forEach($scope.record_details, function (el, k, obj) {
                         promises2.push(
                             egNet.request(
                                 'open-ils.search',
@@ -229,7 +283,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                         );
                     });
 
-                    $q.all(promises2).then(function() {
+                    $q.all(promises2).then(function () {
                         // today, staff, current_location, etc.
                         egCore.print.fleshPrintScope($scope.preview_scope);
                         $scope.template_changed(); // load the default
@@ -246,13 +300,13 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
     }
 
     $scope.fetchTemplates = function (set_default) {
-        return egCore.hatch.getItem('cat.printlabels.templates').then(function(t) {
+        return egCore.hatch.getItem('cat.printlabels.templates').then(function (t) {
             if (t) {
                 $scope.templates = t;
                 $scope.template_name_list = Object.keys(t);
                 if (set_default) {
-                    egCore.hatch.getItem('cat.printlabels.default_template').then(function(d) {
-                        if ($scope.template_name_list.indexOf(d,0) > -1) {
+                    egCore.hatch.getItem('cat.printlabels.default_template').then(function (d) {
+                        if ($scope.template_name_list.indexOf(d, 0) > -1) {
                             $scope.template_name = d;
                         }
                     });
@@ -269,6 +323,10 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         for (var s in $scope.templates[n].settings) {
             $scope.preview_scope.settings[s] = $scope.templates[n].settings[s];
         }
+        if ($scope.templates[n].toolbox_settings) {
+            $scope.preview_scope.toolbox_settings = $scope.templates[n].toolbox_settings;
+            $scope.create_print_label_table();
+        }
         egCore.hatch.setItem('cat.printlabels.default_template', n);
         $scope.save_locally();
     }
@@ -281,7 +339,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
             $scope.fetchTemplates();
             ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_DELETE);
-            egCore.hatch.getItem('cat.printlabels.default_template').then(function(d) {
+            egCore.hatch.getItem('cat.printlabels.default_template').then(function (d) {
                 if (d && d == n) {
                     egCore.hatch.removeItem('cat.printlabels.default_template');
                 }
@@ -293,10 +351,11 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         if (n) {
 
             $scope.templates[n] = {
-                 content : $scope.print.template_content
-                ,context : $scope.print.template_context
-                ,cn_content : $scope.print.cn_template_content
-                ,settings : $scope.preview_scope.settings
+                content: $scope.print.template_content
+                , context: $scope.print.template_context
+                , cn_content: $scope.print.cn_template_content
+                , settings: $scope.preview_scope.settings
+                , toolbox_settings: $scope.preview_scope.toolbox_settings
             };
             $scope.template_name_list = Object.keys($scope.templates);
 
@@ -313,53 +372,52 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
     }
 
     $scope.templates = {};
-    $scope.imported_templates = { data : '' };
+    $scope.imported_templates = { data: '' };
     $scope.template_name = '';
     $scope.template_name_list = [];
 
-    $scope.print_labels = function() {
+    $scope.print_labels = function () {
         return egCore.print.print({
-            context : $scope.print.template_context,
-            template : $scope.print.template_name,
-            scope : $scope.preview_scope,
+            context: $scope.print.template_context,
+            template: $scope.print.template_name,
+            scope: $scope.preview_scope,
         });
     }
 
-    $scope.template_changed = function() {
+    $scope.template_changed = function () {
         $scope.print.load_failed = false;
         egCore.print.getPrintTemplate('item_label')
         .then(
-            function(html) { 
+            function (html) {
                 $scope.print.template_content = html;
             },
-            function() {
+            function () {
                 $scope.print.template_content = '';
                 $scope.print.load_failed = true;
             }
         );
         egCore.print.getPrintTemplateContext('item_label')
-        .then(function(template_context) {
+        .then(function (template_context) {
             $scope.print.template_context = template_context;
         });
         egCore.print.getPrintTemplate('item_label_cn')
         .then(
-            function(html) {
+            function (html) {
                 $scope.print.cn_template_content = html;
             },
-            function() {
+            function () {
                 $scope.print.cn_template_content = '';
                 $scope.print.load_failed = true;
             }
         );
-        egCore.hatch.getItem('cat.printlabels.last_settings').then(function(s) {
+        egCore.hatch.getItem('cat.printlabels.last_settings').then(function (s) {
             if (s) {
                 $scope.preview_scope.settings = s;
             }
         });
-
     }
 
-    $scope.reset_to_default = function() {
+    $scope.reset_to_default = function () {
         egCore.print.removePrintTemplate(
             'item_label'
         );
@@ -374,14 +432,14 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             $scope.preview_scope.settings[s] = undefined;
         }
         $scope.preview_scope.settings = {};
-        egCore.org.settings($scope.org_unit_setting_list).then(function(res) {
+        egCore.org.settings($scope.org_unit_setting_list).then(function (res) {
             $scope.preview_scope.settings = res;
         });
 
         $scope.template_changed();
     }
 
-    $scope.save_locally = function() {
+    $scope.save_locally = function () {
         egCore.print.storePrintTemplate(
             'item_label',
             $scope.print.template_content
@@ -397,17 +455,18 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         egCore.hatch.setItem('cat.printlabels.last_settings', $scope.preview_scope.settings);
     }
 
-    $scope.imported_print_templates = { data : '' };
-    $scope.$watch('imported_templates.data', function(newVal, oldVal) {
+    $scope.imported_print_templates = { data: '' };
+    $scope.$watch('imported_templates.data', function (newVal, oldVal) {
         if (newVal && newVal != oldVal) {
             try {
                 var data = JSON.parse(newVal);
-                angular.forEach(data, function(el,k) {
+                angular.forEach(data, function (el, k) {
                     $scope.templates[k] = {
-                         content : el.content
-                        ,context : el.context
-                        ,cn_content : el.cn_content
-                        ,settings : el.settings
+                        content: el.content
+                        , context: el.context
+                        , cn_content: el.cn_content
+                        , settings: el.settings
+                        , toolbox_settings: el.toolbox_settings
                     };
                 });
                 $scope.saveTemplate();
@@ -421,18 +480,18 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
 
     $scope.rendered_call_number_set = {};
     $scope.rendered_cn_key_by_copy_id = {};
-    $scope.rebuild_cn_set = function() {
-        $timeout(function(){
+    $scope.rebuild_cn_set = function () {
+        $timeout(function () {
             $scope.rendered_call_number_set = {};
             $scope.rendered_cn_key_by_copy_id = {};
             for (var i = 0; i < $scope.preview_scope.copies.length; i++) {
                 var copy = $scope.preview_scope.copies[i];
-                var rendered_cn = document.getElementById('cn_for_copy_'+copy.id);
+                var rendered_cn = document.getElementById('cn_for_copy_' + copy.id);
                 if (rendered_cn && rendered_cn.textContent) {
                     var key = rendered_cn.textContent;
                     if (typeof $scope.rendered_call_number_set[key] == 'undefined') {
                         $scope.rendered_call_number_set[key] = {
-                            value : key
+                            value: key
                         };
                     }
                     $scope.rendered_cn_key_by_copy_id[copy.id] = key;
@@ -442,46 +501,191 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         });
     }
 
-    $scope.$watch('print.cn_template_content', function(newVal, oldVal) {
+    $scope.create_print_label_table = function () {
+        if ($scope.print_label_form.$valid && $scope.print.template_content && $scope.preview_scope) {
+            $scope.preview_scope.label_output_copies = labelOutputRowsFilter($scope.preview_scope.copies, $scope.preview_scope.toolbox_settings);
+            var html = $scope.print.template_content;
+            var d = new Date(); //Added to table ID with 'eg_plt_' to cause $complie on $scope.print.template_content to fire due to template content change.
+            var table = "<table id=\"eg_plt_" + d.getTime().toString() + "_{{$index}}\" eg-print-label-table style=\"border-collapse: collapse; border: 0 solid transparent; border-spacing: 0; margin: {{$index === 0 ?toolbox_settings.page.margins.top.size : 0}} 0 0 0;\" class=\"custom-label-table{{$index % toolbox_settings.page.dimensions.rows === 0 && $index > 0 && toolbox_settings.feed_option.selected === 'sheet' ? ' page-break' : ''}}\" ng-init=\"parentIndex = $index\" ng-repeat=\"row in label_output_copies\">\n";
+            table += "<tr>\n";
+            table += "<td style=\"border: 0 solid transparent; padding: {{parentIndex % toolbox_settings.page.dimensions.rows === 0 && toolbox_settings.feed_option.selected === 'sheet' && parentIndex > 0 ? toolbox_settings.page.space_between_labels.vertical.size : parentIndex > 0 ? toolbox_settings.page.space_between_labels.vertical.size : 0}} 0 0 {{$index === 0 ? toolbox_settings.page.margins.left.size : col.styl ? col.styl : toolbox_settings.page.space_between_labels.horizontal.size}};\" ng-repeat=\"col in row.columns\">\n";
+            table += "<pre class=\"{{col.cls}}\" style=\"border: none; margin-bottom: 0; margin-top: 0; overflow: hidden;\" ng-if=\"col.cls === 'spine'\">\n";
+            table += "{{col.c ? get_cn_for(col.c) : ''}}";
+            table += "</pre>\n";
+            table += "<pre class=\"{{col.cls}}{{parentIndex % toolbox_settings.page.dimensions.rows === 0 && parentIndex > 0 && toolbox_settings.feed_option.selected === 'sheet' ? ' page-break' : ''}}\" style=\"border: none;  margin-bottom: 0; margin-top: 0; overflow: hidden;\" ng-if=\"col.cls === 'pocket'\">\n";
+            table += "{{col.c ? col.c.barcode : ''}}\n";
+            table += "{{col.c ? col.c['call_number.label'] : ''}}\n";
+            table += "{{col.c ? get_bib_for(col.c).author : ''}}\n";
+            table += "{{col.c ? (get_bib_for(col.c).title | wrap:28:'once':'  ') : ''}}\n";
+            table += "</pre>\n";
+            table += "</td>\n"
+            table += "</tr>\n";
+            table += "</table>";
+            var comments = html.match(/\<\!\-\-(?:(?!\-\-\>)(?:.|\s))*\-\-\>\s*/g);
+            html = html.replace(/\<\!\-\-(?:(?!\-\-\>)(?:.|\s))*\-\-\>\s*/g, "");
+            var style = html.match(/\<style[^\>]*\>(?:(?!\<\/style\>)(?:.|\s))*\<\/style\>\s*/gi);
+            var output = (style ? style.join("\n") : "") + (comments ? comments.join("\n") : "") + table;
+            output = output.replace(/\n+/, "\n");
+            $scope.print.template_content = output;
+        }
+    }
+
+    $scope.redraw_label_table = function () {
+        var d = new Date(); //Added to table ID with 'eg_plt_' to cause $complie on $scope.print.template_content to fire due to template content change.
+        var table = "<table id=\"eg_plt_" + d.getTime().toString() + "\"\></table>\n";
+        $scope.print.template_content += table;
+        $scope.create_print_label_table();
+    }
+
+    $scope.$watch('preview_scope.toolbox_settings.page.dimensions.columns',
+        function (newVal, oldVal) {
+            if (newVal && newVal != oldVal && $scope.preview_scope) {
+                $scope.redraw_label_table();
+            }
+        }
+    );
+
+    $scope.$watch('print.cn_template_content', function (newVal, oldVal) {
         if (newVal && newVal != oldVal) {
             $scope.rebuild_cn_set();
         }
     });
 
-    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function(newVal, oldVal) {
+    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function (newVal, oldVal) {
         if (newVal && newVal != oldVal) {
             $scope.rebuild_cn_set();
         }
     });
 
-    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function(newVal, oldVal) {
+    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function (newVal, oldVal) {
         if (newVal && newVal != oldVal) {
             $scope.rebuild_cn_set();
         }
     });
 
+    $scope.$watchGroup(['preview_scope.toolbox_settings.page.margins.top.size', 'preview_scope.toolbox_settings.page.margins.left.size', 'preview_scope.toolbox_settings.page.dimensions.rows', 'preview_scope.toolbox_settings.page.space_between_labels.horizontal.size', 'preview_scope.toolbox_settings.page.space_between_labels.vertical.size', 'preview_scope.toolbox_settings.page.start_position.row', 'preview_scope.toolbox_settings.page.start_position.column', 'preview_scope.toolbox_settings.page.label.gap.size'], function (newVal, oldVal) {
+        if (newVal && newVal != oldVal && $scope.preview_scope.label_output_copies) {
+            $scope.redraw_label_table();
+        }
+    });
+
+    $scope.$watch("preview_scope.toolbox_settings.mode.selected", function (newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            var ts_p = $scope.preview_scope.toolbox_settings.page;
+            if (ts_p.label.set.size === 1) {
+                if (newVal === "spine-pocket") {
+                    ts_p.column_class = ["spine", "pocket"];
+                    ts_p.label.set.size = 2;
+                } else {
+                    ts_p.column_class = ["spine"];
+                }
+            } else {
+                if (newVal === "spine-only") {
+                    for (var i = 0; i < ts_p.label.set.size; i++) {
+                        ts_p.column_class[i] = "spine";
+                    }
+                } else {
+                    ts_p.label.set.size === 2 ? ts_p.column_class = ["spine", "pocket"] : false;
+                }
+            }
+            $scope.redraw_label_table();
+        }
+    });
+
+    $scope.$watch("preview_scope.toolbox_settings.page.label.set.size", function (newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            var ts_p = $scope.preview_scope.toolbox_settings.page;
+            if (angular.isNumber(newVal)) {
+                while (ts_p.column_class.length > ts_p.label.set.size) {
+                    ts_p.column_class.splice((ts_p.column_class.length - 1), 1);
+                }
+                while (ts_p.column_class.length < ts_p.label.set.size) {
+                    ts_p.column_class.push("spine");
+                }
+            }
+            $scope.redraw_label_table();
+        }
+    });
+
     $scope.current_tab = 'call_numbers';
-    $scope.set_tab = function(tab) {
+    $scope.set_tab = function (tab) {
         $scope.current_tab = tab;
     }
 
 }])
 
-// 
-.directive('egPrintTemplateOutput', ['$compile',function($compile) {
-    return function(scope, element, attrs) {
+.directive("egPrintLabelColumnBounds", function () {
+    return {
+        link: function (scope, element, attr, ctrl) {
+            function withinBounds(v) {
+                scope.$watch("preview_scope.toolbox_settings.page.dimensions.columns", function (newVal, oldVal) {
+                    ctrl.$setValidity("egWithinPrintColumnBounds", scope.preview_scope.valid_print_label_start_column())
+                });
+                return v;
+            }
+            ctrl.$parsers.push(withinBounds);
+            ctrl.$formatters.push(withinBounds);
+        },
+        require: "ngModel"
+    }
+})
+
+.directive("egPrintLabelRowBounds", function () {
+    return {
+        link: function (scope, element, attr, ctrl) {
+            function withinBounds(v) {
+                scope.$watch("preview_scope.toolbox_settings.page.dimensions.rows", function (newVal, oldVal) {
+                    ctrl.$setValidity("egWithinPrintRowBounds", scope.preview_scope.valid_print_label_start_row());
+                });
+                return v;
+            }
+            ctrl.$parsers.push(withinBounds);
+            ctrl.$formatters.push(withinBounds);
+        },
+        require: "ngModel"
+    }
+})
+
+.directive("egPrintLabelValidCss", function () {
+    return {
+        require: "ngModel",
+        link: function (scope, element, attr, ctrl) {
+            function floatValidation(v) {
+                ctrl.$setValidity("isFloat", v.toString().match(/^\-*(?:^0$|(?:\d+)(?:\.\d{1,})*([a-z]{2}))$/) ? true : false);
+                return v;
+            }
+            ctrl.$parsers.push(floatValidation);
+        }
+    }
+})
+
+.directive("egPrintLabelValidInt", function () {
+    return {
+        require: "ngModel",
+        link: function (scope, element, attr, ctrl) {
+            function intValidation(v) {
+                ctrl.$setValidity("isInteger", v.toString().match(/^\d+$/));
+                return v;
+            }
+            ctrl.$parsers.push(intValidation);
+        }
+    }
+})
+
+.directive('egPrintTemplateOutput', ['$compile', function ($compile) {
+    return function (scope, element, attrs) {
         scope.$watch(
-            function(scope) {
+            function (scope) {
                 return scope.$eval(attrs.content);
             },
-            function(value) {
+            function (value) {
                 // create an isolate scope and copy the print context
                 // data into the new scope.
                 // TODO: see also print security concerns in egHatch
                 var result = element.html(value);
                 var context = scope.$eval(attrs.context);
                 var print_scope = scope.$new(true);
-                angular.forEach(context, function(val, key) {
+                angular.forEach(context, function (val, key) {
                     print_scope[key] = val;
                 })
                 $compile(element.contents())(print_scope);
@@ -490,8 +694,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
     };
 }])
 
-.filter('cn_wrap', function() {
-    return function(input, w, h, wrap_type) {
+.filter('cn_wrap', function () {
+    return function (input, w, h, wrap_type) {
         var names;
         var prefix = input[0];
         var callnum = input[1];
@@ -506,11 +710,11 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                on the spine label: subclass letters, subclass numbers, cutter numbers, trailing stuff (date) */
             var patt1 = /^([A-Z]{1,3})\s*(\d+(?:\.\d+)?)\s*(\.[A-Z]\d*)\s*([A-Z]\d*)?\s*(\d\d\d\d(?:-\d\d\d\d)?)?\s*(.*)$/i;
             var result = callnum.match(patt1);
-            if (result) { 
-                callnum = result.slice(1).join('\t');  
+            if (result) {
+                callnum = result.slice(1).join('\t');
             } else {
                 callnum = callnum.split(/\s+/).join('\t');
-            } 
+            }
 
             /* If result is null, leave callnum alone. Can't parse this malformed call num */
         } else {
@@ -527,17 +731,17 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         /* At this point, the call number pieces are separated by tab characters.  This allows
         *  some space-containing constructs like "v. 1" to appear on one line
         */
-        callnum = callnum.replace(/\t\t/g,'\t');  /* Squeeze out empties */ 
+        callnum = callnum.replace(/\t\t/g, '\t');  /* Squeeze out empties */
         names = callnum.split('\t');
         var j = 0; var tb = [];
         while (j < h) {
-            
+
             /* spine */
             if (j < w) {
 
                 var name = names.shift();
                 if (name) {
-                    name = String( name );
+                    name = String(name);
 
                     /* if the name is greater than the label width... */
                     if (name.length > w) {
@@ -548,20 +752,20 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                             if (name.match(/^\./)) sname[0] = '.' + sname[0];
                             for (var k = 1; k < sname.length; k++) sname[k] = '.' + sname[k];
                             /* and put all but the first one back into the names array */
-                            names = sname.slice(1).concat( names );
+                            names = sname.slice(1).concat(names);
                             /* if the name fragment is still greater than the label width... */
                             if (sname[0].length > w) {
                                 /* then just truncate and throw the rest back into the names array */
-                                tb[j] = sname[0].substr(0,w);
-                                names = [ sname[0].substr(w) ].concat( names );
+                                tb[j] = sname[0].substr(0, w);
+                                names = [sname[0].substr(w)].concat(names);
                             } else {
                                 /* otherwise we're set */
                                 tb[j] = sname[0];
                             }
                         } else {
                             /* if we can't split on periods, then just truncate and throw the rest back into the names array */
-                            tb[j] = name.substr(0,w);
-                            names = [ name.substr(w) ].concat( names );
+                            tb[j] = name.substr(0, w);
+                            names = [name.substr(w)].concat(names);
                         }
                     } else {
                         /* otherwise we're set */
@@ -575,8 +779,58 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
     }
 })
 
-.filter('wrap', function() {
-    return function(input, w, wrap_type, indent) {
+.filter("columnRowRange", function () {
+    return function (i) {
+        var res = [];
+        for (var j = 0; j < i; j++) {
+            res.push(j);
+        }
+        return res;
+    }
+})
+
+//Accepts $scope.preview_scope.copies and $scope.preview_scope.toolbox_settings as its parameters.
+.filter("labelOutputRows", function () {
+    return function (copies, settings) {
+        var cols = [], rows = [];
+        for (var j = 0; j < (settings.page.start_position.row - 1) ; j++) {
+            cols = [];
+            for (var k = 0; k < settings.page.dimensions.columns; k++) {
+                cols.push({ c: null, index: k, cls: getPrintLabelOutputClass(k, settings), styl: getPrintLabelStyle(k, settings) });
+            }
+            rows.push({ columns: cols });
+        }
+        cols = [];
+        for (var j = 0; j < (settings.page.start_position.column - 1) ; j++) {
+            cols.push({ c: null, index: j, cls: getPrintLabelOutputClass(j, settings), styl: getPrintLabelStyle(j, settings) });
+        }
+        var m = cols.length;
+        for (var j = 0; j < copies.length; j++) {
+            for (var n = 0; n < settings.page.label.set.size; n++) {
+                if (m < settings.page.dimensions.columns) {
+                    cols.push({ c: copies[j], index: cols.length, cls: getPrintLabelOutputClass(m, settings), styl: getPrintLabelStyle(m, settings) });
+                    m += 1;
+                }
+                if (m === settings.page.dimensions.columns) {
+                    m = 0;
+                    rows.push({ columns: cols });
+                    cols = [];
+                    n = settings.page.label.set.size;
+                }
+            }
+        }
+        cols.length > 0 ? rows.push({ columns: cols }) : false;
+        if (rows.length > 0) {
+            while ((rows[(rows.length - 1)].columns.length) < settings.page.dimensions.columns) {
+                rows[(rows.length - 1)].columns.push({ c: null, index: rows[(rows.length - 1)].columns.length, cls: getPrintLabelOutputClass(rows[(rows.length - 1)].columns.length, settings), styl: getPrintLabelStyle(rows[(rows.length - 1)].columns.length, settings) });
+            }
+        }
+        return rows;
+    }
+})
+
+.filter('wrap', function () {
+    return function (input, w, wrap_type, indent) {
         var output;
 
         if (!w) return input;
@@ -589,17 +843,17 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                 if_cant_wrap_then_truncate,
                 idx
         ) {
-            if (idx>10) {
+            if (idx > 10) {
                 console.log('possible infinite recursion, aborting');
                 return '';
             }
             if (String(text).length <= length) {
                 return text;
             } else {
-                var truncated_text = String(text).substr(0,length);
+                var truncated_text = String(text).substr(0, length);
                 var pivot_pos = truncated_text.lastIndexOf(' ');
-                var left_chunk = text.substr(0,pivot_pos).replace(/\s*$/,'');
-                var right_chunk = String(text).substr(pivot_pos+1);
+                var left_chunk = text.substr(0, pivot_pos).replace(/\s*$/, '');
+                var right_chunk = String(text).substr(pivot_pos + 1);
 
                 var wrapped_line;
                 if (left_chunk.length == 0) {
@@ -621,7 +875,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                                     length,
                                     false,
                                     if_cant_wrap_then_truncate,
-                                    idx+1)
+                                    idx + 1)
                                 : right_chunk
                             )
                         )
@@ -631,16 +885,23 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             }
         }
 
-        switch(wrap_type) {
+        switch (wrap_type) {
             case 'once':
-                output = wrap_on_space(input,w,true,false,0);
-            break;
+                output = wrap_on_space(input, w, true, false, 0);
+                break;
             default:
-                output = wrap_on_space(input,w,false,false,0);
-            break;
+                output = wrap_on_space(input, w, false, false, 0);
+                break;
         }
 
         return output;
     }
-})
+});
+
+function getPrintLabelOutputClass(index, settings) {
+    return settings.page.column_class[index % settings.page.label.set.size];
+}
 
+function getPrintLabelStyle(index, settings) {
+    return index > 0 && (index % settings.page.label.set.size === 0) ? settings.page.label.gap.size : "";
+}