| 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>Also see
articles/Implementing-a-Monaco-Editorsquarearticles/iConnectionTestsquarearticles/imagick-fiddlesquarearticles/javascript-camerasquarearticles/list-editsquarearticles/minifierssquarearticles/opayosquarearticles/table-drag-sortersquarearticles/typewatchsquareprojects/MFCalculatorPopupsquareprojects/MFCalendarPopupsquareprojects/MFChartColumnsquareprojects/MFColorPickersquareprojects/MFColorPickerBasicsquareprojects/MFColumnDataBarsquareprojects/MFColumnGradientsquareprojects/MFCropCornerUIsquareprojects/MFFloatawayMsgsquareprojects/MFHourglasssquareprojects/MFLazyImagessquareprojects/MFPanZoomsquareprojects/MFPanelssquareprojects/MFScrollIndicatorssquareprojects/MFSelectorsquareprojects/MFShowConsolesquareprojects/MFTableCellSummingsquare