001    // Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002    // The Molly framework is freely distributable under the terms of an
003    // MIT-style license. For details, see the molly pages web site at:
004    // http://www.mollypages.org/. Use, modify, have fun !
005    
006    package fc.io;
007    
008    import java.io.*;
009    import java.util.*;
010    import java.util.regex.*;
011    
012    import fc.util.*;
013    
014    /** 
015    Prints a table formatted using plain text/ascii characters.
016    Useful for console based output and/or formatted output to a
017    file.
018    
019    @author hursh jain
020    **/
021    public class TablePrinter
022      {
023      private static final boolean dbg = false;
024      private static final Object Extra_Content_Marker = new Object();
025      
026      String      linesep = IOUtil.LINE_SEP;
027      PrintConfig   config;
028      PrintWriter   out;
029      int       columncount;  
030      String      rowborder;  
031      int       cellwidth;
032      String      spacing_str;
033      String      padding_str;
034      boolean     usingSpacing;   
035      boolean     printingHeader;  //printing a header line ?
036      int       current_linenum; //not including headers
037      int       current_cellnum;
038      String[]    wrappedcells;
039      int       wrapcells_left;
040    
041      //use for max cell widths when using autofit
042      int[]     largest_width;
043      //used to cache table data when using autofit
044      //list of String[]
045      List      autoFitTableData = new ArrayList(); 
046      
047      int       currentAutoFitRow;
048      int       currentAutoFitCell;
049      
050      
051      /**
052      Constructs a new table printer.
053      
054      @param  columnCount   the number of columns in the table.
055                  Attemps to print more than these
056                  number of columns will result in 
057                  a runtime exception.
058      @param  pw        the destination print stream
059      @param  config      the printing configuration
060      **/
061      public TablePrinter (int columnCount, PrintStream ps, PrintConfig config) 
062        {
063        this(columnCount, new PrintWriter(ps), config); 
064        }
065    
066      /**
067      Constructs a new table printer.
068      
069      @param  columnCount   the number of columns in the table.
070                  Attemps to print more than these
071                  number of columns will result in 
072                  a runtime exception.
073      @param  pw        the destination print writer
074      @param  config      the printing configuration
075      **/
076      public TablePrinter (int columnCount, PrintWriter pw, PrintConfig config) 
077        {
078        Argcheck.notnull(config, "config was null");
079        Argcheck.notnull(pw, "printwriter/stream was null");
080        this.columncount= columnCount;
081        this.out = pw;
082        this.config = config;
083        cellwidth = config.cellwidth; 
084        padding_str = StringUtil.repeatToWidth(
085          config.cellpadding_glyph, config.cellpadding);
086        
087        spacing_str = StringUtil.repeatToWidth(
088            config.cellspacing_glyph, config.cellspacing);
089    
090        usingSpacing = config.cellspacing > 0;
091        printingHeader = false;
092        wrappedcells = new String[columncount];
093        initRowBorder();
094        }
095    
096    
097      /*******************************************************  
098    
099      Layout rules:
100      upper and lower cell borders look like:
101      
102      +-----+------+-----+------+
103      |   |    |     |    |  
104      |   |    |     |    |
105      +-----+------+-----+------+
106    
107      or:
108      
109      +-----+ +-----+ +-----+
110      |     | |   | |   |
111      |   | |   | |   |
112      +-----+ +-----+ +-----+
113    
114          
115      We don't support cell specific borders and know the 
116      number of columns in the table beforehand.
117      
118      0) startTable
119        - prints the initial table header is applicable
120      
121      1) startRow - 
122        - initializes some variables
123        - if this is a new row, 
124          - writes spacing
125          - writes entire top border
126      
127      2) printcell 
128        - if spacing
129          - write spacing, left border, content, right border
130        - no spacing
131          - write left border, content    
132        - tracks whether row contents need to be wrapped
133        if wrapping needed, sets a flag ( we have to wait
134        until entire row is written before writing a new
135        row containing the wrapped contents )
136      
137      3) endRow
138        - if no spacing 
139          writes last most right border (not needed if we
140          are using spacing, since in that case each cell
141          writes it's right border)
142        - write spacing (if using spacing)
143        - if wrapping is needed, calls startRow again
144        specifying that this is *not* a new row (and
145        hence the new row border will not be written)
146        - if wrapping not needed, and using spacing, writes
147        the bottom border (needed to delimit rows with spacing)
148        - write header row again if applicable (if header rows are 
149        repeated every x number of lines)
150          
151      4) endTable
152        - write border and spacing after the last row (we don't know
153        beforehand the # of rows, so we wait until the last
154        row is written before writing the ending spacing)
155        
156      ********************************************************/
157          
158      /** This method should be called to start the table **/
159      public void startTable() 
160        {
161        if (config.autoFit) {
162          startTableAutoFit();
163          return;
164          }
165        
166        if (config.header != null) 
167          printHeader();
168        }
169      
170      /** This method should be called to finish the table **/
171      public void endTable() 
172        {
173        if (config.autoFit) {
174          endTableAutoFit();
175          return;
176          }
177      
178        if (! usingSpacing) {
179          printRowBorder();
180          }
181          
182        if (usingSpacing) {
183          printVerticalSpacing(); 
184          }
185          
186        //if prinstream->printwriter, then unless we flush,
187        //writes to underlying printstream may not appear
188        //as expected (and the sequence can be screwy if
189        //we are also writing to the underlying printstream
190        //independently
191        out.flush();
192        }
193      
194      /** This method should be called to start a new row **/
195      public void startRow() 
196        {
197        if (config.autoFit)  {
198          startRowAutoFit();
199          return;
200          }
201    
202        current_cellnum = 0;
203          
204        //logical non header lines
205        if (! printingHeader && rowFinished()) 
206          current_linenum++;  //first line = 1
207        
208        if (rowFinished()) 
209          {
210          if (usingSpacing) {
211            printVerticalSpacing(); 
212            }
213          printRowBorder();
214          } 
215        }
216        
217      void endRowCommon() 
218        {
219        if (! usingSpacing) {
220          if (config.printborder) {
221            out.print(config.cellborder_v); //right most border
222            }
223          }
224        else  
225          out.print(spacing_str); //right most spacing
226        
227        out.print(linesep); 
228        } 
229      
230      /** This method should be called to finish the existing row **/
231      public void endRow() 
232        { 
233        if (config.autoFit)  {
234          endRowAutoFit();
235          return;
236          }
237                
238        endRowCommon();
239            
240        if (config.cellwrap) 
241          {
242          while (! rowFinished() ) 
243            {
244            startRow(); 
245            for (int n = 0; n < columncount; n++) {
246              printCell(wrappedcells[n]);
247              }
248            endRowCommon();   
249            }
250          }   
251      
252        if (usingSpacing) {
253          printRowBorder();   
254          }
255        
256        if (! printingHeader && repeatHeader()) {
257          printHeader();
258          }
259        }
260        
261      boolean repeatHeader() 
262        {
263        int pagesize = config.pageSize;
264        
265        if (pagesize <= 0 || ! config.headerEveryPage)
266          return false;
267        
268        if (current_linenum == 0)
269          return false;
270        
271        //System.out.println("current_linenum[" + current_linenum + "] % pagesize[" + pagesize + "] = " + (current_linenum % pagesize));
272      
273        if ((current_linenum % pagesize) == 0) {
274          return true;
275          }
276          
277        return false;
278        }
279    
280      /** 
281      This method should be called to print a new cell in the
282      current row. This method should not be invoked for more
283      columns than the table was instantiated with, otherwise a
284      runtime exception will be thrown.
285      **/
286      public void printCell(String str) 
287        {   
288        if (current_cellnum == columncount)
289          throw new IllegalArgumentException("Cannot add more cells than number of columns in this table. (max columns=" + columncount + ")");
290        
291        str = removeEmbeddedSpaces(str);
292            
293        if (config.autoFit)  {
294          printCellAutoFit(str);
295          return;
296          }
297      
298        //left spacing
299        if (usingSpacing) {
300          out.print(spacing_str);
301          }
302            
303        //left border
304        if (config.printborder) {
305          out.print(config.cellborder_v); 
306          }
307    
308        printCellContent(str);
309      
310        //right border if spaced
311        if (usingSpacing) {
312          if (config.printborder) {
313            out.print(config.cellborder_v); 
314            }
315          }   
316      
317        current_cellnum++;
318        }
319          
320      public String toString() {
321        return "TablePrinter. Config = " + config;
322        } 
323      
324      //== impl. methods ===================================
325              
326      void printCellContent(String str) 
327        {   
328        int width = config.getCellWidthForColumn(current_cellnum);
329    
330        int strlen = (str == null) ? 0 : str.length();
331        
332        //align cell contents appropriately
333        String content = StringUtil.fixedWidth(str, width, config.align); 
334        
335        out.print(padding_str);
336        out.print(content);
337        out.print(padding_str);
338        
339        if (dbg) System.out.println("current cell=" + current_cellnum);
340      
341        if (config.cellwrap)
342          {
343          if (strlen > width) 
344            {
345            //don't double count wrapped cells
346            if (wrappedcells[current_cellnum] == null)
347              wrapcells_left++;
348    
349            //this cell needs to be wrapped,save the extra portion
350            wrappedcells[current_cellnum] = str.substring(width, strlen); 
351            }
352          else {
353            if (wrappedcells[current_cellnum] != null)
354              wrapcells_left--;
355            wrappedcells[current_cellnum] = null;
356            }
357          }   
358        if (dbg) System.out.println("Wrapped cells left=" + wrapcells_left + "; Wrapped:" + Arrays.asList(wrappedcells));
359        }
360    
361      boolean rowFinished() {
362        return wrapcells_left == 0;
363        }
364    
365      void printHeader() 
366        {
367        if (config.header == null)
368          return;
369          
370        printingHeader = true;
371        startRow();
372        for (int n = 0; n < config.header.length; n++)
373          printCell(config.header[n]);
374        endRow();
375        printingHeader = false;
376        }
377      
378      void printRowBorder() 
379        {
380        if (! config.printborder)
381          return;
382          
383        out.print(rowborder); 
384        } 
385    
386      private void initRowBorder() 
387        {
388        //new Exception().printStackTrace();
389        int default_padded_cellwidth = 
390            cellwidth + 2 * config.cellpadding;
391        
392        if (! config.printborder)
393          return;
394        
395        StringBuffer buf = new StringBuffer(
396            columncount * (1+default_padded_cellwidth+config.cellspacing));
397          
398        for (int n = 0; n < columncount; n++)
399          {
400          if (usingSpacing)
401            buf.append(spacing_str);
402          buf.append(config.cellcorner);
403          int padded_cellwidth = config.getCellWidthForColumn(n) 
404                      + 2 * config.cellpadding;
405          for (int k = 0; k < padded_cellwidth; k++) 
406            {
407            buf.append(config.cellborder_h);
408            }
409          if (usingSpacing)
410            buf.append(config.cellcorner);
411          }
412        if (! usingSpacing)
413          buf.append(config.cellcorner);
414        buf.append(linesep);
415        rowborder = buf.toString();
416        }
417    
418    void printVerticalSpacing() 
419      {
420      //1 lesser for aesthetics
421      for (int k = 1; k < config.cellspacing; k++) { 
422        out.print(linesep);
423        }
424      }     
425    
426    //== autofit methods ================
427    
428    void startTableAutoFit() {
429      largest_width = new int[columncount];
430      }
431      
432    void endTableAutoFit() 
433      {
434      if (dbg) {
435        for (Iterator it = autoFitTableData.iterator(); it.hasNext(); /*empty*/) {
436          String[] item = (String[]) it.next();
437          System.out.println(Arrays.asList(item));
438          } 
439        }
440        
441      // == 2nd pass ================================== 
442      int rowcount = autoFitTableData.size();
443        if (rowcount == 0)
444          return;
445    
446        //we are already handling autofit rendering, so terminate
447        //further recursion
448        config.setAutoFit(false); 
449    
450        for (int n = 0; n < largest_width.length; n++) {
451        if (dbg) System.out.println("largest_width["+n+"]="+largest_width[n]);
452          config.setCellWidthForColumn(n, largest_width[n]);
453          }
454    
455        //TablePrinter printer = new TablePrinter(columncount, out, config);
456      TablePrinter printer = this;
457      printer.initRowBorder();
458      
459        printer.startTable();
460        int n = 0;
461        while (n < rowcount) 
462          {
463          printer.startRow();
464          for (int k = 0; k < columncount; k++) {
465            printer.printCell(
466            ((String[])autoFitTableData.get(n))[k] );
467            }
468          printer.endRow();
469          n++;
470          }
471        
472        printer.endTable(); 
473      }
474      
475    void startRowAutoFit() {
476      currentAutoFitCell = 0;
477      autoFitTableData.add(currentAutoFitRow, new String[columncount]);
478      }
479      
480    void endRowAutoFit() {
481      currentAutoFitRow++;
482      }
483    
484    void printCellAutoFit(String str) 
485      {
486      int len = (str != null) ? str.length() : "null".length();
487      
488      if (largest_width[currentAutoFitCell] < len)
489         largest_width[currentAutoFitCell] = len;
490    
491      String[] row = (String[]) autoFitTableData.get(currentAutoFitRow);
492      row[currentAutoFitCell++] = str;  
493      }
494    
495    final private Pattern newlines = Pattern.compile("\\r|\\n|\\r\\n");
496    String removeEmbeddedSpaces(String str)
497      {
498      /*
499      we convert embedded newlines in the string to spaces
500      otherwise the embedded newline will cause printing
501      to start at the left most margin. i.e.,
502      
503      baz\n\baz2 will print like
504          
505      A    B    C
506      x  y    baz
507      baz2
508      
509      but we want
510            <---> 
511      A    B      C
512      x  y    baz
513            baz2
514      
515      */
516    
517      if (str != null)
518        {
519        str = newlines.matcher(str).replaceAll(" ");
520        }
521      return str;
522      }
523    
524    /** 
525    Configuration object containing for table printing object.
526    
527    Some methods in this class return <tt>this</tt> object for method
528    chaining convenience. 
529    <p>
530    Note: Alas ! TablePrinter does not support cellspans across
531    columns or rows. That would make things too complicated for
532    this implementation.
533    **/
534    public static class PrintConfig
535      {
536      static final ToString.Style style = new ToString.Style();
537      static {
538        style.reflectVisibleLevel = ToString.Style.VisibleLevel.PRIVATE;
539        }
540          
541      private int    cellwidth       =  20;
542      
543      //individual widths if specified
544      private Map    cellwidthMap    = new HashMap();
545       
546      private String   cellcorner      =  "+";
547      private String   cellborder_h      =  "-";
548      private String   cellborder_v      =  "|";
549      private String   cellpadding_glyph   =  " ";
550      private String   cellspacing_glyph   =  " ";
551      private boolean  cellwrap      =  true;
552      private boolean  autoFit       =  false;
553      private boolean  printborder       =  true;
554      private int    cellspacing       =  0;
555      private int    cellpadding       =  0;
556      private HAlign   align         =  HAlign.LEFT;
557      private int    pageSize      = -1;    
558      private boolean  headerEveryPage   = true;
559      private String[] header;
560      
561      /** 
562      Constructs a new PrintConfig with the following default options. 
563      <ul>
564        <li>cell corner: the <tt>+</tt> character 
565        <li>horizontal cell border: the <tt>-</tt> character 
566        <li>vertical cell border, the <tt>|</tt> character 
567        <li>width of each column: 20 chars 
568        <li>horizontal alignment of each cell: {@link HAlign#LEFT}
569        <li>The character used for cellpadding and cellspacing is a
570        blank space.
571      </ul>
572      **/
573      public PrintConfig() {
574        }
575      
576      /** 
577      Optionally sets the header row for the table **/
578      public void setHeader(String[] header) {
579        this.header = header;
580        } 
581    
582      /** 
583      Gets the header row for the table if set. If not
584      set, return null
585      **/
586      public String[] getHeader() {
587        return this.header;
588        } 
589    
590      
591      /** 
592      Sets the number of lines on each page. A page is a
593      logical unit that typically shows some number of lines
594      on the screen without the need to scroll the page. This
595      value is useful in conjunction with other page specific
596      settings like {@link showPageHeading()}
597      
598      @param  lines number of lines on the page, specify a zero or
599              negative quantity for a single page. (number of
600              lines <b>not</b> including lines occupied by the table 
601              header itself, if the header is printed).
602      **/
603      public PrintConfig setPageSize(int lines) {
604        this.pageSize = lines;
605        return this;
606        }
607        
608      /** 
609      Specifies that page heading (if set) on each separate
610      page. This may be ignored if no heading has been set.
611      <tt>true</tt> by default, although headers will still
612      not be printed until the {@link setPageSize} method is
613      invoked.
614      **/
615      public PrintConfig headerEveryPage(boolean show) {
616        this.headerEveryPage = show;
617        return this;
618        }
619      
620      /** Sets the alignment of each cell. By default: 
621      <tt>{@link HAlign#LEFT left}</tt>
622      **/
623      public PrintConfig setAlign(HAlign align) {
624        this.align = align;
625        return this;
626        }
627    
628      /** 
629      Sets the string (typically a single character) that makes up a 
630      cell corner. Defaults to <tt>+</tt>
631      **/
632      public PrintConfig setCellCorner(String str) {
633        this.cellcorner = str;
634        return this;
635        }
636    
637      /** 
638      Sets the string (typically a single character) that makes up a 
639      horizontal cell border. Defaults to <tt>-</tt>
640      **/
641      public PrintConfig setCellBorderHorizontal(String str) {
642        this.cellborder_h = str;
643        return this;
644        }
645    
646      /** 
647      Sets the string (typically a single character) that makes up a 
648      vertical cell border. Defaults to <tt>|</tt>
649      **/
650      public PrintConfig setCellBorderVertical(String str) {
651        this.cellborder_v = str;
652        return this;
653        }
654    
655      /** 
656      Sets the string (typically a single character) that makes up
657      the cellpadding. Defaults to <tt>" "</tt>
658      **/
659      public PrintConfig setCellPaddingGlyph(String str) {
660        this.cellpadding_glyph = str;
661        return this;
662        }
663    
664      /** 
665      Sets the string (typically a single character) that makes up
666      the cellspacing. Defaults to <tt>" "</tt>
667      **/
668      public PrintConfig setCellSpacingGlyph(String str) {
669        this.cellspacing_glyph = str;
670        return this;
671        }
672    
673      /** 
674      Sets the width of each cell. Defaults to <tt>20</tt>. 
675      This width is common to all cells.
676    
677      @param  width the width of each cell
678      **/
679      public PrintConfig setCellWidth(int width) {
680        if (width <= 0) {
681          throw new IllegalArgumentException("cell width must be greater than zero");
682          }
683        this.cellwidth = width;
684        return this;
685        } 
686    
687      /** 
688      Sets the cell width of the the specified column. cells
689      are numbered starting from 0 
690      
691      @param  column  the cell number
692      @param  width the desired character width
693      **/
694      public PrintConfig setCellWidthForColumn(int column, int width) {
695        cellwidthMap.put(new Integer(column), new Integer(width));
696        return this;
697        }
698      
699      /** 
700      convenience method for internal use:
701      returns the cellwidth if set otherwise the default cellwidth
702      **/
703      int getCellWidthForColumn(int column) 
704        {
705        Object obj = cellwidthMap.get(new Integer(column));
706        if (obj != null) {
707          return ((Integer) obj).intValue();
708          }
709        return cellwidth;
710        }
711      
712      
713      /** 
714      Cell wrapping is on by default and the contents of any column that 
715      exceed the width are wrapped within each cell (using the current platforms line 
716      seperator for newlines within the cell). 
717        
718      @param  wrap <tt>true</tt>  To turn cell wrapping on, <tt>false</tt> 
719                    to turn cell wrapping off and show
720                    fixed width contents only.
721      **/ 
722      public PrintConfig setCellWrap(boolean wrap) {
723        cellwrap = wrap;  
724        return this;
725        }
726      
727      /** 
728      Sets each cell to expand to the size needed for the maximum
729      sized cell in that column. By default, this is <tt>false</tt>.
730      Setting this to <tt>true</tt> automatically turns <b>off</b> 
731      cell wrapping.
732      <p>
733      Note: using autofit may result in slower performance
734      because the table almost always has to be rendered in
735      two passes.
736      
737      @param  autofit   <tt>true</tt> to turn autofit on, 
738                <tt>false</tt> otherwise.
739      **/
740      public PrintConfig setAutoFit(boolean autofit) 
741        {
742        autoFit = autofit;
743        if (autofit)
744          cellwrap = false;
745        return this;
746        } 
747      
748      /**  
749      Specifies whether table and cell borders are printed. By
750      default this is true.
751    
752      @param  print <tt>true</tt> to print borders, <tt>false</tt> otherwise
753      **/
754      public PrintConfig setPrintBorders(boolean print) {
755        printborder = print;
756        return this;
757        }
758      
759      /**  
760      Specifies the cell spacing between cells. This is useful
761      for tables with no borders. By default, this value is
762      zero.
763      **/
764      public PrintConfig setCellSpacing(int width) {
765        cellspacing = width;
766        return this;
767        }
768        
769      /**
770      Specifies the cell padding for each cell. This is useful for
771      tables with no borders. By default, this value is zero.
772      **/
773      public PrintConfig setCellPadding(int width) {
774        cellpadding = width;
775        return this;
776        }
777      
778      /** 
779      Prints a short description of this object. 
780      **/
781      public String toString() 
782        { 
783        return new ToString(this, style).reflect().render();
784        }
785        
786      } //~PrintConfig
787    
788    public static void main(String[] args)
789      { 
790      System.out.println("Table with borders and *NO* wrapping, (truncated to fixed cell width)");
791      //borders
792      PrintConfig config = new PrintConfig();
793      config.setCellWrap(false);
794      printTest(config, null);
795    
796    
797      System.out.println("Table with borders and autofit");
798      //borders
799      config = new PrintConfig();
800      config.setAutoFit(true);
801      printTest(config, null);
802    
803    
804      System.out.println("Table with borders and wrapping");
805      //borders
806      config = new PrintConfig();
807      printTest(config, null);
808      
809      System.out.println(""); 
810      System.out.println("Table with borders and padding & spacing");
811      //borders + padding
812      config = new PrintConfig();
813      config.setAutoFit(true);
814      config.setCellSpacing(2);
815      config.setCellPadding(1);
816      config.setCellWidth(5);
817      printTest(config, null);
818    
819      System.out.println(""); 
820      System.out.println("Table with borders and padding & spacing and AUTOFIT");
821      //borders + padding
822      config = new PrintConfig();
823      config.setCellSpacing(2);
824      config.setCellPadding(1);
825      config.setCellWidth(5);
826      printTest(config, null);
827    
828      System.out.println(""); 
829      System.out.println("Table with borders, spacing=2 page size = 2 lines and headers = true");
830      config = new PrintConfig();
831      config.setCellSpacing(2);
832      config.setCellWidth(5);
833      config.setPageSize(2);
834      config.headerEveryPage(true);
835      printTest(config, new String[] {"Header", "A", "helloworld", "d", "End Header"});
836    
837      System.out.println(""); 
838      System.out.println("Table with borders, no padding and default alignment of CENTER");
839      //borders + padding
840      config = new PrintConfig();
841      config.setCellSpacing(0);
842      config.setCellPadding(0);
843      config.setCellWidth(15);
844      config.setAlign(HAlign.CENTER);
845      printTest(config, null);
846    
847      System.out.println("");
848      System.out.println("Table with no borders and padding & spacing");
849      //no borders + padding
850      config = new PrintConfig();
851      config.setPrintBorders(false);
852      config.setCellWidth(5);
853      config.setCellSpacing(2);
854      config.setCellPadding(3);
855      printTest(config, null);
856    
857      }
858    
859    
860    static void printTest(PrintConfig config, String[] header) 
861      {
862      TablePrinter printer = new TablePrinter(5, System.out, config);
863    
864      if (header != null)
865        config.setHeader(header);
866        
867      printer.startTable();
868    
869      //row 1
870      printer.startRow();
871      printer.printCell("abcdef");
872      printer.printCell("4324");
873      printer.printCell("Q");
874      printer.printCell("");
875      printer.printCell("abc");
876      printer.endRow();
877        
878      //2
879      printer.startRow();
880      printer.printCell("row2: abc");
881      printer.printCell("the quick brown \n-fox jumps over the lazy dog");
882      printer.printCell("hello");
883      printer.printCell("world");
884      printer.printCell(null);
885      printer.endRow();
886    
887    
888      //3
889      printer.startRow();
890      printer.printCell("row3");
891      printer.printCell("a");
892      printer.printCell(null);
893      printer.printCell(null);
894      printer.printCell(null);
895      printer.endRow();
896      
897      printer.endTable();
898      
899      System.out.println("TablePrinter.toString()=" + printer);
900      System.out.println("");
901      }
902      
903    } //~TablePrinter