This content covers free image crop tool in detail.
Enhanced Passport Photo Crop Tool
:root {
–bg-color: #3f005a; /* Dark purple */
–primary-color: #bb86fc; /* Light purple for accents */
–secondary-color: #03dac6; /* Teal for highlights */
–text-color: #e0e0e0;
–card-bg: #2d003f; /* Slightly lighter purple for cards */
–border-color: #5a0080; /* Darker purple border */
–shadow-color: rgba(0, 0, 0, 0.4);
–gradient-start: #6a11cb;
–gradient-end: #2575fc;
–button-hover-bg: #9a67ea;
–button-text-color: #fff;
–input-bg: #1c0028;
–input-border: #440060;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: ‘Segoe UI’, sans-serif;
background: linear-gradient(135deg, var(–bg-color), var(–card-bg));
color: var(–text-color);
padding: 20px;
text-align: center;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 10px;
flex-grow: 1;
}
h1 {
color: var(–secondary-color);
text-shadow: 0 0 10px var(–secondary-color);
margin-bottom: 30px;
font-size: 2.5em;
}
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.control-group {
background: var(–card-bg);
padding: 20px;
border-radius: 15px;
border: 1px solid var(–border-color);
box-shadow: 0 5px 20px var(–shadow-color);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.control-group label {
display: block;
margin-bottom: 8px;
font-size: 0.95em;
color: var(–primary-color);
font-weight: 600;
}
input[type=”file”] {
width: 100%;
max-width: 250px;
padding: 10px;
border-radius: 8px;
background: var(–input-bg);
color: var(–text-color);
border: 1px solid var(–input-border);
cursor: pointer;
text-align: center;
}
input[type=”file”]::file-selector-button {
background: var(–primary-color);
color: var(–button-text-color);
border: none;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s ease;
margin-right: 10px;
}
input[type=”file”]::file-selector-button:hover {
background-color: var(–button-hover-bg);
}
.crop-size-inputs {
display: flex;
align-items: center;
gap: 10px;
justify-content: center;
width: 100%;
}
.crop-size-inputs input[type=”number”] {
width: 80px;
padding: 8px;
border-radius: 6px;
background: var(–input-bg);
color: var(–text-color);
border: 1px solid var(–input-border);
text-align: center;
}
input[type=”range”] {
width: 100%;
-webkit-appearance: none;
height: 8px;
background: var(–input-border);
border-radius: 5px;
outline: none;
transition: opacity .2s;
margin-top: 5px;
}
input[type=”range”]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(–primary-color);
cursor: pointer;
border: 2px solid var(–text-color);
}
.zoom-display {
font-weight: bold;
color: var(–secondary-color);
margin-top: 5px;
}
select {
padding: 8px;
border-radius: 6px;
background: var(–input-bg);
color: var(–text-color);
border: 1px solid var(–input-border);
appearance: none; /* Remove default arrow */
-webkit-appearance: none;
-moz-appearance: none;
background-image: url(‘data:image/svg+xml;utf8, ‘);
background-repeat: no-repeat;
background-position: right 8px top 50%;
background-size: 16px;
cursor: pointer;
width: 100%;
max-width: 200px;
}
.mode-selection {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 20px;
margin-bottom: 20px;
}
.mode-button {
padding: 10px 18px;
font-size: 0.95rem;
border: none;
border-radius: 25px; /* Pill shape */
background-color: var(–primary-color);
color: var(–button-text-color);
cursor: pointer;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease;
white-space: nowrap;
}
.mode-button:hover {
background-color: var(–button-hover-bg);
transform: translateY(-2px);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.3);
}
.mode-button.active {
background-color: var(–secondary-color);
color: #000;
box-shadow: 0 0 15px var(–secondary-color);
transform: scale(1.05);
font-weight: bold;
}
.info-bar {
background: var(–card-bg);
padding: 15px 25px;
border-radius: 15px;
border: 1px solid var(–border-color);
box-shadow: 0 5px 20px var(–shadow-color);
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px 30px;
font-size: 0.9em;
color: var(–primary-color);
margin-bottom: 30px;
}
.info-item span {
font-weight: bold;
color: var(–secondary-color);
margin-left: 5px;
}
.tool-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 25px;
margin-bottom: 30px;
}
.canvas-box {
background: var(–card-bg);
padding: 20px;
border-radius: 15px;
border: 1px solid var(–border-color);
box-shadow: 0 5px 20px var(–shadow-color);
max-width: 100%;
flex: 1 1 calc(50% – 25px); /* Adjusted for gap */
min-width: 300px;
display: flex;
flex-direction: column;
align-items: center;
}
.canvas-box h3 {
color: var(–primary-color);
margin-top: 0;
margin-bottom: 15px;
font-size: 1.3em;
}
canvas {
max-width: 100%;
height: auto;
background: #fff;
display: block;
margin: 0 auto;
border: 1px dashed var(–secondary-color);
border-radius: 8px;
cursor: default; /* Default cursor, managed by JS */
}
canvas:active {
/* Managed by JS for specific actions */
}
.action-buttons {
margin-top: 20px;
margin-bottom: 20px;
}
.action-buttons button {
margin: 10px 10px;
padding: 14px 30px;
font-size: 1.1rem;
border: none;
border-radius: 10px;
background-color: var(–secondary-color);
color: #000;
cursor: pointer;
box-shadow: 0 0 15px var(–secondary-color);
transition: background-color 0.3s ease, box-shadow 0.3s ease, transform 0.2s ease;
font-weight: 600;
}
.action-buttons button:hover {
background-color: #00cccc;
box-shadow: 0 0 20px var(–secondary-color);
transform: translateY(-2px);
}
#downloadLink {
display: none;
margin-top: 20px;
font-weight: bold;
color: var(–secondary-color);
text-decoration: underline;
font-size: 1.1em;
}
footer {
margin-top: 40px;
font-size: 0.85em;
color: var(–primary-color);
text-shadow: 0 0 5px rgba(187, 134, 252, 0.3);
padding: 10px;
}
/* Initial canvas message */
.initial-message {
color: #aaa;
font-size: 1.2em;
text-align: center;
padding: 50px;
}
@media screen and (max-width: 768px) {
h1 {
font-size: 2em;
}
.controls-grid {
grid-template-columns: 1fr;
}
.tool-container {
flex-direction: column;
align-items: center;
}
.canvas-box {
flex-basis: 100%;
}
.mode-selection {
flex-direction: column;
align-items: center;
}
.mode-button {
width: 100%;
max-width: 250px;
}
.action-buttons button {
width: calc(100% – 20px);
margin: 8px 10px;
}
}
@media screen and (max-width: 480px) {
body {
padding: 15px;
}
.main-content {
padding: 0 5px;
}
.control-group, .info-bar, .canvas-box {
padding: 15px;
}
.crop-size-inputs input[type=”number”] {
width: 70px;
}
}
Enhanced Passport Photo Crop Tool
📸 Upload Image
📐 Aspect Ratio
Custom
Passport (132:170)
Default (162:200)
0 FREE CROP
✋ HAND TOOL
Mode: Free Crop
Selection: No selection
Size: –
File Size: –
✂️ CROP & RESIZE
🔄 RESET
Enhanced Passport Photo Crop Tool | Responsive Design | Multiple Selection Modes
Passport size: 132x170px | Final file 20-80KB | © 2025
const upload = document.getElementById(‘upload’);
const originalCanvas = document.getElementById(‘originalCanvas’);
const originalCtx = originalCanvas.getContext(‘2d’);
const croppedCanvas = document.getElementById(‘croppedCanvas’);
const croppedCtx = croppedCanvas.getContext(‘2d’);
const downloadLink = document.getElementById(‘downloadLink’);
const cropWidthInput = document.getElementById(‘cropWidth’);
const cropHeightInput = document.getElementById(‘cropHeight’);
const zoomSlider = document.getElementById(‘zoomSlider’);
const zoomDisplay = document.getElementById(‘zoomDisplay’);
const aspectRatioSelect = document.getElementById(‘aspectRatioSelect’);
// Mode Buttons
const freeCropBtn = document.getElementById(‘freeCropBtn’);
const handToolBtn = document.getElementById(‘handToolBtn’);
// Info Bar elements
const currentModeDisplay = document.getElementById(‘currentMode’);
const selectionStatusDisplay = document.getElementById(‘selectionStatus’);
const cropBoxSizeDisplay = document.getElementById(‘cropBoxSize’);
const fileSizeDisplay = document.getElementById(‘fileSizeDisplay’);
let img = new Image();
let isDragging = false; // For drawing a new crop box in FREE_CROP mode
let isMovingCropBox = false; // For moving the entire existing crop box
let isResizingCropBox = false; // For resizing existing crop box edges/corners
let resizeHandle = null; // Stores which handle is being dragged (e.g., ‘n’, ‘sw’, ‘e’)
let startX = 0, startY = 0; // Top-left of the crop box on the original image (unscaled)
let endX = 0, endY = 0; // Bottom-right of the crop box on the original image (unscaled)
let offsetX = 0, offsetY = 0; // Offset for moving the crop box (mouse/touch pos relative to cropbox start)
let zoom = 1;
let panX = 0, panY = 0; // Pan for the image display
let isPanning = false; // For panning the image
let panStartX = 0, panStartY = 0; // Start coordinates for panning (mouse/touch pos on screen)
// Cropping Modes
const CROP_MODE = {
FREE_CROP: ‘Free Crop’,
HAND_TOOL: ‘Hand Tool’,
};
let currentCropMode = CROP_MODE.FREE_CROP;
// Constants for file size target
const MIN_FILE_SIZE_BYTES = 20000; // 20KB
const MAX_FILE_SIZE_BYTES = 80000; // 80KB
let JPEG_QUALITY = 0.8; // Initial JPEG quality – often a good starting point for 20-80KB
const MIN_ZOOM = 0.1; // Allowing smaller zoom
const MAX_ZOOM = 5; // Allowing larger zoom
const INITIAL_CANVAS_MESSAGE = “Upload an image to start cropping”;
const CROP_LINE_WIDTH = 3; // Crop line width in pixels
const HANDLE_SIZE = 10; // Area around handle for detecting interaction (in original image px, unscaled)
// Default crop dimensions – UPDATED TO 162×200
const DEFAULT_CROP_WIDTH = 162;
const DEFAULT_CROP_HEIGHT = 200;
// — Event Listeners —
zoomSlider.addEventListener(“input”, () => {
zoom = parseFloat(zoomSlider.value);
zoomDisplay.textContent = `${zoom.toFixed(1)}x`;
drawAll(); // Redraw everything when zoom changes
});
cropWidthInput.addEventListener(‘input’, updateCropDimensions);
cropHeightInput.addEventListener(‘input’, updateCropDimensions);
aspectRatioSelect.addEventListener(‘change’, () => {
const selectedValue = aspectRatioSelect.value;
if (selectedValue === “custom”) {
// Do nothing, manual inputs are active
} else {
const [widthRatio, heightRatio] = selectedValue.split(‘:’).map(Number);
cropWidthInput.value = widthRatio;
cropHeightInput.value = heightRatio;
}
updateCropDimensions(); // This will also call centerCropBoxBasedOnDimensions and drawAll
});
upload.addEventListener(‘change’, function () {
const file = this.files[0];
if (!file) return;
if (![‘image/jpeg’, ‘image/jpg’, ‘image/png’].includes(file.type)) {
alert(‘Only JPG, JPEG or PNG files are allowed.’);
return;
}
const reader = new FileReader();
reader.onload = function (e) {
img.onload = () => {
originalCanvas.width = img.width;
originalCanvas.height = img.height;
panX = 0;
panY = 0;
zoom = 1;
zoomSlider.value = 1;
zoomDisplay.textContent = ‘1.0x’;
// Set default crop box size (162×200) and center it on image upload
cropWidthInput.value = DEFAULT_CROP_WIDTH;
cropHeightInput.value = DEFAULT_CROP_HEIGHT;
// Also ensure the “Default (162:200)” option is selected in the dropdown
aspectRatioSelect.value = `${DEFAULT_CROP_WIDTH}:${DEFAULT_CROP_HEIGHT}`;
updateCropDimensions(); // This calls centerCropBoxBasedOnDimensions internally
downloadLink.style.display = “none”;
selectionStatusDisplay.textContent = ‘Active’;
// Automatically switch to Free Crop if not already
if (currentCropMode !== CROP_MODE.FREE_CROP) {
setCropMode(CROP_MODE.FREE_CROP);
} else {
drawAll(); // Redraw with initial crop box and cursor
}
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
// Mode button event listeners
freeCropBtn.addEventListener(‘click’, () => setCropMode(CROP_MODE.FREE_CROP));
handToolBtn.addEventListener(‘click’, () => setCropMode(CROP_MODE.HAND_TOOL));
originalCanvas.addEventListener(‘mousedown’, startInteraction);
originalCanvas.addEventListener(‘mousemove’, duringInteraction);
originalCanvas.addEventListener(‘mouseup’, endInteraction);
originalCanvas.addEventListener(‘mouseleave’, endInteraction);
// Touch events (simplified for brevity, consider separate handlers for robust mobile UX)
originalCanvas.addEventListener(‘touchstart’, startInteraction);
originalCanvas.addEventListener(‘touchmove’, duringInteraction);
originalCanvas.addEventListener(‘touchend’, endInteraction);
originalCanvas.addEventListener(“wheel”, function (e) {
e.preventDefault();
if (!img.src) return;
const delta = e.deltaY 0 && Math.abs(endY – startY) > 0) {
originalCtx.strokeStyle = ‘#00ffff’;
originalCtx.lineWidth = CROP_LINE_WIDTH / zoom; // Adjust line width based on zoom for responsiveness
originalCtx.setTransform(zoom, 0, 0, zoom, panX, panY);
originalCtx.strokeRect(startX, startY, endX – startX, endY – startY);
originalCtx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
}
updateInfoBar();
}
function drawInitialMessage(ctx, width, height) {
ctx.font = “20px ‘Segoe UI'”;
ctx.textAlign = “center”;
ctx.fillStyle = “#aaa”;
ctx.fillText(INITIAL_CANVAS_MESSAGE, width / 2, height / 2);
}
function getScaledCoords(e) {
const rect = originalCanvas.getBoundingClientRect();
const scaleX = originalCanvas.width / rect.width;
const scaleY = originalCanvas.height / rect.height;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
// Convert canvas display coordinates to original image coordinates
return {
x: ((clientX – rect.left) * scaleX – panX) / zoom,
y: ((clientY – rect.top) * scaleY – panY) / zoom
};
}
function getCropBoxCoords() {
return {
x: Math.min(startX, endX),
y: Math.min(startY, endY),
width: Math.abs(endX – startX),
height: Math.abs(endY – startY)
};
}
function getHandle(x, y) {
const { x: cropX, y: cropY, width: cropWidth, height: cropHeight } = getCropBoxCoords();
const tolerance = HANDLE_SIZE / zoom; // Adjusted tolerance based on zoom
const nearLeft = Math.abs(x – cropX) < tolerance;
const nearRight = Math.abs(x – (cropX + cropWidth)) < tolerance;
const nearTop = Math.abs(y – cropY) < tolerance;
const nearBottom = Math.abs(y – (cropY + cropHeight)) cropX + tolerance && x cropY + tolerance && y CROP_LINE_WIDTH && cropHeight > CROP_LINE_WIDTH) return ‘move’; // Ensure box is large enough to be moved
return null; // No handle detected
}
function updateCursor(e) {
if (!img.src || isDragging || isMovingCropBox || isResizingCropBox || isPanning) {
return; // Cursor is already set by interaction start
}
const coords = getScaledCoords(e);
if (currentCropMode === CROP_MODE.HAND_TOOL) {
originalCanvas.style.cursor = ‘grab’;
} else if (currentCropMode === CROP_MODE.FREE_CROP) {
const handle = getHandle(coords.x, coords.y);
if (handle) {
if (handle === ‘n’ || handle === ‘s’) originalCanvas.style.cursor = ‘ns-resize’;
else if (handle === ‘e’ || handle === ‘w’) originalCanvas.style.cursor = ‘ew-resize’;
else if (handle === ‘nw’ || handle === ‘se’) originalCanvas.style.cursor = ‘nwse-resize’;
else if (handle === ‘ne’ || handle === ‘sw’) originalCanvas.style.cursor = ‘nesw-resize’;
else if (handle === ‘move’) originalCanvas.style.cursor = ‘grab’;
} else {
originalCanvas.style.cursor = ‘crosshair’;
}
} else {
originalCanvas.style.cursor = ‘default’;
}
}
function startInteraction(e) {
e.preventDefault();
if (!img.src) return;
const coords = getScaledCoords(e);
const { x: cropX, y: cropY, width: cropWidth, height: cropHeight } = getCropBoxCoords();
// Panning initiated by right-click or middle-click or Ctrl-click OR if Hand Tool is active
if (e.button === 1 || e.ctrlKey || e.button === 2 || currentCropMode === CROP_MODE.HAND_TOOL) {
isPanning = true;
panStartX = e.clientX;
panStartY = e.clientY;
originalCanvas.style.cursor = ‘grabbing’;
return;
}
if (currentCropMode === CROP_MODE.FREE_CROP) {
const handle = getHandle(coords.x, coords.y);
if (handle && cropWidth > 0 && cropHeight > 0) { // Interaction with existing crop box
if (handle === ‘move’) {
isMovingCropBox = true;
offsetX = coords.x – cropX;
offsetY = coords.y – cropY;
originalCanvas.style.cursor = ‘grabbing’;
} else {
isResizingCropBox = true;
resizeHandle = handle;
originalCanvas.style.cursor = getComputedStyle(originalCanvas).cursor; // Keep the current resize cursor
}
} else { // Drawing a new crop box or pin-point selection
isDragging = true;
startX = coords.x;
startY = coords.y;
endX = coords.x;
endY = coords.y;
originalCanvas.style.cursor = ‘crosshair’;
}
}
drawAll();
}
function duringInteraction(e) {
if (!img.src) return;
const coords = getScaledCoords(e);
if (isPanning) {
const dx = e.clientX – panStartX;
const dy = e.clientY – panStartY;
panX += dx;
panY += dy;
panStartX = e.clientX;
panStartY = e.clientY;
drawAll();
return;
}
// Only update cursor if not actively dragging/moving/resizing
if (!isDragging && !isMovingCropBox && !isResizingCropBox) {
updateCursor(e);
}
let currentMinX = Math.min(startX, endX);
let currentMinY = Math.min(startY, endY);
let currentMaxX = Math.max(startX, endX);
let currentMaxY = Math.max(startY, endY);
let currentCropWidth = currentMaxX – currentMinX;
let currentCropHeight = currentMaxY – currentMinY;
if (currentCropMode === CROP_MODE.FREE_CROP) {
if (isMovingCropBox) {
let newMinX = coords.x – offsetX;
let newMinY = coords.y – offsetY;
// Clamp new position within image boundaries
newMinX = Math.max(0, Math.min(newMinX, img.width – currentCropWidth));
newMinY = Math.max(0, Math.min(newMinY, img.height – currentCropHeight));
startX = newMinX;
startY = newMinY;
endX = newMinX + currentCropWidth;
endY = newMinY + currentCropHeight;
} else if (isResizingCropBox) {
// Free resizing of crop lines/corners
switch (resizeHandle) {
case ‘n’: currentMinY = Math.min(Math.max(0, coords.y), currentMaxY – CROP_LINE_WIDTH); break;
case ‘s’: currentMaxY = Math.max(Math.min(img.height, coords.y), currentMinY + CROP_LINE_WIDTH); break;
case ‘w’: currentMinX = Math.min(Math.max(0, coords.x), currentMaxX – CROP_LINE_WIDTH); break;
case ‘e’: currentMaxX = Math.max(Math.min(img.width, coords.x), currentMinX + CROP_LINE_WIDTH); break;
case ‘nw’:
currentMinX = Math.min(Math.max(0, coords.x), currentMaxX – CROP_LINE_WIDTH);
currentMinY = Math.min(Math.max(0, coords.y), currentMaxY – CROP_LINE_WIDTH);
break;
case ‘ne’:
currentMaxX = Math.max(Math.min(img.width, coords.x), currentMinX + CROP_LINE_WIDTH);
currentMinY = Math.min(Math.max(0, coords.y), currentMaxY – CROP_LINE_WIDTH);
break;
case ‘sw’:
currentMinX = Math.min(Math.max(0, coords.x), currentMaxX – CROP_LINE_WIDTH);
currentMaxY = Math.max(Math.min(img.height, coords.y), currentMinY + CROP_LINE_WIDTH);
break;
case ‘se’:
currentMaxX = Math.max(Math.min(img.width, coords.x), currentMinX + CROP_LINE_WIDTH);
currentMaxY = Math.max(Math.min(img.height, coords.y), currentMinY + CROP_LINE_WIDTH);
break;
}
startX = currentMinX;
startY = currentMinY;
endX = currentMaxX;
endY = currentMaxY;
} else if (isDragging) { // Drawing a new box
endX = coords.x;
endY = coords.y;
// Ensure the crop box stays within image boundaries while drawing
startX = Math.max(0, Math.min(startX, img.width));
startY = Math.max(0, Math.min(startY, img.height));
endX = Math.max(0, Math.min(endX, img.width));
endY = Math.max(0, Math.min(endY, img.height));
}
}
drawAll();
}
function endInteraction(e) {
// Pin Point Crop Selection: If it was a click (not a drag) in Free Crop mode
// Check if the drag distance was negligible (e.g., less than 5 pixels)
if (img.src && currentCropMode === CROP_MODE.FREE_CROP && isDragging &&
Math.abs(startX – getScaledCoords(e).x) < 5 / zoom && Math.abs(startY – getScaledCoords(e).y) < 5 / zoom) {
const cw = parseInt(cropWidthInput.value);
const ch = parseInt(cropHeightInput.value);
const coords = getScaledCoords(e); // Get the final click coordinates
let newStartX = coords.x – cw / 2;
let newStartY = coords.y – ch / 2;
// Clamp to image boundaries
newStartX = Math.max(0, Math.min(newStartX, img.width – cw));
newStartY = Math.max(0, Math.min(newStartY, img.height – ch));
startX = newStartX;
startY = newStartY;
endX = newStartX + cw;
endY = newStartY + ch;
}
isDragging = false;
isMovingCropBox = false;
isResizingCropBox = false;
resizeHandle = null;
isPanning = false;
updateCursor(e); // Reset cursor based on new state
drawAll();
}
// — Action Functions —
function updateCropDimensions() {
const cw = parseInt(cropWidthInput.value);
const ch = parseInt(cropHeightInput.value);
// Set cropped canvas size for preview
croppedCanvas.width = cw;
croppedCanvas.height = ch;
croppedCtx.clearRect(0, 0, cw, ch);
if (img.src) {
// When dimensions change, re-center the crop box with the new ratio
centerCropBoxBasedOnDimensions();
}
updateDownloadLinkText();
}
function cropAndResize() {
if (!img.src) {
alert("Please upload an image first.");
return;
}
const currentCropWidthPx = Math.abs(endX – startX);
const currentCropHeightPx = Math.abs(endY – startY);
if (currentCropWidthPx < CROP_LINE_WIDTH || currentCropHeightPx {
return new Promise(resolve => {
let currentQuality = initialQuality;
let attempt = 0;
const generateBlob = () => {
canvas.toBlob(blob => {
if (!blob) {
resolve(null); // Failed to create blob
return;
}
const fileSize = blob.size;
// If within range, or max attempts reached, or quality is at min/max, resolve
// Allow a small buffer for min/max quality to prevent infinite loops near boundaries
if ((fileSize >= minSize && fileSize = maxAttempts – 1 || currentQuality = 0.95) {
resolve(blob);
} else {
// Adjust quality
if (fileSize maxSize) {
currentQuality = Math.max(0.1, currentQuality – 0.05); // Decrease quality
}
attempt++;
// console.log(`Attempt ${attempt}: Quality = ${currentQuality.toFixed(2)}, Size = ${fileSize / 1024} KB`); // For debugging
generateBlob(); // Try again
}
}, mimeType, currentQuality);
};
generateBlob();
});
};
// Use the iterative function
getBlobWithTargetSize(tempCanvas, ‘image/jpeg’, JPEG_QUALITY, MIN_FILE_SIZE_BYTES, MAX_FILE_SIZE_BYTES)
.then(blob => {
if (!blob) {
alert(“Failed to create image blob or adjust size. Please try again.”);
downloadLink.style.display = “none”;
fileSizeDisplay.textContent = ‘-‘;
return;
}
const url = URL.createObjectURL(blob);
downloadLink.href = url;
downloadLink.style.display = “inline-block”;
updateDownloadLinkText(Math.round(blob.size / 1024));
});
}
function resetCrop() {
if (!img.src) {
originalCtx.clearRect(0, 0, originalCanvas.width, originalCanvas.height);
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
drawInitialMessage(originalCtx, originalCanvas.width, originalCanvas.height);
downloadLink.style.display = “none”;
fileSizeDisplay.textContent = ‘-‘;
selectionStatusDisplay.textContent = ‘No selection’;
return;
}
// Reset zoom and pan for original canvas
zoom = 1;
zoomSlider.value = 1;
zoomDisplay.textContent = ‘1.0x’;
panX = 0;
panY = 0;
// Set default crop box size (162×200) and center it
cropWidthInput.value = DEFAULT_CROP_WIDTH;
cropHeightInput.value = DEFAULT_CROP_HEIGHT;
aspectRatioSelect.value = `${DEFAULT_CROP_WIDTH}:${DEFAULT_CROP_HEIGHT}`; // Ensure dropdown reflects this
centerCropBoxBasedOnDimensions();
drawAll(); // Draw image and the new crop box
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
downloadLink.style.display = “none”;
updateDownloadLinkText();
selectionStatusDisplay.textContent = ‘Active’;
setCropMode(CROP_MODE.FREE_CROP); // Default to Free Crop after reset
}
// Function to center the crop box based on current input dimensions
function centerCropBoxBasedOnDimensions() {
if (!img.src) {
return;
}
const cw = parseInt(cropWidthInput.value);
const ch = parseInt(cropHeightInput.value);
const targetAspectRatio = cw / ch;
let idealCropWidth, idealCropHeight;
// Determine the largest possible crop box with the target aspect ratio that fits within the image
// We want to scale the target crop dimensions (cw, ch) to fit within the image while maintaining aspect ratio
const imageAspectRatio = img.width / img.height;
if (imageAspectRatio > targetAspectRatio) {
// Image is wider relative to its height than the target crop.
// The crop height will be limited by the image height, and width will be proportional.
idealCropHeight = img.height;
idealCropWidth = idealCropHeight * targetAspectRatio;
} else {
// Image is taller relative to its width than the target crop, or aspect ratios are similar.
// The crop width will be limited by the image width, and height will be proportional.
idealCropWidth = img.width;
idealCropHeight = idealCropWidth / targetAspectRatio;
}
// If the calculated ideal dimensions are larger than the actual requested crop dimensions,
// we should use the actual requested crop dimensions. This ensures the crop box
// starts at the requested size, not necessarily filling the entire image if smaller.
// However, the goal here is “auto crop line present on image, crop line size 162px X 200px”
// meaning the *initial* crop box should be exactly 162×200 and centered.
// We need to ensure it doesn’t exceed image bounds.
let newCropWidth = cw;
let newCropHeight = ch;
// Adjust if desired crop size is larger than image itself
if (newCropWidth > img.width) {
newCropWidth = img.width;
newCropHeight = img.width / targetAspectRatio; // Scale height to maintain aspect ratio
}
if (newCropHeight > img.height) {
newCropHeight = img.height;
newCropWidth = img.height * targetAspectRatio; // Scale width to maintain aspect ratio
}
// Ensure calculated newCropWidth and newCropHeight are integer values
newCropWidth = Math.round(newCropWidth);
newCropHeight = Math.round(newCropHeight);
// Center the calculated crop box
startX = (img.width – newCropWidth) / 2;
startY = (img.height – newCropHeight) / 2;
endX = startX + newCropWidth;
endY = startY + newCropHeight;
selectionStatusDisplay.textContent = ‘Centered’;
}
function setCropMode(mode) {
currentCropMode = mode;
currentModeDisplay.textContent = mode;
// Update active button state
document.querySelectorAll(‘.mode-button’).forEach(btn => {
btn.classList.remove(‘active’);
});
// Set active class for the selected mode button
if (mode === CROP_MODE.FREE_CROP) {
freeCropBtn.classList.add(‘active’);
} else if (mode === CROP_MODE.HAND_TOOL) {
handToolBtn.classList.add(‘active’);
}
// Reset interaction states and update cursor
isDragging = false;
isMovingCropBox = false;
isResizingCropBox = false;
resizeHandle = null;
isPanning = false;
// Immediately set canvas cursor based on new mode
if (mode === CROP_MODE.HAND_TOOL) {
originalCanvas.style.cursor = ‘grab’;
} else if (mode === CROP_MODE.FREE_CROP) {
originalCanvas.style.cursor = ‘crosshair’; // Default for drawing/manipulating
}
drawAll(); // Redraw to update info bar, etc.
}
function updateDownloadLinkText(fileSizeBytes = null) {
const cw = cropWidthInput.value;
const ch = cropHeightInput.value;
let sizeInfo = ”;
if (fileSizeBytes !== null) {
sizeInfo = `(${fileSizeBytes} KB / ${MIN_FILE_SIZE_BYTES / 1024}-${MAX_FILE_SIZE_BYTES / 1024}KB)`;
} else {
sizeInfo = `(target ${MIN_FILE_SIZE_BYTES / 1024}-${MAX_FILE_SIZE_BYTES / 1024}KB)`;
}
downloadLink.textContent = `📥 Download Photo ${cw} X ${ch} ${sizeInfo}`;
fileSizeDisplay.textContent = fileSizeBytes !== null ? `${fileSizeBytes} KB` : ‘-‘;
}
function updateInfoBar() {
if (!img.src) {
selectionStatusDisplay.textContent = ‘No image loaded’;
cropBoxSizeDisplay.textContent = ‘-‘;
fileSizeDisplay.textContent = ‘-‘;
return;
}
const currentCropWidthPx = Math.round(Math.abs(endX – startX));
const currentCropHeightPx = Math.round(Math.abs(endY – startY));
cropBoxSizeDisplay.textContent = `${currentCropWidthPx} x ${currentCropHeightPx} px`;
if (currentCropWidthPx > CROP_LINE_WIDTH && currentCropHeightPx > CROP_LINE_WIDTH) {
selectionStatusDisplay.textContent = ‘Selected’;
} else {
selectionStatusDisplay.textContent = ‘No selection’;
}
}
// Initialize on load
function initializePage() {
// Set initial crop size inputs and preview canvas dimensions
cropWidthInput.value = DEFAULT_CROP_WIDTH;
cropHeightInput.value = DEFAULT_CROP_HEIGHT;
croppedCanvas.width = DEFAULT_CROP_WIDTH;
croppedCanvas.height = DEFAULT_CROP_HEIGHT;
// Ensure the correct aspect ratio is selected by default
const defaultAspectRatioOption = aspectRatioSelect.querySelector(`option[value=”${DEFAULT_CROP_WIDTH}:${DEFAULT_CROP_HEIGHT}”]`);
if (defaultAspectRatioOption) {
defaultAspectRatioOption.selected = true;
} else {
// If for some reason the default option doesn’t exist, fall back to custom
aspectRatioSelect.value = “custom”;
}
updateDownloadLinkText();
drawInitialMessage(originalCtx, originalCanvas.width, originalCanvas.height);
updateInfoBar();
setCropMode(CROP_MODE.FREE_CROP); // Set initial mode
}
window.onload = initializePage;
My Name is Sourav Mukherjee. I am a Website Developer and SEO Expart. AllBestTool.com is a browser-based collection of free online utilities — everything from PDF converters and text tools to SEO helpers and calculators. The idea is pretty simple: make a bunch of useful web tools available in one place without requiring users to sign up.
Latest posts by allbesttool.com
(see all )