MFCropCornerUI / beta / demo.html
file last updated : 2024-11-07
<!doctype html>
<html>
<head>
    <meta name='viewport' content='width=device-width, initial-scale=0.9, maximum-scale=1.0, user-scalable=no'/>
    <meta http-equiv='content-type' content='text/html; charset=utf-8'>
    <link rel="icon" type="image/png" href="https://www.methodfish.com/images/methodfish.ico">
    <title>Methodfish Crop Corner Drawing Tool</title>
    <meta charset="utf-8">
    <style>
        * {
            font-family:Roboto;
        }
        h1 {
            font-size:17px;
        }
        p, div {
            font-size: 14px;
            max-width:700px;
            line-height:31px;
        }
        button {
            border:solid 1px black;
            border-radius:1px;
            padding:4px 8px 4px 8px;
            color:black;
        }
        select {
            border-radius:3px;
            padding:4px;
            color:black;
            margin-bottom:10px;
        }
        #imageInfo {
            font-size: 11px;
        }
        .zoomed-img {
            max-width:unset!important;
            max-height:unset!important;
            width:100vw!important;
        }
        .overlay-sample {
            width:100%;
            height:100%;
        }
        .hg-spinner {
            width: 40px;                /* Spinner size */
            height: 40px;
            border: 4px solid #f3f3f3;  /* Background color for the spinner */
            border-top: 4px solid #3498db; /* Main color for the spinner */
            border-radius: 50%;         /* Makes it circular */
            animation: hg-spin 1s linear infinite; /* Controls speed and looping */
        }
        @keyframes hg-spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        /* Optional: Styles for making the page content semi-transparent when spinner is active */
        body.loading {
            pointer-events: none; /* Prevents interaction with content behind the spinner */
            opacity: 0.5;         /* Reduces visibility of content */
        }
    </style>


    <script>
        let _cvReady= false;
        function onOpenCvReady() {
            cv['onRuntimeInitialized'] = function () {
                _cvReady = true;
                console.log('cv ready');
            };
        }
    </script>


    <script defer async src='https://docs.opencv.org/4.5.0/opencv.js' type='text/javascript' onload='onOpenCvReady();'></script>
    <link href='https://fonts.googleapis.com/icon?family=Roboto|Material+Icons|Material+Icons+Outlined' rel='stylesheet preload prefetch' as='style'>
    <script src='https://methodfish.com/Download/MFShowConsole/files/latest/js/mf-showconsole.js'></script>
    <link href='https://methodfish.com/Download/MFShowConsole/files/latest/css/mf-showconsole.css' rel='stylesheet'></link>

<!--    <script src='https://methodfish.com/Download/MFCropCornerUI/files/latest/js/mf-cropcorner-ui.js'></script>-->
<!--    <link href='https://methodfish.com/Download/MFCropCornerUI/files/latest/css/mf-cropcorner-ui.css' rel='stylesheet'></link>-->
    <script src='js/mf-cropcorner-ui.js'></script>
    <link href='css/mf-cropcorner-ui.css' rel='stylesheet'></link>
