btg-sandbox-background-remover/index.php
djp1971 1db5547bad Initial commit: Background Remover - Clipping Magic Processor
- Web-based image processing application using Clipping Magic API
- Batch processing with PNG and TIFF output formats
- Microsoft Azure AD authentication integration
- Automatic file cleanup after 24 hours
- Real-time processing status and bulk download features

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-16 20:36:45 -04:00

675 lines
No EOL
22 KiB
PHP
Executable file

<?php
// Load configuration
$config = require 'config.php';
function generateUniqueId($length = 4) {
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
$id = '';
for ($i = 0; $i < $length; $i++) {
$id .= $characters[rand(0, strlen($characters) - 1)];
}
return $id;
}
function logMessage($type, $message, $data = null) {
global $config;
$logFile = $config['directories']['logs'] . date('Y-m-d') . '.log';
$timestamp = date('Y-m-d H:i:s');
$logData = [
'timestamp' => $timestamp,
'type' => $type,
'message' => $message,
'data' => $data
];
file_put_contents($logFile, json_encode($logData) . "\n", FILE_APPEND);
}
// Create necessary directories
foreach ($config['directories'] as $dir) {
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
}
function cleanupOldFiles($directories) {
// 24 hours in seconds
$timeThreshold = 24 * 60 * 60;
$now = time();
foreach ($directories as $dir) {
if (!is_dir($dir)) {
continue;
}
$files = glob($dir . '*');
foreach ($files as $file) {
// Skip if it's not a file
if (!is_file($file)) {
continue;
}
// Check file age
$fileAge = $now - filemtime($file);
if ($fileAge > $timeThreshold) {
unlink($file);
logMessage('info', 'Deleted old file', ['file' => $file, 'age' => $fileAge]);
}
}
}
}
$response = [];
$fileMapping = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['files'])) {
// Cleanup old files before processing new ones
cleanupOldFiles([
$config['directories']['upload'],
$config['directories']['processed']
]);
$files = reArrayFiles($_FILES['files']);
foreach ($files as $file) {
$originalFileName = basename($file['name']);
$uniqueId = generateUniqueId();
$fileInfo = pathinfo($originalFileName);
$newFileName = $uniqueId . '_' . $fileInfo['filename'] . '.' . $fileInfo['extension'];
$targetFilePath = $config['directories']['upload'] . $newFileName;
// Store original filename for later use
$fileMapping[$newFileName] = $originalFileName;
// Validate file
if (!in_array($file['type'], $config['processing']['allowed_types'])) {
$response[$originalFileName] = [
'success' => false,
'error' => $config['errors']['invalid_type']
];
continue;
}
if ($file['size'] > $config['processing']['max_file_size']) {
$response[$originalFileName] = [
'success' => false,
'error' => $config['errors']['file_too_large']
];
continue;
}
if (move_uploaded_file($file['tmp_name'], $targetFilePath)) {
try {
// Process with Clipping Magic API
$results = processWithClippingMagic($targetFilePath, $newFileName, $config);
if (!empty($results['png']) && !empty($results['tiff'])) {
$response[$originalFileName] = [
'success' => true,
'results' => [
'png' => $results['png'],
'tiff' => $results['tiff']
],
'uniqueId' => $uniqueId,
'originalName' => $originalFileName,
'debug' => [
'newFileName' => $newFileName,
'processedFiles' => $results
]
];
logMessage('debug', 'File processed successfully', $response[$originalFileName]);
} else {
throw new Exception('Failed to generate both PNG and TIFF outputs');
}
} catch (Exception $e) {
$response[$originalFileName] = [
'success' => false,
'error' => sprintf($config['errors']['processing'], $e->getMessage())
];
}
} else {
$response[$originalFileName] = [
'success' => false,
'error' => $config['errors']['upload']
];
}
}
}
header('Content-Type: application/json');
echo json_encode(['response' => $response]);
exit;
}
function processWithClippingMagic($filePath, $fileName, $config) {
// Get base filename without extension but keep the unique ID prefix
$baseFileName = pathinfo($fileName, PATHINFO_FILENAME);
// Process for PNG result
$pngResult = sendToClippingMagic($filePath, $fileName, $config['processing']['formats']['png'], $config);
// Process for TIFF with clipping path
$tiffResult = sendToClippingMagic($filePath, $fileName, $config['processing']['formats']['tiff'], $config);
return [
'png' => $baseFileName . '_result.png',
'tiff' => $baseFileName . '_clipping_path_tiff.tiff'
];
}
function sendToClippingMagic($filePath, $fileName, $formatConfig, $config) {
logMessage('info', 'Starting API request', [
'file' => $fileName,
'format' => $formatConfig['format']
]);
$ch = curl_init($config['api']['base_url'] . $config['api']['endpoints']['images']);
$postFields = [
'image' => new CURLFile($filePath),
'format' => $formatConfig['format'],
'test' => 'false'
];
curl_setopt_array($ch, [
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Basic ' . $config['api']['key']
]
]);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
logMessage('debug', 'API response received', [
'httpCode' => $httpCode,
'responseSize' => strlen($result),
'firstBytes' => bin2hex(substr($result, 0, 8))
]);
if (curl_errno($ch)) {
throw new Exception(curl_error($ch));
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check if the response looks like it contains image data
$firstBytes = substr($result, 0, 4);
$isPNG = strpos($firstBytes, "\x89\x50\x4E\x47") === 0;
$isTIFF = strpos($firstBytes, "\x49\x49\x2A\x00") === 0 || strpos($firstBytes, "\x4D\x4D\x00\x2A") === 0;
if (!$isPNG && !$isTIFF) {
$jsonResponse = json_decode($result, true);
if ($jsonResponse && isset($jsonResponse['error'])) {
throw new Exception($jsonResponse['error']);
} else if ($httpCode !== 200) {
throw new Exception(sprintf($config['errors']['api_error'], $httpCode));
}
}
// Generate output filename using the unique ID prefix
$outputFileName = pathinfo($fileName, PATHINFO_FILENAME) .
'_' . $formatConfig['format'] .
$formatConfig['extension'];
$outputPath = $config['directories']['processed'] . $outputFileName;
// Save the result
file_put_contents($outputPath, $result);
return $outputFileName;
}
function reArrayFiles($files) {
$file_array = array();
$file_count = count($files['name']);
$file_keys = array_keys($files);
for ($i = 0; $i < $file_count; $i++) {
foreach ($file_keys as $key) {
$file_array[$i][$key] = $files[$key][$i];
}
}
return $file_array;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clipping Magic Processor</title>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Montserrat', sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
text-align: center; /* Center child elements horizontally */
}
.upload-container {
text-align: center;
margin-bottom: 30px;
}
.logo {
width: 300px;
height: auto;
margin: 0 auto 20px; /* Ensures the logo is centered horizontally with margin for spacing */
}
.upload-button {
background-color: #4CAF50;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.upload-button:hover {
background-color: #45a049;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.image-box {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.image-preview {
width: 100%;
height: 200px;
object-fit: contain;
margin-bottom: 10px;
}
.status {
margin: 10px 0;
padding: 8px;
border-radius: 4px;
text-align: center;
}
.processing { background: #fff3cd; color: #856404; }
.complete { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.download-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
}
.download-button {
flex: 1;
padding: 8px;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}
.download-button:hover {
background: #0056b3;
}
.bulk-download {
text-align: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.preview-wrapper {
width: 100%;
height: 200px;
position: relative;
margin-bottom: 10px;
}
.image-preview {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
</style>
<!-- auth 1 of 4 -->
<script src="https://alcdn.msauth.net/browser/2.15.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
<style>
#protected-content {
display: none;
}
</style>
<!-- end auth block -->
</head>
<body>
<div class="app-container">
<!-- auth 2 of 4 -->
<div style="text-align: left;">
<button id="logout-button" onclick="signOut()" style="display:none;">Log Out</button>
<button id="login-button" onclick="signIn()" style="display:none;">Log In</button>
</div>
<!-- end auth block -->
<img src="clipping_magic.svg" alt="Clipping Magic" class="logo">
<div class="upload-container" id="protected-content">
<input type="file" id="fileUpload" multiple accept="image/*" style="display: none;">
<button class="upload-button" onclick="document.getElementById('fileUpload').click()">
Upload Images
</button>
</div>
<div class="bulk-download" id="bulkDownloadContainer" style="display: none;">
<button class="download-button" onclick="downloadAll('png')">Download All PNGs</button>
<button class="download-button" onclick="downloadAll('tiff')">Download All TIFFs</button>
</div>
<div id="imageGrid" class="image-grid"></div>
</div>
<script>
let processedFiles = {};
document.getElementById('fileUpload').addEventListener('change', function(event) {
const files = Array.from(event.target.files);
files.forEach(file => {
createImageBox(file);
uploadFile(file);
});
if (files.length > 0) {
document.getElementById('bulkDownloadContainer').style.display = 'block';
}
});
function createImageBox(file) {
const box = document.createElement('div');
box.className = 'image-box';
box.id = `box-${file.name}`;
// Create the preview wrapper first with the checkerboard background
const previewWrapper = document.createElement('div');
previewWrapper.className = 'preview-wrapper';
previewWrapper.style.cssText = `
background-image: linear-gradient(45deg, #808080 25%, transparent 25%),
linear-gradient(-45deg, #808080 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #808080 75%),
linear-gradient(-45deg, transparent 75%, #808080 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
background-color: #ffffff;
width: 100%;
height: 200px;
position: relative;
`;
// Create and set up the image with proper constraints from the start
const img = document.createElement('img');
img.className = 'image-preview';
img.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
`;
// Read and set the initial image
const reader = new FileReader();
reader.onload = e => img.src = e.target.result;
reader.readAsDataURL(file);
// Add image to wrapper
previewWrapper.appendChild(img);
// Create status element
const status = document.createElement('div');
status.className = 'status processing';
status.textContent = 'Processing...';
status.id = `status-${file.name}`;
// Create download buttons container
const downloadButtons = document.createElement('div');
downloadButtons.className = 'download-buttons';
downloadButtons.id = `downloads-${file.name}`;
downloadButtons.style.display = 'none';
// Assemble the box
box.appendChild(previewWrapper);
box.appendChild(status);
box.appendChild(downloadButtons);
document.getElementById('imageGrid').appendChild(box);
}
async function uploadFile(file) {
const formData = new FormData();
formData.append('files[]', file);
try {
const response = await fetch('', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log('Full API Response:', result);
// Get the file result using original filename
const fileResult = result.response[file.name];
console.log('Processing file:', file.name, 'Result:', fileResult);
// Check if we have a successful response
if (fileResult && fileResult.success === true && fileResult.results) {
console.log('File processed successfully:', fileResult);
// Store processed files info
processedFiles[file.name] = {
uniqueId: fileResult.uniqueId,
png: fileResult.results.png,
tiff: fileResult.results.tiff,
originalName: file.name
};
// Update status
const status = document.getElementById(`status-${file.name}`);
status.className = 'status complete';
status.textContent = 'Processing complete';
// Update download buttons
const downloadButtons = document.getElementById(`downloads-${file.name}`);
downloadButtons.style.display = 'flex';
downloadButtons.innerHTML = `
<button class="download-button" onclick="downloadFile('${fileResult.results.png}', '${file.name.replace(/\.[^/.]+$/, '')}_result.png')">
Download PNG
</button>
<button class="download-button" onclick="downloadFile('${fileResult.results.tiff}', '${file.name.replace(/\.[^/.]+$/, '')}_clipping_path.tiff')">
Download TIFF
</button>
`;
// Create a wrapper for the image preview if it doesn't exist
const box = document.getElementById(`box-${file.name}`);
let previewWrapper = box.querySelector('.preview-wrapper');
if (!previewWrapper) {
previewWrapper = document.createElement('div');
previewWrapper.className = 'preview-wrapper';
previewWrapper.style.cssText = `
background-image: linear-gradient(45deg, #808080 25%, transparent 25%),
linear-gradient(-45deg, #808080 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #808080 75%),
linear-gradient(-45deg, transparent 75%, #808080 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
background-color: #ffffff;
width: 100%;
height: 200px;
position: relative;
`;
// Move the existing image into the wrapper
const existingImg = box.querySelector('.image-preview');
if (existingImg) {
existingImg.style.position = 'absolute';
existingImg.style.top = '0';
existingImg.style.left = '0';
existingImg.style.width = '100%';
existingImg.style.height = '100%';
existingImg.style.objectFit = 'contain';
previewWrapper.appendChild(existingImg);
}
// Insert wrapper where the image was
box.insertBefore(previewWrapper, box.firstChild);
}
// Update the image
const img = box.querySelector('.image-preview');
setTimeout(() => {
console.log('Updating image preview for:', file.name);
console.log('New image URL:', `processed/${fileResult.results.png}`);
img.src = `processed/${fileResult.results.png}?t=${new Date().getTime()}`;
}, 1500);
} else {
console.error('Invalid or failed file result:', fileResult);
showError(file.name, fileResult?.error || 'Failed to process image');
}
} catch (error) {
console.error('Upload error:', error);
showError(file.name, 'Failed to process image');
}
}
async function checkFileExists(url) {
try {
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
} catch (error) {
console.error('File check error:', error);
return false;
}
}
function showError(fileName, error) {
const status = document.getElementById(`status-${fileName}`);
status.className = 'status error';
status.textContent = error;
}
function downloadFile(processedFilename, desiredFilename) {
const link = document.createElement('a');
link.href = `processed/${processedFilename}`;
link.download = desiredFilename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function downloadAll(type) {
Object.values(processedFiles).forEach(fileInfo => {
const processedFilename = fileInfo[type];
const originalName = fileInfo.originalName;
const extension = type === 'png' ? '_result.png' : '_clipping_path.tiff';
downloadFile(processedFilename, originalName.replace(/\.[^/.]+$/, "") + extension);
});
}
</script>
<!-- auth 4 of 4 NOTE: ensure values for clientID, authority (URL with tenant ID) and redirectUri are correct below -->
<script>
const msalConfig = {
auth: {
clientId: "9079054c-9620-4757-a256-23413042f1ef",
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
redirectUri: "https://ai-sandbox.oliver.solutions/format"
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: true,
}
};
const loginRequest = {
scopes: ["user.read"]
};
const myMSALObj = new msal.PublicClientApplication(msalConfig);
signIn();
function signIn() {
myMSALObj.loginPopup(loginRequest)
.then(loginResponse => {
console.log("User logged in:", loginResponse.account.username);
thisUser = loginResponse.account.username;
sessionStorage.setItem('accessToken', loginResponse.accessToken);
showProtectedContent(); // Show protected content after successful login
onAuthenticated(); // Special for this app
}).catch(error => {
console.error("Error during login:", error);
});
}
function signOut() {
// Clear the session storage and (does not) sign out from Microsoft Identity
sessionStorage.removeItem('accessToken');
//myMSALObj.logoutPopup();
console.log("User logged out.");
document.getElementById('protected-content').style.display = 'none'; // Hide protected content
document.getElementById('logout-button').style.display = 'none'; // Hide logout button
document.getElementById('login-button').style.display = 'flex'; // Show login button
}
function showProtectedContent() {
// Verify that the access token exists before showing protected content
const accessToken = sessionStorage.getItem('accessToken');
if (accessToken) {
document.getElementById('protected-content').style.display = 'flex';
document.getElementById('logout-button').style.display = 'flex'; // Show logout button
document.getElementById('login-button').style.display = 'none'; // Hide login button
}
}
// Check if the user is already logged in when the page loads
window.addEventListener('load', showProtectedContent);
</script>
<!-- end auth block -->
</body>
</html>