/** * Draws the report. * * Two rules, in this order: nothing is ever cut off, and nothing ever * wraps. The original broke both — it sized columns as fixed fractions of * A4 landscape, so all 26 fields gave each column 31pt while a header like * "Originator Reference" needs 69pt, and it clipped whatever didn't fit * with an ellipsis. A report with "Origina…" and "…VERSIO…" in it is not a * report. * * So: every cell is flattened to one line, every column is measured from * its own widest content, and the page grows sideways to hold the lot. A * wide page is not a problem for a report — every print dialog scales to * fit, and on screen it just scrolls. Past a limit the *type* shrinks * rather than the columns, because scaling columns means cutting text and * scaling type doesn't: widths are linear in font size, so a smaller face * fits exactly the same words in less room. */ function buildPdf( rows ) { if ( ! window.jspdf || ! window.jspdf.jsPDF ) { return null; } var columns = pdfColumnsForSelection(); var margin = 32; var padding = 5; var rowHeight = 18; var fontSize = columns.length > 16 ? 6.5 : ( columns.length > 11 ? 7 : 8 ); var A4_WIDTH = 841.89; var A4_HEIGHT = 595.28; /** ~85cm. Past this the type shrinks instead of the page growing. */ var MAX_PAGE = 2400; /** Smaller than this stops being readable, at which point a wider page * is the better trade — a cut value is worse than either. */ var MIN_FONT = 4.5; /** Float noise in the width arithmetic must never cost a glyph. */ var SLACK = 0.75; /** * One line, always. * * Coding History is the obvious offender — EBU 3285 defines it as CRLF * separated lines, and there are usually two or three — but a Note or a * Description can carry newlines too. jsPDF renders those as extra * lines drawn *below* the baseline it was given, straight through the * rows underneath. Each break becomes a visible separator instead, so * the text is all still there and reads as the list it is. */ function oneLine( text ) { return String( text ) .replace( /\s*[\r\n]+\s*/g, " \u00b7 " ) .replace( /[\t\v\f]+/g, " " ) .replace( / +/g, " " ) .replace( / \u00b7 $/, "" ) .trim(); } function cellText( row, col ) { var raw = extract( row, col.key ); if ( col.render ) { return oneLine( col.render( raw ) ); } return ( raw === null || raw === undefined || raw === "" ) ? "-" : oneLine( raw ); } // Measuring needs a document to measure in, at the same font the table // will use. Headers are drawn bold, values normal, so each is measured // as it will be drawn. var doc = new window.jspdf.jsPDF( { orientation: "landscape", unit: "pt", format: "a4" } ); /** The width each column needs at a given size, cell padding included. */ function measureColumns( size ) { doc.setFontSize( size ); return columns.map( function ( col ) { doc.setFont( "helvetica", "bold" ); var widest = doc.getTextWidth( col.label ); doc.setFont( "helvetica", "normal" ); rows.forEach( function ( row ) { var width = doc.getTextWidth( cellText( row, col ) ); if ( width > widest ) { widest = width; } } ); return widest + padding * 2 + SLACK; } ); } function total( list ) { return list.reduce( function ( sum, width ) { return sum + width; }, 0 ); } var widths = measureColumns( fontSize ); // Too wide for the page limit: shrink the type until it isn't, or until // the type would stop being readable — in which case the page grows // past the limit instead. Text is never the thing that gives. if ( total( widths ) + margin * 2 > MAX_PAGE && fontSize > MIN_FONT ) { var wanted = fontSize * ( MAX_PAGE - margin * 2 ) / total( widths ); fontSize = Math.max( MIN_FONT, wanted ); widths = measureColumns( fontSize ); } var naturalWidth = total( widths ); var pageWidth = Math.max( A4_WIDTH, naturalWidth + margin * 2 ); var pageHeight = A4_HEIGHT; var usableWidth = pageWidth - margin * 2; // Spare room is shared out proportionally so the table spans the page // exactly rather than stopping short of the edge. This only ever widens // columns: the page is never narrower than the content needs. var scale = usableWidth / naturalWidth; if ( scale > 1 ) { widths = widths.map( function ( width ) { return width * scale; } ); } if ( pageWidth !== A4_WIDTH ) { doc = new window.jspdf.jsPDF( { orientation: "landscape", unit: "pt", format: [ pageWidth, pageHeight ] } ); } var y = margin; function drawTitle() { doc.setFont( "helvetica", "bold" ); doc.setFontSize( 14 ); doc.text( t( "pdfTitle" ), margin, y ); doc.setFont( "helvetica", "normal" ); doc.setFontSize( 9 ); doc.setTextColor( 100 ); var meta = t( "pdfGeneratedOn" ) + ": " + new Date().toLocaleString() + " " + t( "pdfFileCount" ) + ": " + rows.length; doc.text( meta, margin, y + 16 ); doc.setTextColor( 0 ); y += 34; // The production details. Two to a line where they fit, so seven of // them cost four lines rather than pushing the table down the page — // but measured first, because a long project name in a narrow column // would otherwise print into its neighbour. Only what was filled in // appears: an empty "Director:" is worse than no line at all. var pairs = reportDetailPairs(); if ( pairs.length ) { var detailSize = 9; var lineHeight = 12; function widestPair( size ) { doc.setFontSize( size ); return pairs.reduce( function ( widest, pair ) { doc.setFont( "helvetica", "bold" ); var width = doc.getTextWidth( pair[ 0 ] + ": " ); doc.setFont( "helvetica", "normal" ); width += doc.getTextWidth( oneLine( pair[ 1 ] ) ); return Math.max( widest, width ); }, 0 ); } var widest = widestPair( detailSize ); var gutter = 24; // One pair per line if two won't fit; and if even one won't fit, // the type shrinks rather than the text being cut. var perLine = ( widest * 2 + gutter <= usableWidth ) ? 2 : 1; if ( widest > usableWidth ) { detailSize = Math.max( 6, detailSize * usableWidth / widest ); widest = widestPair( detailSize ); } // The second column starts just past the longest pair, not at half // the page: on a 1600pt sheet, half-and-half leaves a lake of // white between a label and its value. var columnWidth = perLine === 2 ? Math.min( widest + gutter, ( usableWidth - gutter ) / 2 + gutter ) : usableWidth; doc.setFontSize( detailSize ); pairs.forEach( function ( pair, index ) { var x = margin + ( index % perLine ) * columnWidth; var top = y + Math.floor( index / perLine ) * lineHeight; doc.setFont( "helvetica", "bold" ); var label = pair[ 0 ] + ": "; var labelWidth = doc.getTextWidth( label ); doc.text( label, x, top ); doc.setFont( "helvetica", "normal" ); doc.text( oneLine( pair[ 1 ] ), x + labelWidth, top ); } ); y += Math.ceil( pairs.length / perLine ) * lineHeight + 10; } } function drawHeaderRow() { doc.setFillColor( 242, 242, 242 ); doc.rect( margin, y, usableWidth, rowHeight, "F" ); doc.setFont( "helvetica", "bold" ); doc.setFontSize( fontSize ); var x = margin; columns.forEach( function ( col, index ) { doc.text( col.label, x + padding, y + rowHeight - 6 ); x += widths[ index ]; } ); doc.setFont( "helvetica", "normal" ); y += rowHeight; } /** Column separators. With this many columns, a value and the one to * its right otherwise read as a single sentence. */ function drawColumnRules( top, bottom ) { doc.setDrawColor( 232, 232, 232 ); var x = margin; for ( var index = 0; index < widths.length - 1; index++ ) { x += widths[ index ]; doc.line( x, top, x, bottom ); } } function ensureSpace() { if ( y + rowHeight > pageHeight - margin ) { drawColumnRules( bandTop, y ); doc.addPage(); y = margin; drawHeaderRow(); bandTop = y - rowHeight; } } drawTitle(); drawHeaderRow(); var bandTop = y - rowHeight; doc.setFontSize( fontSize ); rows.forEach( function ( row, rowIndex ) { ensureSpace(); if ( rowIndex % 2 === 1 ) { doc.setFillColor( 250, 250, 250 ); doc.rect( margin, y, usableWidth, rowHeight, "F" ); } var x = margin; columns.forEach( function ( col, index ) { doc.text( cellText( row, col ), x + padding, y + rowHeight - 6 ); x += widths[ index ]; } ); doc.setDrawColor( 225, 225, 225 ); doc.line( margin, y + rowHeight, margin + usableWidth, y + rowHeight ); y += rowHeight; } ); drawColumnRules( bandTop, y ); var totalPages = doc.internal.getNumberOfPages(); for ( var p = 1; p <= totalPages; p++ ) { doc.setPage( p ); doc.setFontSize( 8 ); doc.setTextColor( 130 ); doc.text( "Page " + p + " / " + totalPages, pageWidth - margin - 60, pageHeight - 14 ); doc.setTextColor( 0 ); } return doc; }