BWF Analyser: browser page and macOS app
Reads and edits BWF metadata for production sound. One source tree builds a single self-contained page and a native Tauri app with a Rust audio engine and WAV writer. Around 370 checks across seven test suites. First commit of the existing state, so that from here every change can be seen and undone.
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user