Jump to content

MediaWiki:Gadget-ProfessionalTunebookPDF.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 allows the user to select which scores (rendered by abcjs)
         * to include in the tunebook. If more than one score is detected, a textbox
         * for a custom tunebook title and a checkbox to include an index page are shown.
         ********************************************************************/
        function TuneBookPrintDialog(config) {
            TuneBookPrintDialog.super.call(this, config);
        }
        OO.inheritClass(TuneBookPrintDialog, OO.ui.ProcessDialog);

        TuneBookPrintDialog.static.name = 'tunebookPrintDialog';
        TuneBookPrintDialog.static.title = 'Print Sheet Music';
        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 ($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.includeIndexCheckbox = new OO.ui.CheckboxInputWidget({ selected: false });
                    console.log("DEBUG: includeIndexCheckbox created, initial value: " + this.includeIndexCheckbox.isSelected());
                    var indexField = new OO.ui.FieldLayout(this.includeIndexCheckbox, {
                        label: 'Include Index Page',
                        align: 'top'
                    });
                    this.content.$element.append(indexField.$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);
            }
        };

        // Open a new window (placeholder) synchronously to avoid popup blockers.
        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;
                        }
                    }
                    
                    var includeIndex = false;
                    if (dialog.includeIndexCheckbox) {
                        includeIndex = dialog.includeIndexCheckbox.isSelected();
                        console.log("DEBUG: includeIndex: " + includeIndex);
                    }
                    
                    var newWindow = window.open("about:blank", '_blank', 'width=900,height=1000,top=100,left=100');
                    if (!newWindow) {
                        console.error("Popup blocked!");
                        return;
                    }
                    newWindow.document.open();
                    newWindow.document.write(
                        '<!DOCTYPE html>' +
                        '<html lang="en">' +
                        '<head><meta charset="UTF-8"><title>Generating PDF</title></head>' +
                        '<body style="text-align: center; font-family: Arial, sans-serif; margin-top: 50px;">' +
                        '<h2>Generating PDF, please wait...</h2>' +
                        '</body></html>'
                    );
                    newWindow.document.close();
                    
                    dialog.close({ action: action });
                    console.log("Starting PDF Tunebook generation...");
                    generateTunebookPDF(selectedScores, customTunebookTitle, includeIndex, newWindow);
                });
            } else if (action === 'cancel') {
                return new OO.ui.Process(function () {
                    console.log("Closing dialog without printing.");
                    dialog.close({ action: action });
                });
            }
            return TuneBookPrintDialog.super.prototype.getActionProcess.call(this, action);
        };

        /********************************************************************
         * 2) PDF Generation Function
         *
         * Options:
         *   - Collapsed scores (without <svg>) are skipped.
         *   - If exactly one expanded score exists, no cover or title page is added.
         *   - If multiple expanded scores exist, a cover with a split title is generated.
         *   - The PDF is generated using JPEG images, with compression enabled.
         *   - qualityFactor is reduced to 3 to lower the resolution.
         ********************************************************************/
        function generateTunebookPDF(selectedScores, customTitle, includeIndex, newWindow) {
            $("#pdf-loading-overlay").remove();

            $.getScript("https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js")
            .done(function () {
                console.log("jsPDF loaded successfully.");

                // Create the overlay.
                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);

                // Create jsPDF instance with compression enabled.
                const { jsPDF } = window.jspdf;
                // Pass compress:true to reduce file size.
                var pdf = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4', compress: true });
                pdf.setFont("times", "normal");

                const totalPageWidth = pdf.internal.pageSize.getWidth();
                const totalPageHeight = pdf.internal.pageSize.getHeight();

                const sideMargin = 20, topMargin = 20, bottomMargin = 20;
                const safeMargin = 100; // Additional bottom space.
                const pageWidth = totalPageWidth - (sideMargin * 2);
                const pageHeight = (totalPageHeight - topMargin - bottomMargin) - safeMargin;
                // qualityFactor reduced to 3.
                const qualityFactor = 3;

                const indexLeftMargin = sideMargin + 10;
                const indexAvailableWidth = totalPageWidth - (indexLeftMargin * 2);

                let basePageTitle = document.title.replace(" - Tunearch", "").trim() || "Tunebook";
                let pdfFileName = basePageTitle + ".pdf";
                let tunebookTitle = basePageTitle + "'s Tunebook";

                if (customTitle) {
                    tunebookTitle = customTitle;
                    pdfFileName = customTitle.replace(/[<>:"\/\\|?*]+/g, '').replace(/\s+/g, '_') + ".pdf";
                }

                if (selectedScores.length === 1 && !customTitle) {
                    const singleSvg = selectedScores[0].find('svg').get(0);
                    if (singleSvg) {
                        const titleElement = $(singleSvg).find('title').first();
                        if (titleElement.length > 0) {
                            const tuneTitle = titleElement.text().trim()
                                .replace(/Sheet Music for\s*/i, '')
                                .replace(/"/g, '')
                                .replace(/[<>:"\/\\|?*]+/g, '')
                                .replace(/\s+/g, '_');
                            pdfFileName = tuneTitle + ".pdf";
                            tunebookTitle = tuneTitle;
                        }
                    }
                }
                console.log("Tunebook Title: " + tunebookTitle);

                // Replace " - " with newline in the title.
                const formattedTitle = tunebookTitle.replace(/\s*-\s*/g, "\n");

                // Filter out collapsed scores.
                const expandedScores = selectedScores.filter(function ($el) {
                    return $el.find('svg').length > 0;
                });

                if (expandedScores.length === 0) {
                    console.log("No expanded scores found.");
                    $loadingOverlay.remove();
                    newWindow.document.open();
                    newWindow.document.write(
                        '<!DOCTYPE html>' +
                        '<html lang="en">' +
                        '<head><meta charset="UTF-8"><title>No Expanded Scores</title></head>' +
                        '<body style="text-align: center; font-family: Arial, sans-serif; margin-top: 50px;">' +
                        '<h2>Expand at least one score to generate the PDF.</h2>' +
                        '<p>Please close this window and try again.</p>' +
                        '<button onclick="window.close();">Close</button>' +
                        '</body></html>'
                    );
                    newWindow.document.close();
                    newWindow.focus();
                    return;
                }

                // Determine if cover is needed: if more than one expanded score, add cover.
                const multipleScores = (expandedScores.length > 1);

                if (multipleScores) {
                    pdf.setFontSize(26);
                    // Use splitTextToSize to wrap the title.
                    const titleLines = pdf.splitTextToSize(formattedTitle, totalPageWidth - 40);
                    pdf.text(titleLines, totalPageWidth / 2, 250, { align: 'center' });

                    // Add logo and additional cover text.
                    const 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("Cover drawn on first page (cover is page 1, not numbered).");
                }
                // (If only one expanded score, no cover and no title page.)

                // Helper function to process an SVG into one or more JPEG images.
function processSVG(svgElement) {
    return new Promise(function (resolve, reject) {
        const svgData = new XMLSerializer().serializeToString(svgElement);
        const img = new Image();
        img.onload = function () {
            const originalWidth = img.width;
            const originalHeight = img.height;
            const scale = pageWidth / originalWidth;
            const scaledHeight = originalHeight * scale;

            // Create a canvas with a white background
            function drawCanvas(width, height) {
                const canvas = document.createElement("canvas");
                canvas.width = width;
                canvas.height = height;
                const ctx = canvas.getContext("2d");

                // Fill the canvas with white
                ctx.fillStyle = "#ffffff";
                ctx.fillRect(0, 0, width, height);

                return canvas;
            }

            if (scaledHeight <= pageHeight) {
                // Single page
                const canvas = drawCanvas(pageWidth * qualityFactor, scaledHeight * qualityFactor);
                const ctx = canvas.getContext("2d");
                ctx.drawImage(
                    img,
                    0, 0, originalWidth, originalHeight,
                    0, 0, canvas.width, canvas.height
                );
                // Convert to JPEG (quality 0.8 for example)
                resolve([{
                    imgData: canvas.toDataURL("image/jpeg", 0.8),
                    imgWidth: pageWidth,
                    imgHeight: scaledHeight
                }]);
            } else {
                // Multi-page scenario
                const numPages = Math.ceil(scaledHeight / pageHeight);
                const pageImages = [];

                // Draw the entire image once on a large canvas
                const fullCanvas = drawCanvas(pageWidth * qualityFactor, scaledHeight * qualityFactor);
                const fullCtx = fullCanvas.getContext("2d");
                fullCtx.drawImage(
                    img,
                    0, 0, originalWidth, originalHeight,
                    0, 0, fullCanvas.width, fullCanvas.height
                );

                for (let i = 0; i < numPages; i++) {
                    const segmentHeight = (i === numPages - 1)
                        ? (scaledHeight - pageHeight * i)
                        : pageHeight;

                    const segmentCanvas = drawCanvas(pageWidth * qualityFactor, segmentHeight * qualityFactor);
                    const segmentCtx = segmentCanvas.getContext("2d");

                    segmentCtx.drawImage(
                        fullCanvas,
                        0, (pageHeight * i) * qualityFactor,
                        pageWidth * qualityFactor, segmentHeight * qualityFactor,
                        0, 0,
                        segmentCanvas.width, segmentCanvas.height
                    );

                    pageImages.push({
                        imgData: segmentCanvas.toDataURL("image/jpeg", 0.8),
                        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 expanded score.
                const scorePromises = expandedScores.map(function ($el, idx) {
                    const svg = $el.find('svg').get(0);
                    let scoreTitle = "Score " + (idx + 1);
                    const $svg = $el.find('svg');
                    if ($svg.length > 0) {
                        const titleEl = $svg.find('title').first();
                        if (titleEl.length > 0) {
                            scoreTitle = titleEl.text().trim();
                        }
                    }
                    console.log("Processing " + scoreTitle);
                    return processSVG(svg).then(function (pages) {
                        console.log(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) {
                    // If multiple scores and index option selected, add an index page.
                    if (multipleScores && includeIndex && scoreDataArray.length > 1) {
                        pdf.addPage();
                        pdf.setFontSize(20);
                        pdf.text("Index", totalPageWidth / 2, 50, { align: 'center' });
                        pdf.setFontSize(14);
                        let yPos = 80;
                        let pageCounter = 1;
                        const indexData = [];
                        scoreDataArray.forEach(function (scoreObj) {
                            indexData.push({ title: scoreObj.title, startPage: pageCounter });
                            pageCounter += scoreObj.pages.length;
                        });
                        indexData.forEach(function (item) {
                            let cleanTitle = item.title.replace(/Sheet Music for\s*/i, '').replace(/"/g, '');
                            const pageText = String(item.startPage);
                            const titleWidth = pdf.getTextDimensions(cleanTitle).w;
                            const pageWidthText = pdf.getTextDimensions(pageText).w;
                            const available = indexAvailableWidth - titleWidth - pageWidthText - 20;
                            const dotWidth = pdf.getTextDimensions(".").w;
                            const numDots = Math.floor(available / dotWidth);
                            const dots = new Array(numDots + 1).join(".");
                            pdf.text(`${cleanTitle} ${dots} ${pageText}`, indexLeftMargin, yPos);
                            yPos += 20;
                        });
                        console.log("Index page added.");
                    }

                    let printedCounter = 1;
                    // Generate the score pages.
                    scoreDataArray.forEach(function (scoreObj) {
                        scoreObj.pages.forEach(function (pageObj) {
                            // If multiple scores, add a new page for each score page.
                            // If only one score is expanded, do not add an extra cover/page.
                            if (multipleScores || printedCounter > 1) {
                                pdf.addPage();
                            }
                            pdf.addImage(pageObj.imgData, 'JPEG', sideMargin + 10, 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("Added score page, printed number: " + printedCounter);
                            printedCounter++;
                        });
                    });

                    const pdfBlob = pdf.output('blob');
                    const pdfUrl = URL.createObjectURL(pdfBlob);
                    console.log("PDF generation complete.");

                    setTimeout(function () {
                        $loadingOverlay.remove();
                        newWindow.document.open();
                        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;">' +
                            (multipleScores ? `<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();
                        console.log("PDF generated and new window updated after delay.");
                    }, 2000);
                }).catch(function (err) {
                    console.error("Error processing scores:", err);
                    $loadingOverlay.remove();
                });
            })
            .fail(function () {
                console.error("Error loading jsPDF library!");
            });
        }

        /********************************************************************
         * 3) Toolbox Integration
         ********************************************************************/
        function openTuneBookPrintDialog() {
            const windowManager = new OO.ui.WindowManager();
            $(document.body).append(windowManager.$element);
            const dialog = new TuneBookPrintDialog();
            windowManager.addWindows([dialog]);
            windowManager.openWindow(dialog);
        }

        function addTunebookPrintLink() {
            var $actions = $('#p-cactions ul');
            if ($actions.length === 0) { return; }
            if ($('#tunebook-print').length === 0) {
                var $newLink = $('<li><a href="#" id="tunebook-print">📄 Print Sheet Music</a></li>');
                $actions.append($newLink);
                console.log("DEBUG: 'Print Tunebook' link added to the Actions menu.");
           }
        }

        $(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.