Jump to content

MediaWiki:Gadget-TunebookPDF.js

Find traditional instrumental music

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);
Cookies help us deliver our services. By using The Traditional Tune Archive services, you agree to our use of cookies.