MediaWiki:Gadget-TunebookPDF.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
(function ($, mw) {
"use strict";
// Load required OOUI libraries for dialog windows
mw.loader.using(["oojs-ui-core", "oojs-ui-widgets", "oojs-ui-windows"]).done(function () {
console.log("✅ TunebookPDF Gadget loaded successfully.");
/********************************************************************
* 1) Dialog Definition
* This dialog lets the user select which scores (SVGs) to include in the tunebook.
* If there is more than one score, it also shows a textbox for a custom tunebook title.
* (The index functionality has been removed.)
********************************************************************/
function TuneBookPrintDialog(config) {
TuneBookPrintDialog.super.call(this, config);
}
OO.inheritClass(TuneBookPrintDialog, OO.ui.ProcessDialog);
TuneBookPrintDialog.static.name = 'tunebookPrintDialog';
TuneBookPrintDialog.static.title = 'Print Tunebook';
TuneBookPrintDialog.static.actions = [
{ action: 'cancel', label: 'Cancel', flags: 'safe' },
{ action: 'accept', label: 'Generate PDF', flags: ['primary', 'progressive'] }
];
TuneBookPrintDialog.prototype.initialize = function () {
TuneBookPrintDialog.super.prototype.initialize.call(this);
this.content = new OO.ui.PanelLayout({ padded: true, expanded: false });
var $scores = $('.abcrendered');
console.log("DEBUG: Found " + $scores.length + " .abcrendered elements.");
var scoresCheckboxes = [];
if ($scores.length === 0) {
this.content.$element.append($('<p>').text('⚠️ No score found on this page.'));
} else {
this.content.$element.append($('<p>').text('Select the scores to include in the tunebook.'));
$scores.each(function (index, element) {
var $svg = $(element).find('svg');
var scoreTitle = "Score " + (index + 1);
if ($svg.length > 0) {
var titleElement = $svg.find('title').first();
if (titleElement.length > 0) {
scoreTitle = titleElement.text().trim();
}
}
var checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
console.log("DEBUG: Created checkbox for " + scoreTitle);
scoresCheckboxes.push({
checkbox: checkbox,
title: scoreTitle,
element: $(element)
});
var field = new OO.ui.FieldLayout(checkbox, { label: scoreTitle, align: 'inline' });
this.content.$element.append(field.$element);
}.bind(this));
// If there is more than one score, add the textbox for a custom tunebook title.
if ($scores.length > 1) {
this.customTitleInput = new OO.ui.TextInputWidget({
placeholder: 'Enter your custom Tunebook title...'
});
console.log("DEBUG: customTitleInput created.");
var titleField = new OO.ui.FieldLayout(this.customTitleInput, {
label: 'Custom Tunebook Title',
align: 'top'
});
this.content.$element.append(titleField.$element);
}
}
this.scoresCheckboxes = scoresCheckboxes;
this.$body.append(this.content.$element);
};
TuneBookPrintDialog.prototype.onOpen = function () {
TuneBookPrintDialog.super.prototype.onOpen.call(this);
if (this.scoresCheckboxes.length === 0) {
this.$actionArea.find('button[data-action="accept"]').prop('disabled', true);
}
};
TuneBookPrintDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === 'accept') {
return new OO.ui.Process(function () {
var selectedScores = dialog.scoresCheckboxes
.filter(function (item) { return item.checkbox.isSelected(); })
.map(function (item) { return item.element; });
console.log("DEBUG: Selected scores: " + selectedScores.length);
if (selectedScores.length === 0) {
dialog.close({ action: action });
return;
}
var customTunebookTitle = null;
if (dialog.customTitleInput) {
var rawTitle = dialog.customTitleInput.getValue().trim();
console.log("DEBUG: customTitleInput value: " + rawTitle);
if (rawTitle.length > 0) {
customTunebookTitle = rawTitle;
}
}
dialog.close({ action: action });
console.log("🎵 Starting PDF Tunebook generation...");
generateTunebookPDF(selectedScores, customTunebookTitle);
});
} else if (action === 'cancel') {
return new OO.ui.Process(function () {
console.log("❌ Closing the dialog without printing.");
dialog.close({ action: action });
});
}
return TuneBookPrintDialog.super.prototype.getActionProcess.call(this, action);
};
/********************************************************************
* 2) PDF Generation Function (Pixel-based splitting with safe margin)
*
* This function loads jsPDF, converts each SVG into high-resolution PNG images
* (splitting them by pixel if necessary) and creates a PDF.
* It uses a safe margin to ensure that the last line is not cut off.
*
* Parameters:
* - selectedScores: array of jQuery elements (score containers)
* - customTitle: (optional) custom tunebook title
*
* The cover page (if more than one score) is added as before.
* The printed numbering for the score pages starts at 1.
********************************************************************/
function generateTunebookPDF(selectedScores, customTitle) {
$.getScript("https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js")
.done(function () {
console.log("DEBUG: jsPDF 2.5.1 loaded successfully.");
var $loadingOverlay = $('<div id="pdf-loading-overlay">Generating PDF, please wait...</div>').css({
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(255, 255, 255, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
zIndex: 10000
});
$('body').append($loadingOverlay);
const { jsPDF } = window.jspdf;
var pdf = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' });
pdf.setFont("times", "normal");
var totalPageWidth = pdf.internal.pageSize.getWidth();
var totalPageHeight = pdf.internal.pageSize.getHeight();
var sideMargin = 20, topMargin = 20, bottomMargin = 20;
var safeMargin = 100; // Extra space at bottom
var pageWidth = totalPageWidth - sideMargin * 2;
var pageHeight = (totalPageHeight - topMargin - bottomMargin) - safeMargin;
var qualityFactor = 4;
var imagePromises = [];
// Determine tunebook title and filename from document title; override with customTitle if provided.
var basePageTitle = document.title.replace(" - Tunearch", "").trim() || "Tunebook";
var pdfFileName = basePageTitle + ".pdf";
var tunebookTitle = basePageTitle + "'s Tunebook";
if (customTitle) {
tunebookTitle = customTitle;
pdfFileName = customTitle.replace(/[<>:"\/\\|?*]+/g, '').replace(/\s+/g, '_') + ".pdf";
}
if (selectedScores.length === 1 && !customTitle) {
var titleElement = selectedScores[0].find('svg').get(0).querySelector('title');
if (titleElement) {
var tuneTitle = titleElement.textContent.trim().replace(/[<>:"\/\\|?*]+/g, '').replace(/\s+/g, '_');
pdfFileName = tuneTitle + ".pdf";
tunebookTitle = tuneTitle;
}
}
var formattedTitle = tunebookTitle.replace(' - ', '\n');
console.log("DEBUG: Tunebook Title: " + tunebookTitle);
// --- Cover Page ---
if (selectedScores.length > 1) {
pdf.setFontSize(26);
pdf.text(formattedTitle, totalPageWidth / 2, 250, { align: 'center' });
var logoImg = new Image();
logoImg.src = 'https://ttadev.org/w/images/tmp/UniversalClef.jpg';
pdf.addImage(logoImg, 'JPEG', 210, totalPageHeight - 300, 140, 140);
pdf.setFontSize(14);
pdf.text("The Traditional Tune Archive", totalPageWidth / 2, totalPageHeight - 130, { align: 'center' });
pdf.setFontSize(12);
pdf.text("The Semantic Index of North American,", totalPageWidth / 2, totalPageHeight - 110, { align: 'center' });
pdf.text("British and Irish traditional instrumental music with annotation.", totalPageWidth / 2, totalPageHeight - 90, { align: 'center' });
console.log("DEBUG: Cover page added.");
pdf.addPage();
}
// --- Helper: processSVG ---
function processSVG(svgElement, pageWidth, pageHeight, qualityFactor) {
return new Promise(function (resolve, reject) {
var svgData = new XMLSerializer().serializeToString(svgElement);
var img = new Image();
img.onload = function () {
var originalWidth = img.width;
var originalHeight = img.height;
var scale = pageWidth / originalWidth;
var scaledHeight = originalHeight * scale;
if (scaledHeight <= pageHeight) {
var canvas = document.createElement("canvas");
canvas.width = pageWidth * qualityFactor;
canvas.height = scaledHeight * qualityFactor;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, originalWidth, originalHeight, 0, 0, canvas.width, canvas.height);
var imgData = canvas.toDataURL("image/png");
resolve([{ imgData: imgData, imgWidth: pageWidth, imgHeight: scaledHeight }]);
} else {
var numPages = Math.ceil(scaledHeight / pageHeight);
var pageImages = [];
var fullCanvas = document.createElement("canvas");
fullCanvas.width = pageWidth * qualityFactor;
fullCanvas.height = scaledHeight * qualityFactor;
var fullCtx = fullCanvas.getContext("2d");
fullCtx.drawImage(img, 0, 0, originalWidth, originalHeight, 0, 0, fullCanvas.width, fullCanvas.height);
for (var i = 0; i < numPages; i++) {
var segmentHeight = (i === numPages - 1) ? (scaledHeight - pageHeight * i) : pageHeight;
var segmentCanvas = document.createElement("canvas");
segmentCanvas.width = pageWidth * qualityFactor;
segmentCanvas.height = segmentHeight * qualityFactor;
var segmentCtx = segmentCanvas.getContext("2d");
segmentCtx.drawImage(
fullCanvas,
0, (pageHeight * i) * qualityFactor,
pageWidth * qualityFactor, segmentHeight * qualityFactor,
0, 0,
segmentCanvas.width, segmentCanvas.height
);
var segmentData = segmentCanvas.toDataURL("image/png");
pageImages.push({
imgData: segmentData,
imgWidth: pageWidth,
imgHeight: segmentHeight
});
}
resolve(pageImages);
}
};
img.onerror = function () { reject(new Error("Error loading SVG image")); };
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
});
}
// --- Process each selected score ---
var scorePromises = [];
selectedScores.forEach(function ($el, idx) {
var svg = $el.find('svg').get(0);
if (!svg) return;
var scoreTitle = "Score " + (idx + 1);
var $svg = $el.find('svg');
if ($svg.length > 0) {
var titleEl = $svg.find('title').first();
if (titleEl.length > 0) { scoreTitle = titleEl.text().trim(); }
}
console.log("DEBUG: Processing " + scoreTitle);
scorePromises.push(
processSVG(svg, pageWidth, pageHeight, qualityFactor)
.then(function(pages) {
console.log("DEBUG: " + scoreTitle + " produced " + pages.length + " page(s).");
return { title: scoreTitle, pages: pages };
})
.catch(function(err) {
console.error(err);
return { title: scoreTitle, pages: [] };
})
);
});
Promise.all(scorePromises).then(function(scoreDataArray) {
// Build printed numbering for score pages (starting at 1)
var printedCounter = 1;
scoreDataArray.forEach(function(scoreObj) {
scoreObj.startPage = printedCounter;
printedCounter += scoreObj.pages.length;
});
console.log("DEBUG: Printed numbering for scores set.");
// Reset printedCounter for printing the pages (start from 1)
printedCounter = 1;
scoreDataArray.forEach(function(scoreObj) {
scoreObj.pages.forEach(function(pageObj) {
pdf.addPage();
pdf.addImage(pageObj.imgData, 'PNG', sideMargin, topMargin, pageObj.imgWidth, pageObj.imgHeight);
pdf.setFontSize(10);
pdf.text("øCopyLeft - The Traditional Tune Archive (www.tunearch.org)", totalPageWidth / 2, totalPageHeight - 20, { align: 'center' });
pdf.text("page " + printedCounter, totalPageWidth - 20, totalPageHeight - 20, { align: 'right' });
console.log("DEBUG: Added score page, printed number: " + printedCounter);
printedCounter++;
});
});
var pdfBlob = pdf.output('blob');
var pdfUrl = URL.createObjectURL(pdfBlob);
$loadingOverlay.remove();
var newWindow = window.open("", '_blank', 'width=900,height=1000,top=100,left=100');
if (newWindow) {
newWindow.document.write(
'<!DOCTYPE html>' +
'<html lang="en">' +
'<head><meta charset="UTF-8"><title>' + tunebookTitle + '</title></head>' +
'<body style="text-align: center; font-family: Times New Roman, serif;">' +
'<h2>' + tunebookTitle + '</h2>' +
'<embed width="90%" height="600px" src="' + pdfUrl + '" type="application/pdf"></embed>' +
'<br><br>' +
'<a href="' + pdfUrl + '" download="' + pdfFileName + '" style="padding: 10px 20px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px;">📥 Download PDF</a>' +
'</body></html>'
);
newWindow.document.close();
newWindow.focus();
}
}).catch(function (err) {
console.error("Error processing scores:", err);
$loadingOverlay.remove();
});
})
.fail(function () {
console.error("Error loading jsPDF!");
});
}
/********************************************************************
* 3) Toolbox Integration
********************************************************************/
function openTuneBookPrintDialog() {
var windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
var dialog = new TuneBookPrintDialog();
windowManager.addWindows([dialog]);
windowManager.openWindow(dialog);
}
function addTunebookPrintLink() {
var $toolbox = $('#p-tb ul');
if ($toolbox.length === 0) { return; }
if ($('#tunebook-print').length === 0) {
var $newLink = $('<li><a href="#" id="tunebook-print">📄 Print Tunebook</a></li>');
$toolbox.append($newLink);
console.log("DEBUG: 'Print Tunebook' link added to the toolbox.");
}
}
$(document).on('click', '#tunebook-print', function (e) {
e.preventDefault();
openTuneBookPrintDialog();
});
setTimeout(function () {
$(addTunebookPrintLink);
}, 0);
});
})(jQuery, mediaWiki);