</head>
<body>

    <h1>MFCropCornerUI - Crop Corner Drawing Tool (Javascript Class)</h1>
    <p>
        This is a demonstrator for MFCropCornerUI (MF Crop Corner Drawing User Interface) to provide a UI for a user to move corner points over an image in order to follow with a crop function process.
    </p>
    <p>
        For more information, see <a href="https://methodfish.com/Projects/MFCropCornerUI">methodfish.com</a>.
    </p>

    <p style="background-color:palegoldenrod; padding:10px;">Note, the crop-corner UI class only handles the drawing and collection of the corner points that would be used for cropping,
    this example also uses opencv to apply the crop - which you can see from the page source, but that function is not part of the
        cropcorner-ui (the reason for that is that it leaves the choice of cropping routines up to your own implementation).
    </p>

    <p>
        To set the coordinates of a shape over the following image,
        click :
        <span style="white-space:nowrap">
            <button onClick="showCropCornerUI('apply')">Crop with Apply/Save...</button>
            or
            <button onClick="showCropCornerUI('save')">Crop with Save...</button>
        </span>
        and then move the red corner points to where you wish to crop. On Windows, you can also use right-mouse click to jump the
        nearest point to the click-position.
    </p>

    <br><br>

    <div style="vertical-align:top; margin-left: 25%">
        Change the test image: <select onChange="changeImage(this)">
            <option>dummyLetter1 (529kb)</option>
            <option>dummyLetter2 (429kb)</option>
            <option>dummyLetter3 (10mb)</option>
            <option>dummyLetter4 (7mb)</option>
            <option>dummyLetter5 (1mb)</option>
            <option>dummyLetter6 (1mb)</option>
            <option>dummyLetter7 (1mb)</option>
            <option>dummyLetter8 (1mb)</option>
            <option>dummyLetter9 (1mb)</option>
            <option>dummyLetter10 (1mb)</option>
            <option>dummyLetter11 (11mb)</option>
            <option>dummyLetter13 (16mb)</option>
        </select> <button onClick="changeImage(this.previousElementSibling)">reload</button>
        <br>

        <img id='originalImage' src="../../../images/dummyLetter1.png" onClick='toggleImgSize(this)' style="max-height:50vh; max-width:50vw; width:auto; border:solid 1px orange;">
        <div id="imageInfo">dummyLetter2 Image size:529Kb</div>
    </div>
    <script>
        const _ALLOW_TRAPEZIUM_CROPS=true;
        const _SAVE_IMG_QUALITY=1;
        const _SAVE_EXT = 'webp'; // png or webp
        let _cropFlag;

        //-----------------------------------------------------------------------------------------
        function showCropCornerUI(flag) {
            let img = document.querySelector('img');
            let existingMask='';
            _cropFlag = flag;
            _cropper=new MFCropCornerUI();
            _cropper.doOpenCropUI(img
                , existingMask
                , false
                , _ALLOW_TRAPEZIUM_CROPS
                , doCropCornerUICallback
                , doCropCornerUICancelCallback
                , flag);
            _cropper.doCropTrapezium();
        }
        //-----------------------------------------------------------------------------------------
        function doCropCornerUICallback(srcImg, maskTypeCd, csv) {
            console.log('doCropCornerUICallback example');
            const cropImg = document.querySelector('.mf-cropContainer > .mf-cropElements > img');

            let t = document.querySelector('.cropSample');
            if ( t ) showHourglass(t);
            else showHourglass(document.querySelector('.mf-crop2Canvas'));

            setTimeout(function() {
                let buttonIconTx = document.querySelector('.mf-cropApplyMask i').innerText.toLowerCase();
                console.log('Saving...['+_cropFlag+']['+buttonIconTx+']');

                if ( buttonIconTx.includes('save')==(_cropFlag=='save') ) {
                    console.log('STEP A (Apply)----------------------------');
                    let overlaySampleImg = document.createElement('img');
                    overlaySampleImg.className = 'overlay-sample mf-cropFitScreen mf-cropHidden';
                    overlaySampleImg.style.width = cropImg.style.width;

                    const container = document.querySelector('.mf-cropElements');
                    container.insertBefore(overlaySampleImg, container.firstChild);

                    overlaySampleImg.onload = function() {
                        _cropper.addClass(cropImg, 'mf-cropHidden');
                        doCropImageTransform(overlaySampleImg, csv);

                        let applyButton = document.querySelector('.mf-cropApplyMask');
                        doRecursiveTextReplace(applyButton, 'Apply', 'Save');
                        doRecursiveTextReplace(applyButton, 'done',  'save');

                        if ( buttonIconTx.includes('save') ) {
                            console.log('STEP B (Save)--------------------');
                            doCropImage(srcImg, csv)
                        }
                    };
                    overlaySampleImg.src = srcImg.src;
                    removeHourglass();
                }
                else {

                    if (buttonIconTx.includes('save')) {
                        console.log('STEP C (Save)-------------------------');
                        doCropImage(srcImg, csv)
                    }
                }
            },10);
        }
        //-----------------------------------------------------------------------------------------
        function doCropCornerUICancelCallback() {
            console.log('doCropCornerUICancelCallback example');
            let applyBut = document.querySelector('.mf-cropApplyMask');
            if ( _cropFlag!='save' && applyBut.innerText.toLowerCase().indexOf('save')>-1 ) {
                console.log('undoing crop attempt');
                _cropper.removeElement('.cropSample');
                _cropper.removeClass(document.querySelector('.mf-crop2Canvas'), 'mf-cropHidden');
                _cropper.removeClass(document.querySelector('.mf-cropImageCopy'), 'mf-cropHidden');
                let applyButton = document.querySelector('.mf-cropApplyMask');
                doRecursiveTextReplace(applyButton, 'Save', 'Apply');
                doRecursiveTextReplace(applyButton, 'save', 'done');
                return false;
            }
            else {
                console.log('cancelling crop');
                return true;
            }
        }
        //-----------------------------------------------------------------------------------------
        function doCropImage(srcImg, csv) {
            // Example cropping method using opencv and then use ajaxPost to send the result up to the server.....

            _cropper.addClass('#originalImage', 'mf-cropProcessing');
            document.querySelector('.mf-cropOverlay').style.visibility='hidden';
            removeHourglass();
            showHourglass(document.querySelector('#originalImage'));

            doCropImageTransform(srcImg, csv);
            let canvas = document.querySelector('.cropSample');
            canvas.toBlob(function (blob) {
                let reader    = new FileReader();
                reader.onload = function (event) {

                    let transformedDataUrl = event.target.result; // base64
                    let transformedSize    = getFileSize(transformedDataUrl.length);
                    console.log('Transformed size is ' + transformedSize);

                    const formData = new FormData();
                    formData.append('file', blob, 'canvas.' + _SAVE_EXT);
                    if (1) {
                        srcImg.onload   = function () {
                            // Upload to server...........
                            removeHourglass();
                            _cropper.removeClass('#originalImage', 'mf-cropProcessing');
                            ajaxPost(`actionSaveCropped=${srcImg.id}`, formData);
                            removeHourglass();
                        };
                        srcImg.src      = transformedDataUrl;

                        let infoEl  = document.querySelector('#imageInfo');
                        let estSize = getEstSizeFromBase64(srcImg.src);
                        infoEl.innerHTML += '<br>Estimated crop size = ' + getFileSize(estSize);

                    }

                };
                reader.readAsDataURL(blob);
            }, 'image/' + _SAVE_EXT, _SAVE_IMG_QUALITY);
            _cropper.removeElement('.mf-cropOverlay');
        }
        //-----------------------------------------------------------------------------------------
        function doCropImageTransform(img, csv) {
            let srcId = img.id;

            const cropImg = document.querySelector('.mf-cropContainer > .mf-cropElements > img');
            const [scaledWidth, scaledHeight, ...otherPoints] = csv.split(',').map(Number);
            const naturalWidth      = img.naturalWidth;
            const naturalHeight      = img.naturalHeight;
            const scaleRatio = naturalWidth / scaledWidth;
            console.log('doCropImageTransform.280', csv, scaleRatio, scaledWidth, scaledHeight);

            const topLeft     = [ parseInt(otherPoints[0] * scaleRatio), parseInt(otherPoints[1] * scaleRatio)];
            const topRight    = [ parseInt(otherPoints[2] * scaleRatio), parseInt(otherPoints[3] * scaleRatio)];
            const bottomRight = [ parseInt(otherPoints[4] * scaleRatio), parseInt(otherPoints[5] * scaleRatio)];
            const bottomLeft  = [ parseInt(otherPoints[6] * scaleRatio), parseInt(otherPoints[7] * scaleRatio)];
            const scaledSrcPoints = [...topLeft, ...topRight, ...bottomRight, ...bottomLeft];

            const height      = Math.max(Math.abs(bottomRight[1] - topRight[1]), Math.abs(bottomLeft[1] - topLeft[1]));
            const topWidth    = Math.abs(topRight[0] - topLeft[0]);
            const bottomWidth = Math.abs(bottomRight[0] - bottomLeft[0]);
            const weightTop   = (bottomWidth - topWidth) / height;

            const widthDifference = Math.abs(topWidth - bottomWidth);
            const dynamicFactor = Math.sqrt(widthDifference);
            const adjustmentStrength = 0.01;
            const avgWidth = parseInt(((topWidth + bottomWidth) / 2) + (adjustmentStrength * dynamicFactor));

            const destHeight = scaledHeight * scaleRatio;
            const destWidth = avgWidth;
            const scaledDstPoints = [
                0, 0,                    // Top-left
                destWidth, 0,            // Top-right
                destWidth, destHeight,   // Bottom-right
                0, destHeight            // Bottom-left
            ];

            try {
                doApplyTransformInner(cropImg, scaledSrcPoints, scaledDstPoints, destWidth, destHeight );
            }
            catch(error) {
                removeHourglass();
                alert('transformation failed');
                console.error('transform error',error);
            }
            img.style.filter = cropImg.style.filter;
        }
        //-----------------------------------------------------------------------------------------
        function doApplyTransformInner(cropImg, srcPoints, dstPoints, width, height) {
            console.log('doApplyTransformInner(',cropImg.id, srcPoints, dstPoints);
            console.log('....', width, height);
            let h0 = cropImg.style.height;
            cropImg.style.height='';
            let src = cv.imread(cropImg);
            let dst = new cv.Mat();

            let srcMat = cv.matFromArray(4, 1, cv.CV_32FC2, srcPoints);
            let dstMat = cv.matFromArray(4, 1, cv.CV_32FC2, dstPoints);
            let M = cv.getPerspectiveTransform(srcMat, dstMat);
            let dsize = new cv.Size(width, height);
            try {
                //cv.warpPerspective(src, dst, M, dsize, cv.INTER_LINEAR, cv.BORDER_CONSTANT, new cv.Scalar());
                cv.warpPerspective(src, dst, M, dsize, cv.INTER_CUBIC, cv.BORDER_CONSTANT, new cv.Scalar());

            }
            catch(error) {
                removeHourglass();
                alert('Warp failed');
                console.error('Error in warpping', error);
            }

            let croppedMat = dst.roi(new cv.Rect(0, 0, width, height));

            _cropper.removeElement('.cropSample');
            let cropElems = document.querySelector('.mf-cropElements');
            let cropCanvas = cropElems.querySelector('canvas');
            console.log('cropCanvas img size = '+getFileSize(calculateCanvasSizeInBytes(cropCanvas)));

            let tempCanvas = document.createElement('canvas');
            tempCanvas.className = 'cropSample mf-cropFitScreen';
            tempCanvas.width = width;
            tempCanvas.height = height;
            tempCanvas.style.left = cropCanvas.getBoundingClientRect().left+'px';
            tempCanvas.style.top = cropCanvas.getBoundingClientRect().top+'px';
            tempCanvas.style.zIndex = _cropper.getMaxZIndex()+1;
            tempCanvas.style.filter = cropImg.style.filter;
            cropElems.appendChild(tempCanvas);

            cv.imshow(tempCanvas, dst);
            _cropper.addClass(document.querySelector('.mf-crop2Canvas'), 'mf-cropHidden');
            _cropper.addClass(document.querySelector('.mf-cropImageCopy'),'mf-cropHidden');

            if (cropImg.getAttribute('data-fs')) {
                console.log('Original size is ' + getFileSize(cropImg.getAttribute('data-fs')));
            }
            else {
                let originalDataUrl = cropImg.src;
                if ( performance.getEntriesByName(cropImg.src) && performance.getEntriesByName(cropImg.src)[0]) {
                    var originalSize = performance.getEntriesByName(cropImg.src)[0].transferSize;
                    console.log('Original size is ' + getFileSize(originalSize));
                    cropImg.setAttribute('data-fs', originalSize);
                }
            }

            cropImg.style.height = h0;

            // Release resources
            src.delete();
            dst.delete();
            srcMat.delete();
            dstMat.delete();
            M.delete();
            croppedMat.delete();
            console.log('ready');
        }
        //-----------------------------------------------------------------------------------------
        function calculateCanvasSizeInBytes(canvas) {
            let dataUrl = canvas.toDataURL();  // Convert canvas to data URL
            let dataSizeInBytes = dataUrl.length;
            let dataSizeInKb = dataSizeInBytes / 1024;
            console.log("Canvas size:", dataSizeInBytes, "bytes (", dataSizeInKb.toFixed(2), "KB)");
            return dataSizeInBytes;
        }
        //-----------------------------------------------------------------------------------------
        function doRecursiveTextReplace(node, a, b) {
            node.childNodes.forEach(child => {
                if (child.nodeType === Node.TEXT_NODE) {
                    if (child.textContent.trim() === a) {
                        child.textContent = b;
                    }
                }
                else if (child.nodeType === Node.ELEMENT_NODE) {
                    doRecursiveTextReplace(child, a,b);
                }
            });
        }
        //-----------------------------------------------------------------------------------------
        function getEstSizeFromBase64(base64String) {
            const base64Data = base64String.split(',')[1];
            const base64Size = Math.round((base64Data.length * 3) / 4);
            const estimatedBinarySize = Math.round(base64Size * 0.75);
            return estimatedBinarySize;
        }
        //-----------------------------------------------------------------------------------------
        function ajaxPost(params, formData) {
            // Define your own ajaxPost function....

            let formElemsTx='';
            for (const [key, value] of formData.entries()) formElemsTx+=','+key
            console.log(`Apply an ajax call for ${params} + ${formElemsTx.substring(1)}`);
        }
        //-----------------------------------------------------------------------------------------
        function showHourglass(el = document.body) {
            const spinner = document.createElement("div");
            spinner.className = "hg-spinner";
            spinner.title = "Loading...";

            if (el !== document.body) {
                const rect = el.getBoundingClientRect();
                spinner.style.position = "absolute";
                spinner.style.left = `${rect.left + rect.width / 2}px`;
                spinner.style.top = `${rect.top + rect.height / 2}px`;
                spinner.style.transform = "translate(-50%, -50%)";
                document.body.appendChild(spinner);
            } else {
                spinner.style.position = "fixed";
                spinner.style.left = "50%";
                spinner.style.top = "50%";
                spinner.style.transform = "translate(-50%, -50%)";
                document.body.appendChild(spinner);
                document.body.classList.add("loading");
            }
            spinner.style.zIndex = getMaxZIndex()+1;
        }
        //-----------------------------------------------------------------------------------------
        function removeHourglass() {
            const spinner = document.querySelector(".hg-spinner");
            if (spinner) spinner.remove();
            document.body.classList.remove("loading");
        }
        //-----------------------------------------------------------------------------------------
        function getFileSize(bytes, dp=0) {
            if (bytes < 1000) return bytes + 'bytes';
            if (bytes < 1000 * 1000) return (bytes / 1000).toFixed(dp) + 'Kb';
            if (bytes < 1000 * 1000 * 1000) return (bytes / 1000 / 1000).toFixed(dp) + 'Mb';
            return (bytes / 1000 / 1000 / 1000).toFixed(dp) + 'Gb';
        }
        //-----------------------------------------------------------------------------------------
        function getMaxZIndex() {
            function isnull(tx) {
                if (!tx) return "[null]";
                return tx;
            }
            //--------------------------------------------------
            function isNumeric(n) {
                return !isNaN(parseFloat(n)) && isFinite(n);
            }
            //--------------------------------------------------
            function removePX(x) { // Return "a" from inside "apx"
                if (x == '') return parseInt(0);
                var x2 = '' + x;
                return parseInt(x2.replace(/px/, ''), 10);
            }
            //--------------------------------------------------
            function getComputedStyleX(elem, attr, doRemovePX, nulls) {
                if (nulls == undefined) nulls = '[null]';
                if (doRemovePX == undefined) doRemovePX = false;
                if (typeof elem === 'string') {
                    let elems = document.querySelectorAll(elem);
                    for (let i = elems.length - 1; i >= 0; i--) getComputedStyleX(elems[i], attr, doRemovePX, nulls);
                    return;
                }
                if (!elem) return "Null given in getComputedStyleX(NULL,'" + attr + "')";

                var style  = window.getComputedStyle(elem);
                var result = style.getPropertyValue(attr);
                if (doRemovePX) {
                    var r = parseFloat(removePX(result));
                    if (!r) return nulls;
                    else return r;
                }
                return isnull(result);
            }
            //--------------------------------------------------

            var elems = document.getElementsByTagName('div');
            var max   = 0;
            for (var i = 0; i < elems.length; i++) {
                var t = getComputedStyleX(elems[i], 'z-index');
                if (t != 'auto') {
                    if (isNumeric(t) && parseInt(t) > max) {
                        max = parseInt(t);
                    }
                }
            }
            var elems = document.getElementsByTagName('canvas');
            for (var i = 0; i < elems.length; i++) {
                var t = getComputedStyleX(elems[i], 'z-index');
                if (isNumeric(t) && parseInt(t) > max) {
                    max = parseInt(t);
                }
            }
            return parseInt(max);
        }
        //-----------------------------------------------------------------------------------------
        function toggleImgSize(el) {
            el.classList.toggle('zoomed-img');
        }
        //-----------------------------------------------------------------------------------------
        function changeImage(el) {
            showHourglass(document.querySelector('#originalImage'));

            let imgUrl = '../../../images/'+el.value.split(' ')[0]+'.png';
            document.querySelector('img').onload = function() {
                removeHourglass();
            }
            document.querySelector('#imageInfo').innerHTML='loading...';
            document.querySelector('img').src = '';
            document.querySelector('img').src = imgUrl;

            async function getImageSizeFromURL(imgUrl) {
                try {
                    const response = await fetch(imgUrl);
                    const blob = await response.blob();
                    let infoEl = document.querySelector('#imageInfo');
                    infoEl.innerHTML=el.value.split(' ')[0]+' image size:'+getFileSize(blob.size);
                } catch (error) {
                    console.error('Error fetching image:', error);
                }
            }
            getImageSizeFromURL(imgUrl);
        }
    </script>

</body>
</html>

About

License

Latest Release

Version 1.032024-12-01